use crate::walk_builder::{EntryFilter, EntrySorter};
use crate::walk_platform::{
DirectoryIdentity, FileSystemId, PlatformDirectoryInfo, directory_info,
};
pub use crate::walk_types::{
ErrorPolicy, RootSymlinkPolicy, WalkEntry, WalkError, WalkOperation, WalkOptions,
WalkSkipReason,
};
use std::collections::{HashSet, VecDeque};
use std::fs::{self, FileType};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug)]
pub(crate) struct PendingDirectory {
pub(crate) path: PathBuf,
pub(crate) depth: usize,
pub(crate) identity: Option<DirectoryIdentity>,
pub(crate) post_entry: Option<WalkEntry>,
}
#[allow(clippy::large_enum_variant)]
pub(crate) enum DirectoryEntries {
Open(fs::ReadDir),
Buffered(VecDeque<io::Result<fs::DirEntry>>),
}
impl DirectoryEntries {
#[allow(clippy::inline_always)]
#[inline(always)]
pub(crate) fn next(&mut self) -> Option<io::Result<fs::DirEntry>> {
match self {
Self::Open(entries) => entries.next(),
Self::Buffered(entries) => entries.pop_front(),
}
}
#[allow(clippy::inline_always)]
#[inline(always)]
pub(crate) const fn is_open(&self) -> bool {
matches!(self, Self::Open(_))
}
}
pub(crate) struct DirectoryFrame {
pub(crate) path: PathBuf,
pub(crate) depth: usize,
pub(crate) entries: DirectoryEntries,
pub(crate) identity: Option<DirectoryIdentity>,
pub(crate) post_entry: Option<WalkEntry>,
}
#[allow(clippy::struct_excessive_bools)]
pub struct Walker {
pub(crate) root: Arc<PathBuf>,
pub(crate) root_components: usize,
pub(crate) root_file_type: Option<FileType>,
pub(crate) root_bytes: Option<u64>,
pub(crate) root_version: Option<crate::FileVersion>,
pub(crate) root_file_system: Option<FileSystemId>,
pub(crate) root_directory_info: Option<PlatformDirectoryInfo>,
pub(crate) options: WalkOptions,
pub(crate) frames: Vec<DirectoryFrame>,
pub(crate) open_handles: usize,
pub(crate) yield_root: bool,
pub(crate) pending_directory: Option<PendingDirectory>,
pub(crate) skip_pending_directory: bool,
pub(crate) active_directories: HashSet<DirectoryIdentity>,
pub(crate) finished: bool,
pub(crate) sorter: Option<EntrySorter>,
pub(crate) filter: Option<EntryFilter>,
pub(crate) skip_stdout: Option<crate::FileIdentity>,
pub(crate) contents_first: bool,
pub(crate) deferred_entry: Option<WalkEntry>,
pub(crate) plain_entries: bool,
}
impl Walker {
pub fn new(root: impl AsRef<Path>) -> Result<Self, WalkError> {
Self::with_options(root, WalkOptions::default())
}
pub fn with_options(root: impl AsRef<Path>, options: WalkOptions) -> Result<Self, WalkError> {
Self::with_behavior(root, options, None, None, None, false)
}
pub(crate) fn with_behavior(
root: impl AsRef<Path>,
options: WalkOptions,
sorter: Option<EntrySorter>,
filter: Option<EntryFilter>,
skip_stdout: Option<crate::FileIdentity>,
contents_first: bool,
) -> Result<Self, WalkError> {
let requested = root.as_ref();
let options = options.normalized();
if options.root_symlink_policy == RootSymlinkPolicy::Reject {
let metadata = fs::symlink_metadata(requested).map_err(|source| {
WalkError::new(requested, 0, WalkOperation::ReadMetadata, source)
})?;
if metadata.file_type().is_symlink() {
return Err(WalkError::new(
requested,
0,
WalkOperation::ReadMetadata,
io::Error::new(
io::ErrorKind::InvalidInput,
"root symlink rejected by policy",
),
));
}
}
let canonical = if options.follow_links || options.same_file_system {
requested.canonicalize().map_err(|source| {
WalkError::new(requested, 0, WalkOperation::Canonicalize, source)
})?
} else if requested.is_absolute() {
requested.to_path_buf()
} else {
std::env::current_dir()
.map_err(|source| {
WalkError::new(requested, 0, WalkOperation::Canonicalize, source)
})?
.join(requested)
};
let metadata = fs::metadata(&canonical)
.map_err(|source| WalkError::new(&canonical, 0, WalkOperation::ReadMetadata, source))?;
let root_directory_info =
if metadata.is_dir() && (options.follow_links || options.same_file_system) {
Some(directory_info(&canonical, &metadata).map_err(|source| {
WalkError::new(&canonical, 0, WalkOperation::ReadMetadata, source)
})?)
} else {
None
};
let root_file_system = if options.same_file_system {
root_directory_info.map(|info| info.file_system)
} else {
None
};
let (root_bytes, root_version) = if options.collect_metadata && metadata.is_file() {
(
Some(metadata.len()),
Some(crate::file_version::from_metadata(&metadata)),
)
} else {
(None, None)
};
let plain_entries = !options.follow_links
&& !options.same_file_system
&& !options.collect_metadata
&& options.max_depth.is_none()
&& options.min_depth == 0
&& filter.is_none()
&& skip_stdout.is_none()
&& !contents_first;
let root = Arc::new(canonical.clone());
Ok(Self {
root: Arc::clone(&root),
root_components: canonical.components().count(),
root_file_type: Some(metadata.file_type()),
root_bytes,
root_version,
root_file_system,
root_directory_info,
options,
frames: Vec::new(),
open_handles: 0,
yield_root: true,
pending_directory: None,
skip_pending_directory: false,
active_directories: HashSet::new(),
finished: false,
sorter,
filter,
skip_stdout,
contents_first,
deferred_entry: None,
plain_entries,
})
}
pub(crate) fn from_known_directory(
root: &Arc<PathBuf>,
directory: PathBuf,
depth: usize,
options: WalkOptions,
root_file_system: Option<FileSystemId>,
) -> Self {
Self::from_known_directory_with_ancestry(
root,
directory,
depth,
options,
root_file_system,
None,
HashSet::new(),
)
}
pub(crate) fn from_known_directory_with_ancestry(
root: &Arc<PathBuf>,
directory: PathBuf,
depth: usize,
options: WalkOptions,
root_file_system: Option<FileSystemId>,
directory_identity: Option<DirectoryIdentity>,
active_directories: HashSet<DirectoryIdentity>,
) -> Self {
let options = options.normalized();
let root_components = root.components().count();
let plain_entries = !options.follow_links
&& !options.same_file_system
&& !options.collect_metadata
&& options.max_depth.is_none()
&& options.min_depth == 0;
Self {
root: Arc::clone(root),
root_components,
root_file_type: None,
root_bytes: None,
root_version: None,
root_file_system,
root_directory_info: None,
options,
frames: Vec::new(),
open_handles: 0,
yield_root: false,
pending_directory: Some(PendingDirectory {
path: directory,
depth,
identity: directory_identity,
post_entry: None,
}),
skip_pending_directory: false,
active_directories,
finished: false,
sorter: None,
filter: None,
skip_stdout: None,
contents_first: false,
deferred_entry: None,
plain_entries,
}
}
#[must_use]
pub fn root(&self) -> &Path {
self.root.as_path()
}
#[must_use]
pub const fn options(&self) -> &WalkOptions {
&self.options
}
pub fn skip_current_dir(&mut self) {
if self.pending_directory.is_some() {
self.skip_pending_directory = true;
}
}
pub(crate) fn schedule_pending_directory(&mut self) -> Option<WalkError> {
let pending = self.pending_directory.take()?;
if self.skip_pending_directory {
self.skip_pending_directory = false;
return None;
}
if self.open_handles >= self.options.max_open {
self.buffer_oldest_open_directory();
}
match fs::read_dir(&pending.path) {
Ok(entries) => {
if let Some(identity) = pending.identity {
self.active_directories.insert(identity);
}
let (entries, opened) = match self.sorter.as_ref() {
None => (DirectoryEntries::Open(entries), true),
Some(sorter) => {
let mut entries = entries.collect::<Vec<_>>();
entries.sort_by(|left, right| match (left, right) {
(Ok(left), Ok(right)) => sorter(left, right),
(Err(_), Ok(_)) => std::cmp::Ordering::Less,
(Ok(_), Err(_)) => std::cmp::Ordering::Greater,
(Err(_), Err(_)) => std::cmp::Ordering::Equal,
});
(DirectoryEntries::Buffered(entries.into()), false)
}
};
self.frames.push(DirectoryFrame {
path: pending.path,
depth: pending.depth,
entries,
identity: pending.identity,
post_entry: pending.post_entry,
});
self.open_handles += usize::from(opened);
None
}
Err(source) => {
self.deferred_entry = pending.post_entry;
Some(WalkError::new(
pending.path,
pending.depth,
WalkOperation::ReadDirectory,
source,
))
}
}
}
fn buffer_oldest_open_directory(&mut self) {
let Some(index) = self.frames.iter().position(|frame| frame.entries.is_open()) else {
return;
};
let placeholder = DirectoryEntries::Buffered(VecDeque::new());
let entries = std::mem::replace(&mut self.frames[index].entries, placeholder);
if let DirectoryEntries::Open(entries) = entries {
self.frames[index].entries = DirectoryEntries::Buffered(entries.collect());
self.open_handles -= 1;
}
}
}