1use crate::report::{CompactScanReport, FileVersion, ScanReport};
2use crate::watch::WatchPlan;
3use std::path::{Path, PathBuf};
4
5pub const SCAN_CACHE_FORMAT_VERSION: u32 = 2;
7
8#[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#[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 #[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 #[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 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 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 #[must_use]
102 pub fn to_cache(&self) -> ScanCache {
103 ScanCache::from_report(self)
104 }
105}
106
107impl CompactScanReport {
108 #[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}