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)]
66#[allow(clippy::struct_excessive_bools)]
67pub struct ScanOptions {
68 pub max_file_bytes: u64,
69 pub extensions: BTreeSet<String>,
70 pub file_types: NamedFileTypes,
72 pub ignore_files: Vec<String>,
73 pub override_rules: Vec<String>,
75 pub ignore_policy: IgnorePolicy,
77 pub ignore_case_insensitive: bool,
79 pub skip_hidden: bool,
81 pub standard_skips: StandardSkips,
82 pub hash_file_contents: bool,
83 pub detect_binary_files: bool,
84 pub evidence: EvidenceMode,
86 pub parallelism: usize,
89 pub traversal_parallelism: Option<usize>,
92 pub content_parallelism: Option<usize>,
95 pub limits: ScanLimits,
97 pub cancellation: Option<CancellationToken>,
99 pub cache_validation: CacheValidationPolicy,
101 pub content_validation: ContentValidationPolicy,
103 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 #[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 #[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 #[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 #[must_use]
219 pub const fn selected_files_only(mut self) -> Self {
220 self.evidence = EvidenceMode::SelectedFiles;
221 self
222 }
223
224 #[must_use]
228 pub const fn with_parallelism(mut self, parallelism: usize) -> Self {
229 self.parallelism = parallelism;
230 self
231 }
232
233 #[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 #[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}