Skip to main content

weavatrix_scan/config/
builder.rs

1use super::{
2    CacheValidationPolicy, CancellationToken, ContentDiscoveryMode, ContentValidationPolicy,
3    DEFAULT_SKIP_DIRECTORIES, Duration, EvidenceMode, FileTypeMatch, IgnorePolicy, NamedFileTypes,
4    OsStr, Path, ScanOptions, StandardSkips,
5};
6
7impl ScanOptions {
8    #[must_use]
9    pub fn with_extensions<I, S>(mut self, extensions: I) -> Self
10    where
11        I: IntoIterator<Item = S>,
12        S: AsRef<str>,
13    {
14        self.extensions = extensions
15            .into_iter()
16            .map(|item| item.as_ref().trim_start_matches('.').to_ascii_lowercase())
17            .collect();
18        self
19    }
20
21    /// Replaces named file-type definitions and selections.
22    #[must_use]
23    pub fn with_file_types(mut self, file_types: NamedFileTypes) -> Self {
24        self.file_types = file_types;
25        self
26    }
27
28    #[must_use]
29    pub fn with_ignore_files<I, S>(mut self, names: I) -> Self
30    where
31        I: IntoIterator<Item = S>,
32        S: AsRef<str>,
33    {
34        self.ignore_files = names
35            .into_iter()
36            .map(|item| item.as_ref().to_owned())
37            .collect();
38        self
39    }
40
41    /// Replaces request-level override globs.
42    ///
43    /// Like `ignore::Override`, ordinary patterns include matching paths and
44    /// leading `!` patterns exclude them.
45    #[must_use]
46    pub fn with_override_rules<I, S>(mut self, rules: I) -> Self
47    where
48        I: IntoIterator<Item = S>,
49        S: AsRef<str>,
50    {
51        self.override_rules = rules
52            .into_iter()
53            .map(|item| item.as_ref().to_owned())
54            .collect();
55        self
56    }
57
58    #[must_use]
59    pub const fn with_ignore_case_insensitive(mut self, enabled: bool) -> Self {
60        self.ignore_case_insensitive = enabled;
61        self
62    }
63
64    #[must_use]
65    pub fn with_ignore_policy(mut self, policy: IgnorePolicy) -> Self {
66        self.ignore_policy = policy;
67        self
68    }
69
70    #[must_use]
71    pub const fn with_skip_hidden(mut self, enabled: bool) -> Self {
72        self.skip_hidden = enabled;
73        self
74    }
75
76    /// Disables file-content reads for the fastest metadata-only discovery.
77    ///
78    /// The resulting report does not contain content hashes and may include
79    /// binary files whose extension matches the configured filter.
80    #[must_use]
81    pub fn metadata_only(mut self) -> Self {
82        self.hash_file_contents = false;
83        self.detect_binary_files = false;
84        self
85    }
86
87    /// Keeps only the selected manifest and warnings, without skip evidence.
88    #[must_use]
89    pub const fn selected_files_only(mut self) -> Self {
90        self.evidence = EvidenceMode::SelectedFiles;
91        self
92    }
93
94    /// Sets the shared traversal and content worker default.
95    ///
96    /// A later traversal- or content-specific override takes precedence.
97    #[must_use]
98    pub const fn with_parallelism(mut self, parallelism: usize) -> Self {
99        self.parallelism = parallelism;
100        self
101    }
102
103    /// Sets traversal workers without changing content-inspection workers.
104    #[must_use]
105    pub const fn with_traversal_parallelism(mut self, parallelism: usize) -> Self {
106        self.traversal_parallelism = Some(parallelism);
107        self
108    }
109
110    /// Sets content-inspection workers without changing traversal workers.
111    #[must_use]
112    pub const fn with_content_parallelism(mut self, parallelism: usize) -> Self {
113        self.content_parallelism = Some(parallelism);
114        self
115    }
116
117    /// Sets candidate discovery for content visits.
118    #[must_use]
119    pub const fn with_content_discovery(mut self, mode: ContentDiscoveryMode) -> Self {
120        self.content_discovery = mode;
121        self
122    }
123
124    #[must_use]
125    pub const fn with_max_entries(mut self, max_entries: Option<u64>) -> Self {
126        self.limits.max_entries = max_entries;
127        self
128    }
129
130    #[must_use]
131    pub const fn with_max_total_bytes(mut self, max_total_bytes: Option<u64>) -> Self {
132        self.limits.max_total_bytes = max_total_bytes;
133        self
134    }
135
136    #[must_use]
137    pub const fn with_timeout(mut self, timeout: Option<Duration>) -> Self {
138        self.limits.timeout = timeout;
139        self
140    }
141
142    #[must_use]
143    pub fn with_cancellation(mut self, cancellation: CancellationToken) -> Self {
144        self.cancellation = Some(cancellation);
145        self
146    }
147
148    #[must_use]
149    pub const fn with_cache_validation(mut self, policy: CacheValidationPolicy) -> Self {
150        self.cache_validation = policy;
151        self
152    }
153
154    #[must_use]
155    pub const fn with_content_validation(mut self, policy: ContentValidationPolicy) -> Self {
156        self.content_validation = policy;
157        self
158    }
159
160    pub(crate) fn should_skip_directory(&self, name: &OsStr) -> bool {
161        self.standard_skips == StandardSkips::Enabled
162            && DEFAULT_SKIP_DIRECTORIES
163                .iter()
164                .any(|candidate| name == OsStr::new(candidate))
165    }
166
167    pub(crate) fn accepts_extension(&self, path: &Path, relative: &str) -> bool {
168        if self.extensions.is_empty() && !self.file_types.is_active() {
169            return true;
170        }
171        match self.file_types.matched(path, relative) {
172            FileTypeMatch::Include => return true,
173            FileTypeMatch::Exclude => return false,
174            FileTypeMatch::None => {}
175        }
176        if self.extensions.is_empty() && self.file_types.has_includes() {
177            return false;
178        }
179        if self.extensions.is_empty() {
180            return true;
181        }
182        let Some(extension) = path.extension().and_then(|value| value.to_str()) else {
183            return false;
184        };
185        self.contains_extension(extension)
186            || (extension.bytes().any(|byte| byte.is_ascii_uppercase())
187                && self.contains_extension(&extension.to_ascii_lowercase()))
188    }
189
190    fn contains_extension(&self, extension: &str) -> bool {
191        if self.extensions.len() <= 8 {
192            self.extensions
193                .iter()
194                .any(|candidate| candidate == extension)
195        } else {
196            self.extensions.contains(extension)
197        }
198    }
199}