use std::fmt;
use std::str::FromStr;
use indexmap::{IndexMap, IndexSet};
use itertools::{Either, Itertools};
use owo_colors::OwoColorize;
use pkgcraft::dep::{Cpn, Cpv};
use pkgcraft::error::Error::InvalidPkg;
use pkgcraft::pkg::Package;
use pkgcraft::pkg::ebuild::{EbuildPkg, EbuildRawPkg, keyword::KeywordStatus};
use pkgcraft::repo::PkgRepository;
use pkgcraft::restrict::{self, Restrict, Restriction, Scope};
use pkgcraft::types::OrderedMap;
use strum::{AsRefStr, Display, EnumIter, IntoEnumIterator};
use crate::check::{CheckRun, CheckRunner};
use crate::error::Error;
use crate::scan::ScannerRun;
#[derive(Display, EnumIter, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
#[strum(serialize_all = "kebab-case")]
pub enum SourceKind {
Cpv,
EbuildPkg,
EbuildRawPkg,
Cpn,
Category,
Repo,
}
impl SourceKind {
pub(crate) fn scope(&self) -> Scope {
match self {
Self::Cpv => Scope::Version,
Self::EbuildPkg => Scope::Version,
Self::EbuildRawPkg => Scope::Version,
Self::Cpn => Scope::Package,
Self::Category => Scope::Category,
Self::Repo => Scope::Repo,
}
}
}
#[derive(AsRefStr, EnumIter, Debug, PartialEq, Eq, Hash, Clone)]
#[strum(serialize_all = "kebab-case")]
pub enum PkgFilter {
Latest(bool),
LatestSlots(bool),
Live(bool),
Masked(bool),
Restrict(bool, Restrict),
Stable(bool),
}
impl PkgFilter {
fn filter<'a>(
&'a self,
iter: Box<dyn Iterator<Item = EbuildPkg> + 'a>,
) -> Box<dyn Iterator<Item = EbuildPkg> + 'a> {
match self {
Self::Latest(inverted) => {
let items: Vec<_> = iter.collect();
let len = items.len();
if items.is_empty() {
Box::new(items.into_iter())
} else if *inverted {
Box::new(items.into_iter().take(len - 1))
} else {
Box::new(items.into_iter().skip(len - 1))
}
}
Self::LatestSlots(inverted) => Box::new(
iter.map(|pkg| (pkg.slot().to_string(), pkg))
.collect::<OrderedMap<_, Vec<_>>>()
.into_values()
.flat_map(|pkgs| {
let len = pkgs.len();
if *inverted {
Either::Left(pkgs.into_iter().take(len - 1))
} else {
Either::Right(pkgs.into_iter().skip(len - 1))
}
}),
),
Self::Live(inverted) => Box::new(iter.filter(move |pkg| inverted ^ pkg.live())),
Self::Masked(inverted) => {
Box::new(iter.filter(move |pkg| inverted ^ pkg.masked()))
}
Self::Stable(inverted) => {
let status = if *inverted {
KeywordStatus::Unstable
} else {
KeywordStatus::Stable
};
Box::new(iter.filter(move |pkg| {
!pkg.keywords().is_empty()
&& pkg.keywords().iter().all(|k| k.status() == status)
}))
}
Self::Restrict(inverted, restrict) => {
Box::new(iter.filter(move |pkg| inverted ^ restrict.matches(pkg)))
}
}
}
}
impl FromStr for PkgFilter {
type Err = Error;
fn from_str(s: &str) -> crate::Result<Self> {
let stripped = s.strip_prefix('!');
let inverted = stripped.is_some();
match stripped.unwrap_or(s) {
"latest" => Ok(Self::Latest(inverted)),
"latest-slots" => Ok(Self::LatestSlots(inverted)),
"live" => Ok(Self::Live(inverted)),
"masked" => Ok(Self::Masked(inverted)),
"stable" => Ok(Self::Stable(inverted)),
s if s.contains(|c: char| c.is_whitespace()) => {
Ok(restrict::parse::pkg(s).map(|r| Self::Restrict(inverted, r))?)
}
s => {
let possible = Self::iter()
.filter(|r| !matches!(r, Self::Restrict(_, _)))
.map(|r| r.as_ref().green().to_string())
.join(", ");
let message = indoc::formatdoc! {r#"
invalid filter: {s}
[possible values: {possible}]
Custom restrictions are supported, for example to target all packages
maintained by the python project use the following command:
pkgcruft scan -f "maintainers any email == 'python@gentoo.org'""#};
Err(Error::InvalidValue(message))
}
}
}
}
struct PkgFilters<'a>(&'a IndexSet<PkgFilter>);
impl<'a> PkgFilters<'a> {
fn is_empty(&self) -> bool {
self.0.is_empty()
}
fn iter_restrict<R: Into<Restrict>>(
&self,
run: &ScannerRun,
val: R,
) -> Box<dyn Iterator<Item = EbuildPkg> + 'a> {
let mut iter: Box<dyn Iterator<Item = EbuildPkg>> =
Box::new(run.repo.iter_restrict(val).filter_map(Result::ok));
for f in self.0 {
iter = f.filter(iter);
}
iter
}
fn iter_restrict_ordered<R: Into<Restrict>>(
&self,
run: &ScannerRun,
val: R,
) -> Box<dyn Iterator<Item = EbuildPkg> + 'a> {
let mut iter: Box<dyn Iterator<Item = EbuildPkg>> =
Box::new(run.repo.iter_restrict_ordered(val).filter_map(Result::ok));
for f in self.0 {
iter = f.filter(iter);
}
iter
}
}
pub(crate) trait Source: fmt::Display {
type Item;
const KIND: SourceKind;
fn iter_restrict<'a, R: Into<Restrict>>(
&self,
run: &'a ScannerRun,
val: R,
) -> impl Iterator<Item = Self::Item> + 'a;
fn iter_restrict_ordered<'a, R: Into<Restrict>>(
&self,
run: &'a ScannerRun,
val: R,
) -> impl Iterator<Item = Self::Item> + 'a;
}
pub(crate) trait PkgSource {
type Pkg;
fn run_pkg(&self, runner: &CheckRunner, pkg: &Self::Pkg, run: &ScannerRun);
fn run_pkg_set(
&self,
runner: &CheckRunner,
cpn: &Cpn,
pkgs: &[Self::Pkg],
run: &ScannerRun,
);
}
#[derive(Default)]
pub(crate) struct EbuildPkgSource;
impl fmt::Display for EbuildPkgSource {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Self::KIND.fmt(f)
}
}
impl Source for EbuildPkgSource {
type Item = pkgcraft::Result<EbuildPkg>;
const KIND: SourceKind = SourceKind::EbuildPkg;
fn iter_restrict<'a, R: Into<Restrict>>(
&self,
run: &'a ScannerRun,
val: R,
) -> impl Iterator<Item = Self::Item> + 'a {
let filters = PkgFilters(&run.filters);
if !filters.is_empty() {
Either::Left(
filters
.iter_restrict(run, val)
.flat_map(|pkg| run.repo.iter_restrict(&pkg)),
)
} else {
Either::Right(run.repo.iter_restrict(val))
}
}
fn iter_restrict_ordered<'a, R: Into<Restrict>>(
&self,
run: &'a ScannerRun,
val: R,
) -> impl Iterator<Item = Self::Item> + 'a {
let filters = PkgFilters(&run.filters);
if !filters.is_empty() {
Either::Left(
filters
.iter_restrict_ordered(run, val)
.flat_map(|pkg| run.repo.iter_restrict_ordered(&pkg)),
)
} else {
Either::Right(run.repo.iter_restrict_ordered(val))
}
}
}
impl PkgSource for EbuildPkgSource {
type Pkg = EbuildPkg;
fn run_pkg(&self, runner: &CheckRunner, pkg: &Self::Pkg, run: &ScannerRun) {
runner.run_ebuild_pkg(pkg, run)
}
fn run_pkg_set(
&self,
runner: &CheckRunner,
cpn: &Cpn,
pkgs: &[Self::Pkg],
run: &ScannerRun,
) {
runner.run_ebuild_pkg_set(cpn, pkgs, run)
}
}
#[derive(Default)]
pub(crate) struct EbuildRawPkgSource;
impl fmt::Display for EbuildRawPkgSource {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Self::KIND.fmt(f)
}
}
impl Source for EbuildRawPkgSource {
type Item = pkgcraft::Result<EbuildRawPkg>;
const KIND: SourceKind = SourceKind::EbuildRawPkg;
fn iter_restrict<'a, R: Into<Restrict>>(
&self,
run: &'a ScannerRun,
val: R,
) -> impl Iterator<Item = Self::Item> + 'a {
let filters = PkgFilters(&run.filters);
if !filters.is_empty() {
Either::Left(
filters
.iter_restrict(run, val)
.flat_map(|pkg| run.repo.iter_raw_restrict(&pkg)),
)
} else {
Either::Right(run.repo.iter_raw_restrict(val))
}
}
fn iter_restrict_ordered<'a, R: Into<Restrict>>(
&self,
run: &'a ScannerRun,
val: R,
) -> impl Iterator<Item = Self::Item> + 'a {
let filters = PkgFilters(&run.filters);
if !filters.is_empty() {
Either::Left(
filters
.iter_restrict_ordered(run, val)
.flat_map(|pkg| run.repo.iter_raw_restrict_ordered(&pkg)),
)
} else {
Either::Right(run.repo.iter_raw_restrict_ordered(val))
}
}
}
impl PkgSource for EbuildRawPkgSource {
type Pkg = EbuildRawPkg;
fn run_pkg(&self, runner: &CheckRunner, pkg: &Self::Pkg, run: &ScannerRun) {
runner.run_ebuild_raw_pkg(pkg, run)
}
fn run_pkg_set(
&self,
runner: &CheckRunner,
cpn: &Cpn,
pkgs: &[Self::Pkg],
run: &ScannerRun,
) {
runner.run_ebuild_raw_pkg_set(cpn, pkgs, run)
}
}
#[derive(Debug)]
pub(crate) struct PkgCache<T> {
pkgs: pkgcraft::Result<Vec<T>>,
cache: IndexMap<Cpv, pkgcraft::Result<T>>,
}
impl<T: Package + Clone> PkgCache<T> {
pub(crate) fn new<S>(source: &S, run: &ScannerRun) -> Self
where
S: Source<Item = pkgcraft::Result<T>>,
{
let mut cache = IndexMap::new();
for result in source.iter_restrict_ordered(run, &run.restrict) {
if let Ok(pkg) = &result {
cache.insert(pkg.cpv().clone(), result);
} else if let Err(InvalidPkg { cpv, .. }) = &result {
cache.insert(*cpv.clone(), result);
}
}
Self {
pkgs: cache.values().cloned().try_collect(),
cache,
}
}
pub(crate) fn get_pkgs(&self) -> Result<&[T], &pkgcraft::Error> {
self.pkgs.as_deref()
}
pub(crate) fn get_pkg(&self, cpv: &Cpv) -> Option<&pkgcraft::Result<T>> {
self.cache.get(cpv)
}
}