Skip to main content

weavatrix_scan/
walk_builder.rs

1use crate::report::FileIdentity;
2use crate::walker::{WalkEntry, WalkError, WalkOptions, Walker};
3use std::cmp::Ordering;
4use std::ffi::OsStr;
5use std::fs::DirEntry;
6use std::path::PathBuf;
7use std::sync::{Arc, Mutex, PoisonError};
8
9pub(crate) type EntrySorter = Arc<dyn Fn(&DirEntry, &DirEntry) -> Ordering + Send + Sync + 'static>;
10pub(crate) type EntryFilter = Arc<dyn Fn(&WalkEntry) -> bool + Send + Sync + 'static>;
11
12/// Configures flexible single- or multi-root walking.
13pub struct WalkBuilder {
14    roots: Vec<PathBuf>,
15    options: WalkOptions,
16    sorter: Option<EntrySorter>,
17    filter: Option<EntryFilter>,
18    skip_stdout: Option<FileIdentity>,
19    contents_first: bool,
20}
21
22impl WalkBuilder {
23    #[must_use]
24    pub fn new(root: impl Into<PathBuf>) -> Self {
25        Self {
26            roots: vec![root.into()],
27            options: WalkOptions::default(),
28            sorter: None,
29            filter: None,
30            skip_stdout: None,
31            contents_first: false,
32        }
33    }
34
35    #[must_use]
36    pub fn add_root(mut self, root: impl Into<PathBuf>) -> Self {
37        self.roots.push(root.into());
38        self
39    }
40
41    #[must_use]
42    pub const fn options(mut self, options: WalkOptions) -> Self {
43        self.options = options;
44        self
45    }
46
47    /// Sorts each directory by its native file name before descent.
48    #[must_use]
49    pub fn sort_by_file_name(mut self) -> Self {
50        self.sorter = Some(Arc::new(|left, right| {
51            left.file_name().cmp(&right.file_name())
52        }));
53        self
54    }
55
56    /// Sorts each directory with a comparator over complete directory entries.
57    #[must_use]
58    pub fn sort_by<F>(mut self, compare: F) -> Self
59    where
60        F: Fn(&DirEntry, &DirEntry) -> Ordering + Send + Sync + 'static,
61    {
62        self.sorter = Some(Arc::new(compare));
63        self
64    }
65
66    /// Sorts with a native-name comparator without lossy UTF-8 conversion.
67    #[must_use]
68    pub fn sort_by_name<F>(mut self, compare: F) -> Self
69    where
70        F: Fn(&OsStr, &OsStr) -> Ordering + Send + Sync + 'static,
71    {
72        self.sorter = Some(Arc::new(move |left, right| {
73            compare(&left.file_name(), &right.file_name())
74        }));
75        self
76    }
77
78    /// Skips a regular file that refers to redirected standard output.
79    ///
80    /// This prevents feedback loops such as scanning a tree while writing
81    /// command output into a file inside that same tree.
82    #[must_use]
83    pub fn skip_stdout(mut self, enabled: bool) -> Self {
84        self.skip_stdout = enabled.then(crate::stdout::identity).flatten();
85        self
86    }
87
88    /// Emits a directory after all accepted descendants.
89    #[must_use]
90    pub const fn contents_first(mut self, enabled: bool) -> Self {
91        self.contents_first = enabled;
92        self
93    }
94
95    /// Filters entries before emission and prunes rejected directories.
96    #[must_use]
97    pub fn filter_entry<F>(mut self, filter: F) -> Self
98    where
99        F: Fn(&WalkEntry) -> bool + Send + Sync + 'static,
100    {
101        self.filter = Some(Arc::new(filter));
102        self
103    }
104
105    /// Invokes a directory predicate before descent; files remain accepted.
106    #[must_use]
107    pub fn filter_directories<F>(mut self, filter: F) -> Self
108    where
109        F: Fn(&WalkEntry) -> bool + Send + Sync + 'static,
110    {
111        self.filter = Some(Arc::new(move |entry| !entry.is_dir() || filter(entry)));
112        self
113    }
114
115    /// Invokes one mutable directory predicate across every configured root.
116    ///
117    /// The callback is serialized behind a mutex so captured state remains
118    /// coherent if builder internals gain concurrent root traversal later.
119    #[must_use]
120    pub fn filter_directories_stateful<F>(mut self, filter: F) -> Self
121    where
122        F: FnMut(&WalkEntry) -> bool + Send + 'static,
123    {
124        let filter = Mutex::new(filter);
125        self.filter = Some(Arc::new(move |entry| {
126            if !entry.is_dir() {
127                return true;
128            }
129            let mut filter = filter.lock().unwrap_or_else(PoisonError::into_inner);
130            filter(entry)
131        }));
132        self
133    }
134
135    #[must_use]
136    pub fn build(self) -> MultiWalker {
137        MultiWalker {
138            roots: self.roots.into_iter(),
139            options: self.options,
140            sorter: self.sorter,
141            filter: self.filter,
142            skip_stdout: self.skip_stdout,
143            contents_first: self.contents_first,
144            current: None,
145        }
146    }
147}
148
149/// Iterator returned by [`WalkBuilder`].
150pub struct MultiWalker {
151    roots: std::vec::IntoIter<PathBuf>,
152    options: WalkOptions,
153    sorter: Option<EntrySorter>,
154    filter: Option<EntryFilter>,
155    skip_stdout: Option<FileIdentity>,
156    contents_first: bool,
157    current: Option<Walker>,
158}
159
160impl Iterator for MultiWalker {
161    type Item = Result<WalkEntry, WalkError>;
162
163    fn next(&mut self) -> Option<Self::Item> {
164        loop {
165            if let Some(walker) = self.current.as_mut()
166                && let Some(item) = walker.next()
167            {
168                return Some(item);
169            }
170            self.current = None;
171            let root = self.roots.next()?;
172            match Walker::with_behavior(
173                root,
174                self.options,
175                self.sorter.clone(),
176                self.filter.clone(),
177                self.skip_stdout,
178                self.contents_first,
179            ) {
180                Ok(walker) => self.current = Some(walker),
181                Err(error) => return Some(Err(error)),
182            }
183        }
184    }
185}