use crate::error::PathError;
use crate::identity::{PathIdentityOptions, path_identity_key};
use crate::inspect::{DirectoryInspection, inspect_directory};
use crate::internal::validation::reject_nul_path;
use crate::metadata::{SortMode, TraversalErrorPolicy};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiscoveryOptions {
pub recursive: bool,
pub follow_symlinks: bool,
pub max_depth: Option<usize>,
pub max_entries: Option<usize>,
pub skip_directory_names: Vec<String>,
pub skip_relative_prefixes: Vec<String>,
#[cfg(feature = "search")]
pub skip_globs: Vec<String>,
pub error_policy: TraversalErrorPolicy,
pub deduplicate: bool,
pub identity: PathIdentityOptions,
pub sort: SortMode,
}
impl Default for DiscoveryOptions {
fn default() -> Self {
Self {
recursive: true,
follow_symlinks: false,
max_depth: None,
max_entries: None,
skip_directory_names: Vec::new(),
skip_relative_prefixes: Vec::new(),
#[cfg(feature = "search")]
skip_globs: Vec::new(),
error_policy: TraversalErrorPolicy::FailFast,
deduplicate: true,
identity: PathIdentityOptions::default(),
sort: SortMode::Path,
}
}
}
impl DiscoveryOptions {
pub fn new() -> Self {
Self::default()
}
pub fn recursive(mut self, recursive: bool) -> Self {
self.recursive = recursive;
self
}
pub fn follow_symlinks(mut self, follow: bool) -> Self {
self.follow_symlinks = follow;
self
}
pub fn skip_names(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.skip_directory_names
.extend(names.into_iter().map(Into::into));
self
}
pub fn skip_relative_prefixes(
mut self,
prefixes: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.skip_relative_prefixes
.extend(prefixes.into_iter().map(Into::into));
self
}
pub fn max_depth(mut self, depth: Option<usize>) -> Self {
self.max_depth = depth;
self
}
pub fn max_entries(mut self, max: Option<usize>) -> Self {
self.max_entries = max;
self
}
pub fn error_policy(mut self, policy: TraversalErrorPolicy) -> Self {
self.error_policy = policy;
self
}
pub fn deduplicate(mut self, deduplicate: bool) -> Self {
self.deduplicate = deduplicate;
self
}
pub fn identity(mut self, identity: PathIdentityOptions) -> Self {
self.identity = identity;
self
}
pub fn sort(mut self, sort: SortMode) -> Self {
self.sort = sort;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum VisitControl {
Continue,
SkipChildren,
Stop,
}
pub trait DirectoryVisitor {
fn visit_directory(&mut self, path: &Path, inspection: &DirectoryInspection) -> VisitControl;
fn visit_error(&mut self, path: &Path, error: &PathError) -> VisitControl {
let _ = (path, error);
VisitControl::Continue
}
}
impl<F> DirectoryVisitor for F
where
F: FnMut(&Path, &DirectoryInspection) -> VisitControl,
{
fn visit_directory(&mut self, path: &Path, inspection: &DirectoryInspection) -> VisitControl {
self(path, inspection)
}
}
pub fn discover_directories(
root: impl AsRef<Path>,
options: &DiscoveryOptions,
) -> Result<Vec<PathBuf>, PathError> {
discover_where(root, options, |_, inspection| inspection.is_directory)
}
pub fn discover_where(
root: impl AsRef<Path>,
options: &DiscoveryOptions,
mut predicate: impl FnMut(&Path, &DirectoryInspection) -> bool,
) -> Result<Vec<PathBuf>, PathError> {
let root = root.as_ref();
reject_nul_path(root)?;
let root_inspection = inspect_directory(root)?;
if !root_inspection.exists || !root_inspection.is_directory {
return Err(PathError::invalid(format!(
"discovery root is not a directory: {}",
root.to_string_lossy()
)));
}
#[cfg(feature = "search")]
let skip_globset = build_skip_globset(&options.skip_globs)?;
let max_depth = if options.recursive {
options.max_depth.unwrap_or(usize::MAX)
} else {
0
};
let walker = WalkDir::new(root)
.min_depth(0)
.max_depth(max_depth)
.follow_links(options.follow_symlinks);
let mut out = Vec::new();
let mut seen_keys = HashSet::new();
let mut skip_prefixes: HashSet<PathBuf> = HashSet::new();
for item in walker {
if let Some(max) = options.max_entries {
if out.len() >= max {
break;
}
}
let entry = match item {
Ok(e) => e,
Err(err) => {
let path = err
.path()
.map(Path::to_path_buf)
.unwrap_or_else(|| root.to_path_buf());
let pe = PathError::traversal(format!("{} ({})", err, path.display()));
match options.error_policy {
TraversalErrorPolicy::FailFast => return Err(pe),
TraversalErrorPolicy::SkipErrors => continue,
}
}
};
let path = entry.path();
let file_type = entry.file_type();
if !(file_type.is_dir() || (options.follow_symlinks && file_type.is_symlink())) {
continue;
}
if skip_prefixes
.iter()
.any(|p| path.starts_with(p) && path != p)
{
continue;
}
let is_root = path == root;
let name = entry.file_name().to_string_lossy();
if !is_root {
if options
.skip_directory_names
.iter()
.any(|n| n.as_str() == name)
{
skip_prefixes.insert(path.to_path_buf());
continue;
}
if let Ok(rel) = path.strip_prefix(root) {
let rel_str = rel.to_string_lossy().replace('\\', "/");
if options
.skip_relative_prefixes
.iter()
.any(|p| rel_str == *p || rel_str.starts_with(&format!("{p}/")))
{
skip_prefixes.insert(path.to_path_buf());
continue;
}
#[cfg(feature = "search")]
if let Some(gs) = &skip_globset {
if gs.is_match(rel_str.as_str()) || gs.is_match(name.as_ref()) {
skip_prefixes.insert(path.to_path_buf());
continue;
}
}
}
}
let inspection = match inspect_directory(path) {
Ok(i) => i,
Err(e) => match options.error_policy {
TraversalErrorPolicy::FailFast => return Err(e),
TraversalErrorPolicy::SkipErrors => continue,
},
};
if !inspection.is_directory {
continue;
}
if !predicate(path, &inspection) {
continue;
}
if options.deduplicate {
let key = path_identity_key(path, options.identity)?;
if !seen_keys.insert(key) {
continue;
}
}
out.push(path.to_path_buf());
}
sort_paths(&mut out, options.sort);
Ok(out)
}
pub fn visit_directories(
root: impl AsRef<Path>,
options: &DiscoveryOptions,
visitor: &mut dyn DirectoryVisitor,
) -> Result<(), PathError> {
let root = root.as_ref();
reject_nul_path(root)?;
let max_depth = if options.recursive {
options.max_depth.unwrap_or(usize::MAX)
} else {
0
};
let walker = WalkDir::new(root)
.min_depth(0)
.max_depth(max_depth)
.follow_links(options.follow_symlinks);
let mut skip_prefixes: HashSet<PathBuf> = HashSet::new();
for item in walker {
let entry = match item {
Ok(e) => e,
Err(err) => {
let path = err
.path()
.map(Path::to_path_buf)
.unwrap_or_else(|| root.to_path_buf());
let pe = PathError::traversal(format!("{err}"));
match visitor.visit_error(&path, &pe) {
VisitControl::Stop => return Ok(()),
VisitControl::SkipChildren | VisitControl::Continue => {
if matches!(options.error_policy, TraversalErrorPolicy::FailFast) {
return Err(pe);
}
continue;
}
}
}
};
let path = entry.path();
if !entry.file_type().is_dir() {
continue;
}
if skip_prefixes
.iter()
.any(|p| path.starts_with(p) && path != p)
{
continue;
}
let is_root = path == root;
if !is_root {
let name = entry.file_name().to_string_lossy();
if options
.skip_directory_names
.iter()
.any(|n| n.as_str() == name)
{
skip_prefixes.insert(path.to_path_buf());
continue;
}
}
let inspection = match inspect_directory(path) {
Ok(i) => i,
Err(e) => match visitor.visit_error(path, &e) {
VisitControl::Stop => return Ok(()),
_ if matches!(options.error_policy, TraversalErrorPolicy::FailFast) => {
return Err(e);
}
_ => continue,
},
};
match visitor.visit_directory(path, &inspection) {
VisitControl::Continue => {}
VisitControl::SkipChildren => {
skip_prefixes.insert(path.to_path_buf());
}
VisitControl::Stop => return Ok(()),
}
}
Ok(())
}
fn sort_paths(paths: &mut [PathBuf], mode: SortMode) {
match mode {
SortMode::None => {}
SortMode::Path | SortMode::DirsFirst => {
paths.sort_by(|a, b| a.to_string_lossy().cmp(&b.to_string_lossy()));
}
SortMode::Name => {
paths.sort_by(|a, b| {
let an = a
.file_name()
.map(|n| n.to_string_lossy())
.unwrap_or_default();
let bn = b
.file_name()
.map(|n| n.to_string_lossy())
.unwrap_or_default();
an.cmp(&bn)
.then_with(|| a.to_string_lossy().cmp(&b.to_string_lossy()))
});
}
}
}
#[cfg(feature = "search")]
fn build_skip_globset(patterns: &[String]) -> Result<Option<globset::GlobSet>, PathError> {
if patterns.is_empty() {
return Ok(None);
}
let mut builder = globset::GlobSetBuilder::new();
for p in patterns {
let g = globset::Glob::new(p).map_err(|e| PathError::InvalidGlob {
message: e.to_string(),
})?;
builder.add(g);
}
let set = builder.build().map_err(|e| PathError::InvalidGlob {
message: e.to_string(),
})?;
Ok(Some(set))
}