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) hidden: Option<bool>,
212    pub(crate) directory_identity: Option<DirectoryIdentity>,
213    pub(crate) skip_reason: Option<WalkSkipReason>,
214}
215
216impl WalkEntry {
217    #[must_use]
218    pub fn path(&self) -> &Path {
219        &self.path
220    }
221
222    /// Consumes the entry and returns its owned path without cloning.
223    #[must_use]
224    pub fn into_path(self) -> PathBuf {
225        self.path
226    }
227
228    #[must_use]
229    pub fn relative_path(&self) -> &Path {
230        let mut components = self.path.components();
231        for _ in 0..self.root_components {
232            if components.next().is_none() {
233                return self.path.as_path();
234            }
235        }
236        components.as_path()
237    }
238
239    #[must_use]
240    pub fn file_name(&self) -> &OsStr {
241        self.path
242            .file_name()
243            .unwrap_or_else(|| self.path.as_os_str())
244    }
245
246    #[must_use]
247    pub const fn depth(&self) -> usize {
248        self.depth
249    }
250
251    #[must_use]
252    pub const fn is_file(&self) -> bool {
253        self.is_file
254    }
255
256    #[must_use]
257    pub const fn is_dir(&self) -> bool {
258        self.is_directory
259    }
260
261    #[must_use]
262    pub const fn is_symlink(&self) -> bool {
263        self.is_symlink
264    }
265
266    #[must_use]
267    pub const fn bytes(&self) -> Option<u64> {
268        self.bytes
269    }
270
271    /// Snapshot evidence captured with file metadata, when requested.
272    #[must_use]
273    pub const fn version(&self) -> Option<FileVersion> {
274        self.version
275    }
276
277    pub(crate) const fn hidden(&self) -> Option<bool> {
278        self.hidden
279    }
280
281    #[must_use]
282    pub const fn skip_reason(&self) -> Option<WalkSkipReason> {
283        self.skip_reason
284    }
285
286    pub(crate) fn rebase(&mut self, root: &Path, depth_offset: usize) {
287        self.root_components = root.components().count();
288        self.depth += depth_offset;
289    }
290
291    pub(crate) fn clear_depth_skip(&mut self) {
292        if self.skip_reason == Some(WalkSkipReason::MaxDepth) {
293            self.skip_reason = None;
294        }
295    }
296
297    pub(crate) const fn directory_identity(&self) -> Option<DirectoryIdentity> {
298        self.directory_identity
299    }
300}
301
302const fn operation_name(operation: WalkOperation) -> &'static str {
303    match operation {
304        WalkOperation::Canonicalize => "canonicalize",
305        WalkOperation::ReadDirectory => "read directory",
306        WalkOperation::ReadEntry => "read entry",
307        WalkOperation::ReadMetadata => "read metadata",
308        WalkOperation::ScheduleWorker => "schedule worker",
309    }
310}