Skip to main content

cap_std_ext/
dirext.rs

1//! Extensions for [`cap_std::fs::Dir`].  Key features here include:
2//!
3//! - "optional" variants that return `Result<Option<T>>` for nonexistent paths, when
4//!   it is a normal case that paths may not exist.
5//! - A helper to update timestamps
6//! - "atomic write" APIs that create a new file, then rename over the existing one
7//!   to avoid half-written updates to files.
8//!
9//! [`cap_std::fs::Dir`]: https://docs.rs/cap-std/latest/cap_std/fs/struct.Dir.html
10
11use cap_primitives::fs::FileType;
12use cap_std::fs::{Dir, File, Metadata};
13use cap_tempfile::cap_std;
14use cap_tempfile::cap_std::fs::DirEntry;
15#[cfg(any(target_os = "android", target_os = "linux"))]
16use rustix::path::Arg;
17use std::cmp::Ordering;
18use std::ffi::OsStr;
19use std::io::{self, Write};
20use std::io::{Read, Result};
21use std::ops::Deref;
22#[cfg(unix)]
23use std::os::fd::OwnedFd;
24use std::path::{Path, PathBuf};
25
26#[cfg(feature = "fs_utf8")]
27use cap_std::fs_utf8;
28#[cfg(feature = "fs_utf8")]
29use fs_utf8::camino::Utf8Path;
30
31/// A directory entry encountered when using the `walk` function.
32#[non_exhaustive]
33#[derive(Debug)]
34pub struct WalkComponent<'p, 'd> {
35    /// The relative path to the entry. This will
36    /// include the filename of [`entry`]. Note
37    /// that this is purely informative; the filesystem
38    /// traversal provides this path, but does not itself
39    /// use it.
40    ///
41    /// The [`WalkConfiguration::path_base`] function configures
42    /// the base for this path.
43    pub path: &'p Path,
44    /// The parent directory.
45    pub dir: &'d Dir,
46    /// The filename of the directory entry.
47    /// Note that this will also be present in [`path`].
48    pub filename: &'p OsStr,
49    /// The file type.
50    pub file_type: FileType,
51    /// The directory entry.
52    pub entry: &'p DirEntry,
53}
54
55/// Options controlling recursive traversal with `walk`.
56#[non_exhaustive]
57#[derive(Default)]
58pub struct WalkConfiguration<'p> {
59    /// Do not cross devices.
60    noxdev: bool,
61
62    /// Skip mount points entirely (do not visit or traverse them).
63    skip_mountpoints: bool,
64
65    path_base: Option<&'p Path>,
66
67    // It's not *that* complex of a type, come on clippy...
68    #[allow(clippy::type_complexity)]
69    sorter: Option<Box<dyn Fn(&DirEntry, &DirEntry) -> Ordering + 'static>>,
70}
71
72/// The return value of a [`walk`] callback.
73pub type WalkResult<E> = std::result::Result<std::ops::ControlFlow<()>, E>;
74
75impl std::fmt::Debug for WalkConfiguration<'_> {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.debug_struct("WalkConfiguration")
78            .field("noxdev", &self.noxdev)
79            .field("skip_mountpoints", &self.skip_mountpoints)
80            .field("sorter", &self.sorter.as_ref().map(|_| true))
81            .finish()
82    }
83}
84
85impl<'p> WalkConfiguration<'p> {
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).
91    pub fn noxdev(mut self) -> Self {
92        self.noxdev = true;
93        self
94    }
95
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
112    /// Set a function for sorting directory entries.
113    pub fn sort_by<F>(mut self, cmp: F) -> Self
114    where
115        F: Fn(&DirEntry, &DirEntry) -> Ordering + 'static,
116    {
117        self.sorter = Some(Box::new(cmp));
118        self
119    }
120
121    /// Sort directory entries by file name.
122    pub fn sort_by_file_name(self) -> Self {
123        self.sort_by(|a, b| a.file_name().cmp(&b.file_name()))
124    }
125
126    /// Change the inital state for the path. By default the
127    /// computed path is relative. This has no effect
128    /// on the filesystem traversal - it solely affects
129    /// the value of [`WalkComponent::path`].
130    pub fn path_base(mut self, base: &'p Path) -> Self {
131        self.path_base = Some(base);
132        self
133    }
134}
135
136/// Extension trait for [`cap_std::fs::Dir`].
137///
138/// [`cap_std::fs::Dir`]: https://docs.rs/cap-std/latest/cap_std/fs/struct.Dir.html
139pub trait CapStdExtDirExt {
140    /// Open a file read-only, but return `Ok(None)` if it does not exist.
141    fn open_optional(&self, path: impl AsRef<Path>) -> Result<Option<File>>;
142
143    /// Open a directory, but return `Ok(None)` if it does not exist.
144    fn open_dir_optional(&self, path: impl AsRef<Path>) -> Result<Option<Dir>>;
145
146    /// Create a special variant of [`cap_std::fs::Dir`] which uses `RESOLVE_IN_ROOT`
147    /// to support absolute symlinks.
148    #[cfg(any(target_os = "android", target_os = "linux"))]
149    fn open_dir_rooted_ext(&self, path: impl AsRef<Path>) -> Result<crate::RootDir>;
150
151    /// Open the target directory, but return Ok(None) if this would cross a mount point.
152    #[cfg(any(target_os = "android", target_os = "linux"))]
153    fn open_dir_noxdev(&self, path: impl AsRef<Path>) -> Result<Option<Dir>>;
154
155    /// Create the target directory, but do nothing if a directory already exists at that path.
156    /// The return value will be `true` if the directory was created.  An error will be
157    /// returned if the path is a non-directory.  Symbolic links will be followed.
158    fn ensure_dir_with(
159        &self,
160        p: impl AsRef<Path>,
161        builder: &cap_std::fs::DirBuilder,
162    ) -> Result<bool>;
163
164    /// Gather metadata, but return `Ok(None)` if it does not exist.
165    fn metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>>;
166
167    /// Gather metadata (but do not follow symlinks), but return `Ok(None)` if it does not exist.
168    fn symlink_metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>>;
169
170    /// Remove (delete) a file, but return `Ok(false)` if the file does not exist.
171    fn remove_file_optional(&self, path: impl AsRef<Path>) -> Result<bool>;
172
173    /// Remove a file or directory but return `Ok(false)` if the file does not exist.
174    /// Symbolic links are not followed.
175    fn remove_all_optional(&self, path: impl AsRef<Path>) -> Result<bool>;
176
177    /// Read the complete contents of a file, but return `Ok(None)` if the file does not exist.
178    fn read_optional(&self, path: impl AsRef<Path>) -> Result<Option<Vec<u8>>>;
179
180    /// Read the complete contents of a file as a string, but return `Ok(None)` if the file does not exist.
181    fn read_to_string_optional(&self, path: impl AsRef<Path>) -> Result<Option<String>>;
182
183    /// Set the access and modification times to the current time.  Symbolic links are not followed.
184    #[cfg(unix)]
185    fn update_timestamps(&self, path: impl AsRef<Path>) -> Result<()>;
186
187    /// Atomically write a file by calling the provided closure.
188    ///
189    /// This uses [`cap_tempfile::TempFile`], which is wrapped in a [`std::io::BufWriter`]
190    /// and passed to the closure.
191    ///
192    /// # Atomicity
193    ///
194    /// The function takes care of:
195    /// - flushing the BufWriter
196    /// - calling sync_all() on the TempFile
197    /// - calling sync_all() on the parent directory (after the rename)
198    ///
199    /// # Existing files and metadata
200    ///
201    /// If the target path already exists and is a regular file (not a symbolic link or directory),
202    /// then its access permissions (Unix mode) will be preserved.  However, other metadata
203    /// such as extended attributes will *not* be preserved automatically. To do this will
204    /// require a higher level wrapper which queries the existing file and gathers such metadata
205    /// before replacement.
206    ///
207    /// # Example, including setting permissions
208    ///
209    /// The closure may also perform other file operations beyond writing, such as changing
210    /// file permissions:
211    ///
212    /// ```rust
213    /// # use std::io;
214    /// # use std::io::Write;
215    /// # use cap_tempfile::cap_std;
216    /// # fn main() -> io::Result<()> {
217    /// # let somedir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
218    /// use cap_std_ext::prelude::*;
219    /// let contents = b"hello world\n";
220    /// somedir.atomic_replace_with("somefilename", |f| -> io::Result<_> {
221    ///     f.write_all(contents)?;
222    ///     f.flush()?;
223    ///     use cap_std::fs::PermissionsExt;
224    ///     let perms = cap_std::fs::Permissions::from_mode(0o600);
225    ///     f.get_mut().as_file_mut().set_permissions(perms)?;
226    ///     Ok(())
227    /// })
228    /// # }
229    /// ```
230    ///
231    /// Any existing file will be replaced.
232    #[cfg(not(windows))]
233    fn atomic_replace_with<F, T, E>(
234        &self,
235        destname: impl AsRef<Path>,
236        f: F,
237    ) -> std::result::Result<T, E>
238    where
239        F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
240        E: From<std::io::Error>;
241
242    /// Atomically write the provided contents to a file.
243    #[cfg(not(windows))]
244    fn atomic_write(&self, destname: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()>;
245
246    /// Atomically write the provided contents to a file, using specified permissions.
247    #[cfg(not(windows))]
248    fn atomic_write_with_perms(
249        &self,
250        destname: impl AsRef<Path>,
251        contents: impl AsRef<[u8]>,
252        perms: cap_std::fs::Permissions,
253    ) -> Result<()>;
254
255    /// By default, cap-std `Dir` instances are opened using `O_PATH`.
256    /// There are some operations such as `fsync` and `fsetxattr` that
257    /// cannot be performed on `O_PATH` file descriptors. Use this
258    /// function to create a non-`O_PATH` copy of the directory
259    /// file descriptor.
260    #[cfg(unix)]
261    fn reopen_as_ownedfd(&self) -> Result<OwnedFd>;
262
263    #[cfg(any(target_os = "android", target_os = "linux"))]
264    /// Returns `Some(true)` if the target is known to be a mountpoint, or
265    /// `Some(false)` if the target is definitively known not to be a mountpoint.
266    ///
267    /// In some scenarios (such as an older kernel) this currently may not be possible
268    /// to determine, and `None` will be returned in those cases.
269    fn is_mountpoint(&self, path: impl AsRef<Path>) -> Result<Option<bool>>;
270
271    #[cfg(any(target_os = "android", target_os = "linux"))]
272    /// Get the value of an extended attribute. If the attribute is not present,
273    /// this function will return `Ok(None)`.
274    fn getxattr(&self, path: impl AsRef<Path>, key: impl AsRef<OsStr>) -> Result<Option<Vec<u8>>>;
275
276    #[cfg(any(target_os = "android", target_os = "linux"))]
277    /// List all extended attribute keys for this path.
278    fn listxattrs(&self, path: impl AsRef<Path>) -> Result<crate::XattrList>;
279
280    #[cfg(any(target_os = "android", target_os = "linux"))]
281    /// Set the value of an extended attribute.
282    fn setxattr(
283        &self,
284        path: impl AsRef<Path>,
285        key: impl AsRef<OsStr>,
286        value: impl AsRef<[u8]>,
287    ) -> Result<()>;
288
289    /// Recursively walk a directory. If the function returns [`std::ops::ControlFlow::Break`]
290    /// while inspecting a directory, traversal of that directory is skipped. If
291    /// [`std::ops::ControlFlow::Break`] is returned when inspecting a non-directory,
292    /// then all further entries in the directory are skipped.
293    fn walk<C, E>(&self, config: &WalkConfiguration, callback: C) -> std::result::Result<(), E>
294    where
295        C: FnMut(&WalkComponent) -> WalkResult<E>,
296        E: From<std::io::Error>;
297}
298
299#[cfg(feature = "fs_utf8")]
300/// Extension trait for [`cap_std::fs_utf8::Dir`].
301///
302/// [`cap_std::fs_utf8::Dir`]: https://docs.rs/cap-std/latest/cap_std/fs_utf8/struct.Dir.html
303pub trait CapStdExtDirExtUtf8 {
304    /// Open a file read-only, but return `Ok(None)` if it does not exist.
305    fn open_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<fs_utf8::File>>;
306
307    /// Open a directory, but return `Ok(None)` if it does not exist.
308    fn open_dir_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<fs_utf8::Dir>>;
309
310    /// Create the target directory, but do nothing if a directory already exists at that path.
311    /// The return value will be `true` if the directory was created.  An error will be
312    /// returned if the path is a non-directory.  Symbolic links will be followed.
313    fn ensure_dir_with(
314        &self,
315        p: impl AsRef<Utf8Path>,
316        builder: &cap_std::fs::DirBuilder,
317    ) -> Result<bool>;
318
319    /// Gather metadata, but return `Ok(None)` if it does not exist.
320    fn metadata_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<Metadata>>;
321
322    /// Gather metadata (but do not follow symlinks), but return `Ok(None)` if it does not exist.
323    fn symlink_metadata_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<Metadata>>;
324
325    /// Remove (delete) a file, but return `Ok(false)` if the file does not exist.
326    fn remove_file_optional(&self, path: impl AsRef<Utf8Path>) -> Result<bool>;
327
328    /// Remove a file or directory but return `Ok(false)` if the file does not exist.
329    /// Symbolic links are not followed.
330    fn remove_all_optional(&self, path: impl AsRef<Utf8Path>) -> Result<bool>;
331
332    /// Set the access and modification times to the current time.  Symbolic links are not followed.
333    #[cfg(unix)]
334    fn update_timestamps(&self, path: impl AsRef<Utf8Path>) -> Result<()>;
335
336    /// Atomically write a file by calling the provided closure.
337    ///
338    /// This uses [`cap_tempfile::TempFile`], which is wrapped in a [`std::io::BufWriter`]
339    /// and passed to the closure.
340    ///
341    /// # Atomicity
342    ///
343    /// The function takes care of:
344    /// - flushing the BufWriter
345    /// - calling sync_all() on the TempFile
346    /// - calling sync_all() on the parent directory (after the rename)
347    ///
348    /// # Existing files and metadata
349    ///
350    /// If the target path already exists and is a regular file (not a symbolic link or directory),
351    /// then its access permissions (Unix mode) will be preserved.  However, other metadata
352    /// such as extended attributes will *not* be preserved automatically. To do this will
353    /// require a higher level wrapper which queries the existing file and gathers such metadata
354    /// before replacement.
355    ///
356    /// # Example, including setting permissions
357    ///
358    /// The closure may also perform other file operations beyond writing, such as changing
359    /// file permissions:
360    ///
361    /// ```rust
362    /// # use std::io;
363    /// # use std::io::Write;
364    /// # use cap_tempfile::cap_std;
365    /// # fn main() -> io::Result<()> {
366    /// # let somedir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
367    /// # let somedir = cap_std::fs_utf8::Dir::from_cap_std((&*somedir).try_clone()?);
368    /// use cap_std_ext::prelude::*;
369    /// let contents = b"hello world\n";
370    /// somedir.atomic_replace_with("somefilename", |f| -> io::Result<_> {
371    ///     f.write_all(contents)?;
372    ///     f.flush()?;
373    ///     use cap_std::fs::PermissionsExt;
374    ///     let perms = cap_std::fs::Permissions::from_mode(0o600);
375    ///     f.get_mut().as_file_mut().set_permissions(perms)?;
376    ///     Ok(())
377    /// })
378    /// # }
379    /// ```
380    ///
381    /// Any existing file will be replaced.
382    fn atomic_replace_with<F, T, E>(
383        &self,
384        destname: impl AsRef<Utf8Path>,
385        f: F,
386    ) -> std::result::Result<T, E>
387    where
388        F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
389        E: From<std::io::Error>;
390
391    /// Atomically write the provided contents to a file.
392    fn atomic_write(
393        &self,
394        destname: impl AsRef<Utf8Path>,
395        contents: impl AsRef<[u8]>,
396    ) -> Result<()>;
397
398    /// Atomically write the provided contents to a file, using specified permissions.
399    fn atomic_write_with_perms(
400        &self,
401        destname: impl AsRef<Utf8Path>,
402        contents: impl AsRef<[u8]>,
403        perms: cap_std::fs::Permissions,
404    ) -> Result<()>;
405
406    /// Read all filenames in this directory, sorted
407    fn filenames_sorted(&self) -> Result<Vec<String>> {
408        self.filenames_sorted_by(|a, b| a.cmp(b))
409    }
410
411    /// Read all filenames in this directory, sorted by the provided comparison function.
412    fn filenames_sorted_by<C>(&self, compare: C) -> Result<Vec<String>>
413    where
414        C: FnMut(&str, &str) -> std::cmp::Ordering,
415    {
416        self.filenames_filtered_sorted_by(|_, _| true, compare)
417    }
418
419    /// Read all filenames in this directory, applying a filter and sorting the result.
420    fn filenames_filtered_sorted<F>(&self, f: F) -> Result<Vec<String>>
421    where
422        F: FnMut(&fs_utf8::DirEntry, &str) -> bool,
423    {
424        self.filenames_filtered_sorted_by(f, |a, b| a.cmp(b))
425    }
426
427    /// Read all filenames in this directory, applying a filter and sorting the result with a custom comparison function.
428    fn filenames_filtered_sorted_by<F, C>(&self, f: F, compare: C) -> Result<Vec<String>>
429    where
430        F: FnMut(&fs_utf8::DirEntry, &str) -> bool,
431        C: FnMut(&str, &str) -> std::cmp::Ordering;
432}
433
434pub(crate) fn map_optional<R>(r: Result<R>) -> Result<Option<R>> {
435    match r {
436        Ok(v) => Ok(Some(v)),
437        Err(e) => {
438            if e.kind() == std::io::ErrorKind::NotFound {
439                Ok(None)
440            } else {
441                Err(e)
442            }
443        }
444    }
445}
446
447enum DirOwnedOrBorrowed<'d> {
448    Owned(Dir),
449    Borrowed(&'d Dir),
450}
451
452impl Deref for DirOwnedOrBorrowed<'_> {
453    type Target = Dir;
454
455    fn deref(&self) -> &Self::Target {
456        match self {
457            Self::Owned(d) => d,
458            Self::Borrowed(d) => d,
459        }
460    }
461}
462
463/// Given a directory reference and a path, if the path includes a subdirectory (e.g. on Unix has a `/`)
464/// then open up the target directory, and return the file name.
465///
466/// Otherwise, reborrow the directory and return the file name.
467///
468/// It is an error if the target path does not name a file.
469fn subdir_of<'d, 'p>(d: &'d Dir, p: &'p Path) -> io::Result<(DirOwnedOrBorrowed<'d>, &'p OsStr)> {
470    let name = p
471        .file_name()
472        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "Not a file name"))?;
473    let r = if let Some(subdir) = p
474        .parent()
475        .filter(|v| !v.as_os_str().is_empty())
476        .map(|p| d.open_dir(p))
477    {
478        DirOwnedOrBorrowed::Owned(subdir?)
479    } else {
480        DirOwnedOrBorrowed::Borrowed(d)
481    };
482    Ok((r, name))
483}
484
485#[cfg(any(target_os = "android", target_os = "linux"))]
486/// A thin wrapper for [`openat2`] but that retries on interruption.
487fn openat2_with_retry(
488    dirfd: impl std::os::fd::AsFd,
489    path: impl AsRef<Path>,
490    oflags: rustix::fs::OFlags,
491    mode: rustix::fs::Mode,
492    resolve: rustix::fs::ResolveFlags,
493) -> rustix::io::Result<std::os::fd::OwnedFd> {
494    let dirfd = dirfd.as_fd();
495    let path = path.as_ref();
496    // We loop forever on EAGAIN right now. The cap-std version loops just 4 times,
497    // which seems really arbitrary.
498    path.into_with_c_str(|path_c_str| 'start: loop {
499        match rustix::fs::openat2(dirfd, path_c_str, oflags, mode, resolve) {
500            Ok(file) => {
501                return Ok(file);
502            }
503            Err(rustix::io::Errno::AGAIN | rustix::io::Errno::INTR) => {
504                continue 'start;
505            }
506            Err(e) => {
507                return Err(e);
508            }
509        }
510    })
511}
512
513#[cfg(any(target_os = "android", target_os = "linux"))]
514fn is_mountpoint_impl_statx(root: &Dir, path: &Path) -> Result<Option<bool>> {
515    // https://github.com/systemd/systemd/blob/8fbf0a214e2fe474655b17a4b663122943b55db0/src/basic/mountpoint-util.c#L176
516    use rustix::fs::StatxAttributes;
517    use rustix::fs::{AtFlags, StatxFlags};
518    use std::os::fd::AsFd;
519
520    // SAFETY(unwrap): We can infallibly convert an i32 into a u64.
521    match rustix::fs::statx(
522        root.as_fd(),
523        path,
524        AtFlags::NO_AUTOMOUNT | AtFlags::SYMLINK_NOFOLLOW,
525        StatxFlags::empty(),
526    ) {
527        Ok(r) => {
528            let present = r.stx_attributes_mask.contains(StatxAttributes::MOUNT_ROOT);
529            Ok(present.then_some(r.stx_attributes.contains(StatxAttributes::MOUNT_ROOT)))
530        }
531        Err(e) if e == rustix::io::Errno::NOSYS => Ok(None),
532        Err(e) => Err(e.into()),
533    }
534}
535
536/// Open the target directory, but return Ok(None) if this would cross a mount point.
537#[cfg(any(target_os = "android", target_os = "linux"))]
538fn impl_open_dir_noxdev(
539    d: &Dir,
540    path: impl AsRef<std::path::Path>,
541) -> std::io::Result<Option<Dir>> {
542    use rustix::fs::{Mode, OFlags, ResolveFlags};
543    match openat2_with_retry(
544        d,
545        path,
546        OFlags::CLOEXEC | OFlags::DIRECTORY | OFlags::NOFOLLOW,
547        Mode::empty(),
548        ResolveFlags::NO_XDEV | ResolveFlags::BENEATH,
549    ) {
550        Ok(r) => Ok(Some(Dir::reopen_dir(&r)?)),
551        Err(e) if e == rustix::io::Errno::XDEV => Ok(None),
552        Err(e) => Err(e.into()),
553    }
554}
555
556/// Implementation of a recursive directory walk
557fn walk_inner<E>(
558    d: &Dir,
559    path: &mut PathBuf,
560    callback: &mut dyn FnMut(&WalkComponent) -> WalkResult<E>,
561    config: &WalkConfiguration,
562) -> std::result::Result<(), E>
563where
564    E: From<std::io::Error>,
565{
566    let entries = d.entries()?;
567    // If sorting is enabled, then read all entries now and sort them.
568    let entries: Box<dyn Iterator<Item = Result<DirEntry>>> =
569        if let Some(sorter) = config.sorter.as_ref() {
570            let mut entries = entries.collect::<Result<Vec<_>>>()?;
571            entries.sort_by(|a, b| sorter(a, b));
572            Box::new(entries.into_iter().map(Ok))
573        } else {
574            Box::new(entries.into_iter())
575        };
576    // Operate on each entry
577    for entry in entries {
578        let entry = &entry?;
579        // Gather basic data
580        let ty = entry.file_type()?;
581        let is_dir = ty.is_dir();
582        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        }
591        // The path provided to the user includes the current filename
592        path.push(&name);
593        let filename = &name;
594        let component = WalkComponent {
595            path,
596            dir: d,
597            filename,
598            file_type: ty,
599            entry,
600        };
601        // Invoke the user path:callback
602        let flow = callback(&component)?;
603        // Did the callback tell us to stop iteration?
604        let is_break = matches!(flow, std::ops::ControlFlow::Break(()));
605        // Handle the non-directory case first.
606        if !is_dir {
607            path.pop();
608            // If we got a break, then we're completely done.
609            if is_break {
610                return Ok(());
611            } else {
612                // Otherwise, process the next entry.
613                continue;
614            }
615        } else if is_break {
616            // For break on a directory, we continue processing the next entry.
617            path.pop();
618            continue;
619        }
620        // We're operating on a directory, and the callback must have told
621        // us to continue.
622        debug_assert!(matches!(flow, std::ops::ControlFlow::Continue(())));
623        // Open the child directory, using the noxdev API if
624        // we're configured not to cross devices,
625        #[cfg(any(target_os = "android", target_os = "linux"))]
626        let d = {
627            if !config.noxdev {
628                entry.open_dir()?
629            } else if let Some(d) = impl_open_dir_noxdev(d, filename)? {
630                d
631            } else {
632                path.pop();
633                continue;
634            }
635        };
636
637        #[cfg(not(any(target_os = "android", target_os = "linux")))]
638        let d = entry.open_dir()?;
639
640        // Recurse into the target directory
641        walk_inner(&d, path, callback, config)?;
642        path.pop();
643    }
644    Ok(())
645}
646
647// Ensure that the target path isn't absolute, and doesn't
648// have any parent references.
649#[cfg_attr(
650    not(any(target_os = "android", target_os = "linux", test)),
651    allow(dead_code)
652)]
653pub(crate) fn validate_relpath_no_uplinks(path: &Path) -> Result<&Path> {
654    let is_absolute = path.is_absolute();
655    let contains_uplinks = path
656        .components()
657        .any(|e| e == std::path::Component::ParentDir);
658    if is_absolute || contains_uplinks {
659        Err(crate::escape_attempt())
660    } else {
661        Ok(path)
662    }
663}
664
665impl CapStdExtDirExt for Dir {
666    fn open_optional(&self, path: impl AsRef<Path>) -> Result<Option<File>> {
667        map_optional(self.open(path.as_ref()))
668    }
669
670    fn open_dir_optional(&self, path: impl AsRef<Path>) -> Result<Option<Dir>> {
671        map_optional(self.open_dir(path.as_ref()))
672    }
673
674    #[cfg(any(target_os = "android", target_os = "linux"))]
675    fn open_dir_rooted_ext(&self, path: impl AsRef<Path>) -> Result<crate::RootDir> {
676        crate::RootDir::new(self, path)
677    }
678
679    /// Open the target directory, but return Ok(None) if this would cross a mount point.
680    #[cfg(any(target_os = "android", target_os = "linux"))]
681    fn open_dir_noxdev(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<Option<Dir>> {
682        impl_open_dir_noxdev(self, path)
683    }
684
685    fn ensure_dir_with(
686        &self,
687        p: impl AsRef<Path>,
688        builder: &cap_std::fs::DirBuilder,
689    ) -> Result<bool> {
690        let p = p.as_ref();
691        match self.create_dir_with(p, builder) {
692            Ok(()) => Ok(true),
693            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
694                if !self.symlink_metadata(p)?.is_dir() {
695                    // TODO use https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.NotADirectory
696                    // once it's stable.
697                    return Err(io::Error::other("Found non-directory"));
698                }
699                Ok(false)
700            }
701            Err(e) => Err(e),
702        }
703    }
704
705    fn metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>> {
706        map_optional(self.metadata(path.as_ref()))
707    }
708
709    fn symlink_metadata_optional(&self, path: impl AsRef<Path>) -> Result<Option<Metadata>> {
710        map_optional(self.symlink_metadata(path.as_ref()))
711    }
712
713    fn remove_file_optional(&self, path: impl AsRef<Path>) -> Result<bool> {
714        match self.remove_file(path.as_ref()) {
715            Ok(()) => Ok(true),
716            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
717            Err(e) => Err(e),
718        }
719    }
720
721    fn remove_all_optional(&self, path: impl AsRef<Path>) -> Result<bool> {
722        let path = path.as_ref();
723        // This is obviously racy, but correctly matching on the errors
724        // runs into the fact that e.g. https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.NotADirectory
725        // is unstable right now.
726        let meta = match self.symlink_metadata_optional(path)? {
727            Some(m) => m,
728            None => return Ok(false),
729        };
730        if meta.is_dir() {
731            self.remove_dir_all(path)?;
732        } else {
733            self.remove_file(path)?;
734        }
735        Ok(true)
736    }
737
738    fn read_optional(&self, path: impl AsRef<Path>) -> Result<Option<Vec<u8>>> {
739        let mut r = Vec::new();
740        let Some(mut f) = self.open_optional(path.as_ref())? else {
741            return Ok(None);
742        };
743        f.read_to_end(&mut r)?;
744        Ok(Some(r))
745    }
746
747    fn read_to_string_optional(&self, path: impl AsRef<Path>) -> Result<Option<String>> {
748        let mut r = String::new();
749        let Some(mut f) = self.open_optional(path.as_ref())? else {
750            return Ok(None);
751        };
752        f.read_to_string(&mut r)?;
753        Ok(Some(r))
754    }
755
756    #[cfg(unix)]
757    fn update_timestamps(&self, path: impl AsRef<Path>) -> Result<()> {
758        use rustix::fd::AsFd;
759        use rustix::fs::UTIME_NOW;
760
761        let path = path.as_ref();
762        let now = rustix::fs::Timespec {
763            tv_sec: 0,
764            tv_nsec: UTIME_NOW,
765        };
766        // https://github.com/bytecodealliance/rustix/commit/69af396b79e296717bece8148b1f6165b810885c
767        // means that Timespec only implements Copy on 64 bit right now.
768        #[allow(clippy::clone_on_copy)]
769        let times = rustix::fs::Timestamps {
770            last_access: now.clone(),
771            last_modification: now.clone(),
772        };
773        rustix::fs::utimensat(
774            self.as_fd(),
775            path,
776            &times,
777            rustix::fs::AtFlags::SYMLINK_NOFOLLOW,
778        )?;
779        Ok(())
780    }
781
782    #[cfg(not(windows))]
783    fn atomic_replace_with<F, T, E>(
784        &self,
785        destname: impl AsRef<Path>,
786        f: F,
787    ) -> std::result::Result<T, E>
788    where
789        F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
790        E: From<std::io::Error>,
791    {
792        let destname = destname.as_ref();
793        let (d, name) = subdir_of(self, destname)?;
794        let existing_metadata = d.symlink_metadata_optional(destname)?;
795        // If the target is already a file, then acquire its mode, which we will preserve by default.
796        // We don't follow symlinks here for replacement, and so we definitely don't want to pick up its mode.
797        let existing_perms = existing_metadata
798            .filter(|m| m.is_file())
799            .map(|m| m.permissions());
800        let mut t = cap_tempfile::TempFile::new(&d)?;
801        // Apply the permissions, if we have them
802        if let Some(existing_perms) = existing_perms {
803            t.as_file_mut().set_permissions(existing_perms)?;
804        }
805        // We always operate in terms of buffered writes
806        let mut bufw = std::io::BufWriter::new(t);
807        // Call the provided closure to generate the file content
808        let r = f(&mut bufw)?;
809        // Flush the buffer, get the TempFile
810        t = bufw.into_inner().map_err(From::from)?;
811        // fsync the TempFile
812        t.as_file().sync_all()?;
813        // rename the TempFile
814        t.replace(name)?;
815        // fsync the directory
816        d.open(".")?.sync_all()?;
817        Ok(r)
818    }
819
820    #[cfg(not(windows))]
821    fn atomic_write(&self, destname: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
822        self.atomic_replace_with(destname, |f| f.write_all(contents.as_ref()))
823    }
824
825    #[cfg(not(windows))]
826    fn atomic_write_with_perms(
827        &self,
828        destname: impl AsRef<Path>,
829        contents: impl AsRef<[u8]>,
830        perms: cap_std::fs::Permissions,
831    ) -> Result<()> {
832        self.atomic_replace_with(destname, |f| -> io::Result<_> {
833            // If the user is overriding the permissions, let's make the default be
834            // writable by us but not readable by anyone else, in case it has
835            // secret data.
836            #[cfg(unix)]
837            {
838                use cap_std::fs::PermissionsExt;
839                let perms = cap_std::fs::Permissions::from_mode(0o600);
840                f.get_mut().as_file_mut().set_permissions(perms)?;
841            }
842            f.write_all(contents.as_ref())?;
843            f.flush()?;
844            f.get_mut().as_file_mut().set_permissions(perms)?;
845            Ok(())
846        })
847    }
848
849    #[cfg(unix)]
850    fn reopen_as_ownedfd(&self) -> Result<OwnedFd> {
851        use rustix::fs::{Mode, OFlags};
852        use std::os::fd::AsFd;
853        rustix::fs::openat(
854            self.as_fd(),
855            ".",
856            OFlags::CLOEXEC | OFlags::DIRECTORY | OFlags::RDONLY,
857            Mode::empty(),
858        )
859        .map_err(Into::into)
860    }
861
862    #[cfg(any(target_os = "android", target_os = "linux"))]
863    fn is_mountpoint(&self, path: impl AsRef<Path>) -> Result<Option<bool>> {
864        is_mountpoint_impl_statx(self, path.as_ref())
865    }
866
867    #[cfg(any(target_os = "android", target_os = "linux"))]
868    fn getxattr(&self, path: impl AsRef<Path>, key: impl AsRef<OsStr>) -> Result<Option<Vec<u8>>> {
869        crate::xattrs::impl_getxattr(self, path.as_ref(), key.as_ref())
870    }
871
872    #[cfg(any(target_os = "android", target_os = "linux"))]
873    fn listxattrs(&self, path: impl AsRef<Path>) -> Result<crate::XattrList> {
874        crate::xattrs::impl_listxattrs(self, path.as_ref())
875    }
876
877    #[cfg(any(target_os = "android", target_os = "linux"))]
878    fn setxattr(
879        &self,
880        path: impl AsRef<Path>,
881        key: impl AsRef<OsStr>,
882        value: impl AsRef<[u8]>,
883    ) -> Result<()> {
884        crate::xattrs::impl_setxattr(self, path.as_ref(), key.as_ref(), value.as_ref())
885    }
886
887    fn walk<C, E>(&self, config: &WalkConfiguration, mut callback: C) -> std::result::Result<(), E>
888    where
889        C: FnMut(&WalkComponent) -> WalkResult<E>,
890        E: From<std::io::Error>,
891    {
892        let mut pb = config
893            .path_base
894            .as_ref()
895            .map(|v| v.to_path_buf())
896            .unwrap_or_default();
897        walk_inner(self, &mut pb, &mut callback, config)
898    }
899}
900
901// Implementation for the Utf8 variant of Dir. You shouldn't need to add
902// any real logic here, just delegate to the non-UTF8 version via `as_cap_std()`
903// in general.
904#[cfg(feature = "fs_utf8")]
905impl CapStdExtDirExtUtf8 for cap_std::fs_utf8::Dir {
906    fn open_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<fs_utf8::File>> {
907        map_optional(self.open(path.as_ref()))
908    }
909
910    fn open_dir_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<fs_utf8::Dir>> {
911        map_optional(self.open_dir(path.as_ref()))
912    }
913
914    fn ensure_dir_with(
915        &self,
916        p: impl AsRef<Utf8Path>,
917        builder: &cap_std::fs::DirBuilder,
918    ) -> Result<bool> {
919        self.as_cap_std()
920            .ensure_dir_with(p.as_ref().as_std_path(), builder)
921    }
922
923    fn metadata_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<Metadata>> {
924        self.as_cap_std()
925            .metadata_optional(path.as_ref().as_std_path())
926    }
927
928    fn symlink_metadata_optional(&self, path: impl AsRef<Utf8Path>) -> Result<Option<Metadata>> {
929        self.as_cap_std()
930            .symlink_metadata_optional(path.as_ref().as_std_path())
931    }
932
933    fn remove_file_optional(&self, path: impl AsRef<Utf8Path>) -> Result<bool> {
934        self.as_cap_std()
935            .remove_file_optional(path.as_ref().as_std_path())
936    }
937
938    fn remove_all_optional(&self, path: impl AsRef<Utf8Path>) -> Result<bool> {
939        self.as_cap_std()
940            .remove_all_optional(path.as_ref().as_std_path())
941    }
942
943    #[cfg(unix)]
944    fn update_timestamps(&self, path: impl AsRef<Utf8Path>) -> Result<()> {
945        self.as_cap_std()
946            .update_timestamps(path.as_ref().as_std_path())
947    }
948
949    fn atomic_replace_with<F, T, E>(
950        &self,
951        destname: impl AsRef<Utf8Path>,
952        f: F,
953    ) -> std::result::Result<T, E>
954    where
955        F: FnOnce(&mut std::io::BufWriter<cap_tempfile::TempFile>) -> std::result::Result<T, E>,
956        E: From<std::io::Error>,
957    {
958        self.as_cap_std()
959            .atomic_replace_with(destname.as_ref().as_std_path(), f)
960    }
961
962    fn atomic_write(
963        &self,
964        destname: impl AsRef<Utf8Path>,
965        contents: impl AsRef<[u8]>,
966    ) -> Result<()> {
967        self.as_cap_std()
968            .atomic_write(destname.as_ref().as_std_path(), contents)
969    }
970
971    fn atomic_write_with_perms(
972        &self,
973        destname: impl AsRef<Utf8Path>,
974        contents: impl AsRef<[u8]>,
975        perms: cap_std::fs::Permissions,
976    ) -> Result<()> {
977        self.as_cap_std()
978            .atomic_write_with_perms(destname.as_ref().as_std_path(), contents, perms)
979    }
980
981    fn filenames_filtered_sorted_by<F, C>(&self, mut f: F, mut compare: C) -> Result<Vec<String>>
982    where
983        F: FnMut(&fs_utf8::DirEntry, &str) -> bool,
984        C: FnMut(&str, &str) -> std::cmp::Ordering,
985    {
986        let mut r =
987            self.entries()?
988                .try_fold(Vec::new(), |mut acc, ent| -> Result<Vec<String>> {
989                    let ent = ent?;
990                    let name = ent.file_name()?;
991                    if f(&ent, name.as_str()) {
992                        acc.push(name);
993                    }
994                    Ok(acc)
995                })?;
996        r.sort_by(|a, b| compare(a.as_str(), b.as_str()));
997        Ok(r)
998    }
999}
1000
1001#[cfg(test)]
1002mod tests {
1003    use std::path::Path;
1004
1005    use super::*;
1006
1007    #[test]
1008    fn test_validate_relpath_no_uplinks() {
1009        let ok_cases = ["foo", "foo/bar", "foo/bar/"];
1010        #[cfg(unix)]
1011        let err_cases = ["/foo", "/", "../foo", "foo/../bar"];
1012        #[cfg(windows)]
1013        let err_cases = ["C:\\foo", "../foo", "foo/../bar"];
1014
1015        for case in ok_cases {
1016            assert!(validate_relpath_no_uplinks(Path::new(case)).is_ok());
1017        }
1018        for case in err_cases {
1019            assert!(validate_relpath_no_uplinks(Path::new(case)).is_err());
1020        }
1021    }
1022}