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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum CacheValidationPolicy {
48 Fast,
50 Strict,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum ContentValidationPolicy {
57 Fast,
60 Strict,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum ContentDiscoveryMode {
68 Streaming,
72 BufferedParallel,
78}
79
80#[derive(Debug, Clone)]
81#[allow(clippy::struct_excessive_bools)]
82pub struct ScanOptions {
83 pub max_file_bytes: u64,
84 pub extensions: BTreeSet<String>,
85 pub file_types: NamedFileTypes,
87 pub ignore_files: Vec<String>,
88 pub override_rules: Vec<String>,
90 pub ignore_policy: IgnorePolicy,
92 pub ignore_case_insensitive: bool,
94 pub skip_hidden: bool,
96 pub standard_skips: StandardSkips,
97 pub hash_file_contents: bool,
98 pub detect_binary_files: bool,
99 pub evidence: EvidenceMode,
101 pub parallelism: usize,
104 pub traversal_parallelism: Option<usize>,
107 pub content_parallelism: Option<usize>,
110 pub limits: ScanLimits,
112 pub cancellation: Option<CancellationToken>,
114 pub cache_validation: CacheValidationPolicy,
116 pub content_validation: ContentValidationPolicy,
118 pub content_discovery: ContentDiscoveryMode,
120 pub walk: WalkOptions,
122}
123
124impl Default for ScanOptions {
125 fn default() -> Self {
126 Self {
127 max_file_bytes: DEFAULT_MAX_FILE_BYTES,
128 extensions: BTreeSet::new(),
129 file_types: NamedFileTypes::default(),
130 ignore_files: DEFAULT_IGNORE_FILES
131 .iter()
132 .map(ToString::to_string)
133 .collect(),
134 override_rules: Vec::new(),
135 ignore_policy: IgnorePolicy::default(),
136 ignore_case_insensitive: false,
137 skip_hidden: false,
138 standard_skips: StandardSkips::Enabled,
139 hash_file_contents: true,
140 detect_binary_files: true,
141 evidence: EvidenceMode::Complete,
142 parallelism: 0,
143 traversal_parallelism: None,
144 content_parallelism: None,
145 limits: ScanLimits::default(),
146 cancellation: None,
147 cache_validation: CacheValidationPolicy::Fast,
148 content_validation: ContentValidationPolicy::Strict,
149 content_discovery: ContentDiscoveryMode::Streaming,
150 walk: WalkOptions::default().with_metadata(true),
151 }
152 }
153}
154
155impl ScanOptions {
156 #[must_use]
157 pub fn with_extensions<I, S>(mut self, extensions: I) -> Self
158 where
159 I: IntoIterator<Item = S>,
160 S: AsRef<str>,
161 {
162 self.extensions = extensions
163 .into_iter()
164 .map(|item| item.as_ref().trim_start_matches('.').to_ascii_lowercase())
165 .collect();
166 self
167 }
168
169 #[must_use]
171 pub fn with_file_types(mut self, file_types: NamedFileTypes) -> Self {
172 self.file_types = file_types;
173 self
174 }
175
176 #[must_use]
177 pub fn with_ignore_files<I, S>(mut self, names: I) -> Self
178 where
179 I: IntoIterator<Item = S>,
180 S: AsRef<str>,
181 {
182 self.ignore_files = names
183 .into_iter()
184 .map(|item| item.as_ref().to_owned())
185 .collect();
186 self
187 }
188
189 #[must_use]
194 pub fn with_override_rules<I, S>(mut self, rules: I) -> Self
195 where
196 I: IntoIterator<Item = S>,
197 S: AsRef<str>,
198 {
199 self.override_rules = rules
200 .into_iter()
201 .map(|item| item.as_ref().to_owned())
202 .collect();
203 self
204 }
205
206 #[must_use]
207 pub const fn with_ignore_case_insensitive(mut self, enabled: bool) -> Self {
208 self.ignore_case_insensitive = enabled;
209 self
210 }
211
212 #[must_use]
213 pub fn with_ignore_policy(mut self, policy: IgnorePolicy) -> Self {
214 self.ignore_policy = policy;
215 self
216 }
217
218 #[must_use]
219 pub const fn with_skip_hidden(mut self, enabled: bool) -> Self {
220 self.skip_hidden = enabled;
221 self
222 }
223
224 #[must_use]
229 pub fn metadata_only(mut self) -> Self {
230 self.hash_file_contents = false;
231 self.detect_binary_files = false;
232 self
233 }
234
235 #[must_use]
237 pub const fn selected_files_only(mut self) -> Self {
238 self.evidence = EvidenceMode::SelectedFiles;
239 self
240 }
241
242 #[must_use]
246 pub const fn with_parallelism(mut self, parallelism: usize) -> Self {
247 self.parallelism = parallelism;
248 self
249 }
250
251 #[must_use]
253 pub const fn with_traversal_parallelism(mut self, parallelism: usize) -> Self {
254 self.traversal_parallelism = Some(parallelism);
255 self
256 }
257
258 #[must_use]
260 pub const fn with_content_parallelism(mut self, parallelism: usize) -> Self {
261 self.content_parallelism = Some(parallelism);
262 self
263 }
264
265 #[must_use]
267 pub const fn with_content_discovery(mut self, mode: ContentDiscoveryMode) -> Self {
268 self.content_discovery = mode;
269 self
270 }
271
272 #[must_use]
273 pub const fn with_max_entries(mut self, max_entries: Option<u64>) -> Self {
274 self.limits.max_entries = max_entries;
275 self
276 }
277
278 #[must_use]
279 pub const fn with_max_total_bytes(mut self, max_total_bytes: Option<u64>) -> Self {
280 self.limits.max_total_bytes = max_total_bytes;
281 self
282 }
283
284 #[must_use]
285 pub const fn with_timeout(mut self, timeout: Option<Duration>) -> Self {
286 self.limits.timeout = timeout;
287 self
288 }
289
290 #[must_use]
291 pub fn with_cancellation(mut self, cancellation: CancellationToken) -> Self {
292 self.cancellation = Some(cancellation);
293 self
294 }
295
296 #[must_use]
297 pub const fn with_cache_validation(mut self, policy: CacheValidationPolicy) -> Self {
298 self.cache_validation = policy;
299 self
300 }
301
302 #[must_use]
303 pub const fn with_content_validation(mut self, policy: ContentValidationPolicy) -> Self {
304 self.content_validation = policy;
305 self
306 }
307
308 pub(crate) fn should_skip_directory(&self, name: &OsStr) -> bool {
309 self.standard_skips == StandardSkips::Enabled
310 && DEFAULT_SKIP_DIRECTORIES
311 .iter()
312 .any(|candidate| name == OsStr::new(candidate))
313 }
314
315 pub(crate) fn accepts_extension(&self, path: &Path, relative: &str) -> bool {
316 if self.extensions.is_empty() && !self.file_types.is_active() {
317 return true;
318 }
319 match self.file_types.matched(path, relative) {
320 FileTypeMatch::Include => return true,
321 FileTypeMatch::Exclude => return false,
322 FileTypeMatch::None => {}
323 }
324 if self.extensions.is_empty() && self.file_types.has_includes() {
325 return false;
326 }
327 if self.extensions.is_empty() {
328 return true;
329 }
330 let Some(extension) = path.extension().and_then(|value| value.to_str()) else {
331 return false;
332 };
333 self.contains_extension(extension)
334 || (extension.bytes().any(|byte| byte.is_ascii_uppercase())
335 && self.contains_extension(&extension.to_ascii_lowercase()))
336 }
337
338 fn contains_extension(&self, extension: &str) -> bool {
339 if self.extensions.len() <= 8 {
340 self.extensions
341 .iter()
342 .any(|candidate| candidate == extension)
343 } else {
344 self.extensions.contains(extension)
345 }
346 }
347}