use crate::PkgName;
use crate::dewey::{Dewey, DeweyError, DeweyOp, DeweyVersion, dewey_cmp};
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
#[cfg(feature = "serde")]
use serde_with::{DeserializeFromStr, SerializeDisplay};
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
enum PatternType {
Alternate,
Dewey,
Glob,
#[default]
Simple,
}
#[derive(Debug, Error)]
pub enum PatternError {
#[error("Unbalanced braces in pattern")]
Alternate,
#[error(transparent)]
Dewey(#[from] DeweyError),
#[error(transparent)]
Glob(#[from] glob::PatternError),
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
#[allow(clippy::struct_field_names)]
#[cfg_attr(feature = "serde", derive(SerializeDisplay, DeserializeFromStr))]
pub struct Pattern {
matchtype: PatternType,
pattern: String,
likely: bool,
dewey: Option<Dewey>,
glob: Option<glob::Pattern>,
}
impl fmt::Display for Pattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.pattern)
}
}
impl FromStr for Pattern {
type Err = PatternError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl TryFrom<&str> for Pattern {
type Error = PatternError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl Pattern {
pub fn new(pattern: &str) -> Result<Self, PatternError> {
if pattern.contains('{') || pattern.contains('}') {
let matchtype = PatternType::Alternate;
let mut stack = vec![];
for ch in pattern.chars() {
if ch == '{' {
stack.push(ch);
} else if ch == '}' && stack.pop().is_none() {
return Err(PatternError::Alternate);
}
}
if !stack.is_empty() {
return Err(PatternError::Alternate);
}
return Ok(Self {
matchtype,
pattern: pattern.to_string(),
..Default::default()
});
}
if pattern.contains('>') || pattern.contains('<') {
let matchtype = PatternType::Dewey;
let dewey = Some(Dewey::new(pattern)?);
return Ok(Self {
matchtype,
pattern: pattern.to_string(),
dewey,
..Default::default()
});
}
if pattern.contains('*')
|| pattern.contains('?')
|| pattern.contains('[')
|| pattern.contains(']')
{
let matchtype = PatternType::Glob;
let glob = Some(glob::Pattern::new(pattern)?);
return Ok(Self {
matchtype,
pattern: pattern.to_string(),
glob,
..Default::default()
});
}
Ok(Self {
matchtype: PatternType::Simple,
pattern: pattern.to_string(),
..Default::default()
})
}
#[must_use]
pub fn matches(&self, pkg: &str) -> bool {
if !self.likely && !Self::quick_pkg_match(&self.pattern, pkg) {
return false;
}
match self.matchtype {
PatternType::Alternate => Self::alternate_match(&self.pattern, pkg),
PatternType::Dewey => {
let Some(dewey) = &self.dewey else {
return false;
};
dewey.matches(pkg)
}
PatternType::Glob => {
let Some(glob) = &self.glob else {
return false;
};
glob.matches(pkg)
}
PatternType::Simple => self.pattern == pkg,
}
}
pub fn best_match<'a>(
&self,
pkg1: &'a str,
pkg2: &'a str,
) -> Result<Option<&'a str>, PatternError> {
self.best_match_cmp(pkg1, pkg2, std::cmp::Ordering::Less)
}
pub fn best_match_pbulk<'a>(
&self,
pkg1: &'a str,
pkg2: &'a str,
) -> Result<Option<&'a str>, PatternError> {
self.best_match_cmp(pkg1, pkg2, std::cmp::Ordering::Greater)
}
fn best_match_cmp<'a>(
&self,
pkg1: &'a str,
pkg2: &'a str,
tiebreak: std::cmp::Ordering,
) -> Result<Option<&'a str>, PatternError> {
match (self.matches(pkg1), self.matches(pkg2)) {
(true, false) => Ok(Some(pkg1)),
(false, true) => Ok(Some(pkg2)),
(true, true) => {
let d1 = DeweyVersion::new(PkgName::new(pkg1).pkgversion())?;
let d2 = DeweyVersion::new(PkgName::new(pkg2).pkgversion())?;
if dewey_cmp(&d1, &DeweyOp::GT, &d2) {
Ok(Some(pkg1))
} else if dewey_cmp(&d1, &DeweyOp::LT, &d2) {
Ok(Some(pkg2))
} else if pkg1.cmp(pkg2) == tiebreak {
Ok(Some(pkg1))
} else {
Ok(Some(pkg2))
}
}
(false, false) => Ok(None),
}
}
#[must_use]
pub fn pattern(&self) -> &str {
&self.pattern
}
#[must_use]
pub fn pkgbase(&self) -> Option<&str> {
match self.matchtype {
PatternType::Dewey => self.dewey.as_ref().map(|d| d.pkgbase()),
PatternType::Simple => {
self.pattern.rsplit_once('-').map(|(b, _)| b)
}
PatternType::Glob => {
let end = self
.pattern
.find(['*', '?', '['])
.unwrap_or(self.pattern.len());
self.pattern[..end].strip_suffix('-')
}
PatternType::Alternate => None,
}
}
fn alternate_match(pattern: &str, pkg: &str) -> bool {
for (i, _) in
pattern.match_indices('{').collect::<Vec<_>>().iter().rev()
{
let (first, rest) = pattern.split_at(*i);
let Some(n) = rest.find('}') else {
return false;
};
let (matches, last) = rest.split_at(n + 1);
let matches = &matches[1..matches.len() - 1];
for m in matches.split(',') {
let fmt = format!("{first}{m}{last}");
if let Ok(pat) = Self::new(&fmt) {
if pat.matches(pkg) {
return true;
}
}
}
}
false
}
fn quick_pkg_match(pattern: &str, pkg: &str) -> bool {
let mut p1 = pattern.chars();
let mut p2 = pkg.chars();
let p = p1.next();
if !p.is_some_and(Self::is_simple_char) {
return true;
}
if p != p2.next() {
return false;
}
let p = p1.next();
if !p.is_some_and(Self::is_simple_char) {
return true;
}
if p != p2.next() {
return false;
}
true
}
const fn is_simple_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '-'
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! assert_pattern {
($pattern:expr, $pkg:expr, $variant:pat, $result:expr) => {
let p = Pattern::new($pattern)?;
assert!(matches!(&p.matchtype, $variant));
assert_eq!(p.matches($pkg), $result);
};
}
macro_rules! assert_pattern_eq {
($pattern:expr, $pkg:expr, $variant:pat) => {
assert_pattern!($pattern, $pkg, $variant, true);
};
}
macro_rules! assert_pattern_ne {
($pattern:expr, $pkg:expr, $variant:pat) => {
assert_pattern!($pattern, $pkg, $variant, false);
};
}
macro_rules! assert_pattern_err {
($pattern:expr, $variant:pat) => {
let p = Pattern::new($pattern);
assert!(matches!(p, Err($variant)));
};
}
#[test]
fn alternate_match_ok() -> Result<(), PatternError> {
use super::PatternType::Alternate;
assert_pattern_eq!("a-{b,c}-{d{e,f},g}-h>=1", "a-b-de-h-2", Alternate);
assert_pattern_eq!("a-{b,c}-{d{e,f},g}-h>=1", "a-b-de-h-2", Alternate);
assert_pattern_eq!("a-{b,c}-{d{e,f},g}-h>=1", "a-b-df-h-2", Alternate);
assert_pattern_eq!("a-{b,c}-{d{e,f},g}-h>=1", "a-b-g-h-2", Alternate);
assert_pattern_eq!("a-{b,c}-{d{e,f},g}-h>=1", "a-c-de-h-2", Alternate);
assert_pattern_eq!("a-{b,c}-{d{e,f},g}-h>=1", "a-c-df-h-2", Alternate);
assert_pattern_eq!("a-{b,c}-{d{e,f},g}-h>=1", "a-c-g-h-2", Alternate);
Ok(())
}
#[test]
fn alternate_match_notok() -> Result<(), PatternError> {
use super::PatternType::Alternate;
assert_pattern_ne!("a-{b,c}-{d{e,f},g}-h>=1", "a-a-g-h-2", Alternate);
assert_pattern_ne!("a-{b,c}-{d{e,f},g}-h>=1", "a-b-d-h-2", Alternate);
Ok(())
}
#[test]
fn alternate_match_err() {
use super::PatternError::Alternate;
assert_pattern_err!("foo}>=1", Alternate);
assert_pattern_err!("{foo,bar}}>=1", Alternate);
assert_pattern_err!("{{foo,bar}>=1", Alternate);
assert_pattern_err!("}foo,bar}>=1", Alternate);
}
#[test]
fn dewey_match_ok() -> Result<(), PatternError> {
use super::PatternType::Dewey;
assert_pattern_eq!("foo>1", "foo-1.1", Dewey);
assert_pattern_eq!("foo>1", "foo-1.0pl1", Dewey);
assert_pattern_eq!("foo<1", "foo-1.0alpha1", Dewey);
assert_pattern_eq!("foo>=1", "foo-1.0", Dewey);
assert_pattern_eq!("foo<2", "foo-1.0", Dewey);
assert_pattern_eq!("foo>=1", "foo-1.0", Dewey);
assert_pattern_eq!("foo>=1<2", "foo-1.0", Dewey);
assert_pattern_eq!("foo>1<2", "foo-1.0nb2", Dewey);
assert_pattern_eq!("foo>1.1.1<2", "foo-1.22b2", Dewey);
assert_pattern_eq!("librsvg>=2.12", "librsvg-2.13", Dewey);
assert_pattern_eq!("librsvg<2.39", "librsvg-2.13", Dewey);
assert_pattern_eq!("librsvg<2.40", "librsvg-2.13", Dewey);
assert_pattern_eq!("librsvg<2.43", "librsvg-2.13", Dewey);
assert_pattern_eq!("librsvg<2.41", "librsvg-2.13", Dewey);
assert_pattern_eq!("librsvg>=2.12<2.41", "librsvg-2.13", Dewey);
assert_pattern_eq!("pkg>=0", "pkg-", Dewey);
assert_pattern_eq!("foo>1.1", "foo-1.1blah2", Dewey);
assert_pattern_eq!("foo>1.1a2", "foo-1.1blah2", Dewey);
Ok(())
}
#[test]
fn dewey_match_notok() -> Result<(), PatternError> {
use super::PatternType::Dewey;
assert_pattern_ne!("foo>1alpha<2beta", "foo-2.5", Dewey);
assert_pattern_ne!("foo>1", "foo-0.5", Dewey);
assert_pattern_ne!("foo>1", "foo-1.0", Dewey);
assert_pattern_ne!("foo>1", "foo-1.0alpha1", Dewey);
assert_pattern_ne!("foo>1nb3", "foo-1.0nb2", Dewey);
assert_pattern_ne!("foo>1<2", "foo-0.5", Dewey);
assert_pattern_ne!("bar>=1", "foo-1.0", Dewey);
assert_pattern_ne!("foo>=1", "foo", Dewey);
assert_pattern_ne!("foo>1.1c2", "foo-1.1blah2", Dewey);
Ok(())
}
#[test]
fn dewey_match_err() -> std::result::Result<(), &'static str> {
use super::PatternError::Dewey;
assert_pattern_err!("foo>1<2<3", Dewey(_));
assert_pattern_err!("foo<2>3", Dewey(_));
let Err(Dewey(e)) = Pattern::new("<>") else {
return Err("expected Dewey error");
};
assert_eq!(e.pos, 0);
let Err(Dewey(e)) = Pattern::new("foo>=1>2") else {
return Err("expected Dewey error");
};
assert_eq!(e.pos, 3);
let Err(Dewey(e)) = Pattern::new("pkg>=1<2<4") else {
return Err("expected Dewey error");
};
assert_eq!(e.pos, 8);
let Err(Dewey(e)) = Pattern::new("pkg>=20251208143052123456") else {
return Err("expected Dewey error");
};
assert_eq!(e.msg, "Version component overflow");
Ok(())
}
#[test]
fn glob_match_ok() -> Result<(), PatternError> {
use super::PatternType::Glob;
assert_pattern_eq!("foo-[0-9]*", "foo-1.0", Glob);
assert_pattern_eq!("fo?-[0-9]*", "foo-1.0", Glob);
assert_pattern_eq!("fo*-[0-9]*", "foo-1.0", Glob);
assert_pattern_eq!("?oo-[0-9]*", "foo-1.0", Glob);
assert_pattern_eq!("*oo-[0-9]*", "foo-1.0", Glob);
assert_pattern_eq!("foo-[0-9]", "foo-1", Glob);
Ok(())
}
#[test]
fn glob_match_notok() -> Result<(), PatternError> {
use super::PatternType::Glob;
assert_pattern_ne!("boo-[0-9]*", "foo-1.0", Glob);
assert_pattern_ne!("bo?-[0-9]*", "foo-1.0", Glob);
assert_pattern_ne!("bo*-[0-9]*", "foo-1.0", Glob);
assert_pattern_ne!("foo-[2-9]*", "foo-1.0", Glob);
assert_pattern_ne!("fo-[0-9]*", "foo-1.0", Glob);
assert_pattern_ne!("bar-[0-9]*", "foo-1.0", Glob);
Ok(())
}
#[test]
fn glob_match_err() {
use super::PatternError::Glob;
assert_pattern_err!("foo-[0-9", Glob(_));
assert_pattern_err!("foo-[0-9]***", Glob(_));
}
#[test]
fn simple_match() -> Result<(), PatternError> {
use super::PatternType::Simple;
assert_pattern_eq!("foo-1.0", "foo-1.0", Simple);
assert_pattern_ne!("foo-1.1", "foo-1.0", Simple);
assert_pattern_ne!("bar-1.0", "foo-1.0", Simple);
Ok(())
}
#[test]
fn best_match_dewey() -> Result<(), PatternError> {
let m = Pattern::new("pkg>1<3")?;
assert_eq!(m.best_match("pkg-1.1", "pkg-3.0")?, Some("pkg-1.1"));
assert_eq!(m.best_match("pkg-1.1", "pkg-1.1")?, Some("pkg-1.1"));
assert_eq!(m.best_match("pkg-1.1", "pkg-2.0")?, Some("pkg-2.0"));
assert_eq!(m.best_match("pkg-2.0", "pkg-1.1")?, Some("pkg-2.0"));
assert_eq!(m.best_match("pkg", "pkg-2.0")?, Some("pkg-2.0"));
assert_eq!(m.best_match("pkg-2.0", "pkg")?, Some("pkg-2.0"));
assert_eq!(m.best_match("pkg-1", "pkg-3.0")?, None);
assert_eq!(m.best_match("pkg", "pkg")?, None);
Ok(())
}
#[test]
fn best_match_alternate() -> Result<(), PatternError> {
let m = Pattern::new("{foo,bar}-[0-9]*")?;
assert_eq!(m.best_match("foo-1.1", "bar-1.0")?, Some("foo-1.1"));
assert_eq!(m.best_match("foo-1.0", "bar-1.1")?, Some("bar-1.1"));
assert_eq!(m.best_match("foo-1.0", "bar-1.0")?, Some("bar-1.0"));
Ok(())
}
#[test]
fn best_match_order() -> Result<(), PatternError> {
let m = Pattern::new("mpg123{,-esound,-nas}>=0.59.18")?;
let pkg1 = "mpg123-1";
let pkg2 = "mpg123-esound-1";
let pkg3 = "mpg123-nas-1";
assert_eq!(m.best_match(pkg1, pkg2)?, Some(pkg1));
assert_eq!(m.best_match(pkg2, pkg1)?, Some(pkg1));
assert_eq!(m.best_match(pkg2, pkg3)?, Some(pkg2));
assert_eq!(m.best_match(pkg3, pkg2)?, Some(pkg2));
assert_eq!(m.best_match(pkg1, pkg3)?, Some(pkg1));
assert_eq!(m.best_match(pkg3, pkg1)?, Some(pkg1));
assert_eq!(m.best_match_pbulk(pkg1, pkg2)?, Some(pkg2));
assert_eq!(m.best_match_pbulk(pkg2, pkg1)?, Some(pkg2));
assert_eq!(m.best_match_pbulk(pkg2, pkg3)?, Some(pkg3));
assert_eq!(m.best_match_pbulk(pkg3, pkg2)?, Some(pkg3));
assert_eq!(m.best_match_pbulk(pkg1, pkg3)?, Some(pkg3));
assert_eq!(m.best_match_pbulk(pkg3, pkg1)?, Some(pkg3));
Ok(())
}
#[test]
fn best_match_overflow() -> Result<(), PatternError> {
let m = Pattern::new("pkg-[0-9]*")?;
let overflow_ver = "pkg-20251208143052123456";
assert!(m.matches("pkg-1.0"));
assert!(m.matches(overflow_ver));
assert!(matches!(
m.best_match("pkg-1.0", overflow_ver),
Err(PatternError::Dewey(_))
));
Ok(())
}
#[test]
fn display() -> Result<(), PatternError> {
let p = Pattern::new("foo-[0-9]*")?;
assert_eq!(p.to_string(), "foo-[0-9]*");
let p = Pattern::new("pkg>=1.0<2.0")?;
assert_eq!(format!("{p}"), "pkg>=1.0<2.0");
Ok(())
}
#[test]
fn from_str() -> Result<(), PatternError> {
use std::str::FromStr;
let p = Pattern::from_str("foo-[0-9]*")?;
assert!(p.matches("foo-1.0"));
let p: Pattern = "pkg>=1.0".parse()?;
assert!(p.matches("pkg-1.5"));
assert!(Pattern::from_str("{unbalanced").is_err());
Ok(())
}
#[test]
fn pattern_accessor() -> Result<(), PatternError> {
let p = Pattern::new("foo-[0-9]*")?;
assert_eq!(p.pattern(), "foo-[0-9]*");
let p = Pattern::new("{mysql,mariadb}-[0-9]*")?;
assert_eq!(p.pattern(), "{mysql,mariadb}-[0-9]*");
Ok(())
}
#[test]
fn quick_pkg_match_edge_cases() -> Result<(), PatternError> {
let p = Pattern::new("*-1.0")?;
assert!(p.matches("foo-1.0"));
let p = Pattern::new("?oo-[0-9]*")?;
assert!(p.matches("foo-1.0"));
let p = Pattern::new("f*")?;
assert!(p.matches("foo"));
let p = Pattern::new("bar-[0-9]*")?;
assert!(!p.matches("foo-1.0"));
let p = Pattern::new("fa-[0-9]*")?;
assert!(!p.matches("fo-1.0"));
let p = Pattern::new("fo-[0-9]*")?;
assert!(!p.matches("foo-1.0"));
Ok(())
}
#[test]
fn pattern_pkgbase() -> Result<(), PatternError> {
let p = Pattern::new("foo-[0-9]*")?;
assert_eq!(p.pkgbase(), Some("foo"));
let p = Pattern::new("mpg123-nas-[0-9]*")?;
assert_eq!(p.pkgbase(), Some("mpg123-nas"));
let p = Pattern::new("foo-1.[0-9]*")?;
assert_eq!(p.pkgbase(), None);
let p = Pattern::new("foo-bar*-1")?;
assert_eq!(p.pkgbase(), None);
let p = Pattern::new("*-1.0")?;
assert_eq!(p.pkgbase(), None);
let p = Pattern::new("fo?-[0-9]*")?;
assert_eq!(p.pkgbase(), None);
let p = Pattern::new("{foo,bar}-[0-9]*")?;
assert_eq!(p.pkgbase(), None);
let p = Pattern::new("foo>=1.0")?;
assert_eq!(p.pkgbase(), Some("foo"));
let p = Pattern::new("pkg-name>=2.0<3.0")?;
assert_eq!(p.pkgbase(), Some("pkg-name"));
let p = Pattern::new("foo-1.0")?;
assert_eq!(p.pkgbase(), Some("foo"));
let p = Pattern::new("pkg-name-2.0nb1")?;
assert_eq!(p.pkgbase(), Some("pkg-name"));
Ok(())
}
}