use std::{borrow::Cow, cmp::Ordering, path::Path};
use bon::Builder;
use cxx::UniquePtr;
use glob_match::glob_match;
use oma_apt::{
Package, Version,
cache::{Cache, PackageSort},
raw::{IntoRawIter, PkgIterator},
records::RecordField,
};
use oma_utils::{
dpkg::{DpkgError, dpkg_arch},
url_no_escape::url_no_escape,
};
use once_cell::sync::OnceCell;
use spdlog::{debug, info};
use crate::pkginfo::{OmaPackage, OmaPackageWithoutVersion, PtrIsNone};
#[derive(Debug, thiserror::Error)]
pub enum MatcherError {
#[error("Invalid pattern: {0}")]
InvalidPattern(String),
#[error("Can not find package {0} from database")]
NoPackage(String),
#[error("Pkg {0} has no version {1}")]
NoVersion(String, String),
#[error("Pkg {0} No candidate")]
NoCandidate(String),
#[error("Can not find path for local package {0}")]
NoPath(String),
#[error(transparent)]
PtrIsNone(#[from] PtrIsNone),
#[error(transparent)]
DpkgError(#[from] DpkgError),
}
pub enum SearchEngine {
Indicium(Box<dyn Fn(usize)>),
Strsim,
Text,
}
pub enum GetArchMethod<'a> {
SpecifySysroot(&'a Path),
SpecifyArch(&'a str),
DirectRoot,
}
#[derive(Builder)]
pub struct PackagesMatcher<'a> {
cache: &'a Cache,
#[builder(default = true)]
filter_candidate: bool,
#[builder(default = false)]
select_dbg: bool,
#[builder(default = false)]
filter_downloadable_candidate: bool,
#[builder(default = GetArchMethod::DirectRoot)]
native_arch: GetArchMethod<'a>,
#[builder(skip)]
arch: OnceCell<Cow<'a, str>>,
}
pub type MatcherResult<T> = Result<T, MatcherError>;
impl<'a> PackagesMatcher<'a> {
pub fn match_pkgs_and_versions(
&self,
keywords: impl IntoIterator<Item = &'a str>,
) -> MatcherResult<(Vec<OmaPackage>, Vec<&'a str>)> {
let mut pkgs = vec![];
let mut no_result = vec![];
for keyword in keywords {
let res = match keyword {
x if x.ends_with(".deb") => self.match_local_glob(x)?,
x if x.split_once('/').is_some() => self.match_from_branch(x)?,
x if x.split_once('=').is_some() => self.match_from_version(x)?,
x => self.match_pkgs_and_versions_from_glob(x)?,
};
for i in &res {
debug!("{} {}", i.raw_pkg.fullname(true), i.version_raw.version());
}
if res.is_empty() {
no_result.push(keyword);
continue;
}
pkgs.extend(res);
}
Ok((pkgs, no_result))
}
pub fn match_local_glob(&self, file_glob: &str) -> MatcherResult<Vec<OmaPackage>> {
let mut res = vec![];
let sort = PackageSort::default().only_virtual();
let glob = self
.cache
.packages(&sort)
.filter(|x| glob_match::glob_match(file_glob, x.name()));
for i in glob {
let real_pkg = real_pkg(&i);
if let Some(real_pkg) = real_pkg {
let pkg = Package::new(self.cache, real_pkg);
let path = url_no_escape(&format!(
"file:{}",
Path::new(i.name())
.canonicalize()
.map_err(|_| MatcherError::NoPath(pkg.fullname(true)))?
.to_str()
.unwrap_or(pkg.name())
));
let versions = pkg.versions();
for ver in versions {
let info = OmaPackage::new(&ver, &pkg);
let has = ver.uris().iter().any(|x| url_no_escape(x) == path);
if has {
res.push(info);
}
}
}
}
Ok(res.into_iter().flatten().collect())
}
pub fn match_pkgs_from_glob(&self, glob: &str) -> MatcherResult<Vec<OmaPackageWithoutVersion>> {
let sort = PackageSort::default().include_virtual();
if glob == "266" {
info!("吃我一拳!!!");
}
let native_arch = self.get_native_arch()?;
let pkgs = self.cache.packages(&sort).filter(|x| {
if glob.contains(':') {
glob_match(glob, &x.fullname(false))
} else {
glob_match(glob, x.name()) && x.arch() == native_arch
}
});
let pkgs = pkgs
.filter_map(|pkg| real_pkg(&pkg))
.map(|raw_pkg| OmaPackageWithoutVersion { raw_pkg })
.collect::<Vec<_>>();
Ok(pkgs)
}
fn get_native_arch(&self) -> MatcherResult<&str> {
Ok(self.arch.get_or_try_init(|| -> MatcherResult<Cow<str>> {
match self.native_arch {
GetArchMethod::SpecifySysroot(sysroot) => Ok(Cow::Owned(dpkg_arch(sysroot)?)),
GetArchMethod::SpecifyArch(arch) => Ok(Cow::Borrowed(arch)),
GetArchMethod::DirectRoot => Ok(Cow::Owned(dpkg_arch("/")?)),
}
})?)
}
pub fn match_pkgs_and_versions_from_glob(&self, glob: &str) -> MatcherResult<Vec<OmaPackage>> {
let mut res = vec![];
let sort = PackageSort::default().include_virtual();
if glob == "266" {
info!("吃我一拳!!!");
}
let arch = self.get_native_arch()?;
let pkgs = self.cache.packages(&sort).filter(|x| {
if glob.contains(':') {
glob_match(glob, &x.fullname(false))
} else {
glob_match(glob, x.name()) && x.arch() == arch
}
});
let pkgs = pkgs
.filter_map(|x| real_pkg(&x))
.map(|x| Package::new(self.cache, x));
for pkg in pkgs {
debug!("Select pkg: {}", pkg.fullname(true));
let versions = pkg.versions();
let mut candidated = false;
for ver in versions {
let pkginfo = OmaPackage::new(&ver, &pkg)?;
let has_dbg = has_dbg(self.cache, &pkg, &ver);
let is_cand = pkg.candidate().map(|x| x == ver).unwrap_or(false);
debug!("version: {}, is cand: {}", ver, is_cand);
if self.filter_candidate && is_cand {
if !self.filter_downloadable_candidate || ver.is_downloadable() {
if !candidated {
res.push(pkginfo);
}
candidated = true;
} else {
let ver = pkg.versions().find(|x| x.is_downloadable());
if let Some(ver) = ver {
res.push(OmaPackage::new(&ver, &pkg)?);
}
}
} else if !self.filter_candidate {
res.push(pkginfo);
}
if has_dbg && self.select_dbg && (is_cand || !self.filter_candidate) {
self.match_debug_packages(&pkg, &ver, &mut res)?;
}
}
}
if !self.filter_candidate {
res.sort_by(|a, b| {
b.is_candidate_version(self.cache)
.cmp(&a.is_candidate_version(self.cache))
});
}
Ok(res)
}
pub fn match_from_version(&self, pat: &str) -> MatcherResult<Vec<OmaPackage>> {
let (pkgname, version_str) = pat
.split_once('=')
.ok_or_else(|| MatcherError::InvalidPattern(pat.to_string()))?;
let pkg = self
.cache
.get(pkgname)
.ok_or_else(|| MatcherError::NoPackage(pat.to_string()))?;
let version = pkg
.get_version(version_str)
.ok_or_else(|| MatcherError::NoVersion(pkgname.to_string(), version_str.to_string()))?;
let mut res = vec![];
let pkginfo = OmaPackage::new(&version, &pkg)?;
let has_dbg = has_dbg(self.cache, &pkg, &version);
res.push(pkginfo);
if has_dbg && self.select_dbg {
self.match_debug_packages(&pkg, &version, &mut res)?;
}
Ok(res)
}
pub fn match_from_branch(&self, pat: &str) -> MatcherResult<Vec<OmaPackage>> {
let mut res = vec![];
let (pkgname, branch) = pat
.split_once('/')
.ok_or_else(|| MatcherError::InvalidPattern(pat.to_string()))?;
let pkg = self
.cache
.get(pkgname)
.ok_or_else(|| MatcherError::NoPackage(pat.to_string()))?;
let mut sort = vec![];
for i in pkg.versions() {
let item = i.get_record(RecordField::Filename);
if let Some(item) = item
&& item.split('/').nth(1) == Some(branch)
{
sort.push(i)
}
}
sort.sort_by(|x, y| {
oma_apt::util::cmp_versions(x.version(), y.version()).unwrap_or(Ordering::Equal)
});
if self.filter_candidate {
let version = sort.last();
if let Some(version) = version {
let pkginfo = OmaPackage::new(version, &pkg)?;
let has_dbg = has_dbg(self.cache, &pkg, version);
if has_dbg && self.select_dbg {
self.match_debug_packages(&pkg, version, &mut res)?;
}
res.push(pkginfo);
}
} else {
for i in sort {
let pkginfo = OmaPackage::new(&i, &pkg)?;
let has_dbg = has_dbg(self.cache, &pkg, &i);
if has_dbg && self.select_dbg {
self.match_debug_packages(&pkg, &i, &mut res)?;
}
res.push(pkginfo);
}
}
Ok(res)
}
fn match_debug_packages(
&self,
pkg: &Package,
version: &Version,
res: &mut Vec<OmaPackage>,
) -> MatcherResult<()> {
let dbg_pkg_name = format!("{}-dbg:{}", pkg.name(), version.arch());
let version_str = version.version();
if let Some(dbg_pkg) = self.cache.get(&dbg_pkg_name)
&& let Some(dbg_ver) = dbg_pkg.get_version(version_str)
{
let pkginfo_dbg = OmaPackage::new(&dbg_ver, &dbg_pkg)?;
res.push(pkginfo_dbg);
}
Ok(())
}
pub fn find_candidate_by_pkgname(&self, pkg: &str) -> MatcherResult<OmaPackage> {
if let Some(pkg) = self.cache.get(pkg) {
for version in pkg.versions() {
if version.is_downloadable() {
let pkginfo = OmaPackage::new(&version, &pkg)?;
debug!(
"Pkg: {} selected version: {}",
pkg.fullname(true),
version.version(),
);
return Ok(pkginfo);
}
}
}
Err(MatcherError::NoCandidate(pkg.to_string()))
}
}
pub fn real_pkg(pkg: &Package) -> Option<UniquePtr<PkgIterator>> {
if !pkg.has_versions()
&& let Some(provide) = pkg.provides().next()
{
return unsafe { provide.target_pkg() }.make_safe();
}
unsafe { pkg.unique() }.make_safe()
}
pub fn has_dbg(cache: &Cache, pkg: &Package<'_>, ver: &Version) -> bool {
let dbg_pkg = format!("{}-dbg:{}", pkg.name(), ver.arch());
let dbg_pkg = cache.get(&dbg_pkg);
if let Some(dbg_pkg) = dbg_pkg {
dbg_pkg.versions().any(|x| x.version() == ver.version())
} else {
false
}
}
#[cfg(test)]
mod test {
use crate::{matches::GetArchMethod, test::TEST_LOCK};
use super::PackagesMatcher;
use oma_apt::new_cache;
#[test]
fn test_glob_search() {
let _lock = TEST_LOCK.lock().unwrap();
let cache = new_cache!().unwrap();
let matcher = PackagesMatcher::builder()
.cache(&cache)
.filter_candidate(true)
.filter_downloadable_candidate(false)
.select_dbg(false)
.native_arch(GetArchMethod::DirectRoot)
.build();
let res_filter = matcher.match_pkgs_and_versions_from_glob("apt*").unwrap();
let matcher = PackagesMatcher::builder()
.cache(&cache)
.filter_candidate(false)
.filter_downloadable_candidate(false)
.select_dbg(false)
.native_arch(GetArchMethod::DirectRoot)
.build();
let res = matcher.match_pkgs_and_versions_from_glob("apt*").unwrap();
for i in res_filter {
i.pkg_info(&cache).unwrap();
}
println!("---\n");
for i in res {
i.pkg_info(&cache).unwrap();
}
}
#[test]
fn test_virtual_pkg_search() {
let _lock = TEST_LOCK.lock().unwrap();
let cache = new_cache!().unwrap();
let matcher = PackagesMatcher::builder()
.cache(&cache)
.filter_candidate(true)
.filter_downloadable_candidate(false)
.select_dbg(false)
.native_arch(GetArchMethod::DirectRoot)
.build();
let res_filter = matcher
.match_pkgs_and_versions_from_glob("telegram")
.unwrap();
for i in res_filter {
i.pkg_info(&cache).unwrap();
}
}
#[test]
fn test_branch_search() {
let _lock = TEST_LOCK.lock().unwrap();
let cache = new_cache!().unwrap();
let matcher = PackagesMatcher::builder()
.cache(&cache)
.filter_candidate(true)
.filter_downloadable_candidate(false)
.select_dbg(false)
.native_arch(GetArchMethod::DirectRoot)
.build();
let res_filter = matcher.match_from_branch("apt/stable").unwrap();
for i in res_filter {
i.pkg_info(&cache).unwrap();
}
}
}