use std::collections::HashSet;
use std::path::{Path, PathBuf};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(unix)]
type Identity = (u64, u64);
#[cfg(not(unix))]
type Identity = PathBuf;
#[cfg(unix)]
fn identity(p: &Path) -> Option<Identity> {
std::fs::metadata(p).ok().map(|md| (md.dev(), md.ino()))
}
#[cfg(not(unix))]
fn identity(p: &Path) -> Option<Identity> {
std::fs::canonicalize(p).ok()
}
fn push_unique(items: &mut Vec<PathBuf>, seen: &mut HashSet<Identity>, p: PathBuf) {
match identity(&p) {
Some(id) if !seen.insert(id) => {} _ => items.push(p),
}
}
pub fn resolve(paths: &[PathBuf]) -> (Vec<PathBuf>, Vec<String>) {
resolve_with(paths, None)
}
pub fn resolve_with(
paths: &[PathBuf],
archive_globs: Option<&ArchiveGlobs>,
) -> (Vec<PathBuf>, Vec<String>) {
let mut items = Vec::new();
let mut errors = Vec::new();
let mut seen = HashSet::new();
let mut walked_any = false;
let mut kept_any = false;
for p in paths {
match std::fs::metadata(p) {
Err(e) => errors.push(format!("{}: {e}", p.display())),
Ok(md) if md.is_dir() => {
walked_any = true;
let mut found = walk(p, &mut errors);
if let Some(g) = archive_globs {
found.retain(|f| g.is_match(p, f));
}
kept_any |= !found.is_empty();
found.sort();
for f in found {
push_unique(&mut items, &mut seen, f);
}
}
Ok(_) => push_unique(&mut items, &mut seen, p.clone()),
}
}
if archive_globs.is_some() && walked_any && !kept_any {
errors.push("--archive-glob matched no files in the directories searched".to_string());
}
(items, errors)
}
struct Rules {
negated: Vec<bool>,
any_positive: bool,
}
impl Rules {
fn keep(&self, last: Option<usize>) -> bool {
match last {
Some(i) => !self.negated[i],
None => !self.any_positive,
}
}
}
fn parse_rule(p: &str) -> (bool, &str) {
match p.strip_prefix('!') {
Some(rest) => (true, rest),
None => (false, p),
}
}
pub struct Globs {
set: globset::GlobSet,
rules: Rules,
}
impl Globs {
pub fn is_match(&self, candidate: impl AsRef<Path>) -> bool {
self.rules.keep(self.set.matches(candidate.as_ref()).into_iter().max())
}
}
pub fn build_globs(pats: &[String]) -> anyhow::Result<Option<Globs>> {
if pats.is_empty() {
return Ok(None);
}
let mut b = globset::GlobSetBuilder::new();
let mut negated = Vec::new();
for p in pats {
let (neg, glob) = parse_rule(p);
b.add(globset::Glob::new(glob)?);
negated.push(neg);
}
let any_positive = negated.contains(&false);
Ok(Some(Globs { set: b.build()?, rules: Rules { negated, any_positive } }))
}
pub struct ArchiveGlobs {
names: globset::GlobSet,
paths: globset::GlobSet,
name_at: Vec<usize>,
path_at: Vec<usize>,
rules: Rules,
}
impl ArchiveGlobs {
fn is_match(&self, root: &Path, file: &Path) -> bool {
let by_name = file.file_name().map(|n| self.names.matches(n)).unwrap_or_default();
let by_path = file.strip_prefix(root).map(|r| self.paths.matches(r)).unwrap_or_default();
let last = by_name.into_iter().map(|i| self.name_at[i])
.chain(by_path.into_iter().map(|i| self.path_at[i]))
.max();
self.rules.keep(last)
}
}
pub fn build_archive_globs(pats: &[String]) -> anyhow::Result<Option<ArchiveGlobs>> {
if pats.is_empty() {
return Ok(None);
}
let mut names = globset::GlobSetBuilder::new();
let mut paths = globset::GlobSetBuilder::new();
let (mut name_at, mut path_at, mut negated) = (Vec::new(), Vec::new(), Vec::new());
for (i, p) in pats.iter().enumerate() {
let (neg, glob) = parse_rule(p);
let g = globset::Glob::new(glob)?;
if glob.contains('/') {
paths.add(g);
path_at.push(i);
} else {
names.add(g);
name_at.push(i);
}
negated.push(neg);
}
let any_positive = negated.contains(&false);
Ok(Some(ArchiveGlobs {
names: names.build()?,
paths: paths.build()?,
name_at,
path_at,
rules: Rules { negated, any_positive },
}))
}
fn walk(root: &Path, errors: &mut Vec<String>) -> Vec<PathBuf> {
let mut out = Vec::new();
for res in ignore::WalkBuilder::new(root)
.standard_filters(false)
.follow_links(true)
.build()
{
match res {
Ok(e) => {
if e.file_type().is_some_and(|t| t.is_file()) {
out.push(e.into_path());
}
}
Err(e) => errors.push(format!("{}: {e}", root.display())),
}
}
out
}