driven/fusion/
binary_cache.rs1use crate::Result;
6use std::collections::HashMap;
7use std::fs::{self, File};
8use std::io::{Read, Write};
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct CacheKey {
14 pub template_hash: u64,
16 pub source_hash: u64,
18}
19
20impl CacheKey {
21 pub fn new(template_hash: u64, source_hash: u64) -> Self {
23 Self {
24 template_hash,
25 source_hash,
26 }
27 }
28
29 pub fn filename(&self) -> String {
31 format!("{:016x}_{:016x}.dtm", self.template_hash, self.source_hash)
32 }
33
34 pub fn from_filename(name: &str) -> Option<Self> {
36 let name = name.strip_suffix(".dtm")?;
37 let mut parts = name.split('_');
38
39 let template_hash = u64::from_str_radix(parts.next()?, 16).ok()?;
40 let source_hash = u64::from_str_radix(parts.next()?, 16).ok()?;
41
42 Some(Self {
43 template_hash,
44 source_hash,
45 })
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct CacheEntry {
52 pub key: CacheKey,
54 pub path: PathBuf,
56 pub size: u64,
58 pub created: std::time::SystemTime,
60}
61
62#[derive(Debug)]
64pub struct BinaryCache {
65 cache_dir: PathBuf,
67 index: HashMap<u64, CacheEntry>,
69 max_size: u64,
71 current_size: u64,
73}
74
75impl BinaryCache {
76 pub fn open(cache_dir: &Path) -> Result<Self> {
78 fs::create_dir_all(cache_dir)?;
79
80 let mut cache = Self {
81 cache_dir: cache_dir.to_path_buf(),
82 index: HashMap::new(),
83 max_size: 512 * 1024 * 1024, current_size: 0,
85 };
86
87 cache.scan()?;
88
89 Ok(cache)
90 }
91
92 pub fn with_max_size(mut self, max_size: u64) -> Self {
94 self.max_size = max_size;
95 self
96 }
97
98 fn scan(&mut self) -> Result<()> {
100 for entry in fs::read_dir(&self.cache_dir)? {
101 let entry = entry?;
102 let path = entry.path();
103
104 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
105 if let Some(key) = CacheKey::from_filename(name) {
106 if let Ok(metadata) = entry.metadata() {
107 let cache_entry = CacheEntry {
108 key,
109 path: path.clone(),
110 size: metadata.len(),
111 created: metadata
112 .created()
113 .unwrap_or(std::time::SystemTime::UNIX_EPOCH),
114 };
115 self.current_size += cache_entry.size;
116 self.index.insert(key.template_hash, cache_entry);
117 }
118 }
119 }
120 }
121
122 Ok(())
123 }
124
125 pub fn get(&self, template_hash: u64, source_hash: u64) -> Result<Option<Vec<u8>>> {
127 if let Some(entry) = self.index.get(&template_hash) {
128 if entry.key.source_hash == source_hash {
130 let mut file = File::open(&entry.path)?;
131 let mut data = Vec::new();
132 file.read_to_end(&mut data)?;
133 return Ok(Some(data));
134 }
135 }
137
138 Ok(None)
139 }
140
141 pub fn put(&mut self, template_hash: u64, source_hash: u64, data: &[u8]) -> Result<()> {
143 let key = CacheKey::new(template_hash, source_hash);
144
145 while self.current_size + data.len() as u64 > self.max_size {
147 self.evict_oldest()?;
148 }
149
150 if let Some(old) = self.index.remove(&template_hash) {
152 self.current_size -= old.size;
153 let _ = fs::remove_file(&old.path);
154 }
155
156 let path = self.cache_dir.join(key.filename());
158 let mut file = File::create(&path)?;
159 file.write_all(data)?;
160
161 let entry = CacheEntry {
162 key,
163 path,
164 size: data.len() as u64,
165 created: std::time::SystemTime::now(),
166 };
167
168 self.current_size += entry.size;
169 self.index.insert(template_hash, entry);
170
171 Ok(())
172 }
173
174 pub fn remove(&mut self, template_hash: u64) -> Result<bool> {
176 if let Some(entry) = self.index.remove(&template_hash) {
177 self.current_size -= entry.size;
178 fs::remove_file(&entry.path)?;
179 return Ok(true);
180 }
181 Ok(false)
182 }
183
184 pub fn clear(&mut self) -> Result<()> {
186 for entry in self.index.values() {
187 let _ = fs::remove_file(&entry.path);
188 }
189 self.index.clear();
190 self.current_size = 0;
191 Ok(())
192 }
193
194 pub fn stats(&self) -> BinaryCacheStats {
196 BinaryCacheStats {
197 entries: self.index.len(),
198 size_bytes: self.current_size,
199 max_size_bytes: self.max_size,
200 }
201 }
202
203 fn evict_oldest(&mut self) -> Result<()> {
205 let oldest = self
206 .index
207 .iter()
208 .min_by_key(|(_, e)| e.created)
209 .map(|(k, _)| *k);
210
211 if let Some(key) = oldest {
212 self.remove(key)?;
213 }
214
215 Ok(())
216 }
217}
218
219#[derive(Debug, Clone)]
221pub struct BinaryCacheStats {
222 pub entries: usize,
224 pub size_bytes: u64,
226 pub max_size_bytes: u64,
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn test_cache_key_filename() {
236 let key = CacheKey::new(0x1234567890ABCDEF, 0xFEDCBA0987654321);
237 let filename = key.filename();
238
239 assert_eq!(filename, "1234567890abcdef_fedcba0987654321.dtm");
240
241 let parsed = CacheKey::from_filename(&filename).unwrap();
242 assert_eq!(parsed, key);
243 }
244
245 #[test]
246 fn test_cache_operations() {
247 let temp_dir = std::env::temp_dir().join("driven_cache_test");
248 let _ = fs::remove_dir_all(&temp_dir);
249
250 let mut cache = BinaryCache::open(&temp_dir).unwrap();
251
252 cache.put(1, 100, b"test data").unwrap();
254 let data = cache.get(1, 100).unwrap();
255 assert_eq!(data.as_deref(), Some(b"test data".as_slice()));
256
257 let stale = cache.get(1, 200).unwrap();
259 assert!(stale.is_none());
260
261 let _ = fs::remove_dir_all(&temp_dir);
263 }
264}