diff options
Diffstat (limited to 'crates/atom')
| -rw-r--r-- | crates/atom/Cargo.toml | 11 | ||||
| -rw-r--r-- | crates/atom/meson.build | 9 | ||||
| -rw-r--r-- | crates/atom/src/lib.rs | 786 | ||||
| -rw-r--r-- | crates/atom/src/meson.build | 1 | ||||
| -rw-r--r-- | crates/atom/src/parsers.rs | 599 |
5 files changed, 1406 insertions, 0 deletions
diff --git a/crates/atom/Cargo.toml b/crates/atom/Cargo.toml new file mode 100644 index 0000000..092759f --- /dev/null +++ b/crates/atom/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "atom" +version = "0.1.0" +edition = "2024" + +[dependencies] +mon = { workspace = true } +get = { workspace = true } +useflag = { path = "../useflag" } +parseable = { path = "../parseable" } +itertools = "0.14.0"
\ No newline at end of file diff --git a/crates/atom/meson.build b/crates/atom/meson.build new file mode 100644 index 0000000..ff4b6e6 --- /dev/null +++ b/crates/atom/meson.build @@ -0,0 +1,9 @@ +subdir('src') + +pkg = cargo.package('atom') +lib = pkg.library() + +meson.override_dependency( + 'atom-' + pkg.api() + '-rs', + declare_dependency(link_with: lib), +) diff --git a/crates/atom/src/lib.rs b/crates/atom/src/lib.rs new file mode 100644 index 0000000..39e32af --- /dev/null +++ b/crates/atom/src/lib.rs @@ -0,0 +1,786 @@ +#![deny(clippy::pedantic, unused_imports)] +#![allow( + dead_code, + unstable_name_collisions, + clippy::missing_errors_doc, + clippy::missing_panics_doc +)] +#![feature(impl_trait_in_assoc_type)] + +use core::{ + fmt::{self}, + option::Option, +}; + +use std::cmp::Ordering; + +// TODO: wtf? +#[allow(unused_imports)] +use parseable::Parseable; + +use useflag::UseFlag; + +use get::Get; + +use itertools::Itertools; + +mod parsers; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Blocker { + Weak, + Strong, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum VersionOperator { + Lt, + Gt, + Eq, + LtEq, + GtEq, + Roughly, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Get)] +pub struct Category(#[get(method = "get", kind = "deref")] String); + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Get)] +pub struct Name(#[get(method = "get", kind = "deref")] String); + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Get)] +pub struct VersionNumber(#[get(method = "get", kind = "deref")] String); + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Get)] +struct VersionNumbers(#[get(method = "get", kind = "deref")] Vec<VersionNumber>); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum VersionSuffixKind { + Alpha, + Beta, + Pre, + Rc, + P, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, Get)] +pub struct VersionSuffix { + kind: VersionSuffixKind, + number: Option<VersionNumber>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Get)] +pub struct VersionSuffixes(#[get(method = "get", kind = "deref")] Vec<VersionSuffix>); + +#[derive(Debug, Clone, Get, PartialEq, Eq, Hash)] +pub struct BuildId(#[get(method = "get", kind = "deref")] String); + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Get)] +pub struct Version { + numbers: VersionNumbers, + letter: Option<char>, + suffixes: VersionSuffixes, + rev: Option<VersionNumber>, + build_id: Option<BuildId>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Wildcard; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum SlotOperator { + Eq, + Star, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Get)] +pub struct SlotName(#[get(method = "name", kind = "deref")] String); + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Slot { + Wildcard, + Equal, + NameEqual { + primary: SlotName, + sub: Option<SlotName>, + }, + Name { + primary: SlotName, + sub: Option<SlotName>, + }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum UseDepNegate { + Minus, + Exclamation, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum UseDepSign { + Enabled, + Disabled, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum UseDepCondition { + Eq, + Question, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Repo(String); + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Get)] +pub struct UseDep { + negate: Option<UseDepNegate>, + flag: UseFlag, + sign: Option<UseDepSign>, + condition: Option<UseDepCondition>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Get)] +pub struct Cp { + category: Category, + name: Name, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Get)] +pub struct Cpv { + category: Category, + name: Name, + version: Version, + slot: Option<Slot>, +} + +#[derive(Clone, Debug, Get, PartialEq, Eq, Hash)] +pub struct Atom { + blocker: Option<Blocker>, + category: Category, + name: Name, + version: Option<(VersionOperator, Version, Option<Wildcard>)>, + slot: Option<Slot>, + repo: Option<Repo>, + #[get(kind = "deref")] + usedeps: Vec<UseDep>, +} + +impl Cpv { + #[must_use] + pub fn into_cp(self) -> Cp { + Cp { + name: self.name, + category: self.category, + } + } +} + +impl Atom { + #[must_use] + pub fn version_operator(&self) -> Option<VersionOperator> { + self.version.clone().map(|(oper, _, _)| oper) + } + + #[must_use] + pub fn into_cp(self) -> Cp { + Cp { + category: self.category, + name: self.name, + } + } + + #[must_use] + pub fn into_cpv(self) -> Option<Cpv> { + match self.version { + Some((_, version, _)) => Some(Cpv { + category: self.category, + name: self.name, + version, + slot: self.slot, + }), + None => None, + } + } +} + +impl VersionNumber { + #[must_use] + pub fn cmp_as_ints(&self, other: &Self) -> Ordering { + let a = self.get().trim_start_matches('0'); + let b = other.get().trim_start_matches('0'); + + a.len().cmp(&b.len()).then_with(|| a.cmp(b)) + } + + #[must_use] + pub fn cmp_as_str(&self, other: &Self) -> Ordering { + if self.get().starts_with('0') || other.get().starts_with('0') { + let a = self.get().trim_end_matches('0'); + let b = other.get().trim_end_matches('0'); + + a.cmp(b) + } else { + self.cmp_as_ints(other) + } + } +} + +impl PartialOrd for BuildId { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for BuildId { + fn cmp(&self, other: &Self) -> Ordering { + // build-id may not start with a zero so we dont need to strip them + self.get() + .len() + .cmp(&other.get().len()) + .then_with(|| self.get().cmp(other.get())) + } +} + +impl PartialOrd for VersionSuffix { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for VersionSuffix { + fn cmp(&self, other: &Self) -> Ordering { + match &self.kind.cmp(&other.kind) { + Ordering::Less => Ordering::Less, + Ordering::Greater => Ordering::Greater, + Ordering::Equal => match (&self.number, &other.number) { + (Some(a), Some(b)) => a.cmp_as_ints(b), + (Some(a), None) if a.get().chars().all(|c| c == '0') => Ordering::Equal, + (None, Some(b)) if b.get().chars().all(|c| c == '0') => Ordering::Equal, + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }, + } + } +} + +impl PartialOrd for VersionSuffixes { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for VersionSuffixes { + fn cmp(&self, other: &Self) -> Ordering { + let mut a = self.get().iter(); + let mut b = other.get().iter(); + + loop { + match (a.next(), b.next()) { + (Some(a), Some(b)) => match a.cmp(b) { + Ordering::Less => break Ordering::Less, + Ordering::Greater => break Ordering::Greater, + Ordering::Equal => (), + }, + (Some(a), None) if matches!(a.kind, VersionSuffixKind::P) => { + break Ordering::Greater; + } + (Some(_), None) => break Ordering::Less, + (None, Some(b)) if matches!(b.kind, VersionSuffixKind::P) => break Ordering::Less, + (None, Some(_)) => break Ordering::Greater, + (None, None) => break Ordering::Equal, + } + } + } +} + +impl PartialOrd for VersionNumbers { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for VersionNumbers { + fn cmp(&self, other: &Self) -> Ordering { + match self + .get() + .first() + .unwrap() + .cmp_as_ints(other.get().first().unwrap()) + { + Ordering::Less => Ordering::Less, + Ordering::Greater => Ordering::Greater, + Ordering::Equal => { + let mut a = self.get().iter().skip(1); + let mut b = other.get().iter().skip(1); + + loop { + match (a.next(), b.next()) { + (Some(a), Some(b)) => match a.cmp_as_str(b) { + Ordering::Less => break Ordering::Less, + Ordering::Greater => break Ordering::Greater, + Ordering::Equal => (), + }, + + (Some(_), None) => break Ordering::Greater, + (None, Some(_)) => break Ordering::Less, + (None, None) => break Ordering::Equal, + } + } + } + } + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for Version { + fn cmp(&self, other: &Self) -> Ordering { + match self.numbers.cmp(&other.numbers) { + Ordering::Less => return Ordering::Less, + Ordering::Greater => return Ordering::Greater, + Ordering::Equal => (), + } + + match (self.letter, other.letter) { + (Some(a), Some(b)) if a < b => return Ordering::Less, + (Some(a), Some(b)) if a > b => return Ordering::Greater, + (Some(a), Some(b)) if a == b => (), + (Some(_), None) => return Ordering::Greater, + (None, Some(_)) => return Ordering::Less, + (None, None) => (), + _ => unreachable!(), + } + + match self.suffixes.cmp(&other.suffixes) { + Ordering::Less => return Ordering::Less, + Ordering::Greater => return Ordering::Greater, + Ordering::Equal => (), + } + + match (&self.rev, &other.rev) { + (Some(a), Some(b)) => match a.cmp_as_ints(b) { + Ordering::Less => return Ordering::Less, + Ordering::Greater => return Ordering::Greater, + Ordering::Equal => (), + }, + (Some(a), None) if a.get().chars().all(|c| c == '0') => (), + (Some(_), None) => return Ordering::Greater, + (None, Some(b)) if b.get().chars().all(|c| c == '0') => (), + (None, Some(_)) => return Ordering::Less, + (None, None) => (), + } + + match (&self.build_id, &other.build_id) { + (Some(a), Some(b)) => a.cmp(b), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + } + } +} + +impl PartialOrd for Cpv { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + if self.category == other.category && self.name == other.name { + Some(self.version.cmp(&other.version)) + } else { + None + } + } +} + +impl fmt::Display for Blocker { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Weak => write!(f, "!"), + Self::Strong => write!(f, "!!"), + } + } +} + +impl fmt::Display for VersionOperator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Lt => write!(f, "<"), + Self::Gt => write!(f, ">"), + Self::Eq => write!(f, "="), + Self::LtEq => write!(f, "<="), + Self::GtEq => write!(f, ">="), + Self::Roughly => write!(f, "~"), + } + } +} + +impl fmt::Display for Category { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl fmt::Display for Name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl fmt::Display for VersionNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl fmt::Display for BuildId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.get()) + } +} + +impl fmt::Display for VersionSuffixKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Alpha => write!(f, "alpha"), + Self::Beta => write!(f, "beta"), + Self::Pre => write!(f, "pre"), + Self::Rc => write!(f, "rc"), + Self::P => write!(f, "p"), + } + } +} + +impl fmt::Display for VersionSuffix { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.kind)?; + + if let Some(number) = self.number.as_ref() { + write!(f, "{number}")?; + } + + Ok(()) + } +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let numbers = self + .numbers + .get() + .iter() + .map(VersionNumber::get) + .intersperse(".") + .collect::<String>(); + + let suffixes = self + .suffixes + .get() + .iter() + .map(VersionSuffix::to_string) + .intersperse("_".to_string()) + .collect::<String>(); + + write!(f, "{numbers}")?; + + if let Some(letter) = self.letter { + write!(f, "{letter}")?; + } + + if !suffixes.is_empty() { + write!(f, "_{suffixes}")?; + } + + if let Some(rev) = self.rev.as_ref() { + write!(f, "-r{rev}")?; + } + + if let Some(build_id) = self.build_id.as_ref() { + write!(f, "-{build_id}")?; + } + + Ok(()) + } +} + +impl fmt::Display for SlotOperator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Eq => write!(f, "="), + Self::Star => write!(f, "*"), + } + } +} + +impl fmt::Display for SlotName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl fmt::Display for Slot { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Wildcard => write!(f, "*"), + Self::Equal => { + write!(f, "=") + } + Self::NameEqual { primary, sub } => { + write!(f, "{primary}")?; + + if let Some(sub) = sub { + write!(f, "/{sub}")?; + } + + write!(f, "=") + } + Self::Name { primary, sub } => { + write!(f, "{primary}")?; + + if let Some(sub) = sub { + write!(f, "/{sub}")?; + } + + Ok(()) + } + } + } +} + +impl fmt::Display for UseDepNegate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Minus => write!(f, "-"), + Self::Exclamation => write!(f, "!"), + } + } +} + +impl fmt::Display for UseDepSign { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Enabled => write!(f, "(+)"), + Self::Disabled => write!(f, "(-)"), + } + } +} + +impl fmt::Display for UseDepCondition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Eq => write!(f, "="), + Self::Question => write!(f, "?"), + } + } +} + +impl fmt::Display for UseDep { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(negate) = self.negate.as_ref() { + write!(f, "{negate}")?; + } + + write!(f, "{}", self.flag)?; + + if let Some(sign) = self.sign.as_ref() { + write!(f, "{sign}")?; + } + + if let Some(condition) = self.condition.as_ref() { + write!(f, "{condition}")?; + } + + Ok(()) + } +} + +impl fmt::Display for Cp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}/{}", &self.category, &self.name) + } +} + +impl fmt::Display for Cpv { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}/{}-{}", &self.category, &self.name, &self.version)?; + + if let Some(slot) = self.slot.as_ref() { + write!(f, ":{slot}")?; + } + + Ok(()) + } +} + +impl fmt::Display for Atom { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(blocker) = self.blocker.as_ref() { + write!(f, "{blocker}")?; + } + + if let Some(version_operator) = self.version_operator().as_ref() { + write!(f, "{version_operator}")?; + } + + write!(f, "{}", self.category)?; + write!(f, "/")?; + write!(f, "{}", self.name)?; + + if let Some((_, version, None)) = self.version() { + write!(f, "-{version}")?; + } else if let Some((_, version, Some(_))) = self.version() { + write!(f, "-{version}*")?; + } + + if let Some(slot) = self.slot.as_ref() { + write!(f, ":{slot}")?; + } + + let usedeps = self + .usedeps + .iter() + .map(UseDep::to_string) + .intersperse(",".to_string()) + .collect::<String>(); + + if !usedeps.is_empty() { + write!(f, "[{usedeps}]")?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use mon::{Parser, input::InputIter}; + + use super::*; + + use crate::Parseable; + + macro_rules! assert_cmp_display { + ($a:expr, $b:expr, $ordering:expr) => { + if $a.cmp(&$b) != $ordering { + panic!("{} ~ {} != {:?}", $a, $b, $ordering) + } + }; + } + + macro_rules! assert_partial_cmp_display { + ($a:expr, $b:expr, $ordering:expr) => { + if $a.partial_cmp(&$b) != $ordering { + panic!("{} ~ {} != {:?}", $a, $b, $ordering) + } + }; + } + + #[test] + fn test_version_display() { + let s = "1.0.0_alpha1_beta1-r1"; + let version = Version::parser().parse_finished(InputIter::new(s)).unwrap(); + + assert_eq!(version.to_string().as_str(), s); + } + + #[test] + fn test_display_atom() { + let s = "!!>=foo/bar-1.0.0v_alpha1_beta1-r1:slot/sub=[a,b,c]"; + let atom = Atom::parser().parse_finished(InputIter::new(s)).unwrap(); + + assert_eq!(atom.to_string().as_str(), s); + } + + #[test] + fn test_version_cmp() { + let versions = [ + ("1.0.1", "1.0", Ordering::Greater), + ("1.0.0", "1.0.0_alpha", Ordering::Greater), + ("1.0.0_alpha", "1.0.0_alpha_p", Ordering::Less), + ("1.0.0-r0", "1.0.0", Ordering::Equal), + ("1.0.0-r0000", "1.0.0", Ordering::Equal), + ("1.0.0-r1-1", "1.0.0-r1-2", Ordering::Less), + ]; + + for (a, b, ordering) in versions.iter().map(|(a, b, ordering)| { + ( + Version::parser().parse_finished(InputIter::new(a)).unwrap(), + Version::parser().parse_finished(InputIter::new(b)).unwrap(), + ordering, + ) + }) { + assert_cmp_display!(a, b, *ordering); + } + } + + #[test] + fn test_cpv_eq() { + let cpvs = [ + ("foo/bar-1", "foo/bar-1", Some(Ordering::Equal)), + ("foo/baz-1", "foo/bar-1", None), + ]; + + for (a, b, ordering) in cpvs.iter().copied().map(|(a, b, ordering)| { + ( + Cpv::parser().parse_finished(InputIter::new(a)).unwrap(), + Cpv::parser().parse_finished(InputIter::new(b)).unwrap(), + ordering, + ) + }) { + assert_partial_cmp_display!(a, b, ordering); + } + } + + #[test] + fn test_version_cmp_letter() { + let a = Version::parser() + .parse_finished(InputIter::new("1.0.0")) + .unwrap(); + let b = Version::parser() + .parse_finished(InputIter::new("1.0.0a")) + .unwrap(); + + assert_cmp_display!(a, b, Ordering::Less); + } + + #[test] + fn test_version_cmp_where_b_has_leading_zeros() { + let a = Version::parser() + .parse_finished(InputIter::new("1.2")) + .unwrap(); + let b = Version::parser() + .parse_finished(InputIter::new("1.054")) + .unwrap(); + + assert_cmp_display!(a, b, Ordering::Greater); + } + + #[test] + fn test_version_has_more_zeros() { + let a = Version::parser() + .parse_finished(InputIter::new("1.0.0")) + .unwrap(); + let b = Version::parser() + .parse_finished(InputIter::new("1.0")) + .unwrap(); + + assert_cmp_display!(a, b, Ordering::Greater); + } + + #[test] + fn test_fuzzer_cases() { + let control = Version::parser() + .parse_finished(InputIter::new("1.2.0a_alpha1_beta2-r1-8")) + .unwrap(); + + #[allow(clippy::single_element_loop)] + for (version_str, expected) in [("1.2.0", Ordering::Greater)] { + let version = Version::parser() + .parse_finished(InputIter::new(version_str)) + .unwrap(); + + assert_cmp_display!(control, version, expected); + } + } +} diff --git a/crates/atom/src/meson.build b/crates/atom/src/meson.build new file mode 100644 index 0000000..0293be6 --- /dev/null +++ b/crates/atom/src/meson.build @@ -0,0 +1 @@ +sources += files('lib.rs', 'parsers.rs') diff --git a/crates/atom/src/parsers.rs b/crates/atom/src/parsers.rs new file mode 100644 index 0000000..2f8cb8c --- /dev/null +++ b/crates/atom/src/parsers.rs @@ -0,0 +1,599 @@ +use core::option::Option::None; + +use crate::{ + Atom, Blocker, BuildId, Category, Cp, Cpv, Name, Repo, Slot, SlotName, SlotOperator, + UseDep, UseDepCondition, UseDepNegate, UseDepSign, Version, VersionNumber, VersionNumbers, + VersionOperator, VersionSuffix, VersionSuffixKind, VersionSuffixes, Wildcard, +}; + +use mon::{ + Parser, ParserIter, ascii_alphanumeric, ascii_numeric, ascii_numeric1, eof, r#if, + input::InputIter, one_of, tag, +}; + +use parseable::Parseable; + +use useflag::UseFlag; + +impl<'a> Parseable<'a, &'a str> for Blocker { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + tag("!!") + .map(|_| Blocker::Strong) + .or(tag("!").map(|_| Blocker::Weak)) + } +} + +impl<'a> Parseable<'a, &'a str> for VersionOperator { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + tag("<=") + .map(|_| VersionOperator::LtEq) + .or(tag(">=").map(|_| VersionOperator::GtEq)) + .or(tag("<").map(|_| VersionOperator::Lt)) + .or(tag(">").map(|_| VersionOperator::Gt)) + .or(tag("=").map(|_| VersionOperator::Eq)) + .or(tag("~").map(|_| VersionOperator::Roughly)) + } +} + +impl<'a> Parseable<'a, &'a str> for VersionNumber { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + ascii_numeric1().map(|output: &str| VersionNumber(output.to_string())) + } +} + +impl<'a> Parseable<'a, &'a str> for BuildId { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + let start = ascii_numeric().and_not(tag("0")); + let rest = ascii_numeric().repeated().many(); + + start + .and(rest) + .recognize() + .or(tag("0")) + .map(|output: &str| BuildId(output.to_string())) + } +} + +impl<'a> Parseable<'a, &'a str> for VersionSuffixKind { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + tag("alpha") + .map(|_| VersionSuffixKind::Alpha) + .or(tag("beta").map(|_| VersionSuffixKind::Beta)) + .or(tag("pre").map(|_| VersionSuffixKind::Pre)) + .or(tag("rc").map(|_| VersionSuffixKind::Rc)) + .or(tag("p").map(|_| VersionSuffixKind::P)) + } +} + +impl<'a> Parseable<'a, &'a str> for VersionSuffix { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + VersionSuffixKind::parser() + .and(VersionNumber::parser().opt()) + .map(|(kind, number)| VersionSuffix { kind, number }) + } +} + +impl<'a> Parseable<'a, &'a str> for VersionNumbers { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + VersionNumber::parser() + .separated_by(tag(".")) + .at_least(1) + .map(VersionNumbers) + } +} + +impl<'a> Parseable<'a, &'a str> for VersionSuffixes { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + VersionSuffix::parser() + .separated_by(tag("_")) + .at_least(1) + .map(VersionSuffixes) + } +} + +impl<'a> Parseable<'a, &'a str> for Version { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + let rev = VersionNumber::parser().preceded_by(tag("-r")); + let build_id = BuildId::parser().preceded_by(tag("-")); + + VersionNumbers::parser() + .and(r#if(|c: &char| c.is_ascii_alphabetic() && c.is_ascii_lowercase()).opt()) + .and(VersionSuffixes::parser().preceded_by(tag("_")).opt()) + .and(rev.opt()) + .and(build_id.opt()) + .map(|((((numbers, letter), suffixes), rev), build_id)| Version { + numbers, + letter, + suffixes: suffixes.unwrap_or(VersionSuffixes(Vec::new())), + rev, + build_id, + }) + } +} + +impl<'a> Parseable<'a, &'a str> for Category { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + let start = ascii_alphanumeric().or(one_of("_".chars())); + let rest = ascii_alphanumeric() + .or(one_of("+_.-".chars())) + .repeated() + .many(); + + start + .and(rest) + .recognize() + .map(|output: &str| Category(output.to_string())) + } +} + +impl<'a> Parseable<'a, &'a str> for Name { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + let start = || ascii_alphanumeric().or(one_of("_".chars())); + + let rest = ascii_alphanumeric() + .or(one_of("_+".chars())) + .or(one_of("-".chars()).and_not( + Version::parser() + .preceded_by(tag("-")) + .followed_by(ascii_alphanumeric().or(one_of("_+-".chars())).not()), + )) + .repeated() + .many(); + + let verify = ascii_alphanumeric() + .or(one_of("_+".chars())) + .or(one_of("-".chars()) + .and_not(Version::parser().preceded_by(tag("-")).followed_by(eof()))) + .repeated() + .many(); + + start() + .and(rest) + .recognize() + .verify_output(move |output: &&str| { + verify.check_finished(InputIter::new(*output)).is_ok() + }) + .map(|output: &str| Name(output.to_string())) + } +} + +impl<'a> Parseable<'a, &'a str> for SlotOperator { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + tag("=") + .map(|_| SlotOperator::Eq) + .or(tag("*").map(|_| SlotOperator::Star)) + } +} + +impl<'a> Parseable<'a, &'a str> for SlotName { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + let start = ascii_alphanumeric().or(one_of("_".chars())); + let rest = ascii_alphanumeric() + .or(one_of("+_.-".chars())) + .repeated() + .many(); + + start + .and(rest) + .recognize() + .map(|output: &str| SlotName(output.to_string())) + } +} + +impl<'a> Parseable<'a, &'a str> for Slot { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + let wildcard = tag("*").map(|_| Slot::Wildcard); + let equal = tag("=").map(|_| Slot::Equal); + let name_equal = SlotName::parser() + .and(SlotName::parser().preceded_by(tag("/")).opt()) + .followed_by(tag("=")) + .map(|(primary, sub)| Slot::NameEqual { primary, sub }); + let name = SlotName::parser() + .and(SlotName::parser().preceded_by(tag("/")).opt()) + .map(|(primary, sub)| Self::Name { primary, sub }); + + wildcard.or(equal).or(name_equal).or(name) + } +} + +impl<'a> Parseable<'a, &'a str> for UseDepSign { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + tag("(-)") + .map(|_| UseDepSign::Disabled) + .or(tag("(+)").map(|_| UseDepSign::Enabled)) + } +} + +impl<'a> Parseable<'a, &'a str> for Repo { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + let start = ascii_alphanumeric().or(one_of("_".chars())); + let rest = ascii_alphanumeric() + .or(one_of("_-".chars())) + .repeated() + .many(); + + start + .and(rest) + .recognize() + .verify_output(move |output: &&str| { + Name::parser() + .check_finished(InputIter::new(*output)) + .is_ok() + }) + .map(|output: &str| Repo(output.to_string())) + } +} + +impl<'a> Parseable<'a, &'a str> for UseDep { + type Parser = impl Parser<&'a str, Output = Self>; + + #[allow(clippy::many_single_char_names)] + fn parser() -> Self::Parser { + let a = UseFlag::parser() + .and(UseDepSign::parser().opt()) + .preceded_by(tag("-")) + .map(|(flag, sign)| UseDep { + negate: Some(UseDepNegate::Minus), + flag, + sign, + condition: None, + }); + + let b = UseFlag::parser() + .and(UseDepSign::parser().opt()) + .preceded_by(tag("!")) + .followed_by(tag("?")) + .map(|(flag, sign)| UseDep { + negate: Some(UseDepNegate::Exclamation), + flag, + sign, + condition: Some(UseDepCondition::Question), + }); + + let c = UseFlag::parser() + .and(UseDepSign::parser().opt()) + .followed_by(tag("?")) + .map(|(flag, sign)| UseDep { + negate: None, + flag, + sign, + condition: Some(UseDepCondition::Question), + }); + + let d = UseFlag::parser() + .and(UseDepSign::parser().opt()) + .preceded_by(tag("!")) + .followed_by(tag("=")) + .map(|(flag, sign)| UseDep { + negate: Some(UseDepNegate::Exclamation), + flag, + sign, + condition: Some(UseDepCondition::Eq), + }); + + let e = UseFlag::parser() + .and(UseDepSign::parser().opt()) + .followed_by(tag("=")) + .map(|(flag, sign)| UseDep { + negate: None, + flag, + sign, + condition: Some(UseDepCondition::Eq), + }); + + let f = UseFlag::parser() + .and(UseDepSign::parser().opt()) + .map(|(flag, sign)| UseDep { + negate: None, + flag, + sign, + condition: None, + }); + + a.or(b).or(c).or(d).or(e).or(f) + } +} + +impl<'a> Parseable<'a, &'a str> for Atom { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + let usedeps = || { + UseDep::parser() + .separated_by(tag(",")) + .at_least(1) + .delimited_by(tag("["), tag("]")) + .opt() + }; + + let without_version = Blocker::parser() + .opt() + .and(Category::parser()) + .and(Name::parser().preceded_by(tag("/"))) + .and(Slot::parser().preceded_by(tag(":")).opt()) + .and(Repo::parser().preceded_by(tag("::")).opt()) + .and(usedeps()) + .map( + |(((((blocker, category), name), slot), repo), usedeps)| Atom { + blocker, + category, + name, + version: None, + slot, + repo, + usedeps: usedeps.unwrap_or(Vec::new()), + }, + ); + + let with_version = Blocker::parser() + .opt() + .and(VersionOperator::parser()) + .and(Category::parser()) + .and(Name::parser().preceded_by(tag("/"))) + .and(Version::parser().preceded_by(tag("-"))) + .and(tag("*").map(|_| Wildcard).opt()) + .and(Slot::parser().preceded_by(tag(":")).opt()) + .and(Repo::parser().preceded_by(tag("::")).opt()) + .and(usedeps()) + .verify_output( + |((((((((_, version_operator), _), _), version), star), _), _), _)| { + matches!( + (version_operator, star), + (VersionOperator::Eq, Some(_) | None) | (_, None) + ) && matches!((version.build_id(), star), (Some(_), None) | (None, _)) + }, + ) + .map( + |( + ( + ((((((blocker, version_operator), category), name), version), star), slot), + repo, + ), + usedeps, + )| { + Atom { + blocker, + category, + name, + version: Some((version_operator, version, star)), + slot, + repo, + usedeps: usedeps.unwrap_or(Vec::new()), + } + }, + ); + + with_version.or(without_version) + } +} + +impl<'a> Parseable<'a, &'a str> for Cp { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + Category::parser() + .and(Name::parser().preceded_by(tag("/"))) + .map(|(category, name)| Cp { category, name }) + } +} + +impl<'a> Parseable<'a, &'a str> for Cpv { + type Parser = impl Parser<&'a str, Output = Self>; + + fn parser() -> Self::Parser { + Category::parser() + .and(Name::parser().preceded_by(tag("/"))) + .and(Version::parser().preceded_by(tag("-"))) + .and(Slot::parser().preceded_by(tag(":")).opt()) + .map(|(((category, name), version), slot)| Cpv { + category, + name, + version, + slot, + }) + } +} + +#[cfg(test)] +mod test { + + use mon::input::InputIter; + + use super::*; + + #[test] + fn test_version() { + let it = InputIter::new("1.0.0v_alpha1_beta1-r1"); + + Version::parser().check_finished(it).unwrap(); + } + + #[test] + fn test_name() { + let it = InputIter::new("foo-1-bar-1.0.0"); + + match Name::parser().parse(it) { + Ok((_, output)) => { + assert_eq!(output.0.as_str(), "foo-1-bar"); + } + _ => unreachable!(), + } + } + + #[test] + fn test_atom() { + let it = InputIter::new( + "!!>=cat/pkg-1-foo-1.0.0v_alpha1_p20250326-r1:primary/sub=[use,use=,!use=,use?,!use?,-use,use(+),use(-)]", + ); + + Atom::parser().check_finished(it).unwrap(); + } + + #[test] + fn test_cursed_atom() { + let it = InputIter::new( + "!!>=_.+-0-/_-test-T-123_beta1_-4a-6+-_p--1.00.02b_alpha3_pre_p4-r5:slot/_-+6-9=[test(+),test(-)]", + ); + + Atom::parser().check_finished(it).unwrap(); + } + + #[test] + fn test_atom_with_star_in_non_empty_slot() { + let it = InputIter::new("foo/bar:*/subslot"); + + assert!(Atom::parser().check_finished(it).is_err()); + } + + #[test] + fn test_invalid_usedep() { + let it = InputIter::new("foo-bar:slot/sub=[!use]"); + + assert!(Atom::parser().check_finished(it).is_err()); + } + + #[test] + fn test_empty_slot() { + let it = InputIter::new("=dev-ml/uucp-17*:"); + + assert!(Atom::parser().check_finished(it).is_err()); + } + + #[test] + fn test_usedep_with_underscore() { + let it = InputIter::new("foo/bar[use_dep]"); + + Atom::parser().check_finished(it).unwrap(); + } + + #[test] + fn test_version_with_uppercase_letter() { + let it = InputIter::new("=foo/bar-1.0.0V"); + + assert!(Atom::parser().check_finished(it).is_err()); + } + + #[test] + fn test_version_with_version_operator_without_version() { + let it = InputIter::new("=foo/bar"); + + assert!(Atom::parser().check_finished(it).is_err()); + } + + #[test] + fn test_version_with_version_without_version_operator() { + let it = InputIter::new("foo/bar-1.0.0"); + + assert!(Atom::parser().check_finished(it).is_err()); + } + + #[test] + fn test_atom_with_eq_version_operator() { + let it = InputIter::new("=foo/bar-1.0.0"); + + Atom::parser().check_finished(it).unwrap(); + } + + #[test] + fn test_atom_with_star_in_version() { + let it = InputIter::new("=foo/bar-1.2*"); + + Atom::parser().check_finished(it).unwrap(); + } + + #[test] + fn test_atom_with_star_in_version_without_eq_version_operator() { + let it = InputIter::new(">=foo/bar-1.2*"); + + assert!(Atom::parser().check_finished(it).is_err()); + } + + #[test] + fn test_atom_with_trailing_dash_and_letter() { + let it = InputIter::new("dev-db/mysql-connector-c"); + + Atom::parser().check_finished(it).unwrap(); + } + + #[test] + fn test_cpv_with_slot() { + let it = InputIter::new("foo/bar-1.0:slot/sub="); + + Cpv::parser().check_finished(it).unwrap(); + } + + #[test] + fn test_cpv_without_version_but_trailing_almost_version() { + let it = InputIter::new("dev-perl/mod-p-2.3_"); + + assert!(Cpv::parser().parse_finished(it).is_err()); + } + + #[test] + fn test_empty_slot_with_operator() { + let it = InputIter::new("foo/bar:="); + + Atom::parser().check_finished(it).unwrap(); + } + + #[test] + fn test_with_repo() { + let it = InputIter::new("=foo/bar-1.0.0:slot/sub=::gentoo[a,b,c]"); + + Atom::parser().check_finished(it).unwrap(); + } + + #[test] + fn test_against_fuzzer_false_positives() { + let atoms = [ + "media-libs/libsdl2[haptitick(+),sound(+)vd,eio(+)]", + "=kde-frameworks/kcodecs-6.19*86", + "=dev-ml/stdio-0.17*t:=[ocamlopt?]", + ">=dev-libs/libgee-0-8.5:0..8=", + "<dev-haskell/wai-3.3:=[]", + ">=kde-frameworks/kcrash-2.16.0:6*", + "0-f/merreka+m::k+", + "iev-a/h:/n=", + "=dev-ml/stdio-0-17*:=[ocamlopt?]", + ]; + + for atom in atoms { + assert!( + Atom::parser().check_finished(InputIter::new(atom)).is_err(), + "{atom}" + ); + } + } +} |
