1use crate::config::{EvidenceMode, ScanOptions};
2use crate::content::inspect_files;
3use crate::error::{Error, Result};
4use crate::ignore::{RepositoryMatch, RepositoryMatcher};
5use crate::path::normalized_relative_path;
6use crate::report::{ScanReport, ScannedFile, SkipKind};
7use crate::scan_finalize::finalize_report;
8use crate::scan_limits::{ScanRuntime, apply_total_bytes_limit};
9use crate::scan_match::skip_match;
10use crate::walker::{ErrorPolicy, WalkEntry, WalkError, WalkOperation, WalkSkipReason, Walker};
11use std::fs;
12use std::path::{Path, PathBuf};
13
14pub struct Scanner {
15 root: PathBuf,
16 options: ScanOptions,
17}
18
19impl Scanner {
20 #[must_use]
21 pub fn new(root: impl Into<PathBuf>) -> Self {
22 Self {
23 root: root.into(),
24 options: ScanOptions::default(),
25 }
26 }
27
28 #[must_use]
29 pub fn options(mut self, options: ScanOptions) -> Self {
30 self.options = options;
31 self
32 }
33
34 pub fn scan(self) -> Result<ScanReport> {
41 scan_repository_with_options(&self.root, &self.options)
42 }
43}
44
45pub fn scan_repository(root: impl AsRef<Path>) -> Result<ScanReport> {
52 scan_repository_with_options(root.as_ref(), &ScanOptions::default())
53}
54
55fn scan_repository_with_options(root: &Path, options: &ScanOptions) -> Result<ScanReport> {
56 let canonical = root
57 .canonicalize()
58 .map_err(|source| Error::io(root, source))?;
59 if !canonical.is_dir() {
60 return Err(Error::InvalidRoot(canonical));
61 }
62
63 let mut report = ScanReport::new(
64 canonical.clone(),
65 options.evidence == EvidenceMode::Complete,
66 );
67 let mut walker = Walker::with_options(&canonical, options.walk_options())
68 .map_err(walker_error_into_scan_error)?;
69 let mut matcher = RepositoryMatcher::with_options(&canonical, options)?;
70 let mut runtime = ScanRuntime::new();
71 loop {
72 if let Some(reason) = runtime.before_next(options) {
73 report.terminate(reason);
74 break;
75 }
76 let Some(item) = walker.next() else {
77 break;
78 };
79 runtime.record_entry();
80 match item {
81 Ok(entry) => {
82 process_entry(&entry, options, &mut report, &mut matcher, &mut walker)?;
83 }
84 Err(error) => record_walk_error(error, &canonical, options, &mut report)?,
85 }
86 }
87 report.ignore_sources = matcher.sources().to_vec();
88 report.portable = matcher.portable();
89 if !matcher.warnings().is_empty() {
90 report.complete = false;
91 report.warnings.extend_from_slice(matcher.warnings());
92 }
93 if report.termination.is_none()
94 && let Some(reason) = runtime.external_termination(options)
95 {
96 report.terminate(reason);
97 }
98 apply_total_bytes_limit(&mut report, options);
99 let inspected = inspect_files(std::mem::take(&mut report.files), options, runtime.started)?;
100 report.files = inspected.files;
101 report.skipped.extend(inspected.skipped);
102 if let Some(reason) = inspected.termination {
103 report.terminate(reason);
104 }
105 if !inspected.warnings.is_empty() {
106 report.complete = false;
107 report.warnings.extend(inspected.warnings);
108 }
109 finalize_report(&mut report);
110 Ok(report)
111}
112
113fn process_entry(
114 entry: &WalkEntry,
115 options: &ScanOptions,
116 report: &mut ScanReport,
117 matcher: &mut RepositoryMatcher,
118 walker: &mut Walker,
119) -> Result<()> {
120 let relative_path = entry.relative_path();
121 let relative = normalized_relative_path(relative_path);
122 if entry.depth() == 0 {
123 if let Some(reason) = entry.skip_reason() {
124 report.skip(".".to_owned(), skip_kind(reason), None);
125 return Ok(());
126 }
127 matcher.prepare_directory(entry.path())?;
128 return Ok(());
129 }
130 if entry.depth() < options.effective_min_depth() && !entry.is_dir() {
131 return Ok(());
132 }
133 if entry.is_symlink() && !options.walk.follow_links {
134 report.skip(relative, SkipKind::Symlink, None);
135 return Ok(());
136 }
137 if let Some(reason) = entry.skip_reason() {
138 report.skip(relative, skip_kind(reason), None);
139 return Ok(());
140 }
141
142 if entry.is_dir() {
143 let parent = entry.path().parent().unwrap_or(entry.path());
144 let decision = matcher.matched_prepared(&relative, parent, entry.path(), true);
145 if skip_match(report, relative.clone(), decision) {
146 walker.skip_current_dir();
147 return Ok(());
148 }
149 if decision != RepositoryMatch::OverrideInclude
150 && options.should_skip_directory(entry.file_name())
151 {
152 walker.skip_current_dir();
153 report.skip(relative, SkipKind::StandardDirectory, None);
154 return Ok(());
155 }
156 matcher.prepare_directory(entry.path())?;
157 return Ok(());
158 }
159 if !entry.is_file() {
160 return Ok(());
161 }
162 let parent = entry.path().parent().unwrap_or(entry.path());
163 let decision = matcher.matched_prepared(&relative, parent, entry.path(), false);
164 if skip_match(report, relative.clone(), decision) {
165 return Ok(());
166 }
167 process_file(
168 entry,
169 relative,
170 decision == RepositoryMatch::OverrideInclude,
171 options,
172 report,
173 )
174}
175
176fn process_file(
177 entry: &WalkEntry,
178 relative: String,
179 override_include: bool,
180 options: &ScanOptions,
181 report: &mut ScanReport,
182) -> Result<()> {
183 let path = entry.path();
184 if !override_include && !options.accepts_extension(path) {
185 report.skip(relative, SkipKind::Extension, None);
186 return Ok(());
187 }
188 let bytes = match entry.bytes() {
189 Some(bytes) => bytes,
190 None => match fs::metadata(path) {
191 Ok(metadata) => metadata.len(),
192 Err(source) => {
193 return record_local_io_error(
194 path,
195 relative,
196 WalkOperation::ReadMetadata,
197 source,
198 options,
199 report,
200 );
201 }
202 },
203 };
204 if bytes > options.max_file_bytes {
205 report.skip(
206 relative,
207 SkipKind::Oversized,
208 Some(format!("{bytes} bytes")),
209 );
210 return Ok(());
211 }
212 report.files.push(ScannedFile {
213 absolute: path.to_path_buf(),
214 relative,
215 bytes,
216 content_hash: None,
217 });
218 Ok(())
219}
220
221fn record_walk_error(
222 error: WalkError,
223 root: &Path,
224 options: &ScanOptions,
225 report: &mut ScanReport,
226) -> Result<()> {
227 if options.walk.error_policy == ErrorPolicy::Abort {
228 return Err(walker_error_into_scan_error(error));
229 }
230 let relative = error.path().strip_prefix(root).map_or_else(
231 |_| normalized_relative_path(error.path()),
232 normalized_relative_path,
233 );
234 let relative = if relative.is_empty() {
235 ".".to_owned()
236 } else {
237 relative
238 };
239 let message = format!(
240 "{}: {}",
241 operation_label(error.operation()),
242 error.io_error()
243 );
244 report.skip(relative.clone(), SkipKind::IoError, Some(message.clone()));
245 report.warn(Some(relative), message);
246 Ok(())
247}
248
249fn record_local_io_error(
250 path: &Path,
251 relative: String,
252 operation: WalkOperation,
253 source: std::io::Error,
254 options: &ScanOptions,
255 report: &mut ScanReport,
256) -> Result<()> {
257 if options.walk.error_policy == ErrorPolicy::Abort {
258 return Err(Error::io(path, source));
259 }
260 let message = format!("{}: {source}", operation_label(operation));
261 report.skip(relative.clone(), SkipKind::IoError, Some(message.clone()));
262 report.warn(Some(relative), message);
263 Ok(())
264}
265
266const fn skip_kind(reason: WalkSkipReason) -> SkipKind {
267 match reason {
268 WalkSkipReason::MaxDepth => SkipKind::MaxDepth,
269 WalkSkipReason::FileSystemBoundary => SkipKind::FileSystemBoundary,
270 WalkSkipReason::PathEscape => SkipKind::PathEscape,
271 WalkSkipReason::SymlinkLoop => SkipKind::SymlinkLoop,
272 }
273}
274
275const fn operation_label(operation: WalkOperation) -> &'static str {
276 match operation {
277 WalkOperation::Canonicalize => "canonicalize",
278 WalkOperation::ReadDirectory => "read directory",
279 WalkOperation::ReadEntry => "read entry",
280 WalkOperation::ReadMetadata => "read metadata",
281 }
282}
283
284fn walker_error_into_scan_error(error: WalkError) -> Error {
285 let (path, source) = error.into_parts();
286 Error::io(path, source)
287}