Skip to main content

weavatrix_scan/
report.rs

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    /// Whole-content validation fingerprint used only for strict cache reuse.
30    #[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/// Selected-file evidence without a duplicated absolute path allocation.
39///
40/// Join `relative` to [`CompactScanReport::root`] only when an absolute path
41/// is needed.
42#[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    /// Allocated only when content inspection was requested.
48    #[cfg_attr(feature = "serde", serde(default))]
49    pub content: Option<Box<CompactContentEvidence>>,
50}
51
52/// Optional rich evidence kept out of metadata-only compact entries.
53#[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    /// Whole-content validation fingerprint used only for strict cache reuse.
58    #[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    /// Returns the strong content hash when content hashing was requested.
68    #[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    /// Whole-file fingerprint reads used to validate cached SHA-256 values.
82    #[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    /// Every ignore input that participated in path selection.
159    #[cfg_attr(feature = "serde", serde(default))]
160    pub ignore_sources: Vec<IgnoreSourceEvidence>,
161    pub revision: String,
162    /// False when local I/O or ignore-rule errors made evidence partial.
163    #[cfg_attr(feature = "serde", serde(default = "default_complete"))]
164    pub complete: bool,
165    /// Why a bounded scan stopped before exhausting the tree.
166    #[cfg_attr(feature = "serde", serde(default))]
167    pub termination: Option<ScanTermination>,
168    /// False when selection depended on host-level configuration.
169    #[cfg_attr(feature = "serde", serde(default = "default_portable"))]
170    pub portable: bool,
171    /// Evidence of content work reused from an older persistent report.
172    #[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/// Memory-efficient deterministic manifest retaining the root path once.
179///
180/// This report preserves scanner selection, hashes, revision, warnings and
181/// typed skip evidence while avoiding one absolute `PathBuf` per selected
182/// file.
183#[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    /// Materializes an absolute path for one compact entry.
206    #[must_use]
207    pub fn absolute_path(&self, file: &CompactScannedFile) -> PathBuf {
208        self.root.join(file.relative.as_ref())
209    }
210}
211
212#[cfg(feature = "serde")]
213const fn default_complete() -> bool {
214    true
215}
216
217#[cfg(feature = "serde")]
218const fn default_record_skipped() -> bool {
219    true
220}
221
222#[cfg(feature = "serde")]
223const fn default_portable() -> bool {
224    true
225}
226
227impl ScanReport {
228    /// Computes the deterministic changed-file set from an older report.
229    #[must_use]
230    pub fn delta_from(&self, previous: &Self) -> crate::ScanDelta {
231        crate::ScanDelta::between(previous, self)
232    }
233
234    pub(crate) fn new(root: PathBuf, record_skipped: bool) -> Self {
235        Self {
236            root,
237            files: Vec::new(),
238            skipped: Vec::new(),
239            warnings: Vec::new(),
240            ignore_sources: Vec::new(),
241            revision: String::new(),
242            complete: true,
243            termination: None,
244            portable: true,
245            cache: ScanCacheStats::default(),
246            record_skipped,
247        }
248    }
249
250    pub(crate) fn skip(&mut self, relative: String, kind: SkipKind, detail: Option<String>) {
251        if self.record_skipped {
252            self.skipped.push(SkippedEntry {
253                relative,
254                kind,
255                detail,
256            });
257        }
258    }
259
260    pub(crate) fn skip_borrowed(&mut self, relative: &str, kind: SkipKind, detail: Option<String>) {
261        if self.record_skipped {
262            self.skipped.push(SkippedEntry {
263                relative: relative.to_owned(),
264                kind,
265                detail,
266            });
267        }
268    }
269
270    pub(crate) fn warn(&mut self, relative: Option<String>, message: impl Into<String>) {
271        self.complete = false;
272        self.warnings.push(ScanWarning {
273            relative,
274            message: message.into(),
275        });
276    }
277
278    pub(crate) fn terminate(&mut self, reason: ScanTermination) {
279        self.complete = false;
280        self.termination.get_or_insert(reason);
281    }
282
283    pub(crate) fn finish_recording(&mut self) {
284        self.record_skipped = true;
285    }
286}