Skip to main content

weavatrix_scan/
config.rs

1use crate::control::CancellationToken;
2use crate::file_types::{FileTypeMatch, NamedFileTypes};
3use crate::walker::WalkOptions;
4use std::collections::BTreeSet;
5use std::ffi::OsStr;
6use std::path::Path;
7use std::time::Duration;
8
9mod ignore_policy;
10mod limits;
11mod runtime;
12mod walk;
13
14pub use ignore_policy::IgnorePolicy;
15pub use limits::ScanLimits;
16
17const DEFAULT_MAX_FILE_BYTES: u64 = 1_500_000;
18const DEFAULT_IGNORE_FILES: &[&str] = &[".gitignore", ".ignore", ".weavatrixignore"];
19const DEFAULT_SKIP_DIRECTORIES: &[&str] = &[
20    ".git",
21    ".hg",
22    ".svn",
23    ".venv",
24    "__pycache__",
25    "build",
26    "coverage",
27    "dist",
28    "node_modules",
29    "target",
30    "vendor",
31];
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum StandardSkips {
35    Enabled,
36    Disabled,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum EvidenceMode {
41    Complete,
42    SelectedFiles,
43}
44
45/// Controls how persistent content hashes are validated before reuse.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum CacheValidationPolicy {
48    /// Trust stable size, timestamp and available native identity evidence.
49    Fast,
50    /// Read a whole-file 128-bit fingerprint before reusing the prior SHA-256.
51    Strict,
52}
53
54/// Controls post-read snapshot verification for newly opened content.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum ContentValidationPolicy {
57    /// Verify the opened handle against discovery evidence before reading.
58    /// This is appropriate for latency-sensitive local search.
59    Fast,
60    /// Also re-check native file evidence after reading to reject concurrent
61    /// same-size modifications.
62    Strict,
63}
64
65#[derive(Debug, Clone)]
66#[allow(clippy::struct_excessive_bools)]
67pub struct ScanOptions {
68    pub max_file_bytes: u64,
69    pub extensions: BTreeSet<String>,
70    /// Reusable named file-pattern groups, combined with `extensions`.
71    pub file_types: NamedFileTypes,
72    pub ignore_files: Vec<String>,
73    /// High-precedence include/exclude globs using `ignore::Override` syntax.
74    pub override_rules: Vec<String>,
75    /// Controls parent, repository-exclude and global Git ignore sources.
76    pub ignore_policy: IgnorePolicy,
77    /// Match ignore patterns without ASCII case sensitivity.
78    pub ignore_case_insensitive: bool,
79    /// Skip dot-prefixed and native Windows-hidden entries unless included.
80    pub skip_hidden: bool,
81    pub standard_skips: StandardSkips,
82    pub hash_file_contents: bool,
83    pub detect_binary_files: bool,
84    /// Record typed evidence for entries excluded by policy.
85    pub evidence: EvidenceMode,
86    /// Traversal and content-inspection workers. Zero selects available
87    /// parallelism. Retained as the shared backward-compatible default.
88    pub parallelism: usize,
89    /// Optional traversal-only worker override. `Some(0)` selects available
90    /// parallelism independently of content inspection.
91    pub traversal_parallelism: Option<usize>,
92    /// Optional content-inspection worker override. `Some(0)` selects
93    /// available parallelism independently of traversal.
94    pub content_parallelism: Option<usize>,
95    /// Whole-scan resource bounds. All limits are disabled by default.
96    pub limits: ScanLimits,
97    /// Optional cooperative cancellation signal.
98    pub cancellation: Option<CancellationToken>,
99    /// Persistent hash validation policy.
100    pub cache_validation: CacheValidationPolicy,
101    /// New content-read validation policy.
102    pub content_validation: ContentValidationPolicy,
103    /// Low-level traversal policy.
104    pub walk: WalkOptions,
105}
106
107impl Default for ScanOptions {
108    fn default() -> Self {
109        Self {
110            max_file_bytes: DEFAULT_MAX_FILE_BYTES,
111            extensions: BTreeSet::new(),
112            file_types: NamedFileTypes::default(),
113            ignore_files: DEFAULT_IGNORE_FILES
114                .iter()
115                .map(ToString::to_string)
116                .collect(),
117            override_rules: Vec::new(),
118            ignore_policy: IgnorePolicy::default(),
119            ignore_case_insensitive: false,
120            skip_hidden: false,
121            standard_skips: StandardSkips::Enabled,
122            hash_file_contents: true,
123            detect_binary_files: true,
124            evidence: EvidenceMode::Complete,
125            parallelism: 0,
126            traversal_parallelism: None,
127            content_parallelism: None,
128            limits: ScanLimits::default(),
129            cancellation: None,
130            cache_validation: CacheValidationPolicy::Fast,
131            content_validation: ContentValidationPolicy::Strict,
132            walk: WalkOptions::default().with_metadata(true),
133        }
134    }
135}
136
137impl ScanOptions {
138    #[must_use]
139    pub fn with_extensions<I, S>(mut self, extensions: I) -> Self
140    where
141        I: IntoIterator<Item = S>,
142        S: AsRef<str>,
143    {
144        self.extensions = extensions
145            .into_iter()
146            .map(|item| item.as_ref().trim_start_matches('.').to_ascii_lowercase())
147            .collect();
148        self
149    }
150
151    /// Replaces named file-type definitions and selections.
152    #[must_use]
153    pub fn with_file_types(mut self, file_types: NamedFileTypes) -> Self {
154        self.file_types = file_types;
155        self
156    }
157
158    #[must_use]
159    pub fn with_ignore_files<I, S>(mut self, names: I) -> Self
160    where
161        I: IntoIterator<Item = S>,
162        S: AsRef<str>,
163    {
164        self.ignore_files = names
165            .into_iter()
166            .map(|item| item.as_ref().to_owned())
167            .collect();
168        self
169    }
170
171    /// Replaces request-level override globs.
172    ///
173    /// Like `ignore::Override`, ordinary patterns include matching paths and
174    /// leading `!` patterns exclude them.
175    #[must_use]
176    pub fn with_override_rules<I, S>(mut self, rules: I) -> Self
177    where
178        I: IntoIterator<Item = S>,
179        S: AsRef<str>,
180    {
181        self.override_rules = rules
182            .into_iter()
183            .map(|item| item.as_ref().to_owned())
184            .collect();
185        self
186    }
187
188    #[must_use]
189    pub const fn with_ignore_case_insensitive(mut self, enabled: bool) -> Self {
190        self.ignore_case_insensitive = enabled;
191        self
192    }
193
194    #[must_use]
195    pub fn with_ignore_policy(mut self, policy: IgnorePolicy) -> Self {
196        self.ignore_policy = policy;
197        self
198    }
199
200    #[must_use]
201    pub const fn with_skip_hidden(mut self, enabled: bool) -> Self {
202        self.skip_hidden = enabled;
203        self
204    }
205
206    /// Disables file-content reads for the fastest metadata-only discovery.
207    ///
208    /// The resulting report does not contain content hashes and may include
209    /// binary files whose extension matches the configured filter.
210    #[must_use]
211    pub fn metadata_only(mut self) -> Self {
212        self.hash_file_contents = false;
213        self.detect_binary_files = false;
214        self
215    }
216
217    /// Keeps only the selected manifest and warnings, without skip evidence.
218    #[must_use]
219    pub const fn selected_files_only(mut self) -> Self {
220        self.evidence = EvidenceMode::SelectedFiles;
221        self
222    }
223
224    /// Sets the shared traversal and content worker default.
225    ///
226    /// A later traversal- or content-specific override takes precedence.
227    #[must_use]
228    pub const fn with_parallelism(mut self, parallelism: usize) -> Self {
229        self.parallelism = parallelism;
230        self
231    }
232
233    /// Sets traversal workers without changing content-inspection workers.
234    #[must_use]
235    pub const fn with_traversal_parallelism(mut self, parallelism: usize) -> Self {
236        self.traversal_parallelism = Some(parallelism);
237        self
238    }
239
240    /// Sets content-inspection workers without changing traversal workers.
241    #[must_use]
242    pub const fn with_content_parallelism(mut self, parallelism: usize) -> Self {
243        self.content_parallelism = Some(parallelism);
244        self
245    }
246
247    #[must_use]
248    pub const fn with_max_entries(mut self, max_entries: Option<u64>) -> Self {
249        self.limits.max_entries = max_entries;
250        self
251    }
252
253    #[must_use]
254    pub const fn with_max_total_bytes(mut self, max_total_bytes: Option<u64>) -> Self {
255        self.limits.max_total_bytes = max_total_bytes;
256        self
257    }
258
259    #[must_use]
260    pub const fn with_timeout(mut self, timeout: Option<Duration>) -> Self {
261        self.limits.timeout = timeout;
262        self
263    }
264
265    #[must_use]
266    pub fn with_cancellation(mut self, cancellation: CancellationToken) -> Self {
267        self.cancellation = Some(cancellation);
268        self
269    }
270
271    #[must_use]
272    pub const fn with_cache_validation(mut self, policy: CacheValidationPolicy) -> Self {
273        self.cache_validation = policy;
274        self
275    }
276
277    #[must_use]
278    pub const fn with_content_validation(mut self, policy: ContentValidationPolicy) -> Self {
279        self.content_validation = policy;
280        self
281    }
282
283    pub(crate) fn should_skip_directory(&self, name: &OsStr) -> bool {
284        self.standard_skips == StandardSkips::Enabled
285            && DEFAULT_SKIP_DIRECTORIES
286                .iter()
287                .any(|candidate| name == OsStr::new(candidate))
288    }
289
290    pub(crate) fn accepts_extension(&self, path: &Path, relative: &str) -> bool {
291        if self.extensions.is_empty() && !self.file_types.is_active() {
292            return true;
293        }
294        match self.file_types.matched(path, relative) {
295            FileTypeMatch::Include => return true,
296            FileTypeMatch::Exclude => return false,
297            FileTypeMatch::None => {}
298        }
299        if self.extensions.is_empty() && self.file_types.has_includes() {
300            return false;
301        }
302        if self.extensions.is_empty() {
303            return true;
304        }
305        let Some(extension) = path.extension().and_then(|value| value.to_str()) else {
306            return false;
307        };
308        self.contains_extension(extension)
309            || (extension.bytes().any(|byte| byte.is_ascii_uppercase())
310                && self.contains_extension(&extension.to_ascii_lowercase()))
311    }
312
313    fn contains_extension(&self, extension: &str) -> bool {
314        if self.extensions.len() <= 8 {
315            self.extensions
316                .iter()
317                .any(|candidate| candidate == extension)
318        } else {
319            self.extensions.contains(extension)
320        }
321    }
322}