Skip to main content

weavatrix_scan/
cache.rs

1use crate::report::{CompactScanReport, FileVersion, ScanReport};
2use std::path::{Path, PathBuf};
3
4/// Current on-disk format understood by [`ScanCache`].
5pub const SCAN_CACHE_FORMAT_VERSION: u32 = 2;
6
7/// Compact reusable evidence for one content-hashed file.
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ScanCacheEntry {
11    pub relative: String,
12    pub bytes: u64,
13    pub content_hash: String,
14    #[cfg_attr(feature = "serde", serde(default))]
15    pub content_fingerprint: String,
16    pub version: FileVersion,
17    pub binary_checked: bool,
18}
19
20/// Versioned local cache for incremental scans.
21///
22/// Unlike [`ScanReport`], this contains no absolute per-file paths, skipped
23/// entries, warnings, ignore diagnostics, or manifest metadata.
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct ScanCache {
27    pub format_version: u32,
28    #[cfg_attr(feature = "serde", serde(with = "crate::path_serde"))]
29    pub root: PathBuf,
30    pub entries: Vec<ScanCacheEntry>,
31}
32
33impl ScanCache {
34    /// Builds a compact cache from reusable SHA-256 evidence.
35    #[must_use]
36    pub fn from_report(report: &ScanReport) -> Self {
37        let entries = report
38            .files
39            .iter()
40            .filter_map(|file| {
41                let content_hash = file
42                    .content_hash
43                    .as_ref()
44                    .filter(|hash| hash.starts_with("sha256:"))?
45                    .clone();
46                Some(ScanCacheEntry {
47                    relative: file.relative.clone(),
48                    bytes: file.bytes,
49                    content_hash,
50                    content_fingerprint: file.content_fingerprint.clone()?,
51                    version: file.version,
52                    binary_checked: file.binary_checked,
53                })
54            })
55            .collect();
56        Self {
57            format_version: SCAN_CACHE_FORMAT_VERSION,
58            root: report.root.clone(),
59            entries,
60        }
61    }
62
63    /// Returns whether this cache can be considered for `root`.
64    #[must_use]
65    pub fn is_compatible(&self, root: &Path) -> bool {
66        self.format_version == SCAN_CACHE_FORMAT_VERSION && self.root == root
67    }
68
69    /// Removes reusable entries for the supplied normalized relative paths.
70    ///
71    /// Returns the number of entries removed.
72    pub fn invalidate<'a, I>(&mut self, relative_paths: I) -> usize
73    where
74        I: IntoIterator<Item = &'a str>,
75    {
76        let paths = relative_paths
77            .into_iter()
78            .collect::<std::collections::HashSet<_>>();
79        let before = self.entries.len();
80        self.entries
81            .retain(|entry| !paths.contains(entry.relative.as_str()));
82        before - self.entries.len()
83    }
84
85    /// Applies a watcher plan, clearing everything when selection may change.
86    ///
87    /// Returns the number of entries removed.
88    pub fn apply_watch_plan(&mut self, plan: &crate::WatchPlan) -> usize {
89        if plan.full_rescan {
90            let removed = self.entries.len();
91            self.entries.clear();
92            return removed;
93        }
94        self.invalidate(plan.invalidated())
95    }
96}
97
98impl ScanReport {
99    /// Extracts the compact, versioned evidence needed for a later scan.
100    #[must_use]
101    pub fn to_cache(&self) -> ScanCache {
102        ScanCache::from_report(self)
103    }
104}
105
106impl CompactScanReport {
107    /// Extracts reusable SHA-256 evidence without materializing absolute paths.
108    #[must_use]
109    pub fn to_cache(&self) -> ScanCache {
110        let entries = self
111            .files
112            .iter()
113            .filter_map(|file| {
114                let content = file.content.as_deref()?;
115                let content_hash = content
116                    .content_hash
117                    .as_deref()
118                    .filter(|hash| hash.starts_with("sha256:"))?
119                    .to_owned();
120                Some(ScanCacheEntry {
121                    relative: file.relative.to_string(),
122                    bytes: file.bytes,
123                    content_hash,
124                    content_fingerprint: content.content_fingerprint.as_deref()?.to_owned(),
125                    version: content.version,
126                    binary_checked: content.binary_checked,
127                })
128            })
129            .collect();
130        ScanCache {
131            format_version: SCAN_CACHE_FORMAT_VERSION,
132            root: self.root.clone(),
133            entries,
134        }
135    }
136}