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