summaryrefslogtreecommitdiff
path: root/crates/atom/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/atom/src')
-rw-r--r--crates/atom/src/lib.rs786
-rw-r--r--crates/atom/src/meson.build1
-rw-r--r--crates/atom/src/parsers.rs599
3 files changed, 1386 insertions, 0 deletions
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}"
+ );
+ }
+ }
+}