1use crate::config::{Admission, CacheConfig, CacheStats, PutOptions};
9use crate::error::CacheError;
10use crate::index::{self, Manifest, ManifestEntry};
11use crate::layout;
12use crate::policy::{EvictionContext, EvictionEntry};
13use dig_store::{get_capsule_identity, CapsuleIdentity};
14use std::collections::HashMap;
15use std::path::{Path, PathBuf};
16use std::sync::{Arc, Mutex};
17
18#[derive(Clone)]
20pub struct Cache {
21 inner: Arc<Mutex<Inner>>,
22}
23
24impl std::fmt::Debug for Cache {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 let inner = self.lock();
27 f.debug_struct("Cache")
28 .field("root", &inner.root)
29 .field("count", &inner.entries.len())
30 .field("bytes_used", &inner.bytes_used)
31 .field("capacity", &inner.config.max_bytes)
32 .finish()
33 }
34}
35
36#[derive(Debug, Clone)]
39pub struct CachedCapsule {
40 id: CapsuleIdentity,
41 path: PathBuf,
42}
43
44impl CachedCapsule {
45 pub fn id(&self) -> CapsuleIdentity {
47 self.id
48 }
49
50 pub fn path(&self) -> &Path {
52 &self.path
53 }
54}
55
56struct Entry {
58 key_hex: String,
59 size: u64,
60 seq: u64,
61 pinned: bool,
62}
63
64struct Inner {
66 root: PathBuf,
67 config: CacheConfig,
68 entries: HashMap<CapsuleIdentity, Entry>,
69 bytes_used: u64,
70 next_seq: u64,
71}
72
73impl Cache {
74 pub fn open(root: &Path, config: CacheConfig) -> Result<Cache, CacheError> {
82 layout::ensure_dirs(root)?;
83 layout::clean_tmp(root)?;
84 let scanned = layout::scan_capsules(root)?;
85 let manifest = index::load(root);
86
87 let inner = Inner::rebuild(root.to_path_buf(), config, scanned, manifest)?;
88 inner.save_manifest()?;
90 Ok(Cache {
91 inner: Arc::new(Mutex::new(inner)),
92 })
93 }
94
95 pub fn put_file(
98 &self,
99 id: CapsuleIdentity,
100 src: &Path,
101 opts: PutOptions,
102 ) -> Result<Admission, CacheError> {
103 let size = std::fs::metadata(src)
104 .map_err(|e| CacheError::io(src, e))?
105 .len();
106 let mut inner = self.lock();
107 inner.reject_if_too_large(id, size)?;
108 let (staged, _) = layout::stage_file(&inner.root, src)?;
109 inner.admit_staged(id, staged, size, opts.pinned)
110 }
111
112 pub fn put_bytes(
115 &self,
116 id: CapsuleIdentity,
117 bytes: &[u8],
118 opts: PutOptions,
119 ) -> Result<Admission, CacheError> {
120 if opts.check_identity {
121 verify_claimed_identity(id, bytes)?;
122 }
123 let size = bytes.len() as u64;
124 let mut inner = self.lock();
125 inner.reject_if_too_large(id, size)?;
126 let staged = layout::stage_bytes(&inner.root, bytes)?;
127 inner.admit_staged(id, staged, size, opts.pinned)
128 }
129
130 pub fn get(&self, id: &CapsuleIdentity) -> Option<CachedCapsule> {
132 let mut inner = self.lock();
133 let seq = inner.next_seq;
134 let key_hex = {
135 let entry = inner.entries.get_mut(id)?;
136 entry.seq = seq; entry.key_hex.clone()
138 };
139 inner.next_seq += 1;
140 let path = layout::capsule_path(&inner.root, &key_hex);
141 Some(CachedCapsule { id: *id, path })
142 }
143
144 pub fn get_bytes(&self, id: &CapsuleIdentity) -> Result<Option<Vec<u8>>, CacheError> {
147 let Some(cached) = self.get(id) else {
148 return Ok(None);
149 };
150 let bytes = std::fs::read(cached.path()).map_err(|e| CacheError::io(cached.path(), e))?;
151 Ok(Some(bytes))
152 }
153
154 pub fn contains(&self, id: &CapsuleIdentity) -> bool {
156 self.lock().entries.contains_key(id)
157 }
158
159 pub fn holdings(&self) -> Vec<CapsuleIdentity> {
161 self.lock().entries.keys().copied().collect()
162 }
163
164 pub fn remove(&self, id: &CapsuleIdentity) -> Result<bool, CacheError> {
166 let mut inner = self.lock();
167 if !inner.entries.contains_key(id) {
168 return Ok(false);
169 }
170 inner.drop_entry(id)?;
171 inner.save_manifest()?;
172 Ok(true)
173 }
174
175 pub fn pin(&self, id: &CapsuleIdentity) -> Result<bool, CacheError> {
177 self.set_pinned(id, true)
178 }
179
180 pub fn unpin(&self, id: &CapsuleIdentity) -> Result<bool, CacheError> {
182 self.set_pinned(id, false)
183 }
184
185 pub fn stats(&self) -> CacheStats {
187 let inner = self.lock();
188 CacheStats {
189 bytes_used: inner.bytes_used,
190 count: inner.entries.len(),
191 capacity: inner.config.max_bytes,
192 }
193 }
194
195 pub fn set_config(&self, config: CacheConfig) -> Result<Admission, CacheError> {
198 let mut inner = self.lock();
199 inner.config = config;
200 let evicted = inner.evict_to_fit();
201 inner.save_manifest()?;
202 Ok(Admission { evicted })
203 }
204
205 fn set_pinned(&self, id: &CapsuleIdentity, pinned: bool) -> Result<bool, CacheError> {
206 let mut inner = self.lock();
207 match inner.entries.get_mut(id) {
208 Some(entry) => {
209 entry.pinned = pinned;
210 inner.save_manifest()?;
211 Ok(true)
212 }
213 None => Ok(false),
214 }
215 }
216
217 fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
218 self.inner
221 .lock()
222 .unwrap_or_else(|poisoned| poisoned.into_inner())
223 }
224}
225
226impl Inner {
227 fn rebuild(
229 root: PathBuf,
230 config: CacheConfig,
231 scanned: Vec<layout::ScannedFile>,
232 manifest: Option<Manifest>,
233 ) -> Result<Inner, CacheError> {
234 let by_key: HashMap<String, ManifestEntry> = manifest
235 .map(|m| {
236 m.entries
237 .into_iter()
238 .map(|e| (e.retrieval_key.clone(), e))
239 .collect()
240 })
241 .unwrap_or_default();
242
243 struct Pending {
246 id: CapsuleIdentity,
247 key_hex: String,
248 size: u64,
249 pinned: bool,
250 sort_key: u128,
251 }
252
253 let mut pending = Vec::new();
254 for file in scanned {
255 let resolved = by_key.get(&file.key_hex).and_then(|entry| {
256 entry
257 .identity()
258 .map(|id| (id, entry.seq as u128, entry.pinned))
259 });
260
261 let (id, sort_key, pinned) = match resolved {
262 Some((id, seq, pinned)) => (id, seq, pinned),
263 None => {
264 match layout::recover_identity(&file.path) {
268 Ok(id) if layout::retrieval_key_hex(&id) == file.key_hex => {
269 (id, file.mtime_nanos, false)
270 }
271 _ => continue,
272 }
273 }
274 };
275 pending.push(Pending {
276 id,
277 key_hex: file.key_hex,
278 size: file.size,
279 pinned,
280 sort_key,
281 });
282 }
283
284 pending.sort_by_key(|p| p.sort_key);
287 let mut entries = HashMap::new();
288 let mut bytes_used = 0u64;
289 for (seq, p) in pending.into_iter().enumerate() {
290 bytes_used = bytes_used.saturating_add(p.size);
291 entries.insert(
292 p.id,
293 Entry {
294 key_hex: p.key_hex,
295 size: p.size,
296 seq: seq as u64,
297 pinned: p.pinned,
298 },
299 );
300 }
301 let next_seq = entries.len() as u64;
302
303 Ok(Inner {
304 root,
305 config,
306 entries,
307 bytes_used,
308 next_seq,
309 })
310 }
311
312 fn reject_if_too_large(&self, id: CapsuleIdentity, size: u64) -> Result<(), CacheError> {
315 if size > self.config.max_bytes {
316 return Err(CacheError::EntryTooLarge {
317 id: Box::new(id),
318 size,
319 capacity: self.config.max_bytes,
320 });
321 }
322 Ok(())
323 }
324
325 fn admit_staged(
328 &mut self,
329 id: CapsuleIdentity,
330 staged: PathBuf,
331 size: u64,
332 pinned: bool,
333 ) -> Result<Admission, CacheError> {
334 let key_hex = layout::retrieval_key_hex(&id);
335
336 let evicted = self.select_evictions(&id, size);
337 if let Err(e) = self.apply_evictions(&evicted) {
338 layout::discard_staged(&staged);
339 return Err(e);
340 }
341
342 if let Err(e) = layout::finalize(&self.root, &key_hex, &staged) {
343 layout::discard_staged(&staged);
344 return Err(e);
345 }
346
347 if let Some(previous) = self.entries.get(&id) {
348 self.bytes_used = self.bytes_used.saturating_sub(previous.size);
349 }
350 self.bytes_used = self.bytes_used.saturating_add(size);
351 let seq = self.next_seq;
352 self.next_seq += 1;
353 self.entries.insert(
354 id,
355 Entry {
356 key_hex,
357 size,
358 seq,
359 pinned,
360 },
361 );
362
363 self.save_manifest()?;
364 Ok(Admission { evicted })
365 }
366
367 fn select_evictions(
370 &self,
371 incoming_id: &CapsuleIdentity,
372 incoming: u64,
373 ) -> Vec<CapsuleIdentity> {
374 let existing = self.entries.get(incoming_id).map(|e| e.size).unwrap_or(0);
375 let current_bytes = self.bytes_used.saturating_sub(existing);
376 let entries: Vec<EvictionEntry> = self
377 .entries
378 .iter()
379 .filter(|(id, _)| *id != incoming_id)
380 .map(|(id, e)| EvictionEntry {
381 id: *id,
382 size: e.size,
383 last_access: e.seq,
384 pinned: e.pinned,
385 })
386 .collect();
387 let ctx = EvictionContext {
388 entries: &entries,
389 current_bytes,
390 capacity: self.config.max_bytes,
391 incoming_size: incoming,
392 };
393 self.config.policy.select_evictions(&ctx)
394 }
395
396 fn evict_to_fit(&mut self) -> Vec<CapsuleIdentity> {
400 let entries: Vec<EvictionEntry> = self
401 .entries
402 .iter()
403 .map(|(id, e)| EvictionEntry {
404 id: *id,
405 size: e.size,
406 last_access: e.seq,
407 pinned: e.pinned,
408 })
409 .collect();
410 let ctx = EvictionContext {
411 entries: &entries,
412 current_bytes: self.bytes_used,
413 capacity: self.config.max_bytes,
414 incoming_size: 0,
415 };
416 let evicted = self.config.policy.select_evictions(&ctx);
417 for id in &evicted {
418 let _ = self.drop_entry(id);
419 }
420 evicted
421 }
422
423 fn apply_evictions(&mut self, evicted: &[CapsuleIdentity]) -> Result<(), CacheError> {
425 for id in evicted {
426 self.drop_entry(id)?;
427 }
428 Ok(())
429 }
430
431 fn drop_entry(&mut self, id: &CapsuleIdentity) -> Result<(), CacheError> {
434 if let Some(entry) = self.entries.remove(id) {
435 layout::remove_capsule(&self.root, &entry.key_hex)?;
436 self.bytes_used = self.bytes_used.saturating_sub(entry.size);
437 }
438 Ok(())
439 }
440
441 fn save_manifest(&self) -> Result<(), CacheError> {
443 let entries = self
444 .entries
445 .iter()
446 .map(|(id, e)| ManifestEntry {
447 store_id: index::bytes32_hex(&id.store_id),
448 root_hash: index::bytes32_hex(&id.root_hash),
449 retrieval_key: e.key_hex.clone(),
450 size: e.size,
451 seq: e.seq,
452 pinned: e.pinned,
453 })
454 .collect();
455 let manifest = Manifest {
456 version: index::MANIFEST_VERSION,
457 next_seq: self.next_seq,
458 entries,
459 };
460 index::save(&self.root, &manifest)
461 }
462}
463
464fn verify_claimed_identity(claimed: CapsuleIdentity, bytes: &[u8]) -> Result<(), CacheError> {
466 let recovered = get_capsule_identity(bytes).map_err(|e| CacheError::CorruptEntry {
467 path: PathBuf::from("<in-memory bytes>"),
468 reason: e.to_string(),
469 })?;
470 if recovered != claimed {
471 return Err(CacheError::IdentityMismatch {
472 claimed: Box::new(claimed),
473 recovered: Box::new(recovered),
474 });
475 }
476 Ok(())
477}