use std::fs;
use std::io;
use std::path::{Component, Path, PathBuf};
use crate::{Format, STDIN};
pub use crate::glob::{GlobError, GlobPattern};
#[derive(Debug, Clone)]
pub struct AccumulateTarget {
path: PathBuf,
format: Format,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AccumulateTargetError {
#[error("discovery does not accept stdin")]
Stdin,
#[error("discovery requires a relative target path without .. components")]
InvalidPath,
#[error("discovery requires a target with a JSON or TOML extension")]
UnknownExtension,
}
impl TryFrom<PathBuf> for AccumulateTarget {
type Error = AccumulateTargetError;
fn try_from(raw: PathBuf) -> Result<Self, Self::Error> {
if raw.as_os_str() == STDIN {
return Err(AccumulateTargetError::Stdin);
}
if raw.components().any(|component| {
matches!(
component,
Component::RootDir | Component::Prefix(_) | Component::ParentDir
)
}) {
return Err(AccumulateTargetError::InvalidPath);
}
let path: PathBuf = raw
.components()
.filter(|component| !matches!(component, Component::CurDir))
.collect();
let format = Format::from_path(&path).ok_or(AccumulateTargetError::UnknownExtension)?;
Ok(Self { path, format })
}
}
#[derive(Debug, thiserror::Error)]
pub enum AccumulateError {
#[error("cannot determine the working directory")]
CurrentDirectory(#[source] io::Error),
#[error("cannot inspect a discovery path")]
Inspect {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("cannot list a discovery directory")]
List {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("discovery target is a directory")]
Directory {
path: PathBuf,
},
#[error("discovery target is not a regular file")]
NonRegular {
path: PathBuf,
},
}
pub fn accumulate(
target: &AccumulateTarget,
base_dir: Option<&Path>,
) -> Result<Vec<PathBuf>, AccumulateError> {
let base = match base_dir {
None => PathBuf::new(),
Some(base) if base.is_absolute() => base.to_path_buf(),
Some(base) => std::path::absolute(if base.as_os_str().is_empty() {
Path::new(".")
} else {
base
})
.map_err(AccumulateError::CurrentDirectory)?,
};
let target_path = base.join(&target.path);
let metadata = fs::metadata(&target_path).map_err(|source| AccumulateError::Inspect {
path: target_path.clone(),
source,
})?;
if metadata.is_dir() {
return Err(AccumulateError::Directory { path: target_path });
}
if !metadata.is_file() {
return Err(AccumulateError::NonRegular { path: target_path });
}
let mut files = Vec::new();
let mut directory = base;
if let Some(parent) = target.path.parent() {
for component in parent.components() {
directory.push(component);
let listing_error = |source| AccumulateError::List {
path: directory.clone(),
source,
};
let entries = fs::read_dir(&directory).map_err(listing_error)?;
let mut matching = Vec::new();
for entry in entries {
let entry = entry.map_err(listing_error)?;
let path = entry.path();
if path == target_path || Format::from_path(&path) != Some(target.format) {
continue;
}
let metadata = fs::metadata(&path).map_err(|source| AccumulateError::Inspect {
path: path.clone(),
source,
})?;
if metadata.is_file() {
matching.push((entry.file_name(), path));
}
}
matching.sort_by(|left, right| left.0.cmp(&right.0));
files.extend(matching.into_iter().map(|(_, path)| path));
}
}
files.push(target_path);
Ok(files)
}
impl GlobPattern {
pub fn matches_path(&self, path: &Path) -> bool {
let bytes = path.as_os_str().as_encoded_bytes();
#[cfg(windows)]
let bytes = bytes
.iter()
.map(|&byte| if byte == b'\\' { b'/' } else { byte })
.collect::<Vec<_>>();
self.matches(bytes)
}
pub fn matches_filename(&self, path: &Path) -> bool {
path.file_name()
.is_some_and(|name| self.matches(name.as_encoded_bytes()))
}
}
pub fn filter_paths<P: AsRef<Path>>(
files: &[P],
pattern: &GlobPattern,
filename_only: bool,
) -> Vec<PathBuf> {
files
.iter()
.map(AsRef::as_ref)
.filter(|path| {
if filename_only {
pattern.matches_filename(path)
} else {
pattern.matches_path(path)
}
})
.map(Path::to_path_buf)
.collect()
}