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