use crate::dewey::{Dewey, DeweyError, DeweyOp, DeweyVersion, dewey_cmp};
use crate::pkgname::{pkgversion, pkgversion_norev};
use hashbrown::HashMap;
use hashbrown::hash_map::EntryRef;
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
const GLOB_START: [u8; 3] = [b'*', b'?', b'['];
#[cfg(feature = "serde")]
use serde_with::{DeserializeFromStr, SerializeDisplay};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum PatternType {
Alternate(Vec<Pattern>),
Dewey(Dewey),
Glob(glob::Pattern),
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, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(SerializeDisplay, DeserializeFromStr))]
pub struct Pattern {
matchtype: PatternType,
pattern: String,
likely: bool,
}
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> {
let mut brace_pos: Option<usize> = None;
let mut brace_is_close = false;
let mut has_dewey = false;
let mut has_glob = false;
for (i, b) in pattern.bytes().enumerate() {
match b {
b'{' if brace_pos.is_none() => brace_pos = Some(i),
b'}' if brace_pos.is_none() => {
brace_pos = Some(i);
brace_is_close = true;
}
b'>' | b'<' => has_dewey = true,
b'*' | b'?' | b'[' => has_glob = true,
_ => {}
}
}
if brace_pos.is_some() {
if brace_is_close {
return Err(PatternError::Alternate);
}
let mut depth = 0usize;
for ch in pattern.chars() {
if ch == '{' {
depth += 1;
} else if ch == '}' {
if depth == 0 {
return Err(PatternError::Alternate);
}
depth -= 1;
}
}
if depth != 0 {
return Err(PatternError::Alternate);
}
let Some(i) = pattern.rfind('{') else {
return Err(PatternError::Alternate);
};
let (first, rest) = pattern.split_at(i);
let Some(n) = rest.find('}') else {
return Err(PatternError::Alternate);
};
let (group, last) = rest.split_at(n + 1);
let alts = &group[1..group.len() - 1];
let mut expanded = Vec::new();
for m in alts.split(',') {
let s = format!("{first}{m}{last}");
expanded.push(Pattern::new(&s)?);
}
return Ok(Self {
matchtype: PatternType::Alternate(expanded),
pattern: pattern.to_string(),
likely: false,
});
}
if has_dewey {
return Ok(Self {
matchtype: PatternType::Dewey(Dewey::new(pattern)?),
pattern: pattern.to_string(),
likely: false,
});
}
if has_glob {
return Ok(Self {
matchtype: PatternType::Glob(glob::Pattern::new(pattern)?),
pattern: pattern.to_string(),
likely: false,
});
}
Ok(Self {
matchtype: PatternType::Simple,
pattern: pattern.to_string(),
likely: false,
})
}
#[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(ref patterns) => {
patterns.iter().any(|p| p.matches(pkg))
}
PatternType::Dewey(ref dewey) => dewey.matches(pkg),
PatternType::Glob(ref glob) => glob.matches(pkg),
PatternType::Simple => self.pattern == pkg,
}
}
pub fn best_match<'a>(
&self,
current: Option<&'a str>,
candidate: &'a str,
) -> Result<Option<&'a str>, PatternError> {
self.best_match_cmp(current, candidate, std::cmp::Ordering::Less)
}
pub fn best_match_pbulk<'a>(
&self,
current: Option<&'a str>,
candidate: &'a str,
) -> Result<Option<&'a str>, PatternError> {
self.best_match_cmp(current, candidate, std::cmp::Ordering::Greater)
}
fn best_match_cmp<'a>(
&self,
current: Option<&'a str>,
candidate: &'a str,
tiebreak: std::cmp::Ordering,
) -> Result<Option<&'a str>, PatternError> {
if !self.matches(candidate) {
return Ok(current);
}
let Some(current) = current else {
return Ok(Some(candidate));
};
let d1 = DeweyVersion::new(pkgversion(current))?;
let d2 = DeweyVersion::new(pkgversion(candidate))?;
if dewey_cmp(&d1, &DeweyOp::GT, &d2) {
Ok(Some(current))
} else if dewey_cmp(&d1, &DeweyOp::LT, &d2) {
Ok(Some(candidate))
} else if current.cmp(candidate) == tiebreak {
Ok(Some(current))
} else {
Ok(Some(candidate))
}
}
#[must_use]
pub fn pattern(&self) -> &str {
&self.pattern
}
#[must_use]
pub fn pkgbases(&self) -> Option<Vec<&str>> {
match self.matchtype {
PatternType::Dewey(ref dewey) => Some(vec![dewey.pkgbase()]),
PatternType::Simple => {
self.pattern.rsplit_once('-').map(|(b, _)| vec![b])
}
PatternType::Glob(_) => {
let end = self
.pattern
.bytes()
.position(|b| GLOB_START.contains(&b))
.unwrap_or(self.pattern.len());
let prefix = &self.pattern[..end];
let base = prefix.strip_suffix('-').or_else(|| {
let (base, ver) = prefix.rsplit_once('-')?;
let norev = pkgversion_norev(ver);
(norev.starts_with(|c: char| c.is_ascii_digit())
&& norev
.bytes()
.all(|b| b.is_ascii_digit() || b == b'.'))
.then_some(base)
})?;
Some(vec![base])
}
PatternType::Alternate(ref patterns) => {
let mut bases = Vec::with_capacity(patterns.len());
for pattern in patterns {
for base in pattern.pkgbases()? {
if !bases.contains(&base) {
bases.push(base);
}
}
}
Some(bases)
}
}
}
fn quick_pkg_match(pattern: &str, pkg: &str) -> bool {
let pb = pattern.as_bytes();
let kb = pkg.as_bytes();
for i in 0..2 {
let Some(&p) = pb.get(i) else {
return true;
};
if !Self::is_simple_byte(p) {
return true;
}
if kb.get(i) != Some(&p) {
return false;
}
}
true
}
const fn is_simple_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'-'
}
}
#[derive(Debug)]
pub struct PatternCache {
cache: HashMap<String, Pattern>,
}
impl PatternCache {
#[must_use]
pub fn new() -> Self {
PatternCache {
cache: HashMap::new(),
}
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
PatternCache {
cache: HashMap::with_capacity(capacity),
}
}
pub fn compile(&mut self, pattern: &str) -> Result<&Pattern, PatternError> {
match self.cache.entry_ref(pattern) {
EntryRef::Occupied(e) => Ok(e.into_mut()),
EntryRef::Vacant(e) => {
let p = Pattern::new(pattern)?;
Ok(e.insert(p))
}
}
}
#[must_use]
pub fn len(&self) -> usize {
self.cache.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
}
impl Default for PatternCache {
fn default() -> Self {
Self::new()
}
}
#[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(_)
);
assert_pattern_eq!("foo*{a,b}-[0-9]*", "fooxa-1", 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(_)
);
assert_pattern_ne!("abc{d,e}-[0-9]*", "abz-1.0", 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;
fn dewey_err(
r: Result<Pattern, PatternError>,
) -> std::result::Result<DeweyError, &'static str> {
match r {
Err(Dewey(e)) => Ok(e),
_ => Err("expected Dewey error"),
}
}
assert_pattern_err!("foo>1<2<3", Dewey(_));
assert_pattern_err!("foo<2>3", Dewey(_));
let e = dewey_err(Pattern::new("<>"))?;
assert_eq!(e.pos, 0);
let e = dewey_err(Pattern::new("foo>=1>2"))?;
assert_eq!(e.pos, 3);
let e = dewey_err(Pattern::new("pkg>=1<2<4"))?;
assert_eq!(e.pos, 8);
let e = dewey_err(Pattern::new("pkg>=20251208143052123456"))?;
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(None, "pkg-0.5")?, None);
assert_eq!(m.best_match(None, "pkg-3.0")?, None);
assert_eq!(m.best_match(None, "pkg-1.1")?, Some("pkg-1.1"));
assert_eq!(m.best_match(Some("pkg-1.1"), "pkg-2.0")?, Some("pkg-2.0"));
assert_eq!(m.best_match(Some("pkg-2.0"), "pkg-1.1")?, Some("pkg-2.0"));
assert_eq!(m.best_match(Some("pkg-2.0"), "pkg-3.0")?, Some("pkg-2.0"));
Ok(())
}
#[test]
fn best_match_alternate() -> Result<(), PatternError> {
let m = Pattern::new("{foo,bar}-[0-9]*")?;
assert_eq!(m.best_match(Some("bar-1.0"), "foo-1.1")?, Some("foo-1.1"));
assert_eq!(m.best_match(Some("foo-1.0"), "bar-1.1")?, Some("bar-1.1"));
assert_eq!(m.best_match(Some("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(Some(pkg1), pkg2)?, Some(pkg1));
assert_eq!(m.best_match(Some(pkg2), pkg1)?, Some(pkg1));
assert_eq!(m.best_match(Some(pkg2), pkg3)?, Some(pkg2));
assert_eq!(m.best_match(Some(pkg3), pkg2)?, Some(pkg2));
assert_eq!(m.best_match(Some(pkg1), pkg3)?, Some(pkg1));
assert_eq!(m.best_match(Some(pkg3), pkg1)?, Some(pkg1));
assert_eq!(m.best_match_pbulk(Some(pkg1), pkg2)?, Some(pkg2));
assert_eq!(m.best_match_pbulk(Some(pkg2), pkg1)?, Some(pkg2));
assert_eq!(m.best_match_pbulk(Some(pkg2), pkg3)?, Some(pkg3));
assert_eq!(m.best_match_pbulk(Some(pkg3), pkg2)?, Some(pkg3));
assert_eq!(m.best_match_pbulk(Some(pkg1), pkg3)?, Some(pkg3));
assert_eq!(m.best_match_pbulk(Some(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(Some("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());
let p: Pattern = "foo-[0-9]*".try_into()?;
assert!(p.matches("foo-1.0"));
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_pkgbases() -> Result<(), PatternError> {
let p = Pattern::new("foo-[0-9]*")?;
assert_eq!(p.pkgbases(), Some(vec!["foo"]));
let p = Pattern::new("mpg123-nas-[0-9]*")?;
assert_eq!(p.pkgbases(), Some(vec!["mpg123-nas"]));
let p = Pattern::new("foo-1.[0-9]*")?;
assert_eq!(p.pkgbases(), Some(vec!["foo"]));
let p = Pattern::new("foo-bar*-1")?;
assert_eq!(p.pkgbases(), None);
let p = Pattern::new("*-1.0")?;
assert_eq!(p.pkgbases(), None);
let p = Pattern::new("fo?-[0-9]*")?;
assert_eq!(p.pkgbases(), None);
let p = Pattern::new("boost-headers-1.90.*")?;
assert_eq!(p.pkgbases(), Some(vec!["boost-headers"]));
let p = Pattern::new("foo-1.[0-9]")?;
assert_eq!(p.pkgbases(), Some(vec!["foo"]));
let p = Pattern::new("foo-1.*")?;
assert_eq!(p.pkgbases(), Some(vec!["foo"]));
let p = Pattern::new("foo-10*")?;
assert_eq!(p.pkgbases(), Some(vec!["foo"]));
let p = Pattern::new("lib2to3-3.1[0-9]*")?;
assert_eq!(p.pkgbases(), Some(vec!["lib2to3"]));
let p = Pattern::new("R-4.*")?;
assert_eq!(p.pkgbases(), Some(vec!["R"]));
let p = Pattern::new("p5-IO-1.2[0-9]*")?;
assert_eq!(p.pkgbases(), Some(vec!["p5-IO"]));
let p = Pattern::new("qt5-qtbase-5.15.18nb[0-9]*")?;
assert_eq!(p.pkgbases(), Some(vec!["qt5-qtbase"]));
let p = Pattern::new("foo-bar*")?;
assert_eq!(p.pkgbases(), None);
let p = Pattern::new("foo-abc[0-9]*")?;
assert_eq!(p.pkgbases(), None);
let p = Pattern::new("foo-2bar*")?;
assert_eq!(p.pkgbases(), None);
let p = Pattern::new("foo-1alpha*")?;
assert_eq!(p.pkgbases(), None);
let p = Pattern::new("foo-1.0rc*")?;
assert_eq!(p.pkgbases(), None);
let p = Pattern::new("font-adobe-100dpi-[0-9]*")?;
assert_eq!(p.pkgbases(), Some(vec!["font-adobe-100dpi"]));
let p = Pattern::new("font-adobe-100dpi-1.*")?;
assert_eq!(p.pkgbases(), Some(vec!["font-adobe-100dpi"]));
let p = Pattern::new("fuse-ntfs-3g-[0-9]*")?;
assert_eq!(p.pkgbases(), Some(vec!["fuse-ntfs-3g"]));
let p = Pattern::new("tex-pst-3dplot-[0-9]*")?;
assert_eq!(p.pkgbases(), Some(vec!["tex-pst-3dplot"]));
let p = Pattern::new("tex-2up-[0-9]*")?;
assert_eq!(p.pkgbases(), Some(vec!["tex-2up"]));
let p = Pattern::new("nerd-fonts-3270-[0-9]*")?;
assert_eq!(p.pkgbases(), Some(vec!["nerd-fonts-3270"]));
let p = Pattern::new("u-boot-rpi3-32-[0-9]*")?;
assert_eq!(p.pkgbases(), Some(vec!["u-boot-rpi3-32"]));
let p = Pattern::new("boost-libs-1.90.*")?;
assert_eq!(p.pkgbases(), Some(vec!["boost-libs"]));
let p = Pattern::new("SDL-1.2.[0-9]*")?;
assert_eq!(p.pkgbases(), Some(vec!["SDL"]));
let p = Pattern::new("mongodb-3*")?;
assert_eq!(p.pkgbases(), Some(vec!["mongodb"]));
let p = Pattern::new("python27-2.7.*")?;
assert_eq!(p.pkgbases(), Some(vec!["python27"]));
let p = Pattern::new("go14-1.4*")?;
assert_eq!(p.pkgbases(), Some(vec!["go14"]));
let p = Pattern::new("go110-1.10.*")?;
assert_eq!(p.pkgbases(), Some(vec!["go110"]));
let p = Pattern::new("mariadb-client-10.11.*")?;
assert_eq!(p.pkgbases(), Some(vec!["mariadb-client"]));
let p = Pattern::new("gcc10-aux-10.*")?;
assert_eq!(p.pkgbases(), Some(vec!["gcc10-aux"]));
let p = Pattern::new("binutils-2.22*")?;
assert_eq!(p.pkgbases(), Some(vec!["binutils"]));
let p = Pattern::new("gtar-base-1.*")?;
assert_eq!(p.pkgbases(), Some(vec!["gtar-base"]));
let p = Pattern::new("{foo,bar}-[0-9]*")?;
assert_eq!(p.pkgbases(), Some(vec!["foo", "bar"]));
let p = Pattern::new("qt5-qtbase-5.15.18{,nb[0-9]*}")?;
assert_eq!(p.pkgbases(), Some(vec!["qt5-qtbase"]));
let p = Pattern::new("boost-build-1.90{,nb*,.*}")?;
assert_eq!(p.pkgbases(), Some(vec!["boost-build"]));
let p = Pattern::new("mpg123{,-esound,-nas}>=0.59.18")?;
assert_eq!(
p.pkgbases(),
Some(vec!["mpg123", "mpg123-esound", "mpg123-nas"])
);
let p = Pattern::new("{gzip>=1.2.4b,gzip-base>=1.2.4b}")?;
assert_eq!(p.pkgbases(), Some(vec!["gzip", "gzip-base"]));
let p = Pattern::new("foo{,bar*}")?;
assert_eq!(p.pkgbases(), None);
let p = Pattern::new("{foo,{bar,baz}}-1.0")?;
assert_eq!(p.pkgbases(), Some(vec!["foo", "bar", "baz"]));
let p = Pattern::new("{foo>=1.0,bar-1.0,baz-[0-9]*}")?;
assert_eq!(p.pkgbases(), Some(vec!["foo", "bar", "baz"]));
let p = Pattern::new("{tcl,tk}-{8,9}.*")?;
assert_eq!(p.pkgbases(), Some(vec!["tcl", "tk"]));
let p = Pattern::new("foo-1.{0,2,4}")?;
assert_eq!(p.pkgbases(), Some(vec!["foo"]));
let p = Pattern::new("{foo*,bar*}-1.0")?;
assert_eq!(p.pkgbases(), None);
let p = Pattern::new("{foo}-1.0")?;
assert_eq!(p.pkgbases(), Some(vec!["foo"]));
let p = Pattern::new("{foo-[0-9]*,bar>=1.0}")?;
assert_eq!(p.pkgbases(), Some(vec!["foo", "bar"]));
let p = Pattern::new("{foo-[0-9]*,bar*-1.0}")?;
assert_eq!(p.pkgbases(), None);
let p = Pattern::new("foo>=1.0")?;
assert_eq!(p.pkgbases(), Some(vec!["foo"]));
let p = Pattern::new("pkg-name>=2.0<3.0")?;
assert_eq!(p.pkgbases(), Some(vec!["pkg-name"]));
let p = Pattern::new("foo-1.0")?;
assert_eq!(p.pkgbases(), Some(vec!["foo"]));
let p = Pattern::new("pkg-name-2.0nb1")?;
assert_eq!(p.pkgbases(), Some(vec!["pkg-name"]));
Ok(())
}
}