8
8
//!
9
9
//! [`cap_std::fs::Dir`]: https://docs.rs/cap-std/latest/cap_std/fs/struct.Dir.html
10
10
11
+ use cap_primitives:: fs:: FileType ;
11
12
use cap_std:: fs:: { Dir , File , Metadata } ;
12
13
use cap_tempfile:: cap_std;
14
+ use cap_tempfile:: cap_std:: fs:: DirEntry ;
13
15
use rustix:: path:: Arg ;
16
+ use std:: cmp:: Ordering ;
14
17
use std:: ffi:: OsStr ;
15
18
use std:: io:: Result ;
16
19
use std:: io:: { self , Write } ;
17
20
use std:: ops:: Deref ;
18
21
use std:: os:: fd:: OwnedFd ;
19
- use std:: path:: Path ;
22
+ use std:: path:: { Path , PathBuf } ;
20
23
21
24
#[ cfg( feature = "fs_utf8" ) ]
22
25
use cap_std:: fs_utf8;
23
26
#[ cfg( feature = "fs_utf8" ) ]
24
27
use fs_utf8:: camino:: Utf8Path ;
25
28
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
+
26
110
/// Extension trait for [`cap_std::fs::Dir`].
27
111
///
28
112
/// [`cap_std::fs::Dir`]: https://docs.rs/cap-std/latest/cap_std/fs/struct.Dir.html
@@ -141,6 +225,15 @@ pub trait CapStdExtDirExt {
141
225
/// In some scenarios (such as an older kernel) this currently may not be possible
142
226
/// to determine, and `None` will be returned in those cases.
143
227
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 > ;
144
237
}
145
238
146
239
#[ cfg( feature = "fs_utf8" ) ]
@@ -371,6 +464,104 @@ fn is_mountpoint_impl_statx(root: &Dir, path: &Path) -> Result<Option<bool>> {
371
464
}
372
465
}
373
466
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
+
374
565
impl CapStdExtDirExt for Dir {
375
566
fn open_optional ( & self , path : impl AsRef < Path > ) -> Result < Option < File > > {
376
567
map_optional ( self . open ( path. as_ref ( ) ) )
@@ -388,18 +579,7 @@ impl CapStdExtDirExt for Dir {
388
579
/// Open the target directory, but return Ok(None) if this would cross a mount point.
389
580
#[ cfg( any( target_os = "android" , target_os = "linux" ) ) ]
390
581
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)
403
583
}
404
584
405
585
fn ensure_dir_with (
@@ -557,6 +737,19 @@ impl CapStdExtDirExt for Dir {
557
737
fn is_mountpoint ( & self , path : impl AsRef < Path > ) -> Result < Option < bool > > {
558
738
is_mountpoint_impl_statx ( self , path. as_ref ( ) ) . map_err ( Into :: into)
559
739
}
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
+ }
560
753
}
561
754
562
755
// Implementation for the Utf8 variant of Dir. You shouldn't need to add
0 commit comments