1use std::path::PathBuf;
2
3#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct FileIdentity {
6 pub file_system: u64,
7 pub file: u64,
8}
9
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
12pub struct FileVersion {
13 #[cfg_attr(feature = "serde", serde(default))]
14 pub modified_ns: Option<u128>,
15 #[cfg_attr(feature = "serde", serde(default))]
16 pub changed_ns: Option<u128>,
17 #[cfg_attr(feature = "serde", serde(default))]
18 pub identity: Option<FileIdentity>,
19}
20
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ScannedFile {
24 #[cfg_attr(feature = "serde", serde(with = "crate::path_serde"))]
25 pub absolute: PathBuf,
26 pub relative: String,
27 pub bytes: u64,
28 pub content_hash: Option<String>,
29 #[cfg_attr(feature = "serde", serde(default))]
31 pub content_fingerprint: Option<String>,
32 #[cfg_attr(feature = "serde", serde(default))]
33 pub version: FileVersion,
34 #[cfg_attr(feature = "serde", serde(default))]
35 pub binary_checked: bool,
36}
37
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct CompactScannedFile {
45 pub relative: Box<str>,
46 pub bytes: u64,
47 #[cfg_attr(feature = "serde", serde(default))]
49 pub content: Option<Box<CompactContentEvidence>>,
50}
51
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct CompactContentEvidence {
56 pub content_hash: Option<Box<str>>,
57 #[cfg_attr(feature = "serde", serde(default))]
59 pub content_fingerprint: Option<Box<str>>,
60 #[cfg_attr(feature = "serde", serde(default))]
61 pub version: FileVersion,
62 #[cfg_attr(feature = "serde", serde(default))]
63 pub binary_checked: bool,
64}
65
66impl CompactScannedFile {
67 #[must_use]
69 pub fn content_hash(&self) -> Option<&str> {
70 self.content
71 .as_deref()
72 .and_then(|content| content.content_hash.as_deref())
73 }
74}
75
76#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
77#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
78pub struct ScanCacheStats {
79 pub reused_hashes: u64,
80 pub content_reads: u64,
81 #[cfg_attr(feature = "serde", serde(default))]
83 pub fingerprint_reads: u64,
84}
85
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
88pub enum SkipKind {
89 Binary,
90 FileSystemBoundary,
91 Extension,
92 Ignored,
93 IoError,
94 MaxDepth,
95 Oversized,
96 PathEscape,
97 StandardDirectory,
98 Hidden,
99 Override,
100 Symlink,
101 SymlinkLoop,
102 ScanLimit,
103 ConcurrentModification,
104}
105
106#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct SkippedEntry {
109 pub relative: String,
110 pub kind: SkipKind,
111 pub detail: Option<String>,
112}
113
114#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct ScanWarning {
117 pub relative: Option<String>,
118 pub message: String,
119}
120
121#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
122#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
123pub enum IgnoreSourceKind {
124 GitGlobal,
125 GitExclude,
126 GitIgnore,
127 DotIgnore,
128 Custom,
129 Explicit,
130 Override,
131}
132
133#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct IgnoreSourceEvidence {
136 pub kind: IgnoreSourceKind,
137 pub location: String,
138 pub content_hash: String,
139}
140
141#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum ScanTermination {
144 MaxEntries,
145 MaxTotalBytes,
146 Timeout,
147 Cancelled,
148}
149
150#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct ScanReport {
153 #[cfg_attr(feature = "serde", serde(with = "crate::path_serde"))]
154 pub root: PathBuf,
155 pub files: Vec<ScannedFile>,
156 pub skipped: Vec<SkippedEntry>,
157 pub warnings: Vec<ScanWarning>,
158 #[cfg_attr(feature = "serde", serde(default))]
160 pub ignore_sources: Vec<IgnoreSourceEvidence>,
161 pub revision: String,
162 #[cfg_attr(feature = "serde", serde(default = "default_complete"))]
164 pub complete: bool,
165 #[cfg_attr(feature = "serde", serde(default))]
167 pub termination: Option<ScanTermination>,
168 #[cfg_attr(feature = "serde", serde(default = "default_portable"))]
170 pub portable: bool,
171 #[cfg_attr(feature = "serde", serde(default))]
173 pub cache: ScanCacheStats,
174 #[cfg_attr(feature = "serde", serde(skip, default = "default_record_skipped"))]
175 record_skipped: bool,
176}
177
178#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct CompactScanReport {
186 #[cfg_attr(feature = "serde", serde(with = "crate::path_serde"))]
187 pub root: PathBuf,
188 pub files: Vec<CompactScannedFile>,
189 pub skipped: Vec<SkippedEntry>,
190 pub warnings: Vec<ScanWarning>,
191 #[cfg_attr(feature = "serde", serde(default))]
192 pub ignore_sources: Vec<IgnoreSourceEvidence>,
193 pub revision: String,
194 #[cfg_attr(feature = "serde", serde(default = "default_complete"))]
195 pub complete: bool,
196 #[cfg_attr(feature = "serde", serde(default))]
197 pub termination: Option<ScanTermination>,
198 #[cfg_attr(feature = "serde", serde(default = "default_portable"))]
199 pub portable: bool,
200 #[cfg_attr(feature = "serde", serde(default))]
201 pub cache: ScanCacheStats,
202}
203
204impl CompactScanReport {
205 #[must_use]
207 pub fn absolute_path(&self, file: &CompactScannedFile) -> PathBuf {
208 self.root.join(file.relative.as_ref())
209 }
210
211 #[must_use]
214 pub fn into_scan_report(self) -> ScanReport {
215 let root = self.root;
216 let files = self
217 .files
218 .into_iter()
219 .map(|file| {
220 let content = file.content.map(|content| *content);
221 ScannedFile {
222 absolute: root.join(file.relative.as_ref()),
223 relative: file.relative.into(),
224 bytes: file.bytes,
225 content_hash: content
226 .as_ref()
227 .and_then(|value| value.content_hash.as_deref())
228 .map(str::to_owned),
229 content_fingerprint: content
230 .as_ref()
231 .and_then(|value| value.content_fingerprint.as_deref())
232 .map(str::to_owned),
233 version: content
234 .as_ref()
235 .map_or_else(FileVersion::default, |value| value.version),
236 binary_checked: content.is_some_and(|value| value.binary_checked),
237 }
238 })
239 .collect();
240 ScanReport {
241 root,
242 files,
243 skipped: self.skipped,
244 warnings: self.warnings,
245 ignore_sources: self.ignore_sources,
246 revision: self.revision,
247 complete: self.complete,
248 termination: self.termination,
249 portable: self.portable,
250 cache: self.cache,
251 record_skipped: true,
252 }
253 }
254}
255
256#[cfg(feature = "serde")]
257const fn default_complete() -> bool {
258 true
259}
260
261#[cfg(feature = "serde")]
262const fn default_record_skipped() -> bool {
263 true
264}
265
266#[cfg(feature = "serde")]
267const fn default_portable() -> bool {
268 true
269}
270
271impl ScanReport {
272 #[must_use]
274 pub fn delta_from(&self, previous: &Self) -> crate::ScanDelta {
275 crate::ScanDelta::between(previous, self)
276 }
277
278 pub(crate) fn new(root: PathBuf, record_skipped: bool) -> Self {
279 Self {
280 root,
281 files: Vec::new(),
282 skipped: Vec::new(),
283 warnings: Vec::new(),
284 ignore_sources: Vec::new(),
285 revision: String::new(),
286 complete: true,
287 termination: None,
288 portable: true,
289 cache: ScanCacheStats::default(),
290 record_skipped,
291 }
292 }
293
294 pub(crate) fn skip(&mut self, relative: String, kind: SkipKind, detail: Option<String>) {
295 if self.record_skipped {
296 self.skipped.push(SkippedEntry {
297 relative,
298 kind,
299 detail,
300 });
301 }
302 }
303
304 pub(crate) fn skip_borrowed(&mut self, relative: &str, kind: SkipKind, detail: Option<String>) {
305 if self.record_skipped {
306 self.skipped.push(SkippedEntry {
307 relative: relative.to_owned(),
308 kind,
309 detail,
310 });
311 }
312 }
313
314 pub(crate) fn warn(&mut self, relative: Option<String>, message: impl Into<String>) {
315 self.complete = false;
316 self.warnings.push(ScanWarning {
317 relative,
318 message: message.into(),
319 });
320 }
321
322 pub(crate) fn terminate(&mut self, reason: ScanTermination) {
323 self.complete = false;
324 self.termination.get_or_insert(reason);
325 }
326
327 pub(crate) fn finish_recording(&mut self) {
328 self.record_skipped = true;
329 }
330}