Skip to main content

weavatrix_scan/walker/
construction.rs

1use super::{
2    Arc, DirectoryEntries, DirectoryFrame, DirectoryIdentity, EntryFilter, EntrySorter,
3    FileSystemId, HashSet, Path, PathBuf, PendingDirectory, RootSymlinkPolicy, VecDeque, WalkError,
4    WalkOperation, WalkOptions, Walker, directory_info, fs, io,
5};
6
7impl Walker {
8    /// Creates a walker with the default traversal policy.
9    ///
10    /// # Errors
11    ///
12    /// Returns an error when the root cannot be resolved or inspected.
13    pub fn new(root: impl AsRef<Path>) -> Result<Self, WalkError> {
14        Self::with_options(root, WalkOptions::default())
15    }
16
17    /// Creates a walker with an explicit traversal policy.
18    ///
19    /// # Errors
20    ///
21    /// Returns an error when the root cannot be resolved or inspected.
22    pub fn with_options(root: impl AsRef<Path>, options: WalkOptions) -> Result<Self, WalkError> {
23        Self::with_behavior(root, options, None, None, None, false)
24    }
25
26    pub(crate) fn with_behavior(
27        root: impl AsRef<Path>,
28        options: WalkOptions,
29        sorter: Option<EntrySorter>,
30        filter: Option<EntryFilter>,
31        skip_stdout: Option<crate::report::FileIdentity>,
32        contents_first: bool,
33    ) -> Result<Self, WalkError> {
34        let requested = root.as_ref();
35        let options = options.normalized();
36        if options.root_symlink_policy == RootSymlinkPolicy::Reject {
37            let metadata = fs::symlink_metadata(requested).map_err(|source| {
38                WalkError::new(requested, 0, WalkOperation::ReadMetadata, source)
39            })?;
40            if metadata.file_type().is_symlink() {
41                return Err(WalkError::new(
42                    requested,
43                    0,
44                    WalkOperation::ReadMetadata,
45                    io::Error::new(
46                        io::ErrorKind::InvalidInput,
47                        "root symlink rejected by policy",
48                    ),
49                ));
50            }
51        }
52        let canonical = if options.follow_links || options.same_file_system {
53            requested.canonicalize().map_err(|source| {
54                WalkError::new(requested, 0, WalkOperation::Canonicalize, source)
55            })?
56        } else if requested.is_absolute() {
57            requested.to_path_buf()
58        } else {
59            std::env::current_dir()
60                .map_err(|source| {
61                    WalkError::new(requested, 0, WalkOperation::Canonicalize, source)
62                })?
63                .join(requested)
64        };
65        let metadata = fs::metadata(&canonical)
66            .map_err(|source| WalkError::new(&canonical, 0, WalkOperation::ReadMetadata, source))?;
67        let root_directory_info =
68            if metadata.is_dir() && (options.follow_links || options.same_file_system) {
69                Some(directory_info(&canonical, &metadata).map_err(|source| {
70                    WalkError::new(&canonical, 0, WalkOperation::ReadMetadata, source)
71                })?)
72            } else {
73                None
74            };
75        let root_file_system = if options.same_file_system {
76            root_directory_info.map(|info| info.file_system)
77        } else {
78            None
79        };
80        let (root_bytes, root_version) = if options.collect_metadata && metadata.is_file() {
81            (
82                Some(metadata.len()),
83                Some(crate::file_version::from_metadata(&metadata)),
84            )
85        } else {
86            (None, None)
87        };
88        let plain_entries = !options.follow_links
89            && !options.same_file_system
90            && !options.collect_metadata
91            && options.max_depth.is_none()
92            && options.min_depth == 0
93            && filter.is_none()
94            && skip_stdout.is_none()
95            && !contents_first;
96        let root = Arc::new(canonical.clone());
97        Ok(Self {
98            root: Arc::clone(&root),
99            root_components: canonical.components().count(),
100            root_file_type: Some(metadata.file_type()),
101            root_bytes,
102            root_version,
103            root_file_system,
104            root_directory_info,
105            options,
106            frames: Vec::new(),
107            open_handles: 0,
108            yield_root: true,
109            pending_directory: None,
110            skip_pending_directory: false,
111            active_directories: HashSet::new(),
112            finished: false,
113            sorter,
114            filter,
115            skip_stdout,
116            contents_first,
117            deferred_entry: None,
118            plain_entries,
119        })
120    }
121
122    pub(crate) fn from_known_directory(
123        root: &Arc<PathBuf>,
124        directory: PathBuf,
125        depth: usize,
126        options: WalkOptions,
127        root_file_system: Option<FileSystemId>,
128    ) -> Self {
129        Self::from_known_directory_with_ancestry(
130            root,
131            directory,
132            depth,
133            options,
134            root_file_system,
135            None,
136            HashSet::new(),
137        )
138    }
139
140    pub(crate) fn from_known_directory_with_ancestry(
141        root: &Arc<PathBuf>,
142        directory: PathBuf,
143        depth: usize,
144        options: WalkOptions,
145        root_file_system: Option<FileSystemId>,
146        directory_identity: Option<DirectoryIdentity>,
147        active_directories: HashSet<DirectoryIdentity>,
148    ) -> Self {
149        let options = options.normalized();
150        let root_components = root.components().count();
151        let plain_entries = !options.follow_links
152            && !options.same_file_system
153            && !options.collect_metadata
154            && options.max_depth.is_none()
155            && options.min_depth == 0;
156        Self {
157            root: Arc::clone(root),
158            root_components,
159            root_file_type: None,
160            root_bytes: None,
161            root_version: None,
162            root_file_system,
163            root_directory_info: None,
164            options,
165            frames: Vec::new(),
166            open_handles: 0,
167            yield_root: false,
168            pending_directory: Some(PendingDirectory {
169                path: directory,
170                depth,
171                identity: directory_identity,
172                post_entry: None,
173            }),
174            skip_pending_directory: false,
175            active_directories,
176            finished: false,
177            sorter: None,
178            filter: None,
179            skip_stdout: None,
180            contents_first: false,
181            deferred_entry: None,
182            plain_entries,
183        }
184    }
185
186    #[must_use]
187    pub fn root(&self) -> &Path {
188        self.root.as_path()
189    }
190
191    #[must_use]
192    pub const fn options(&self) -> &WalkOptions {
193        &self.options
194    }
195
196    /// Prevents descent into the directory returned by the previous `next`.
197    pub fn skip_current_dir(&mut self) {
198        if self.pending_directory.is_some() {
199            self.skip_pending_directory = true;
200        }
201    }
202
203    pub(crate) fn schedule_pending_directory(&mut self) -> Option<WalkError> {
204        let pending = self.pending_directory.take()?;
205        if self.skip_pending_directory {
206            self.skip_pending_directory = false;
207            return None;
208        }
209        if self.open_handles >= self.options.max_open {
210            self.buffer_oldest_open_directory();
211        }
212        match fs::read_dir(&pending.path) {
213            Ok(entries) => {
214                if let Some(identity) = pending.identity {
215                    self.active_directories.insert(identity);
216                }
217                let (entries, opened) = match self.sorter.as_ref() {
218                    None => (DirectoryEntries::Open(entries), true),
219                    Some(sorter) => {
220                        let mut entries = entries.collect::<Vec<_>>();
221                        entries.sort_by(|left, right| match (left, right) {
222                            (Ok(left), Ok(right)) => sorter(left, right),
223                            (Err(_), Ok(_)) => std::cmp::Ordering::Less,
224                            (Ok(_), Err(_)) => std::cmp::Ordering::Greater,
225                            (Err(_), Err(_)) => std::cmp::Ordering::Equal,
226                        });
227                        (DirectoryEntries::Buffered(entries.into()), false)
228                    }
229                };
230                self.frames.push(DirectoryFrame {
231                    path: pending.path,
232                    depth: pending.depth,
233                    entries,
234                    identity: pending.identity,
235                    post_entry: pending.post_entry,
236                });
237                self.open_handles += usize::from(opened);
238                None
239            }
240            Err(source) => {
241                self.deferred_entry = pending.post_entry;
242                Some(WalkError::new(
243                    pending.path,
244                    pending.depth,
245                    WalkOperation::ReadDirectory,
246                    source,
247                ))
248            }
249        }
250    }
251
252    fn buffer_oldest_open_directory(&mut self) {
253        let Some(index) = self.frames.iter().position(|frame| frame.entries.is_open()) else {
254            return;
255        };
256        let placeholder = DirectoryEntries::Buffered(VecDeque::new());
257        let entries = std::mem::replace(&mut self.frames[index].entries, placeholder);
258        if let DirectoryEntries::Open(entries) = entries {
259            self.frames[index].entries = DirectoryEntries::Buffered(entries.collect());
260            self.open_handles -= 1;
261        }
262    }
263}