Skip to main content

weavatrix_scan/
cache.rs

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