Skip to content

Commit 85d24d3

Browse files
authored
Merge pull request #66 from cgwalters/walk
Add a `walk` method
2 parents 158836f + 2fc56cc commit 85d24d3

File tree

3 files changed

+364
-15
lines changed

3 files changed

+364
-15
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ libc = "0.2"
2020

2121
[dev-dependencies]
2222
anyhow = "1.0"
23+
rand = "0.9"
2324
uuid = "1.10"
2425

2526
[features]

src/dirext.rs

Lines changed: 206 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,105 @@
88
//!
99
//! [`cap_std::fs::Dir`]: https://docs.rs/cap-std/latest/cap_std/fs/struct.Dir.html
1010
11+
use cap_primitives::fs::FileType;
1112
use cap_std::fs::{Dir, File, Metadata};
1213
use cap_tempfile::cap_std;
14+
use cap_tempfile::cap_std::fs::DirEntry;
1315
use rustix::path::Arg;
16+
use std::cmp::Ordering;
1417
use std::ffi::OsStr;
1518
use std::io::Result;
1619
use std::io::{self, Write};
1720
use std::ops::Deref;
1821
use std::os::fd::OwnedFd;
19-
use std::path::Path;
22+
use std::path::{Path, PathBuf};
2023

2124
#[cfg(feature = "fs_utf8")]
2225
use cap_std::fs_utf8;
2326
#[cfg(feature = "fs_utf8")]
2427
use fs_utf8::camino::Utf8Path;
2528

29+
/// A directory entry encountered when using the `walk` function.
30+
#[non_exhaustive]
31+
#[derive(Debug)]
32+
pub struct WalkComponent<'p, 'd> {
33+
/// The relative path to the entry. This will
34+
/// include the filename of [`entry`]. Note
35+
/// that this is purely informative; the filesystem
36+
/// traversal provides this path, but does not itself
37+
/// use it.
38+
///
39+
/// The [`WalkConfiguration::path_base`] function configures
40+
/// the base for this path.
41+
pub path: &'p Path,
42+
/// The parent directory.
43+
pub dir: &'d Dir,
44+
/// The filename of the directory entry.
45+
/// Note that this will also be present in [`path`].
46+
pub filename: &'p OsStr,
47+
/// The file type.
48+
pub file_type: FileType,
49+
/// The directory entry.
50+
pub entry: &'p DirEntry,
51+
}
52+
53+
/// Options controlling recursive traversal with `walk`.
54+
#[non_exhaustive]
55+
#[derive(Default)]
56+
pub struct WalkConfiguration<'p> {
57+
/// Do not cross devices.
58+
noxdev: bool,
59+
60+
path_base: Option<&'p Path>,
61+
62+
// It's not *that* complex of a type, come on clippy...
63+
#[allow(clippy::type_complexity)]
64+
sorter: Option<Box<dyn Fn(&DirEntry, &DirEntry) -> Ordering + 'static>>,
65+
}
66+
67+
/// The return value of a [`walk`] callback.
68+
pub type WalkResult<E> = std::result::Result<std::ops::ControlFlow<()>, E>;
69+
70+
impl std::fmt::Debug for WalkConfiguration<'_> {
71+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72+
f.debug_struct("WalkConfiguration")
73+
.field("noxdev", &self.noxdev)
74+
.field("sorter", &self.sorter.as_ref().map(|_| true))
75+
.finish()
76+
}
77+
}
78+
79+
impl<'p> WalkConfiguration<'p> {
80+
/// Enable configuration to not traverse mount points
81+
pub fn noxdev(mut self) -> Self {
82+
self.noxdev = true;
83+
self
84+
}
85+
86+
/// Set a function for sorting directory entries.
87+
pub fn sort_by<F>(mut self, cmp: F) -> Self
88+
where
89+
F: Fn(&DirEntry, &DirEntry) -> Ordering + 'static,
90+
{
91+
self.sorter = Some(Box::new(cmp));
92+
self
93+
}
94+
95+
/// Sort directory entries by file name.
96+
pub fn sort_by_file_name(self) -> Self {
97+
self.sort_by(|a, b| a.file_name().cmp(&b.file_name()))
98+
}
99+
100+
/// Change the inital state for the path. By default the
101+
/// computed path is relative. This has no effect
102+
/// on the filesystem traversal - it solely affects
103+
/// the value of [`WalkComponent::path`].
104+
pub fn path_base(mut self, base: &'p Path) -> Self {
105+
self.path_base = Some(base);
106+
self
107+
}
108+
}
109+
26110
/// Extension trait for [`cap_std::fs::Dir`].
27111
///
28112
/// [`cap_std::fs::Dir`]: https://docs.rs/cap-std/latest/cap_std/fs/struct.Dir.html
@@ -141,6 +225,15 @@ pub trait CapStdExtDirExt {
141225
/// In some scenarios (such as an older kernel) this currently may not be possible
142226
/// to determine, and `None` will be returned in those cases.
143227
fn is_mountpoint(&self, path: impl AsRef<Path>) -> Result<Option<bool>>;
228+
229+
/// Recursively walk a directory. If the function returns [`std::ops::ControlFlow::Break`]
230+
/// while inspecting a directory, traversal of that directory is skipped. If
231+
/// [`std::ops::ControlFlow::Break`] is returned when inspecting a non-directory,
232+
/// then all further entries in the directory are skipped.
233+
fn walk<C, E>(&self, config: &WalkConfiguration, callback: C) -> std::result::Result<(), E>
234+
where
235+
C: FnMut(&WalkComponent) -> WalkResult<E>,
236+
E: From<std::io::Error>;
144237
}
145238

146239
#[cfg(feature = "fs_utf8")]
@@ -371,6 +464,104 @@ fn is_mountpoint_impl_statx(root: &Dir, path: &Path) -> Result<Option<bool>> {
371464
}
372465
}
373466

467+
/// Open the target directory, but return Ok(None) if this would cross a mount point.
468+
#[cfg(any(target_os = "android", target_os = "linux"))]
469+
fn impl_open_dir_noxdev(
470+
d: &Dir,
471+
path: impl AsRef<std::path::Path>,
472+
) -> std::io::Result<Option<Dir>> {
473+
use rustix::fs::{Mode, OFlags, ResolveFlags};
474+
match openat2_with_retry(
475+
d,
476+
path,
477+
OFlags::CLOEXEC | OFlags::DIRECTORY | OFlags::NOFOLLOW,
478+
Mode::empty(),
479+
ResolveFlags::NO_XDEV | ResolveFlags::BENEATH,
480+
) {
481+
Ok(r) => Ok(Some(Dir::reopen_dir(&r)?)),
482+
Err(e) if e == rustix::io::Errno::XDEV => Ok(None),
483+
Err(e) => Err(e.into()),
484+
}
485+
}
486+
487+
/// Implementation of a recursive directory walk
488+
fn walk_inner<E>(
489+
d: &Dir,
490+
path: &mut PathBuf,
491+
callback: &mut dyn FnMut(&WalkComponent) -> WalkResult<E>,
492+
config: &WalkConfiguration,
493+
) -> std::result::Result<(), E>
494+
where
495+
E: From<std::io::Error>,
496+
{
497+
let entries = d.entries()?;
498+
// If sorting is enabled, then read all entries now and sort them.
499+
let entries: Box<dyn Iterator<Item = Result<DirEntry>>> =
500+
if let Some(sorter) = config.sorter.as_ref() {
501+
let mut entries = entries.collect::<Result<Vec<_>>>()?;
502+
entries.sort_by(|a, b| sorter(a, b));
503+
Box::new(entries.into_iter().map(Ok))
504+
} else {
505+
Box::new(entries.into_iter())
506+
};
507+
// Operate on each entry
508+
for entry in entries {
509+
let entry = &entry?;
510+
// Gather basic data
511+
let ty = entry.file_type()?;
512+
let is_dir = ty.is_dir();
513+
let name = entry.file_name();
514+
// The path provided to the user includes the current filename
515+
path.push(&name);
516+
let filename = &name;
517+
let component = WalkComponent {
518+
path,
519+
dir: d,
520+
filename,
521+
file_type: ty,
522+
entry,
523+
};
524+
// Invoke the user path:callback
525+
let flow = callback(&component)?;
526+
// Did the callback tell us to stop iteration?
527+
let is_break = matches!(flow, std::ops::ControlFlow::Break(()));
528+
// Handle the non-directory case first.
529+
if !is_dir {
530+
path.pop();
531+
// If we got a break, then we're completely done.
532+
if is_break {
533+
return Ok(());
534+
} else {
535+
// Otherwise, process the next entry.
536+
continue;
537+
}
538+
} else if is_break {
539+
// For break on a directory, we continue processing the next entry.
540+
path.pop();
541+
continue;
542+
}
543+
// We're operating on a directory, and the callback must have told
544+
// us to continue.
545+
debug_assert!(matches!(flow, std::ops::ControlFlow::Continue(())));
546+
// Open the child directory, using the noxdev API if
547+
// we're configured not to cross devices,
548+
let d = {
549+
if !config.noxdev {
550+
entry.open_dir()?
551+
} else if let Some(d) = impl_open_dir_noxdev(d, filename)? {
552+
d
553+
} else {
554+
path.pop();
555+
continue;
556+
}
557+
};
558+
// Recurse into the target directory
559+
walk_inner(&d, path, callback, config)?;
560+
path.pop();
561+
}
562+
Ok(())
563+
}
564+
374565
impl CapStdExtDirExt for Dir {
375566
fn open_optional(&self, path: impl AsRef<Path>) -> Result<Option<File>> {
376567
map_optional(self.open(path.as_ref()))
@@ -388,18 +579,7 @@ impl CapStdExtDirExt for Dir {
388579
/// Open the target directory, but return Ok(None) if this would cross a mount point.
389580
#[cfg(any(target_os = "android", target_os = "linux"))]
390581
fn open_dir_noxdev(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<Option<Dir>> {
391-
use rustix::fs::{Mode, OFlags, ResolveFlags};
392-
match openat2_with_retry(
393-
self,
394-
path,
395-
OFlags::CLOEXEC | OFlags::DIRECTORY | OFlags::NOFOLLOW,
396-
Mode::empty(),
397-
ResolveFlags::NO_XDEV | ResolveFlags::BENEATH,
398-
) {
399-
Ok(r) => Ok(Some(Dir::reopen_dir(&r)?)),
400-
Err(e) if e == rustix::io::Errno::XDEV => Ok(None),
401-
Err(e) => Err(e.into()),
402-
}
582+
impl_open_dir_noxdev(self, path)
403583
}
404584

405585
fn ensure_dir_with(
@@ -557,6 +737,19 @@ impl CapStdExtDirExt for Dir {
557737
fn is_mountpoint(&self, path: impl AsRef<Path>) -> Result<Option<bool>> {
558738
is_mountpoint_impl_statx(self, path.as_ref()).map_err(Into::into)
559739
}
740+
741+
fn walk<C, E>(&self, config: &WalkConfiguration, mut callback: C) -> std::result::Result<(), E>
742+
where
743+
C: FnMut(&WalkComponent) -> WalkResult<E>,
744+
E: From<std::io::Error>,
745+
{
746+
let mut pb = config
747+
.path_base
748+
.as_ref()
749+
.map(|v| v.to_path_buf())
750+
.unwrap_or_default();
751+
walk_inner(self, &mut pb, &mut callback, config)
752+
}
560753
}
561754

562755
// Implementation for the Utf8 variant of Dir. You shouldn't need to add

0 commit comments

Comments
 (0)