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::dynamic;
7use crate::report::{ScanReport, ScannedFile};
8use crate::runtime::ParallelRuntime;
9use crate::scan_finalize::finalize_report;
10use crate::scan_limits::{ScanRuntime, apply_total_bytes_limit};
11use crate::walker::{ErrorPolicy, Walker};
12use std::path::{Path, PathBuf};
13use std::sync::{Arc, Mutex};
14
15mod batch;
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 let skip = process_entry(&entry, options, &mut report, &matcher, None)?;
244 if skip {
245 walker.skip_current_dir();
246 } else if entry.is_dir() {
247 matcher.prepare_directory(entry.path())?;
248 }
249 }
250 Err(error) if options.walk.error_policy == ErrorPolicy::Abort => {
251 return Err(walker_error_into_scan_error(error));
252 }
253 Err(error) => record_walk_error(&error, canonical, &mut report),
254 }
255 }
256 Ok((report, matcher, runtime))
257}
258
259struct ParallelDiscovery {
260 report: ScanReport,
261 matcher: RepositoryMatcher,
262 runtime: ScanRuntime,
263 error: Option<Error>,
264}
265
266fn discover_parallel(
267 canonical: &Path,
268 options: &ScanOptions,
269 report: ScanReport,
270 matcher: RepositoryMatcher,
271 runtime: ScanRuntime,
272 parallel_runtime: &ParallelRuntime,
273) -> Result<(ScanReport, RepositoryMatcher, ScanRuntime)> {
274 let state = Arc::new(Mutex::new(ParallelDiscovery {
275 report,
276 matcher,
277 runtime,
278 error: None,
279 }));
280 let visitor_state = Arc::clone(&state);
281 let visitor_options = options.clone();
282 let visitor_root = canonical.to_path_buf();
283 let cancellation = options.cancellation.clone().unwrap_or_default();
284 let traversal = dynamic::visit_batched(
285 canonical,
286 options.walk_options(),
287 options.traversal_workers(),
288 parallel_runtime,
289 &cancellation,
290 move |entries, errors| {
291 let mut state = visitor_state
292 .lock()
293 .expect("parallel scanner state is not poisoned");
294 let ParallelDiscovery {
295 report,
296 matcher,
297 runtime,
298 error: scan_error,
299 } = &mut *state;
300 let mut files = std::mem::take(&mut report.files);
301 let control = batch::process_parallel_batch(
302 entries,
303 errors,
304 &visitor_root,
305 &visitor_options,
306 report,
307 matcher,
308 runtime,
309 scan_error,
310 &mut files,
311 |path, relative, bytes, version| ScannedFile {
312 absolute: path.to_path_buf(),
313 relative,
314 bytes,
315 content_hash: None,
316 content_fingerprint: None,
317 version,
318 binary_checked: false,
319 },
320 );
321 report.files = files;
322 control
323 },
324 );
325 if let Err(error) = traversal {
326 return Err(walker_error_into_scan_error(error));
327 }
328 let state = Arc::try_unwrap(state)
329 .ok()
330 .expect("parallel scanner visitor released shared state");
331 let mut state = state
332 .into_inner()
333 .expect("parallel scanner state is not poisoned");
334 if let Some(error) = state.error.take() {
335 return Err(error);
336 }
337 Ok((state.report, state.matcher, state.runtime))
338}