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 ;
1112use cap_std:: fs:: { Dir , File , Metadata } ;
1213use cap_tempfile:: cap_std;
14+ use cap_tempfile:: cap_std:: fs:: DirEntry ;
1315use rustix:: path:: Arg ;
16+ use std:: cmp:: Ordering ;
1417use std:: ffi:: OsStr ;
1518use std:: io:: Result ;
1619use std:: io:: { self , Write } ;
1720use std:: ops:: Deref ;
1821use std:: os:: fd:: OwnedFd ;
19- use std:: path:: Path ;
22+ use std:: path:: { Path , PathBuf } ;
2023
2124#[ cfg( feature = "fs_utf8" ) ]
2225use cap_std:: fs_utf8;
2326#[ cfg( feature = "fs_utf8" ) ]
2427use 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+
374565impl 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