summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/atom/Cargo.toml11
-rw-r--r--crates/atom/meson.build9
-rw-r--r--crates/atom/src/lib.rs786
-rw-r--r--crates/atom/src/meson.build1
-rw-r--r--crates/atom/src/parsers.rs599
-rw-r--r--crates/meson.build1
-rw-r--r--crates/parseable/Cargo.toml7
-rw-r--r--crates/parseable/src/lib.rs18
-rw-r--r--crates/repo/Cargo.toml11
-rw-r--r--crates/repo/src/ebuild/meson.build1
-rw-r--r--crates/repo/src/ebuild/mod.rs83
-rw-r--r--crates/repo/src/ebuild/parsers.rs207
-rw-r--r--crates/repo/src/lib.rs335
-rw-r--r--crates/repo/src/meson.build3
-rw-r--r--crates/useflag/Cargo.toml9
-rw-r--r--crates/useflag/src/lib.rs29
-rw-r--r--crates/useflag/src/meson.build1
-rw-r--r--crates/useflag/src/parsers.rs39
18 files changed, 2150 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}"
+ );
+ }
+ }
+}
diff --git a/crates/meson.build b/crates/meson.build
new file mode 100644
index 0000000..e64f43d
--- /dev/null
+++ b/crates/meson.build
@@ -0,0 +1 @@
+subdir('atom')
diff --git a/crates/parseable/Cargo.toml b/crates/parseable/Cargo.toml
new file mode 100644
index 0000000..25b0c77
--- /dev/null
+++ b/crates/parseable/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "parseable"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+mon = { workspace = true } \ No newline at end of file
diff --git a/crates/parseable/src/lib.rs b/crates/parseable/src/lib.rs
new file mode 100644
index 0000000..38ed418
--- /dev/null
+++ b/crates/parseable/src/lib.rs
@@ -0,0 +1,18 @@
+#![feature(impl_trait_in_assoc_type)]
+
+use mon::{Parser, input::{Input, InputIter}};
+
+pub trait Parseable<'a, I: Input + 'a> {
+ type Parser: Parser<I, Output = Self>;
+
+ fn parser() -> Self::Parser;
+
+ fn parse(input: I) -> Result<Self, I>
+ where
+ Self: Sized,
+ {
+ Self::parser()
+ .parse_finished(InputIter::new(input))
+ .map_err(|e| e.rest())
+ }
+}
diff --git a/crates/repo/Cargo.toml b/crates/repo/Cargo.toml
new file mode 100644
index 0000000..8b13ede
--- /dev/null
+++ b/crates/repo/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "repo"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+atom = { path = "../atom" }
+useflag = { path = "../useflag" }
+parseable = { path = "../parseable" }
+mon = { workspace = true }
+get = { workspace = true } \ No newline at end of file
diff --git a/crates/repo/src/ebuild/meson.build b/crates/repo/src/ebuild/meson.build
new file mode 100644
index 0000000..a7331a8
--- /dev/null
+++ b/crates/repo/src/ebuild/meson.build
@@ -0,0 +1 @@
+sources += files('mod.rs', 'parsers.rs')
diff --git a/crates/repo/src/ebuild/mod.rs b/crates/repo/src/ebuild/mod.rs
new file mode 100644
index 0000000..6c547c5
--- /dev/null
+++ b/crates/repo/src/ebuild/mod.rs
@@ -0,0 +1,83 @@
+use get::Get;
+
+use std::path::PathBuf;
+
+use atom::{Atom, Name, Slot, Version};
+
+use useflag::{IUseFlag, UseFlag};
+
+mod parsers;
+
+#[derive(Clone, Debug)]
+pub enum Conditional {
+ Negative(UseFlag),
+ Positive(UseFlag),
+}
+
+#[derive(Clone, Debug)]
+pub enum Depend<T> {
+ Element(T),
+ AllOf(Vec<Self>),
+ AnyOf(Vec<Self>),
+ OneOf(Vec<Self>),
+ ConditionalGroup(Conditional, Vec<Self>),
+}
+
+#[derive(Debug, Clone)]
+pub enum UriPrefix {
+ Mirror,
+ Fetch,
+}
+
+#[derive(Debug, Clone, Get)]
+pub struct Uri {
+ #[get(kind = "deref")]
+ protocol: String,
+ #[get(kind = "deref")]
+ path: String,
+}
+
+#[derive(Debug, Clone)]
+pub enum SrcUri {
+ Filename(PathBuf),
+ Uri {
+ prefix: Option<UriPrefix>,
+ uri: Uri,
+ filename: Option<PathBuf>,
+ },
+}
+
+#[derive(Debug, Clone, Get)]
+pub struct License(#[get(method = "get", kind = "deref")] String);
+
+#[derive(Debug, Clone, Get)]
+pub struct Eapi(#[get(method = "get", kind = "deref")] String);
+
+#[derive(Debug, Clone, Get)]
+pub struct Eclass(#[get(method = "get", kind = "deref")] String);
+
+#[derive(Debug, Clone, Get)]
+pub struct Ebuild {
+ pub(super) name: Name,
+ pub(super) version: Version,
+ pub(super) slot: Option<Slot>,
+ pub(super) homepage: Option<String>,
+ #[get(kind = "deref")]
+ pub(super) src_uri: Vec<Depend<SrcUri>>,
+ pub(super) eapi: Option<Eapi>,
+ #[get(kind = "deref")]
+ pub(super) inherit: Vec<Eclass>,
+ #[get(kind = "deref")]
+ pub(super) iuse: Vec<IUseFlag>,
+ #[get(kind = "deref")]
+ pub(super) license: Vec<Depend<License>>,
+ pub(super) description: Option<String>,
+ #[get(kind = "deref")]
+ pub(super) depend: Vec<Depend<Atom>>,
+ #[get(kind = "deref")]
+ pub(super) bdepend: Vec<Depend<Atom>>,
+ #[get(kind = "deref")]
+ pub(super) rdepend: Vec<Depend<Atom>>,
+ #[get(kind = "deref")]
+ pub(super) idepend: Vec<Depend<Atom>>,
+}
diff --git a/crates/repo/src/ebuild/parsers.rs b/crates/repo/src/ebuild/parsers.rs
new file mode 100644
index 0000000..6dc3525
--- /dev/null
+++ b/crates/repo/src/ebuild/parsers.rs
@@ -0,0 +1,207 @@
+use std::path::PathBuf;
+
+use crate::ebuild::{Conditional, Depend, Eapi, Eclass, License, SrcUri, Uri, UriPrefix};
+
+use mon::{
+ Parser, ParserIter, ascii_alpha1, ascii_alphanumeric, ascii_whitespace1, r#if, one_of, tag,
+};
+
+use parseable::Parseable;
+
+use useflag::UseFlag;
+
+impl<'a> Parseable<'a, &'a str> for UriPrefix {
+ type Parser = impl Parser<&'a str, Output = Self>;
+
+ fn parser() -> Self::Parser {
+ tag("+mirror")
+ .map(|_| UriPrefix::Mirror)
+ .or(tag("+fetch").map(|_| UriPrefix::Fetch))
+ }
+}
+
+impl<'a> Parseable<'a, &'a str> for Uri {
+ type Parser = impl Parser<&'a str, Output = Self>;
+
+ fn parser() -> Self::Parser {
+ let protocol = ascii_alpha1::<&str>()
+ .followed_by(tag("://"))
+ .map(|output: &str| output.to_string());
+ let path = r#if(|c: &char| !c.is_ascii_whitespace())
+ .repeated()
+ .at_least(1)
+ .recognize()
+ .map(|output: &str| output.to_string());
+
+ protocol
+ .and(path)
+ .map(|(protocol, path)| Uri { protocol, path })
+ }
+}
+
+impl<'a> Parseable<'a, &'a str> for SrcUri {
+ type Parser = impl Parser<&'a str, Output = Self>;
+
+ fn parser() -> Self::Parser {
+ let filename = || {
+ r#if(|c: &char| !c.is_ascii_whitespace())
+ .repeated()
+ .at_least(1)
+ .recognize()
+ .map(|output: &str| PathBuf::from(output))
+ };
+
+ let uri = UriPrefix::parser()
+ .opt()
+ .and(Uri::parser())
+ .and(filename().preceded_by(tag(" -> ")).opt())
+ .map(|((prefix, uri), filename)| SrcUri::Uri {
+ prefix,
+ uri,
+ filename,
+ });
+
+ uri.or(filename().map(|path: PathBuf| SrcUri::Filename(path)))
+ }
+}
+
+impl<'a> Parseable<'a, &'a str> for License {
+ 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| License(output.to_string()))
+ }
+}
+
+impl<'a> Parseable<'a, &'a str> for Eapi {
+ 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| Eapi(output.to_string()))
+ }
+}
+
+// TODO:
+// Cant find information about eclass names in pms so we allow anything except
+// for whitespace.
+impl<'a> Parseable<'a, &'a str> for Eclass {
+ type Parser = impl Parser<&'a str, Output = Self>;
+
+ fn parser() -> Self::Parser {
+ r#if(|c: &char| !c.is_ascii_whitespace())
+ .repeated()
+ .at_least(1)
+ .recognize()
+ .map(|output: &str| Eclass(output.to_string()))
+ }
+}
+
+impl<'a, T> Parseable<'a, &'a str> for Depend<T>
+where
+ T: Parseable<'a, &'a str>,
+{
+ type Parser = impl Parser<&'a str, Output = Self>;
+
+ fn parser() -> Self::Parser {
+ |it| {
+ let exprs = || {
+ Depend::parser()
+ .separated_by_with_trailing(ascii_whitespace1())
+ .at_least(1)
+ .delimited_by(tag("(").followed_by(ascii_whitespace1()), tag(")"))
+ };
+
+ let all_of_group = exprs().map(|exprs| Depend::AllOf(exprs));
+
+ let any_of_group = exprs()
+ .preceded_by(tag("||").followed_by(ascii_whitespace1()))
+ .map(|exprs| Depend::AnyOf(exprs));
+
+ let one_of_group = exprs()
+ .preceded_by(tag("^^").followed_by(ascii_whitespace1()))
+ .map(|exprs| Depend::OneOf(exprs));
+
+ let conditional_group = Conditional::parser()
+ .followed_by(ascii_whitespace1())
+ .and(exprs())
+ .map(|(conditional, exprs)| Depend::ConditionalGroup(conditional, exprs));
+
+ T::parser()
+ .map(|e| Depend::Element(e))
+ .or(conditional_group)
+ .or(any_of_group)
+ .or(all_of_group)
+ .or(one_of_group)
+ .parse(it)
+ }
+ }
+}
+
+impl<'a> Parseable<'a, &'a str> for Conditional {
+ type Parser = impl Parser<&'a str, Output = Self>;
+
+ fn parser() -> Self::Parser {
+ UseFlag::parser()
+ .preceded_by(tag("!"))
+ .followed_by(tag("?"))
+ .map(Conditional::Negative)
+ .or(UseFlag::parser()
+ .followed_by(tag("?"))
+ .map(Conditional::Positive))
+ }
+}
+
+#[cfg(test)]
+mod test {
+
+ use super::*;
+
+ use mon::{ParserIter, input::InputIter};
+
+ use atom::Atom;
+
+ use crate::ebuild::Depend;
+
+ #[test]
+ fn test_src_uri() {
+ let tests = [
+ "https://example.com/foo/bar.tar.gz",
+ "https://example.com/foo/bar.tar.gz -> bar.tar.gz",
+ ];
+
+ for test in tests {
+ SrcUri::parser()
+ .check_finished(InputIter::new(test))
+ .unwrap();
+ }
+ }
+
+ #[test]
+ fn test_expr() {
+ let it = InputIter::new("flag? ( || ( foo/bar foo/bar ) )");
+
+ Depend::<Atom>::parser()
+ .separated_by(ascii_whitespace1())
+ .many()
+ .check_finished(it)
+ .unwrap();
+ }
+}
diff --git a/crates/repo/src/lib.rs b/crates/repo/src/lib.rs
new file mode 100644
index 0000000..df4412d
--- /dev/null
+++ b/crates/repo/src/lib.rs
@@ -0,0 +1,335 @@
+#![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 std::{
+ fs, io,
+ os::unix::ffi::OsStrExt,
+ path::{Path, PathBuf},
+};
+
+use crate::ebuild::{Depend, Eapi, Ebuild, Eclass, License, SrcUri};
+
+use get::Get;
+
+use mon::{Parser, ParserIter, ascii_whitespace1, input::InputIter, tag};
+
+use parseable::Parseable;
+
+use atom::{self, Atom};
+
+use useflag::IUseFlag;
+
+pub mod ebuild;
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ #[error("io error: {0}")]
+ Io(PathBuf, io::Error),
+ #[error("error while reading directory: {0:?}: {1}")]
+ ReadDir(PathBuf, io::Error),
+ #[error("failed to decode path: {0}")]
+ Unicode(PathBuf),
+ #[error("parser error: {0}")]
+ Parser(String),
+}
+
+#[derive(Debug, Clone, Get)]
+pub struct Repo {
+ #[get(kind = "deref")]
+ path: PathBuf,
+}
+
+#[derive(Debug, Clone, Get)]
+pub struct Category {
+ name: atom::Category,
+ #[get(kind = "deref")]
+ path: PathBuf,
+}
+
+#[derive(Debug)]
+pub struct Categories(PathBuf, fs::ReadDir);
+
+#[derive(Debug)]
+pub struct Ebuilds(PathBuf, fs::ReadDir);
+
+impl Repo {
+ pub fn new<P: AsRef<Path>>(path: P) -> Self {
+ Self {
+ path: path.as_ref().to_path_buf(),
+ }
+ }
+
+ pub fn categories(&self) -> Result<Categories, Error> {
+ let path = self.path.as_path().join("metadata/md5-cache");
+
+ Ok(Categories(
+ path.clone(),
+ fs::read_dir(&path).map_err(|e| Error::Io(path, e))?,
+ ))
+ }
+}
+
+impl Category {
+ pub fn ebuilds(&self) -> Result<Ebuilds, Error> {
+ Ok(Ebuilds(
+ self.path.clone(),
+ fs::read_dir(&self.path).map_err(|e| Error::Io(self.path.clone(), e))?,
+ ))
+ }
+}
+
+impl Iterator for Categories {
+ type Item = Result<Category, Error>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ loop {
+ match self.1.next()? {
+ Ok(entry) if entry.path().file_name().unwrap().as_bytes() == b"Manifest.gz" => (),
+ Ok(entry) => match read_category(entry.path()) {
+ Ok(category) => break Some(Ok(category)),
+ Err(e) => break Some(Err(e)),
+ },
+ Err(e) => break Some(Err(Error::ReadDir(self.0.clone(), e))),
+ }
+ }
+ }
+}
+
+impl Iterator for Ebuilds {
+ type Item = Result<Ebuild, Error>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ loop {
+ match self.1.next()? {
+ Ok(entry) if entry.path().file_name().unwrap().as_bytes() == b"Manifest.gz" => (),
+ Ok(entry) => match read_ebuild(entry.path()) {
+ Ok(ebuild) => break Some(Ok(ebuild)),
+ Err(e) => break Some(Err(e)),
+ },
+ Err(e) => break Some(Err(Error::ReadDir(self.0.clone(), e))),
+ }
+ }
+ }
+}
+
+fn read_category(path: PathBuf) -> Result<Category, Error> {
+ let file_name = path
+ .as_path()
+ .file_name()
+ .unwrap()
+ .to_str()
+ .ok_or(Error::Unicode(path.clone()))?;
+
+ let name = atom::Category::parser()
+ .parse_finished(InputIter::new(file_name))
+ .map_err(|_| Error::Parser(file_name.to_string()))?;
+
+ Ok(Category { name, path })
+}
+
+fn read_ebuild(path: PathBuf) -> Result<Ebuild, Error> {
+ let file_name = path
+ .as_path()
+ .file_name()
+ .unwrap()
+ .to_str()
+ .ok_or(Error::Unicode(path.clone()))?;
+
+ let (name, version) = atom::Name::parser()
+ .and(atom::Version::parser().preceded_by(tag("-")))
+ .parse_finished(InputIter::new(file_name))
+ .map_err(|_| Error::Parser(file_name.to_string()))?;
+
+ let metadata = fs::read_to_string(path.as_path()).map_err(|e| Error::Io(path, e))?;
+
+ Ok(Ebuild {
+ name,
+ version,
+ slot: match read_slot(&metadata) {
+ Some(Ok(slot)) => Some(slot),
+ Some(Err(e)) => return Err(e),
+ None => None,
+ },
+ homepage: read_homepage(&metadata),
+ src_uri: match read_src_uri(&metadata) {
+ Some(Ok(src_uri)) => src_uri,
+ Some(Err(e)) => return Err(e),
+ None => Vec::new(),
+ },
+ eapi: match read_eapi(&metadata) {
+ Some(Ok(eapi)) => Some(eapi),
+ Some(Err(e)) => return Err(e),
+ None => None,
+ },
+ inherit: match read_inherit(&metadata) {
+ Some(Ok(inherit)) => inherit,
+ Some(Err(e)) => return Err(e),
+ None => Vec::new(),
+ },
+ iuse: match read_iuse(&metadata) {
+ Some(Ok(iuse)) => iuse,
+ Some(Err(e)) => return Err(e),
+ None => Vec::new(),
+ },
+ license: match read_license(&metadata) {
+ Some(Ok(license)) => license,
+ Some(Err(e)) => return Err(e),
+ None => Vec::new(),
+ },
+ description: read_description(&metadata),
+ depend: match read_depend(&metadata) {
+ Some(Ok(depend)) => depend,
+ Some(Err(e)) => return Err(e),
+ None => Vec::new(),
+ },
+ bdepend: match read_bdepend(&metadata) {
+ Some(Ok(depend)) => depend,
+ Some(Err(e)) => return Err(e),
+ None => Vec::new(),
+ },
+ rdepend: match read_rdepend(&metadata) {
+ Some(Ok(depend)) => depend,
+ Some(Err(e)) => return Err(e),
+ None => Vec::new(),
+ },
+ idepend: match read_idepend(&metadata) {
+ Some(Ok(depend)) => depend,
+ Some(Err(e)) => return Err(e),
+ None => Vec::new(),
+ },
+ })
+}
+
+fn read_slot(input: &str) -> Option<Result<atom::Slot, Error>> {
+ let line = input.lines().find_map(|line| line.strip_prefix("SLOT="))?;
+
+ match atom::Slot::parser().parse_finished(InputIter::new(line)) {
+ Ok(slot) => Some(Ok(slot)),
+ Err(_) => Some(Err(Error::Parser(line.to_string()))),
+ }
+}
+
+fn read_homepage(input: &str) -> Option<String> {
+ input
+ .lines()
+ .find_map(|line| line.strip_prefix("HOMEPAGE=").map(str::to_string))
+}
+
+fn read_src_uri(input: &str) -> Option<Result<Vec<Depend<SrcUri>>, Error>> {
+ let line = input
+ .lines()
+ .find_map(|line| line.strip_prefix("SRC_URI="))?;
+
+ match Depend::<SrcUri>::parser()
+ .separated_by(ascii_whitespace1())
+ .many()
+ .parse_finished(InputIter::new(line))
+ {
+ Ok(slot) => Some(Ok(slot)),
+ Err(_) => Some(Err(Error::Parser(line.to_string()))),
+ }
+}
+
+fn read_eapi(input: &str) -> Option<Result<Eapi, Error>> {
+ let line = input.lines().find_map(|line| line.strip_prefix("EAPI="))?;
+
+ match Eapi::parser().parse_finished(InputIter::new(line)) {
+ Ok(slot) => Some(Ok(slot)),
+ Err(_) => Some(Err(Error::Parser(line.to_string()))),
+ }
+}
+
+fn read_inherit(input: &str) -> Option<Result<Vec<Eclass>, Error>> {
+ let line = input
+ .lines()
+ .find_map(|line| line.strip_prefix("INHERIT="))?;
+
+ match Eclass::parser()
+ .separated_by(ascii_whitespace1())
+ .many()
+ .parse_finished(InputIter::new(line))
+ {
+ Ok(inherit) => Some(Ok(inherit)),
+ Err(_) => Some(Err(Error::Parser(line.to_string()))),
+ }
+}
+
+fn read_iuse(input: &str) -> Option<Result<Vec<IUseFlag>, Error>> {
+ let line = input.lines().find_map(|line| line.strip_prefix("IUSE="))?;
+
+ match IUseFlag::parser()
+ .separated_by(ascii_whitespace1())
+ .many()
+ .parse_finished(InputIter::new(line))
+ {
+ Ok(iuse) => Some(Ok(iuse)),
+ Err(_) => Some(Err(Error::Parser(line.to_string()))),
+ }
+}
+
+fn read_license(input: &str) -> Option<Result<Vec<Depend<License>>, Error>> {
+ let line = input
+ .lines()
+ .find_map(|line| line.strip_suffix("LICENSE="))?;
+
+ match Depend::<License>::parser()
+ .separated_by(ascii_whitespace1())
+ .many()
+ .parse_finished(InputIter::new(line))
+ {
+ Ok(license) => Some(Ok(license)),
+ Err(_) => Some(Err(Error::Parser(line.to_string()))),
+ }
+}
+
+fn read_description(input: &str) -> Option<String> {
+ input
+ .lines()
+ .find_map(|line| line.strip_prefix("DESCRIPTION=").map(str::to_string))
+}
+
+fn read_depend(input: &str) -> Option<Result<Vec<Depend<Atom>>, Error>> {
+ let line = input
+ .lines()
+ .find_map(|line| line.strip_prefix("DEPEND="))?;
+
+ Some(parse_depends(line))
+}
+
+fn read_bdepend(input: &str) -> Option<Result<Vec<Depend<Atom>>, Error>> {
+ let line = input
+ .lines()
+ .find_map(|line| line.strip_prefix("BDEPEND="))?;
+
+ Some(parse_depends(line))
+}
+
+fn read_rdepend(input: &str) -> Option<Result<Vec<Depend<Atom>>, Error>> {
+ let line = input
+ .lines()
+ .find_map(|line| line.strip_prefix("RDEPEND="))?;
+
+ Some(parse_depends(line))
+}
+
+fn read_idepend(input: &str) -> Option<Result<Vec<Depend<Atom>>, Error>> {
+ let line = input
+ .lines()
+ .find_map(|line| line.strip_prefix("IDEPEND="))?;
+
+ Some(parse_depends(line))
+}
+
+fn parse_depends(line: &str) -> Result<Vec<Depend<Atom>>, Error> {
+ Depend::<Atom>::parser()
+ .separated_by(ascii_whitespace1())
+ .many()
+ .parse_finished(InputIter::new(line))
+ .map_err(|_| Error::Parser(line.to_string()))
+}
diff --git a/crates/repo/src/meson.build b/crates/repo/src/meson.build
new file mode 100644
index 0000000..c1be7a7
--- /dev/null
+++ b/crates/repo/src/meson.build
@@ -0,0 +1,3 @@
+sources += files('mod.rs')
+
+subdir('ebuild')
diff --git a/crates/useflag/Cargo.toml b/crates/useflag/Cargo.toml
new file mode 100644
index 0000000..16c8c34
--- /dev/null
+++ b/crates/useflag/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "useflag"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+mon = { workspace = true }
+get = { workspace = true }
+parseable = { path = "../parseable" } \ No newline at end of file
diff --git a/crates/useflag/src/lib.rs b/crates/useflag/src/lib.rs
new file mode 100644
index 0000000..75c40eb
--- /dev/null
+++ b/crates/useflag/src/lib.rs
@@ -0,0 +1,29 @@
+#![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;
+
+use get::Get;
+
+mod parsers;
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Get)]
+pub struct UseFlag(#[get(method = "name", kind = "deref")] String);
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Get)]
+pub struct IUseFlag {
+ default: bool,
+ flag: UseFlag,
+}
+
+impl fmt::Display for UseFlag {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
diff --git a/crates/useflag/src/meson.build b/crates/useflag/src/meson.build
new file mode 100644
index 0000000..a7331a8
--- /dev/null
+++ b/crates/useflag/src/meson.build
@@ -0,0 +1 @@
+sources += files('mod.rs', 'parsers.rs')
diff --git a/crates/useflag/src/parsers.rs b/crates/useflag/src/parsers.rs
new file mode 100644
index 0000000..3007bde
--- /dev/null
+++ b/crates/useflag/src/parsers.rs
@@ -0,0 +1,39 @@
+use parseable::Parseable;
+
+use mon::{Parser, ParserIter, ascii_alphanumeric, one_of, tag};
+
+use crate::{IUseFlag, UseFlag};
+
+impl<'a> Parseable<'a, &'a str> for UseFlag {
+ type Parser = impl Parser<&'a str, Output = Self>;
+
+ fn parser() -> Self::Parser {
+ let start = ascii_alphanumeric();
+ let rest = ascii_alphanumeric()
+ .or(one_of("+_@-".chars()))
+ .repeated()
+ .many();
+
+ start
+ .and(rest)
+ .recognize()
+ .map(|output: &str| UseFlag(output.to_string()))
+ }
+}
+
+impl<'a> Parseable<'a, &'a str> for IUseFlag {
+ type Parser = impl Parser<&'a str, Output = Self>;
+
+ fn parser() -> Self::Parser {
+ UseFlag::parser()
+ .preceded_by(tag("+"))
+ .map(|flag| IUseFlag {
+ default: true,
+ flag,
+ })
+ .or(UseFlag::parser().map(|flag| IUseFlag {
+ default: false,
+ flag,
+ }))
+ }
+}