fallow_extract/cache/
store.rs1use std::path::Path;
4
5use rustc_hash::FxHashMap;
6
7use bitcode::{Decode, Encode};
8
9use super::types::{CACHE_VERSION, CachedModule, MAX_CACHE_SIZE};
10
11#[derive(Debug, Encode, Decode)]
13pub struct CacheStore {
14 version: u32,
15 entries: FxHashMap<String, CachedModule>,
17}
18
19impl CacheStore {
20 #[must_use]
22 pub fn new() -> Self {
23 Self {
24 version: CACHE_VERSION,
25 entries: FxHashMap::default(),
26 }
27 }
28
29 #[must_use]
31 pub fn load(cache_dir: &Path) -> Option<Self> {
32 let cache_file = cache_dir.join("cache.bin");
33 let data = std::fs::read(&cache_file).ok()?;
34 if data.len() > MAX_CACHE_SIZE {
35 tracing::warn!(
36 size_mb = data.len() / (1024 * 1024),
37 "Cache file exceeds size limit, ignoring"
38 );
39 return None;
40 }
41 let store: Self = bitcode::decode(&data).ok()?;
42 if store.version != CACHE_VERSION {
43 return None;
44 }
45 Some(store)
46 }
47
48 pub fn save(&self, cache_dir: &Path) -> Result<(), String> {
55 std::fs::create_dir_all(cache_dir)
56 .map_err(|e| format!("Failed to create cache dir: {e}"))?;
57 write_cache_gitignore(cache_dir)?;
58 let cache_file = cache_dir.join("cache.bin");
59 let data = bitcode::encode(self);
60 std::fs::write(&cache_file, data).map_err(|e| format!("Failed to write cache: {e}"))?;
61 Ok(())
62 }
63
64 #[must_use]
67 pub fn get(&self, path: &Path, content_hash: u64) -> Option<&CachedModule> {
68 let key = path.to_string_lossy().to_string();
69 let entry = self.entries.get(&key)?;
70 if entry.content_hash == content_hash {
71 Some(entry)
72 } else {
73 None
74 }
75 }
76
77 pub fn insert(&mut self, path: &Path, module: CachedModule) {
79 let key = path.to_string_lossy().to_string();
80 self.entries.insert(key, module);
81 }
82
83 #[must_use]
90 pub fn get_by_metadata(
91 &self,
92 path: &Path,
93 mtime_secs: u64,
94 file_size: u64,
95 ) -> Option<&CachedModule> {
96 let key = path.to_string_lossy().to_string();
97 let entry = self.entries.get(&key)?;
98 if entry.mtime_secs == mtime_secs && entry.file_size == file_size && mtime_secs > 0 {
99 Some(entry)
100 } else {
101 None
102 }
103 }
104
105 #[must_use]
109 pub fn get_by_path_only(&self, path: &Path) -> Option<&CachedModule> {
110 let key = path.to_string_lossy().to_string();
111 self.entries.get(&key)
112 }
113
114 pub fn retain_paths(&mut self, files: &[fallow_types::discover::DiscoveredFile]) {
117 use rustc_hash::FxHashSet;
118 let current_paths: FxHashSet<String> = files
119 .iter()
120 .map(|f| f.path.to_string_lossy().to_string())
121 .collect();
122 self.entries.retain(|key, _| current_paths.contains(key));
123 }
124
125 #[must_use]
127 pub fn len(&self) -> usize {
128 self.entries.len()
129 }
130
131 #[must_use]
133 pub fn is_empty(&self) -> bool {
134 self.entries.is_empty()
135 }
136}
137
138fn write_cache_gitignore(cache_dir: &Path) -> Result<(), String> {
139 std::fs::write(cache_dir.join(".gitignore"), "*\n")
140 .map_err(|e| format!("Failed to write cache .gitignore: {e}"))
141}
142
143impl Default for CacheStore {
144 fn default() -> Self {
145 Self::new()
146 }
147}