use super::{
AttrsOptions, ControlFlow, DirEntry, Error, Result, TraversalCompletion, Vfsi, VfsiExt,
WalkControl, WalkEvent, WalkEventKind,
};
use std::path::Path;
pub(super) fn listdir<F: Vfsi + ?Sized>(
fs: &F,
root: &Path,
presentation: super::WalkOptions,
mut visitor: impl FnMut(&WalkEvent) -> Result<WalkControl>,
) -> Result<TraversalCompletion> {
let options = presentation.listing_options();
if presentation.enter_leave() || presentation.sort_by_name() {
return buffered(fs, root, presentation, visitor);
}
let mut stopped = false;
let mut callback_error = None;
let completions = fs
.vlistdirs(&[root], options, |index, page| {
if index != 0 {
return Err(Error::transport(None, "invalid listdir page index"));
}
let relative = page
.path
.strip_prefix(root)
.map_err(|_| Error::transport(None, "invalid listdir page parent"))?;
if (!options.recursive() && page.path != root)
|| relative
.components()
.any(|component| !matches!(component, std::path::Component::Normal(_)))
{
return Err(Error::transport(None, "invalid listdir page parent"));
}
let depth = relative.components().count() + 1;
for entry in page.entries {
let event = WalkEvent {
kind: WalkEventKind::Entry,
entry,
depth,
};
let decision = visitor(&event).inspect_err(|error| {
callback_error = Some(error.clone());
})?;
match decision {
WalkControl::Stop => {
stopped = true;
return Ok(ControlFlow::Break(()));
}
WalkControl::SkipSubtree
if options.recursive() && event.entry.attrs().is_dir() =>
{
return Err(pruning_error(event.entry.path()));
}
_ => {}
}
}
Ok(ControlFlow::Continue(()))
})
.map_err(|error| {
if let Some(error) = callback_error {
error
} else if error.index().is_some_and(|index| index != 0) {
Error::transport(None, "invalid listdir backend error index")
} else {
error
}
})?;
let expected = if stopped {
TraversalCompletion::Stopped
} else {
TraversalCompletion::Complete
};
if completions.as_slice() != [expected] {
return Err(Error::transport(None, "invalid listdir completion count"));
}
Ok(expected)
}
fn pruning_error(path: &Path) -> Error {
Error::client(0, libc::EINVAL as u32)
.with_context("listdir: set enter_leave(true) for SkipSubtree", path)
}
fn limit_error(path: &Path) -> Error {
Error::client(0, libc::EFBIG as u32).with_context("listdir", path)
}
fn buffered<F: Vfsi + ?Sized>(
fs: &F,
root: &Path,
presentation: super::WalkOptions,
mut visitor: impl FnMut(&WalkEvent) -> Result<WalkControl>,
) -> Result<TraversalCompletion> {
let options = presentation.listing_options();
let fields = options.fields();
let lifecycle = presentation.enter_leave();
let attrs = if lifecycle || options.recursive() {
fs.attrs_with_options(
root,
AttrsOptions::new()
.with_fields(fields)
.with_follow_final_symlink(false),
)?
} else {
crate::metadata_from_attrs(crate::VfAttrs {
file: crate::FileOperand::from_os_path(root),
masks: crate::AttrMask::MODE,
ftype: crate::VfType::Directory,
..Default::default()
})
};
if !lifecycle && !attrs.is_dir() {
return Err(Error::client(0, libc::ENOTDIR as u32).with_context("listdir", root));
}
let walk = options.walk_options(fs.limits());
let mut budget = walk;
if !lifecycle {
budget = budget.with_max_entries(
walk.max_entries_or(crate::api::DEFAULT_DIRECTORY_MAX_ENTRIES)
.saturating_add(1),
);
if !options.recursive() {
budget = budget.with_max_path_bytes(
walk.max_path_bytes_or(crate::api::DEFAULT_DIRECTORY_MAX_PATH_BYTES)
.saturating_add(root.as_os_str().len()),
);
}
}
budget = budget
.with_max_depth(crate::api::DepthLimit::unlimited())
.with_truncate_at_max_depth(false);
let mut retained_bytes = 0usize;
super::traversal::walk_events(
DirEntry::new(root.to_path_buf(), attrs),
budget,
presentation.sort_by_name(),
|path, remaining| {
if !lifecycle && options.recursive() && path != root {
retained_bytes = retained_bytes
.checked_add(path.as_os_str().len())
.ok_or_else(|| limit_error(path))?;
}
let bytes = remaining
.max_path_bytes_or(crate::api::DEFAULT_DIRECTORY_MAX_PATH_BYTES)
.checked_sub(retained_bytes)
.ok_or_else(|| limit_error(path))?;
fs.read_dir_with_options(
path,
remaining
.with_recursive(false)
.with_max_path_bytes(bytes)
.with_fields(fields)
.with_allow_symlink_components(
options.allow_symlink_components() && !options.recursive() && !lifecycle,
),
)
},
|event| {
if event.kind == WalkEventKind::Leave {
return if lifecycle && (options.recursive() || event.depth == 0) {
visitor(event)
} else {
Ok(WalkControl::Continue)
};
}
if event.depth == 0 && event.kind == WalkEventKind::Enter {
return if lifecycle {
visitor(event)
} else {
Ok(WalkControl::Continue)
};
}
let decision =
if (lifecycle && options.recursive()) || event.kind == WalkEventKind::Entry {
visitor(event)?
} else {
visitor(&WalkEvent {
kind: WalkEventKind::Entry,
..event.clone()
})?
};
if decision == WalkControl::Stop {
return Ok(decision);
}
if event.kind == WalkEventKind::Enter {
if options.recursive() && !lifecycle && decision == WalkControl::SkipSubtree {
return Err(pruning_error(event.entry.path()));
}
if !options.recursive() || decision == WalkControl::SkipSubtree {
return Ok(WalkControl::SkipSubtree);
}
if event.depth > walk.max_depth_or(Default::default()).get() {
if !walk.truncate_at_max_depth() {
return Err(limit_error(event.entry.path()));
}
return Ok(WalkControl::SkipSubtree);
}
}
Ok(decision)
},
)
}