use core::option::Option::None; use mon::{ Parser, ParserIter, ascii_alphanumeric, ascii_numeric, ascii_numeric1, eof, r#if, input::InputIter, one_of, tag, }; use crate::{ Parseable, atom::{ Atom, Blocker, BuildId, Category, Cp, Cpv, Name, Repo, Slot, SlotName, SlotOperator, UseDep, UseDepCondition, UseDepNegate, UseDepSign, Version, VersionNumber, VersionNumbers, VersionOperator, VersionSuffix, VersionSuffixKind, VersionSuffixes, Wildcard, }, 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() .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 equals = SlotName::parser() .opt() .and(SlotName::parser().preceded_by(tag("/")).opt()) .followed_by(tag("=")) .map(|(primary, sub)| Slot::Equal { primary, sub }); let name = SlotName::parser() .and(SlotName::parser().preceded_by(tag("/")).opt()) .map(|(primary, sub)| Slot::Name { primary, sub }); wildcard.or(equals).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)) } } //A slot name may contain any of the characters [A-Za-z0-9+_.-]. It must not begin with a hyphen, a dot or a plus sign. 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), _), _), _), star), _), _), _)| { matches!( (version_operator, star), (VersionOperator::Eq, 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=", "=kde-frameworks/kcrash-2.16.0:6*", "0-f/merreka+m::k+", ]; for atom in atoms { assert!( Atom::parser().check_finished(InputIter::new(atom)).is_err(), "{atom}" ); } } }