1use crate::cache::ScanCache;
2use crate::config::{EvidenceMode, ScanOptions};
3use crate::content::inspect_files;
4use crate::error::{Error, Result};
5use crate::ignore::RepositoryMatcher;
6use crate::parallel::WalkControl;
7use crate::parallel::dynamic::{self, BatchControl};
8use crate::report::ScanReport;
9use crate::runtime::ParallelRuntime;
10use crate::scan_finalize::finalize_report;
11use crate::scan_limits::{ScanRuntime, apply_total_bytes_limit};
12use crate::walker::{ErrorPolicy, Walker};
13use std::path::{Path, PathBuf};
14use std::sync::{Arc, Mutex};
15
16mod compact;
17mod content_visit;
18mod entry;
19mod stream;
20mod watch_update;
21
22use entry::{process_entry, record_walk_error, walker_error_into_scan_error};
23
24pub use compact::scan_repository_compact;
25
26pub struct Scanner {
27 root: PathBuf,
28 options: ScanOptions,
29 runtime: ParallelRuntime,
30}
31
32impl Scanner {
33 #[must_use]
34 pub fn new(root: impl Into<PathBuf>) -> Self {
35 Self {
36 root: root.into(),
37 options: ScanOptions::default(),
38 runtime: ParallelRuntime::global(),
39 }
40 }
41
42 #[must_use]
43 pub fn options(mut self, options: ScanOptions) -> Self {
44 self.options = options;
45 self
46 }
47
48 #[must_use]
50 pub fn runtime(mut self, runtime: ParallelRuntime) -> Self {
51 self.runtime = runtime;
52 self
53 }
54
55 pub fn scan(self) -> Result<ScanReport> {
62 scan_repository_with_runtime(&self.root, &self.options, None, &self.runtime)
63 }
64
65 pub fn scan_compact(self) -> Result<crate::CompactScanReport> {
74 compact::scan_repository_compact_with_runtime(&self.root, &self.options, &self.runtime)
75 }
76
77 pub fn scan_incremental(self, previous: &ScanReport) -> Result<ScanReport> {
85 let cache = previous.to_cache();
86 scan_repository_with_runtime(&self.root, &self.options, Some(&cache), &self.runtime)
87 }
88
89 pub fn scan_cached(self, cache: &ScanCache) -> Result<ScanReport> {
98 scan_repository_with_runtime(&self.root, &self.options, Some(cache), &self.runtime)
99 }
100
101 pub fn scan_watch_plan(
112 self,
113 previous: &ScanReport,
114 plan: &crate::WatchPlan,
115 ) -> Result<ScanReport> {
116 watch_update::scan_watch_plan(&self.root, &self.options, previous, plan, &self.runtime)
117 }
118}
119
120pub fn scan_repository(root: impl AsRef<Path>) -> Result<ScanReport> {
127 scan_repository_with_options(root.as_ref(), &ScanOptions::default(), None)
128}
129
130pub(crate) fn scan_repository_with_options(
131 root: &Path,
132 options: &ScanOptions,
133 previous: Option<&ScanCache>,
134) -> Result<ScanReport> {
135 scan_repository_with_runtime(root, options, previous, &ParallelRuntime::global())
136}
137
138pub(crate) fn scan_repository_with_runtime(
139 root: &Path,
140 options: &ScanOptions,
141 previous: Option<&ScanCache>,
142 parallel_runtime: &ParallelRuntime,
143) -> Result<ScanReport> {
144 let (mut report, runtime) = discover_repository_with_options(root, options, parallel_runtime)?;
145 let previous = previous.filter(|cache| cache.is_compatible(&report.root));
146 let inspected = inspect_files(
147 std::mem::take(&mut report.files),
148 options,
149 runtime.started,
150 previous,
151 )?;
152 report.files = inspected.files;
153 report.cache = inspected.cache;
154 report.skipped.extend(inspected.skipped);
155 if let Some(reason) = inspected.termination {
156 report.terminate(reason);
157 }
158 if !inspected.warnings.is_empty() {
159 report.complete = false;
160 report.warnings.extend(inspected.warnings);
161 }
162 finalize_report(&mut report);
163 Ok(report)
164}
165
166fn discover_repository_with_options(
167 root: &Path,
168 options: &ScanOptions,
169 parallel_runtime: &ParallelRuntime,
170) -> Result<(ScanReport, ScanRuntime)> {
171 if options.walk.root_symlink_policy == crate::RootSymlinkPolicy::Reject {
172 let metadata = std::fs::symlink_metadata(root).map_err(|source| Error::io(root, source))?;
173 if metadata.file_type().is_symlink() {
174 return Err(Error::io(
175 root,
176 std::io::Error::new(
177 std::io::ErrorKind::InvalidInput,
178 "root symlink rejected by policy",
179 ),
180 ));
181 }
182 }
183 let canonical = root
184 .canonicalize()
185 .map_err(|source| Error::io(root, source))?;
186 if !canonical.is_dir() {
187 return Err(Error::InvalidRoot(canonical));
188 }
189 let report = ScanReport::new(
190 canonical.clone(),
191 options.evidence == EvidenceMode::Complete,
192 );
193 let matcher = RepositoryMatcher::with_options(&canonical, options)?;
194 let runtime = ScanRuntime::new();
195 let (mut report, matcher, runtime) = if options.uses_parallel_traversal() {
196 discover_parallel(
197 &canonical,
198 options,
199 report,
200 matcher,
201 runtime,
202 parallel_runtime,
203 )?
204 } else {
205 discover_serial(&canonical, options, report, matcher, runtime)?
206 };
207
208 report.ignore_sources = matcher.sources().to_vec();
209 report.portable = matcher.portable();
210 if !matcher.warnings().is_empty() {
211 report.complete = false;
212 report.warnings.extend_from_slice(matcher.warnings());
213 }
214 if report.termination.is_none()
215 && let Some(reason) = runtime.external_termination(options)
216 {
217 report.terminate(reason);
218 }
219 apply_total_bytes_limit(&mut report, options);
220 Ok((report, runtime))
221}
222
223fn discover_serial(
224 canonical: &Path,
225 options: &ScanOptions,
226 mut report: ScanReport,
227 mut matcher: RepositoryMatcher,
228 mut runtime: ScanRuntime,
229) -> Result<(ScanReport, RepositoryMatcher, ScanRuntime)> {
230 let mut walker = Walker::with_options(canonical, options.walk_options())
231 .map_err(walker_error_into_scan_error)?;
232 loop {
233 if let Some(reason) = runtime.before_next(options) {
234 report.terminate(reason);
235 break;
236 }
237 let Some(item) = walker.next() else {
238 break;
239 };
240 runtime.record_entry();
241 match item {
242 Ok(entry) => {
243 if process_entry(&entry, options, &mut report, &mut matcher)? {
244 walker.skip_current_dir();
245 }
246 }
247 Err(error) if options.walk.error_policy == ErrorPolicy::Abort => {
248 return Err(walker_error_into_scan_error(error));
249 }
250 Err(error) => record_walk_error(&error, canonical, &mut report),
251 }
252 }
253 Ok((report, matcher, runtime))
254}
255
256struct ParallelDiscovery {
257 report: ScanReport,
258 matcher: RepositoryMatcher,
259 runtime: ScanRuntime,
260 error: Option<Error>,
261}
262
263fn discover_parallel(
264 canonical: &Path,
265 options: &ScanOptions,
266 report: ScanReport,
267 matcher: RepositoryMatcher,
268 runtime: ScanRuntime,
269 parallel_runtime: &ParallelRuntime,
270) -> Result<(ScanReport, RepositoryMatcher, ScanRuntime)> {
271 let state = Arc::new(Mutex::new(ParallelDiscovery {
272 report,
273 matcher,
274 runtime,
275 error: None,
276 }));
277 let visitor_state = Arc::clone(&state);
278 let visitor_options = options.clone();
279 let visitor_root = canonical.to_path_buf();
280 let cancellation = options.cancellation.clone().unwrap_or_default();
281 let traversal = dynamic::visit_batched(
282 canonical,
283 options.walk_options(),
284 options.traversal_workers(),
285 parallel_runtime,
286 &cancellation,
287 move |entries, errors| {
288 let mut state = visitor_state
289 .lock()
290 .expect("parallel scanner state is not poisoned");
291 let mut controls = Vec::with_capacity(entries.len());
292 let mut quit = state.error.is_some();
293 for entry in entries {
294 if quit {
295 controls.push(WalkControl::Quit);
296 continue;
297 }
298 if let Some(reason) = state.runtime.before_next(&visitor_options) {
299 state.report.terminate(reason);
300 controls.push(WalkControl::Quit);
301 quit = true;
302 continue;
303 }
304 state.runtime.record_entry();
305 let ParallelDiscovery {
306 report, matcher, ..
307 } = &mut *state;
308 match process_entry(entry, &visitor_options, report, matcher) {
309 Ok(true) => controls.push(WalkControl::Skip),
310 Ok(false) => controls.push(WalkControl::Continue),
311 Err(error) => {
312 state.error = Some(error);
313 controls.push(WalkControl::Quit);
314 quit = true;
315 }
316 }
317 }
318 for error in errors {
319 if quit {
320 break;
321 }
322 if let Some(reason) = state.runtime.before_next(&visitor_options) {
323 state.report.terminate(reason);
324 quit = true;
325 break;
326 }
327 state.runtime.record_entry();
328 if visitor_options.walk.error_policy == ErrorPolicy::Continue {
329 record_walk_error(error, &visitor_root, &mut state.report);
330 }
331 }
332 BatchControl {
333 entries: controls,
334 quit,
335 }
336 },
337 );
338 if let Err(error) = traversal {
339 return Err(walker_error_into_scan_error(error));
340 }
341 let state = Arc::try_unwrap(state)
342 .ok()
343 .expect("parallel scanner visitor released shared state");
344 let mut state = state
345 .into_inner()
346 .expect("parallel scanner state is not poisoned");
347 if let Some(error) = state.error.take() {
348 return Err(error);
349 }
350 Ok((state.report, state.matcher, state.runtime))
351}