use std::{
fs,
path::{Path, PathBuf},
sync::Arc,
};
use super::{
CycleGuard, DirectoryBackend, ListedEntry, WalkEntry, Walker,
gitignore::{IgnoreReadError, IgnoreScope},
glob_bytes, has_hidden_component, should_skip_git_directory,
};
pub(crate) enum EntryAction {
Skip,
Descend(DirectoryTask),
Emit(EmittedEntry),
DescendAndEmit(EmittedEntry, DirectoryTask),
Failed {
failure: EntryFailure,
descend: Option<DirectoryTask>,
},
}
pub(crate) struct EmittedEntry {
pub(crate) is_dir: bool,
pub(crate) is_symlink: bool,
pub(crate) depth: usize,
pub(crate) metadata: Option<Box<fs::Metadata>>,
pub(crate) root: Arc<Path>,
}
impl EmittedEntry {
pub(crate) fn with_path(self, path: PathBuf) -> WalkEntry {
WalkEntry {
path,
root: self.root,
is_dir: self.is_dir,
is_symlink: self.is_symlink,
depth: self.depth,
metadata: self.metadata,
}
}
}
#[derive(Debug)]
pub(crate) struct DirectoryTask {
pub(crate) path: PathBuf,
pub(crate) root: usize,
pub(crate) cycle_guard: Arc<CycleGuard>,
pub(crate) depth: usize,
pub(crate) ignores: IgnoreScope,
pub(crate) ignore_errors: Vec<IgnoreReadError>,
}
#[derive(Clone, Copy)]
pub(crate) struct TraversalContext<'a> {
pub(crate) root: usize,
pub(crate) cycle_guard: &'a Arc<CycleGuard>,
}
pub(crate) struct EntryFailure {
pub(crate) operation: &'static str,
pub(crate) path: PathBuf,
pub(crate) source: std::io::Error,
}
fn should_emit(
walker: &Walker,
root: usize,
is_dir: bool,
kind_is_dir: Option<bool>,
bytes: &[u8],
git_ignored: bool,
) -> bool {
if git_ignored {
return false;
}
if walker.options.directories_only && kind_is_dir != Some(true) {
return false;
}
if walker.options.files_only && kind_is_dir != Some(false) {
return false;
}
let includes = &walker.roots[root].includes;
includes.is_empty()
|| includes
.iter()
.any(|pattern| pattern.matches(bytes, is_dir, walker.wildcard_mode))
}
pub(crate) fn classify_entry<B: DirectoryBackend + ?Sized>(
walker: &Walker,
backend: &B,
path: &Path,
entry: &ListedEntry,
ignores: &IgnoreScope,
directory_depth: usize,
context: TraversalContext<'_>,
) -> EntryAction {
let plan = &walker.roots[context.root];
let mut is_dir = entry.is_dir();
let path_bytes = path.as_os_str().as_encoded_bytes();
let relative = &path_bytes[plan.relative_start.min(path_bytes.len())..];
let depth = directory_depth + 1;
if walker
.options
.max_depth
.is_some_and(|max_depth| depth > max_depth)
{
return EntryAction::Skip;
}
let bytes = glob_bytes(relative);
if walker.options.skip_hidden && has_hidden_component(bytes.as_ref()) {
return EntryAction::Skip;
}
if should_skip_git_directory(walker, entry.name()) {
return EntryAction::Skip;
}
if plan
.excludes
.iter()
.any(|pattern| pattern.matches(bytes.as_ref(), is_dir, walker.wildcard_mode))
{
return EntryAction::Skip;
}
let git_ignored = ignores.is_ignored(path, is_dir);
if git_ignored && !is_dir {
return EntryAction::Skip;
}
if entry.is_symlink() && walker.options.follow_symlinks {
match backend.metadata(path) {
Ok(metadata) => is_dir = metadata.is_dir(),
Err(source) => {
return EntryAction::Failed {
failure: EntryFailure {
operation: "metadata",
path: path.to_path_buf(),
source,
},
descend: None,
};
}
}
}
if !is_dir && !walker.may_include_file(context.root, bytes.as_ref()) {
return EntryAction::Skip;
}
let descend = is_dir
&& !git_ignored
&& !plan
.excludes
.iter()
.any(|pattern| pattern.covers_subtree(bytes.as_ref(), walker.wildcard_mode))
&& walker.may_descend_at(context.root, depth, bytes.as_ref());
let mut kind_is_dir = Some(is_dir);
if walker.options.resolve_symlink_kind
&& entry.is_symlink()
&& !walker.options.follow_symlinks
&& (walker.options.files_only || walker.options.directories_only)
{
match backend.metadata(path) {
Ok(metadata) => kind_is_dir = Some(metadata.is_dir()),
Err(source) if source.kind() == std::io::ErrorKind::NotFound => kind_is_dir = None,
Err(source) => {
return EntryAction::Failed {
failure: EntryFailure {
operation: "metadata",
path: path.to_path_buf(),
source,
},
descend: None,
};
}
}
}
let emit = should_emit(
walker,
context.root,
is_dir,
kind_is_dir,
bytes.as_ref(),
git_ignored,
);
let task = || DirectoryTask {
path: path.to_path_buf(),
depth,
root: context.root,
cycle_guard: Arc::clone(context.cycle_guard),
ignores: ignores.clone(),
ignore_errors: Vec::new(),
};
if !emit {
if descend {
return EntryAction::Descend(task());
}
return EntryAction::Skip;
}
let metadata = if walker.options.metadata {
match backend.symlink_metadata(path) {
Ok(metadata) => Some(Box::new(metadata)),
Err(source) => {
return EntryAction::Failed {
descend: descend.then(task),
failure: EntryFailure {
operation: "symlink_metadata",
path: path.to_path_buf(),
source,
},
};
}
}
} else {
None
};
let emitted = EmittedEntry {
is_dir,
is_symlink: entry.is_symlink(),
depth,
metadata,
root: Arc::clone(&plan.shared_path),
};
if descend {
EntryAction::DescendAndEmit(emitted, task())
} else {
EntryAction::Emit(emitted)
}
}