use std::cmp::Ordering;
use thiserror::Error;
#[derive(Debug, Error)]
#[error("Pattern syntax error near position {pos}: {msg}")]
pub struct DeweyError {
pub pos: usize,
pub msg: &'static str,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub(crate) enum DeweyOp {
LE,
LT,
GE,
GT,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub(crate) struct DeweyVersion {
version: Vec<i64>,
pkgrevision: i64,
}
impl DeweyVersion {
pub fn new(s: &str) -> Result<Self, DeweyError> {
let mut version: Vec<i64> = Vec::with_capacity(8);
let mut pkgrevision = 0;
let mut idx = 0;
loop {
if idx == s.len() {
break;
}
let slice = &s[idx..];
let Some(c) = slice.chars().next() else {
break;
};
let digit_end =
slice.bytes().take_while(u8::is_ascii_digit).count();
if digit_end > 0 {
let num = slice[..digit_end].parse::<i64>().map_err(|_| {
DeweyError {
pos: idx,
msg: "Version component overflow",
}
})?;
version.push(num);
idx += digit_end;
continue;
}
if c == '.' || c == '_' {
version.push(0);
idx += 1;
continue;
}
if slice.starts_with("nb") {
idx += 2;
let slice = &s[idx..];
let digit_end =
slice.bytes().take_while(u8::is_ascii_digit).count();
pkgrevision = slice[..digit_end].parse::<i64>().unwrap_or(0);
idx += digit_end;
continue;
}
if slice.starts_with("alpha") {
version.push(-3);
idx += 5;
continue;
} else if slice.starts_with("beta") {
version.push(-2);
idx += 4;
continue;
} else if slice.starts_with("pre") {
version.push(-1);
idx += 3;
continue;
} else if slice.starts_with("rc") {
version.push(-1);
idx += 2;
continue;
} else if slice.starts_with("pl") {
version.push(0);
idx += 2;
continue;
}
if c.is_ascii_alphabetic() {
version.push(0);
version.push(c as i64);
idx += 1;
} else {
idx += c.len_utf8();
}
}
Ok(Self {
version,
pkgrevision,
})
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
struct DeweyMatch {
op: DeweyOp,
version: DeweyVersion,
}
impl DeweyMatch {
fn new(op: &DeweyOp, pattern: &str) -> Result<Self, DeweyError> {
let version = DeweyVersion::new(pattern)?;
Ok(Self { op: *op, version })
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Dewey {
pkgbase: String,
matches: Vec<DeweyMatch>,
}
impl Dewey {
pub fn new(pattern: &str) -> Result<Self, DeweyError> {
let mut deweyops: Vec<(usize, usize, DeweyOp)> = vec![];
for (index, matched) in pattern.match_indices(&['>', '<']) {
match (matched, pattern.get(index + 1..index + 2)) {
(">", Some("=")) => {
deweyops.push((index, index + 2, DeweyOp::GE));
}
("<", Some("=")) => {
deweyops.push((index, index + 2, DeweyOp::LE));
}
(">", _) => deweyops.push((index, index + 1, DeweyOp::GT)),
("<", _) => deweyops.push((index, index + 1, DeweyOp::LT)),
_ => unreachable!(),
}
}
let mut matches: Vec<DeweyMatch> = vec![];
match deweyops.len() {
0 => {
return Err(DeweyError {
pos: 0,
msg: "No dewey operators found",
});
}
1 => {
let p = &pattern[deweyops[0].1..];
matches.push(DeweyMatch::new(&deweyops[0].2, p)?);
}
2 => {
match (&deweyops[0].2, &deweyops[1].2) {
(DeweyOp::GT | DeweyOp::GE, DeweyOp::LT | DeweyOp::LE) => {}
_ => {
return Err(DeweyError {
pos: deweyops[0].0,
msg: "Unsupported operator order",
});
}
}
let p = &pattern[deweyops[0].1..deweyops[1].0];
matches.push(DeweyMatch::new(&deweyops[0].2, p)?);
let p = &pattern[deweyops[1].1..];
matches.push(DeweyMatch::new(&deweyops[1].2, p)?);
}
_ => {
return Err(DeweyError {
pos: deweyops[2].0,
msg: "Too many dewey operators found",
});
}
}
let pkgbase = pattern[0..deweyops[0].0].to_string();
Ok(Self { pkgbase, matches })
}
#[must_use]
pub fn matches(&self, pkg: &str) -> bool {
let Some((base, version)) = pkg.rsplit_once('-') else {
return false;
};
if base != self.pkgbase {
return false;
}
let Ok(pkgver) = DeweyVersion::new(version) else {
return false;
};
for m in &self.matches {
if !dewey_cmp(&pkgver, &m.op, &m.version) {
return false;
}
}
true
}
#[must_use]
pub fn pkgbase(&self) -> &str {
&self.pkgbase
}
}
const fn dewey_test(lhs: i64, op: &DeweyOp, rhs: i64) -> bool {
match op {
DeweyOp::GE => lhs >= rhs,
DeweyOp::GT => lhs > rhs,
DeweyOp::LE => lhs <= rhs,
DeweyOp::LT => lhs < rhs,
}
}
pub(crate) fn dewey_cmp(
lhs: &DeweyVersion,
op: &DeweyOp,
rhs: &DeweyVersion,
) -> bool {
let llen = lhs.version.len();
let rlen = rhs.version.len();
for i in 0..std::cmp::min(llen, rlen) {
if lhs.version[i] != rhs.version[i] {
return dewey_test(lhs.version[i], op, rhs.version[i]);
}
}
match llen.cmp(&rlen) {
Ordering::Less => {
for i in llen..rlen {
if rhs.version[i] != 0 {
return dewey_test(0, op, rhs.version[i]);
}
}
}
Ordering::Greater => {
for i in rlen..llen {
if lhs.version[i] != 0 {
return dewey_test(lhs.version[i], op, 0);
}
}
return dewey_test(lhs.pkgrevision, op, rhs.pkgrevision);
}
Ordering::Equal => {}
}
dewey_test(lhs.pkgrevision, op, rhs.pkgrevision)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dewey_version_empty() -> Result<(), DeweyError> {
let dv = DeweyVersion::new("")?;
assert_eq!(dv.version, Vec::<i64>::new());
assert_eq!(dv.pkgrevision, 0);
Ok(())
}
#[test]
fn dewey_no_operators() {
let err = Dewey::new("pkg");
assert!(err.is_err());
let err = err.unwrap_err();
assert_eq!(err.pos, 0);
assert_eq!(err.msg, "No dewey operators found");
}
#[test]
fn dewey_version_utf8() -> Result<(), DeweyError> {
let dv = DeweyVersion::new("é")?;
assert_eq!(dv.version, Vec::<i64>::new());
assert_eq!(dv.pkgrevision, 0);
Ok(())
}
#[test]
fn dewey_version_modifiers() -> Result<(), DeweyError> {
let dv = DeweyVersion::new("1.0alpha1beta2rc3pl4_5nb17")?;
assert_eq!(dv.version, vec![1, 0, 0, -3, 1, -2, 2, -1, 3, 0, 4, 0, 5]);
assert_eq!(dv.pkgrevision, 17);
let dv = DeweyVersion::new("ojnknb30_-")?;
assert_eq!(dv.version, vec![0, 111, 0, 106, 0, 110, 0, 107, 0]);
assert_eq!(dv.pkgrevision, 30);
let m = Dewey::new("spandsp>=0.0.6pre18")?;
assert!(m.matches("spandsp-0.0.6nb5"));
assert!(m.matches("spandsp-0.0.6pre19"));
assert!(m.matches("spandsp-0.0.6rc18"));
assert!(!m.matches("spandsp-0.0.6rc17"));
Ok(())
}
#[test]
fn dewey_version_empty_pkgrevision() -> Result<(), DeweyError> {
let dv = DeweyVersion::new("100nb")?;
assert_eq!(dv.version, vec![100]);
assert_eq!(dv.pkgrevision, 0);
Ok(())
}
#[test]
fn dewey_match_no_version() -> Result<(), DeweyError> {
let m = Dewey::new("pkg>")?;
assert!(!m.matches("pkg"));
assert!(!m.matches("pkg-"));
assert!(!m.matches("pkg-0"));
assert!(m.matches("pkg-0nb1"));
let m = Dewey::new("pkg>=")?;
assert!(!m.matches("pkg"));
assert!(m.matches("pkg-"));
Ok(())
}
#[test]
fn dewey_match_range() -> Result<(), DeweyError> {
let m = Dewey::new("pkg>1.0alpha3nb2<2.0beta4nb7")?;
assert!(m.matches("pkg-1.1"));
assert!(!m.matches("pkg-1.0alpha3nb2"));
assert!(m.matches("pkg-1.0alpha3nb3"));
assert!(m.matches("pkg-2.0alpha3nb3"));
assert!(m.matches("pkg-2.0beta3nb8"));
assert!(!m.matches("pkg-2.0beta5nb6"));
assert!(!m.matches("pkg-2.0beta4nb7"));
assert!(!m.matches("pkg-2.0"));
assert!(!m.matches("pkg-2.0nb1"));
assert!(!m.matches("pkg-2.0nb8"));
Ok(())
}
#[test]
fn dewey_match_length() -> Result<(), DeweyError> {
let m = Dewey::new("pkg>1.0.0.0alphanb1")?;
assert!(m.matches("pkg-1"));
assert!(m.matches("pkg-1.0"));
assert!(m.matches("pkg-1.0.0"));
assert!(m.matches("pkg-1.0.0."));
assert!(m.matches("pkg-1.0.0.0"));
assert!(m.matches("pkg-1.0.0.0alpha1"));
assert!(m.matches("pkg-1.0.0.0alpha1nb0"));
assert!(m.matches("pkg-1.0.0.0alphanb2"));
assert!(m.matches("pkg-1.0.0.0."));
assert!(m.matches("pkg-1.0.0.0_"));
assert!(m.matches("pkg-1.0.0.0beta"));
assert!(m.matches("pkg-1.0.0.0rc"));
assert!(m.matches("pkg-1.0.0.0nb1"));
assert!(!m.matches("pkg-1.0.0.0alphanb1"));
assert!(!m.matches("pkg-1.0.0.0alpha"));
assert!(!m.matches("pkg-1.0.0.beta"));
assert!(!m.matches("pkg-1.0.0alpha"));
assert!(m.matches("pkg-1.0.1"));
assert!(!m.matches("pkg-1.0alpha"));
Ok(())
}
#[test]
fn dewey_pattern_overflow() {
let err = Dewey::new("pkg>=0.20251208143052000000");
assert!(err.is_err());
let err = err.unwrap_err();
assert_eq!(err.msg, "Version component overflow");
}
#[test]
fn dewey_version_overflow() {
let err = DeweyVersion::new("20251208143052000000");
assert!(err.is_err());
let err = err.unwrap_err();
assert_eq!(err.pos, 0);
assert_eq!(err.msg, "Version component overflow");
}
#[test]
fn dewey_version_overflow_position() {
let err = DeweyVersion::new("1.20251208143052000000");
assert!(err.is_err());
let err = err.unwrap_err();
assert_eq!(err.pos, 2);
assert_eq!(err.msg, "Version component overflow");
}
#[test]
fn dewey_matches_version_overflow() -> Result<(), DeweyError> {
let m = Dewey::new("pkg>=1.0")?;
assert!(!m.matches("pkg-20251208143052000000"));
Ok(())
}
#[test]
fn dewey_matches_no_hyphen() -> Result<(), DeweyError> {
let m = Dewey::new("pkg>=1.0")?;
assert!(!m.matches("pkg1.0"));
Ok(())
}
#[test]
fn dewey_pkgbase() -> Result<(), DeweyError> {
let m = Dewey::new("my-package>=1.0")?;
assert_eq!(m.pkgbase(), "my-package");
assert!(!m.matches("other-package-1.0"));
Ok(())
}
#[test]
fn dewey_lt_operator() -> Result<(), DeweyError> {
let m = Dewey::new("pkg<2.0")?;
assert!(m.matches("pkg-1.0"));
assert!(m.matches("pkg-1.9"));
assert!(!m.matches("pkg-2.0"));
assert!(!m.matches("pkg-3.0"));
Ok(())
}
#[test]
fn dewey_le_operator() -> Result<(), DeweyError> {
let m = Dewey::new("pkg<=2.0")?;
assert!(m.matches("pkg-1.0"));
assert!(m.matches("pkg-2.0"));
assert!(!m.matches("pkg-2.1"));
assert!(!m.matches("pkg-3.0"));
Ok(())
}
}