Skip to content

Commit 64ecbc5

Browse files
committed
Add WalkConfiguration::skip_mountpoints()
When walking with noxdev(), mountpoint directories are still visited (passed to the callback) even though they are not traversed. This means callers need to check every directory to determine if it's a mountpoint before acting on it, which largely defeats the purpose of noxdev(). Add a new skip_mountpoints() option that skips mountpoint directories entirely -- they are neither visited nor traversed. This uses the existing statx-based MOUNT_ROOT detection and is Linux-only. The two options are independent: noxdev() controls traversal across devices, while skip_mountpoints() controls whether mountpoints are visited at all. Closes #87 Assisted-by: OpenCode (claude-opus-4-6)
1 parent 708621a commit 64ecbc5

2 files changed

Lines changed: 86 additions & 1 deletion

File tree

src/dirext.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ pub struct WalkConfiguration<'p> {
5959
/// Do not cross devices.
6060
noxdev: bool,
6161

62+
/// Skip mount points entirely (do not visit or traverse them).
63+
skip_mountpoints: bool,
64+
6265
path_base: Option<&'p Path>,
6366

6467
// It's not *that* complex of a type, come on clippy...
@@ -73,18 +76,39 @@ impl std::fmt::Debug for WalkConfiguration<'_> {
7376
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7477
f.debug_struct("WalkConfiguration")
7578
.field("noxdev", &self.noxdev)
79+
.field("skip_mountpoints", &self.skip_mountpoints)
7680
.field("sorter", &self.sorter.as_ref().map(|_| true))
7781
.finish()
7882
}
7983
}
8084

8185
impl<'p> WalkConfiguration<'p> {
82-
/// Enable configuration to not traverse mount points
86+
/// Enable configuration to not traverse mount points.
87+
///
88+
/// Note that the mount point directory itself will still be visited
89+
/// (passed to the callback); only traversal into it is prevented.
90+
/// To skip mount point directories entirely, use [`skip_mountpoints`](Self::skip_mountpoints).
8391
pub fn noxdev(mut self) -> Self {
8492
self.noxdev = true;
8593
self
8694
}
8795

96+
/// Skip mount point directories entirely during the walk.
97+
///
98+
/// When enabled, directories that are mount points will not be visited
99+
/// (the callback will not be invoked for them) and will not be traversed.
100+
///
101+
/// This is independent from [`noxdev`](Self::noxdev) -- `skip_mountpoints`
102+
/// controls whether mount point directories are visited at all, while
103+
/// `noxdev` controls whether the walk traverses into directories on
104+
/// different devices. In practice you may want to use both together.
105+
///
106+
/// This option currently only has an effect on Linux.
107+
pub fn skip_mountpoints(mut self) -> Self {
108+
self.skip_mountpoints = true;
109+
self
110+
}
111+
88112
/// Set a function for sorting directory entries.
89113
pub fn sort_by<F>(mut self, cmp: F) -> Self
90114
where
@@ -556,6 +580,14 @@ where
556580
let ty = entry.file_type()?;
557581
let is_dir = ty.is_dir();
558582
let name = entry.file_name();
583+
// If skip_mountpoints is enabled and this is a directory,
584+
// check if it's a mount point and skip it entirely.
585+
#[cfg(any(target_os = "android", target_os = "linux"))]
586+
if is_dir && config.skip_mountpoints {
587+
if let Some(true) = is_mountpoint_impl_statx(d, Path::new(&name))? {
588+
continue;
589+
}
590+
}
559591
// The path provided to the user includes the current filename
560592
path.push(&name);
561593
let filename = &name;

tests/it/main.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,59 @@ fn test_walk_noxdev() -> Result<()> {
722722
Ok(())
723723
}
724724

725+
#[test]
726+
#[cfg(any(target_os = "android", target_os = "linux"))]
727+
fn test_walk_skip_mountpoints() -> Result<()> {
728+
let rootfs = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
729+
730+
// Count root entries that are mountpoints
731+
let n_root_entries = std::fs::read_dir("/")?.count();
732+
let mut n_mountpoints = 0;
733+
for entry in std::fs::read_dir("/")? {
734+
let entry = entry?;
735+
if entry.file_type()?.is_dir() {
736+
if let Some(true) = rootfs.is_mountpoint(entry.file_name())? {
737+
n_mountpoints += 1;
738+
}
739+
}
740+
}
741+
assert!(
742+
n_mountpoints > 0,
743+
"expected at least one mountpoint under /"
744+
);
745+
746+
// Walk with skip_mountpoints; don't traverse into any directories.
747+
let mut visited = 0;
748+
rootfs
749+
.walk(
750+
&WalkConfiguration::default().skip_mountpoints(),
751+
|e| -> std::io::Result<_> {
752+
// Verify this entry is not a mountpoint
753+
if e.file_type.is_dir() {
754+
let is_mp = e.dir.is_mountpoint(e.filename)?;
755+
assert_ne!(
756+
is_mp,
757+
Some(true),
758+
"mountpoint {:?} should have been skipped",
759+
e.path
760+
);
761+
}
762+
visited += 1;
763+
// Don't traverse into subdirectories, but continue iterating siblings
764+
if e.file_type.is_dir() {
765+
Ok(ControlFlow::Break(()))
766+
} else {
767+
Ok(ControlFlow::Continue(()))
768+
}
769+
},
770+
)
771+
.unwrap();
772+
// We should see all root entries minus the mountpoints
773+
assert_eq!(visited, n_root_entries - n_mountpoints);
774+
775+
Ok(())
776+
}
777+
725778
#[test]
726779
#[cfg(any(target_os = "android", target_os = "linux"))]
727780
fn test_xattrs() -> Result<()> {

0 commit comments

Comments
 (0)