Skip to main content

weavatrix_scan/
walk_types.rs

1use crate::report::FileVersion;
2use crate::walk_platform::DirectoryIdentity;
3use std::ffi::OsStr;
4use std::fmt;
5use std::io;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ErrorPolicy {
10    Continue,
11    Abort,
12}
13
14/// Controls whether the explicitly supplied root itself may be a symlink.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum RootSymlinkPolicy {
17    /// Resolve or traverse the requested root for backward compatibility.
18    Follow,
19    /// Reject a symlink at the final root path component.
20    Reject,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct WalkOptions {
25    pub min_depth: usize,
26    pub max_depth: Option<usize>,
27    pub max_open: usize,
28    pub same_file_system: bool,
29    pub follow_links: bool,
30    pub collect_metadata: bool,
31    pub error_policy: ErrorPolicy,
32    pub root_symlink_policy: RootSymlinkPolicy,
33}
34
35impl Default for WalkOptions {
36    fn default() -> Self {
37        Self {
38            min_depth: 0,
39            max_depth: None,
40            max_open: Self::DEFAULT_MAX_OPEN,
41            same_file_system: false,
42            follow_links: false,
43            collect_metadata: false,
44            error_policy: ErrorPolicy::Continue,
45            root_symlink_policy: RootSymlinkPolicy::Follow,
46        }
47    }
48}
49
50impl WalkOptions {
51    /// Default hard upper bound for simultaneously open directory handles.
52    pub const DEFAULT_MAX_OPEN: usize = 64;
53
54    #[must_use]
55    pub const fn with_min_depth(mut self, min_depth: usize) -> Self {
56        self.min_depth = min_depth;
57        self
58    }
59
60    #[must_use]
61    pub const fn with_max_depth(mut self, max_depth: Option<usize>) -> Self {
62        self.max_depth = max_depth;
63        self
64    }
65
66    #[must_use]
67    pub const fn with_max_open(mut self, max_open: usize) -> Self {
68        self.max_open = if max_open == 0 { 1 } else { max_open };
69        self
70    }
71
72    #[must_use]
73    pub const fn with_same_file_system(mut self, enabled: bool) -> Self {
74        self.same_file_system = enabled;
75        self
76    }
77
78    #[must_use]
79    pub const fn with_follow_links(mut self, enabled: bool) -> Self {
80        self.follow_links = enabled;
81        self
82    }
83
84    #[must_use]
85    pub const fn with_metadata(mut self, enabled: bool) -> Self {
86        self.collect_metadata = enabled;
87        self
88    }
89
90    #[must_use]
91    pub const fn with_error_policy(mut self, policy: ErrorPolicy) -> Self {
92        self.error_policy = policy;
93        self
94    }
95
96    #[must_use]
97    pub const fn with_root_symlink_policy(mut self, policy: RootSymlinkPolicy) -> Self {
98        self.root_symlink_policy = policy;
99        self
100    }
101
102    pub(crate) fn normalized(mut self) -> Self {
103        self.max_open = self.max_open.max(1);
104        if let Some(max_depth) = self.max_depth
105            && self.min_depth > max_depth
106        {
107            self.min_depth = max_depth;
108        }
109        self
110    }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum WalkOperation {
115    Canonicalize,
116    ReadDirectory,
117    ReadEntry,
118    ReadMetadata,
119    ScheduleWorker,
120}
121
122#[derive(Debug)]
123pub struct WalkError {
124    pub(crate) path: PathBuf,
125    pub(crate) depth: usize,
126    pub(crate) operation: WalkOperation,
127    pub(crate) source: io::Error,
128}
129
130impl WalkError {
131    pub(crate) fn new(
132        path: impl Into<PathBuf>,
133        depth: usize,
134        operation: WalkOperation,
135        source: io::Error,
136    ) -> Self {
137        Self {
138            path: path.into(),
139            depth,
140            operation,
141            source,
142        }
143    }
144
145    #[must_use]
146    pub fn path(&self) -> &Path {
147        &self.path
148    }
149
150    #[must_use]
151    pub const fn depth(&self) -> usize {
152        self.depth
153    }
154
155    #[must_use]
156    pub const fn operation(&self) -> WalkOperation {
157        self.operation
158    }
159
160    #[must_use]
161    pub const fn io_error(&self) -> &io::Error {
162        &self.source
163    }
164
165    pub(crate) fn into_parts(self) -> (PathBuf, io::Error) {
166        (self.path, self.source)
167    }
168
169    pub(crate) fn rebase_depth(&mut self, depth_offset: usize) {
170        self.depth += depth_offset;
171    }
172}
173
174impl fmt::Display for WalkError {
175    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176        write!(
177            formatter,
178            "{} at depth {} for {}: {}",
179            operation_name(self.operation),
180            self.depth,
181            self.path.display(),
182            self.source
183        )
184    }
185}
186
187impl std::error::Error for WalkError {
188    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
189        Some(&self.source)
190    }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum WalkSkipReason {
195    MaxDepth,
196    FileSystemBoundary,
197    PathEscape,
198    SymlinkLoop,
199}
200
201#[derive(Debug, Clone)]
202pub struct WalkEntry {
203    pub(crate) root_components: usize,
204    pub(crate) path: PathBuf,
205    pub(crate) depth: usize,
206    pub(crate) is_file: bool,
207    pub(crate) is_directory: bool,
208    pub(crate) is_symlink: bool,
209    pub(crate) bytes: Option<u64>,
210    pub(crate) version: Option<FileVersion>,
211    pub(crate) directory_identity: Option<DirectoryIdentity>,
212    pub(crate) skip_reason: Option<WalkSkipReason>,
213}
214
215impl WalkEntry {
216    #[must_use]
217    pub fn path(&self) -> &Path {
218        &self.path
219    }
220
221    /// Consumes the entry and returns its owned path without cloning.
222    #[must_use]
223    pub fn into_path(self) -> PathBuf {
224        self.path
225    }
226
227    #[must_use]
228    pub fn relative_path(&self) -> &Path {
229        let mut components = self.path.components();
230        for _ in 0..self.root_components {
231            if components.next().is_none() {
232                return self.path.as_path();
233            }
234        }
235        components.as_path()
236    }
237
238    #[must_use]
239    pub fn file_name(&self) -> &OsStr {
240        self.path
241            .file_name()
242            .unwrap_or_else(|| self.path.as_os_str())
243    }
244
245    #[must_use]
246    pub const fn depth(&self) -> usize {
247        self.depth
248    }
249
250    #[must_use]
251    pub const fn is_file(&self) -> bool {
252        self.is_file
253    }
254
255    #[must_use]
256    pub const fn is_dir(&self) -> bool {
257        self.is_directory
258    }
259
260    #[must_use]
261    pub const fn is_symlink(&self) -> bool {
262        self.is_symlink
263    }
264
265    #[must_use]
266    pub const fn bytes(&self) -> Option<u64> {
267        self.bytes
268    }
269
270    /// Snapshot evidence captured with file metadata, when requested.
271    #[must_use]
272    pub const fn version(&self) -> Option<FileVersion> {
273        self.version
274    }
275
276    #[must_use]
277    pub const fn skip_reason(&self) -> Option<WalkSkipReason> {
278        self.skip_reason
279    }
280
281    pub(crate) fn rebase(&mut self, root: &Path, depth_offset: usize) {
282        self.root_components = root.components().count();
283        self.depth += depth_offset;
284    }
285
286    pub(crate) fn clear_depth_skip(&mut self) {
287        if self.skip_reason == Some(WalkSkipReason::MaxDepth) {
288            self.skip_reason = None;
289        }
290    }
291
292    pub(crate) const fn directory_identity(&self) -> Option<DirectoryIdentity> {
293        self.directory_identity
294    }
295}
296
297const fn operation_name(operation: WalkOperation) -> &'static str {
298    match operation {
299        WalkOperation::Canonicalize => "canonicalize",
300        WalkOperation::ReadDirectory => "read directory",
301        WalkOperation::ReadEntry => "read entry",
302        WalkOperation::ReadMetadata => "read metadata",
303        WalkOperation::ScheduleWorker => "schedule worker",
304    }
305}