1use std::fs;
7use std::io::Write;
8use std::path::{Path, PathBuf};
9
10use xlog_core::{Result, XlogError};
11
12use crate::xgcf::XgcfNodeType;
13
14const MAGIC: u32 = 0x584C4743; const FORMAT_VERSION: u32 = 1;
16
17const HEADER_SIZE: usize = 60;
22
23#[derive(Debug, Clone)]
24pub(crate) struct CircuitCacheKey {
25 pub cnf_hash: u64,
26 pub config_hash: u64,
27 pub random_vars_hash: u64,
28 pub sm: u32,
29}
30
31#[derive(Debug)]
32pub(crate) struct CircuitArtifact {
33 pub num_nodes: u32,
34 pub num_edges: u32,
35 pub num_levels: u32,
36 pub root: u32,
37 pub max_var: u32,
38 pub has_free_var_mask: bool,
39 pub node_type: Vec<u8>,
40 pub child_offsets: Vec<u32>,
41 pub child_indices: Vec<u32>,
42 pub lit: Vec<i32>,
43 pub decision_var: Vec<u32>,
44 pub decision_child_false: Vec<u32>,
45 pub decision_child_true: Vec<u32>,
46 pub level_nodes: Vec<u32>,
47 pub level_offsets: Vec<u32>,
48 pub free_var_mask: Vec<u8>,
49}
50
51pub(crate) fn cache_dir() -> PathBuf {
56 if let Ok(dir) = std::env::var("XLOG_CIRCUIT_CACHE_DIR") {
57 PathBuf::from(dir)
58 } else if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
59 PathBuf::from(xdg).join("xlog").join("circuits")
60 } else {
61 PathBuf::from(std::env::var("HOME").unwrap_or_default())
62 .join(".cache")
63 .join("xlog")
64 .join("circuits")
65 }
66}
67
68fn artifact_filename(key: &CircuitCacheKey) -> String {
69 format!(
70 "{:016x}_{:016x}_{:016x}_{:08x}_{:08x}.bin",
71 key.cnf_hash, key.config_hash, key.random_vars_hash, key.sm, FORMAT_VERSION,
72 )
73}
74
75pub(crate) fn write_artifact(key: &CircuitCacheKey, artifact: &CircuitArtifact) -> Result<()> {
79 write_artifact_to(&cache_dir(), key, artifact)
80}
81
82pub(crate) fn read_artifact(key: &CircuitCacheKey) -> Result<Option<CircuitArtifact>> {
87 read_artifact_from(&cache_dir(), key)
88}
89
90pub(crate) fn evict_artifact(key: &CircuitCacheKey) {
98 evict_artifact_from(&cache_dir(), key);
99}
100
101fn evict_artifact_from(dir: &Path, key: &CircuitCacheKey) {
102 let _ = fs::remove_file(dir.join(artifact_filename(key)));
103}
104
105fn io_err(e: std::io::Error) -> XlogError {
110 XlogError::Compilation(format!("circuit cache IO error: {}", e))
111}
112
113fn read_u32_vec(data: &[u8], offset: usize, count: usize) -> Vec<u32> {
119 (0..count)
120 .map(|i| {
121 let s = offset + i * 4;
122 u32::from_le_bytes([data[s], data[s + 1], data[s + 2], data[s + 3]])
123 })
124 .collect()
125}
126
127fn read_i32_vec(data: &[u8], offset: usize, count: usize) -> Vec<i32> {
129 (0..count)
130 .map(|i| {
131 let s = offset + i * 4;
132 i32::from_le_bytes([data[s], data[s + 1], data[s + 2], data[s + 3]])
133 })
134 .collect()
135}
136
137fn checked_bytes(elems: usize, bytes_per_elem: usize) -> Option<usize> {
138 elems.checked_mul(bytes_per_elem)
139}
140
141fn checked_sum(parts: &[usize]) -> Option<usize> {
142 parts
143 .iter()
144 .try_fold(0usize, |acc, part| acc.checked_add(*part))
145}
146
147fn valid_node_type(ty: u8) -> bool {
148 ty <= XgcfNodeType::Decision as u8
149}
150
151fn artifact_topology_is_valid(artifact: &CircuitArtifact) -> bool {
152 if artifact.num_nodes == 0 || artifact.num_levels == 0 || artifact.root >= artifact.num_nodes {
153 return false;
154 }
155
156 let num_nodes = artifact.num_nodes as usize;
157 let num_edges = artifact.num_edges as usize;
158 let num_levels = artifact.num_levels as usize;
159 let Ok(max_var) = usize::try_from(artifact.max_var) else {
160 return false;
161 };
162 let Some(free_var_mask_len) = max_var.checked_add(1) else {
163 return false;
164 };
165
166 if artifact.node_type.len() != num_nodes
167 || artifact.child_offsets.len() != num_nodes + 1
168 || artifact.child_indices.len() != num_edges
169 || artifact.lit.len() != num_nodes
170 || artifact.decision_var.len() != num_nodes
171 || artifact.decision_child_false.len() != num_nodes
172 || artifact.decision_child_true.len() != num_nodes
173 || artifact.level_nodes.len() != num_nodes
174 || artifact.level_offsets.len() != num_levels + 1
175 || artifact.free_var_mask.len() != free_var_mask_len
176 {
177 return false;
178 }
179
180 if artifact.child_offsets.first().copied() != Some(0)
181 || artifact.child_offsets.last().copied() != Some(artifact.num_edges)
182 {
183 return false;
184 }
185 let mut prev = 0u32;
186 for &offset in &artifact.child_offsets {
187 if offset < prev || offset > artifact.num_edges {
188 return false;
189 }
190 prev = offset;
191 }
192 if artifact
193 .child_indices
194 .iter()
195 .any(|&child| child >= artifact.num_nodes)
196 {
197 return false;
198 }
199
200 if artifact.level_offsets.first().copied() != Some(0)
201 || artifact.level_offsets.last().copied() != Some(artifact.num_nodes)
202 {
203 return false;
204 }
205 let mut prev = 0u32;
206 for &offset in &artifact.level_offsets {
207 if offset < prev || offset > artifact.num_nodes {
208 return false;
209 }
210 prev = offset;
211 }
212 if artifact
213 .level_nodes
214 .iter()
215 .any(|&node| node >= artifact.num_nodes)
216 {
217 return false;
218 }
219
220 for idx in 0..num_nodes {
221 let ty = artifact.node_type[idx];
222 if !valid_node_type(ty) {
223 return false;
224 }
225 match ty {
226 t if t == XgcfNodeType::Lit as u8
227 && artifact.lit[idx].unsigned_abs() > artifact.max_var =>
228 {
229 return false;
230 }
231 t if t == XgcfNodeType::Decision as u8 => {
232 if artifact.decision_var[idx] > artifact.max_var {
233 return false;
234 }
235 if artifact.decision_child_false[idx] >= artifact.num_nodes
236 || artifact.decision_child_true[idx] >= artifact.num_nodes
237 {
238 return false;
239 }
240 }
241 _ => {}
242 }
243 }
244
245 true
246}
247
248fn write_artifact_to(dir: &Path, key: &CircuitCacheKey, artifact: &CircuitArtifact) -> Result<()> {
249 fs::create_dir_all(dir).map_err(io_err)?;
250
251 let path = dir.join(artifact_filename(key));
252 let tmp = path.with_extension("tmp");
253 let mut f = fs::File::create(&tmp).map_err(io_err)?;
254
255 f.write_all(&MAGIC.to_le_bytes()).map_err(io_err)?;
257 f.write_all(&FORMAT_VERSION.to_le_bytes()).map_err(io_err)?;
258 f.write_all(&key.cnf_hash.to_le_bytes()).map_err(io_err)?;
259 f.write_all(&key.config_hash.to_le_bytes())
260 .map_err(io_err)?;
261 f.write_all(&key.random_vars_hash.to_le_bytes())
262 .map_err(io_err)?;
263 f.write_all(&key.sm.to_le_bytes()).map_err(io_err)?;
264 f.write_all(&artifact.num_nodes.to_le_bytes())
265 .map_err(io_err)?;
266 f.write_all(&artifact.num_edges.to_le_bytes())
267 .map_err(io_err)?;
268 f.write_all(&artifact.num_levels.to_le_bytes())
269 .map_err(io_err)?;
270 f.write_all(&artifact.root.to_le_bytes()).map_err(io_err)?;
271 f.write_all(&artifact.max_var.to_le_bytes())
272 .map_err(io_err)?;
273 f.write_all(&[artifact.has_free_var_mask as u8])
274 .map_err(io_err)?;
275 f.write_all(&[0u8; 3]).map_err(io_err)?; f.write_all(&artifact.node_type).map_err(io_err)?;
279 f.write_all(bytemuck::cast_slice(&artifact.child_offsets))
280 .map_err(io_err)?;
281 f.write_all(bytemuck::cast_slice(&artifact.child_indices))
282 .map_err(io_err)?;
283 f.write_all(bytemuck::cast_slice(&artifact.lit))
284 .map_err(io_err)?;
285 f.write_all(bytemuck::cast_slice(&artifact.decision_var))
286 .map_err(io_err)?;
287 f.write_all(bytemuck::cast_slice(&artifact.decision_child_false))
288 .map_err(io_err)?;
289 f.write_all(bytemuck::cast_slice(&artifact.decision_child_true))
290 .map_err(io_err)?;
291 f.write_all(bytemuck::cast_slice(&artifact.level_nodes))
292 .map_err(io_err)?;
293 f.write_all(bytemuck::cast_slice(&artifact.level_offsets))
294 .map_err(io_err)?;
295 f.write_all(&artifact.free_var_mask).map_err(io_err)?;
296
297 drop(f);
298 fs::rename(&tmp, &path).map_err(io_err)?;
299
300 evict_if_needed_in(dir)?;
301 Ok(())
302}
303
304fn read_artifact_from(dir: &Path, key: &CircuitCacheKey) -> Result<Option<CircuitArtifact>> {
305 let path = dir.join(artifact_filename(key));
306
307 let data = match fs::read(&path) {
308 Ok(d) => d,
309 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
310 Err(e) => {
311 return Err(XlogError::Compilation(format!(
312 "Failed to read cache file: {}",
313 e
314 )))
315 }
316 };
317
318 parse_artifact(&data, key)
319}
320
321fn parse_artifact(data: &[u8], key: &CircuitCacheKey) -> Result<Option<CircuitArtifact>> {
323 if data.len() < HEADER_SIZE {
325 return Ok(None);
326 }
327
328 let mut cursor = 0usize;
329
330 macro_rules! read_u32 {
331 () => {{
332 let val = u32::from_le_bytes([
333 data[cursor],
334 data[cursor + 1],
335 data[cursor + 2],
336 data[cursor + 3],
337 ]);
338 cursor += 4;
339 val
340 }};
341 }
342
343 macro_rules! read_u64 {
344 () => {{
345 let val = u64::from_le_bytes([
346 data[cursor],
347 data[cursor + 1],
348 data[cursor + 2],
349 data[cursor + 3],
350 data[cursor + 4],
351 data[cursor + 5],
352 data[cursor + 6],
353 data[cursor + 7],
354 ]);
355 cursor += 8;
356 val
357 }};
358 }
359
360 let magic = read_u32!();
362 if magic != MAGIC {
363 return Ok(None);
364 }
365
366 let version = read_u32!();
368 if version != FORMAT_VERSION {
369 return Ok(None);
370 }
371
372 let cnf_hash = read_u64!();
374 let config_hash = read_u64!();
375 let random_vars_hash = read_u64!();
376 let sm = read_u32!();
377
378 if cnf_hash != key.cnf_hash
379 || config_hash != key.config_hash
380 || random_vars_hash != key.random_vars_hash
381 || sm != key.sm
382 {
383 return Ok(None);
384 }
385
386 let num_nodes = read_u32!();
388 let num_edges = read_u32!();
389 let num_levels = read_u32!();
390 let root = read_u32!();
391 let max_var = read_u32!();
392 let has_free_var_mask = data[cursor] != 0;
393 cursor += 1;
394 cursor += 3; debug_assert_eq!(cursor, HEADER_SIZE);
397
398 if num_nodes == 0 || num_levels == 0 || root >= num_nodes {
399 return Ok(None);
400 }
401
402 let Ok(num_nodes_usize) = usize::try_from(num_nodes) else {
405 return Ok(None);
406 };
407 let Ok(num_edges_usize) = usize::try_from(num_edges) else {
408 return Ok(None);
409 };
410 let Ok(num_levels_usize) = usize::try_from(num_levels) else {
411 return Ok(None);
412 };
413 let Ok(max_var_usize) = usize::try_from(max_var) else {
414 return Ok(None);
415 };
416
417 let Some(child_offsets_elems) = num_nodes_usize.checked_add(1) else {
418 return Ok(None);
419 };
420 let Some(level_offsets_elems) = num_levels_usize.checked_add(1) else {
421 return Ok(None);
422 };
423 let Some(free_var_mask_bytes) = max_var_usize.checked_add(1) else {
424 return Ok(None);
425 };
426
427 let node_type_bytes = num_nodes_usize;
428 let Some(child_offsets_bytes) = checked_bytes(child_offsets_elems, 4) else {
429 return Ok(None);
430 };
431 let Some(child_indices_bytes) = checked_bytes(num_edges_usize, 4) else {
432 return Ok(None);
433 };
434 let Some(lit_bytes) = checked_bytes(num_nodes_usize, 4) else {
435 return Ok(None);
436 };
437 let Some(decision_var_bytes) = checked_bytes(num_nodes_usize, 4) else {
438 return Ok(None);
439 };
440 let Some(decision_child_false_bytes) = checked_bytes(num_nodes_usize, 4) else {
441 return Ok(None);
442 };
443 let Some(decision_child_true_bytes) = checked_bytes(num_nodes_usize, 4) else {
444 return Ok(None);
445 };
446 let Some(level_nodes_bytes) = checked_bytes(num_nodes_usize, 4) else {
447 return Ok(None);
448 };
449 let Some(level_offsets_bytes) = checked_bytes(level_offsets_elems, 4) else {
450 return Ok(None);
451 };
452
453 let Some(expected_total) = checked_sum(&[
454 HEADER_SIZE,
455 node_type_bytes,
456 child_offsets_bytes,
457 child_indices_bytes,
458 lit_bytes,
459 decision_var_bytes,
460 decision_child_false_bytes,
461 decision_child_true_bytes,
462 level_nodes_bytes,
463 level_offsets_bytes,
464 free_var_mask_bytes,
465 ]) else {
466 return Ok(None);
467 };
468
469 if data.len() < expected_total {
470 return Ok(None);
471 }
472
473 let node_type = data[cursor..cursor + node_type_bytes].to_vec();
479 cursor += node_type_bytes;
480
481 let child_offsets = read_u32_vec(data, cursor, child_offsets_elems);
482 cursor += child_offsets_bytes;
483
484 let child_indices = read_u32_vec(data, cursor, num_edges_usize);
485 cursor += child_indices_bytes;
486
487 let lit = read_i32_vec(data, cursor, num_nodes_usize);
488 cursor += lit_bytes;
489
490 let decision_var = read_u32_vec(data, cursor, num_nodes_usize);
491 cursor += decision_var_bytes;
492
493 let decision_child_false = read_u32_vec(data, cursor, num_nodes_usize);
494 cursor += decision_child_false_bytes;
495
496 let decision_child_true = read_u32_vec(data, cursor, num_nodes_usize);
497 cursor += decision_child_true_bytes;
498
499 let level_nodes = read_u32_vec(data, cursor, num_nodes_usize);
500 cursor += level_nodes_bytes;
501
502 let level_offsets = read_u32_vec(data, cursor, level_offsets_elems);
503 cursor += level_offsets_bytes;
504
505 let free_var_mask = data[cursor..cursor + free_var_mask_bytes].to_vec();
506 let artifact = CircuitArtifact {
509 num_nodes,
510 num_edges,
511 num_levels,
512 root,
513 max_var,
514 has_free_var_mask,
515 node_type,
516 child_offsets,
517 child_indices,
518 lit,
519 decision_var,
520 decision_child_false,
521 decision_child_true,
522 level_nodes,
523 level_offsets,
524 free_var_mask,
525 };
526
527 if !artifact_topology_is_valid(&artifact) {
528 return Ok(None);
529 }
530
531 Ok(Some(artifact))
532}
533
534fn evict_if_needed_in(dir: &Path) -> Result<()> {
538 let max_mb: u64 = std::env::var("XLOG_CIRCUIT_CACHE_MAX_MB")
539 .ok()
540 .and_then(|v| v.parse().ok())
541 .unwrap_or(512);
542 evict_if_needed_in_with_limit(dir, max_mb)
543}
544
545fn evict_if_needed_in_with_limit(dir: &Path, max_mb: u64) -> Result<()> {
546 let max_bytes = max_mb * 1024 * 1024;
547
548 let entries = match fs::read_dir(dir) {
549 Ok(e) => e,
550 Err(_) => return Ok(()), };
552
553 let mut files: Vec<(PathBuf, u64, std::time::SystemTime)> = Vec::new();
555 let mut total_size: u64 = 0;
556
557 for entry in entries {
558 let entry = match entry {
559 Ok(e) => e,
560 Err(_) => continue,
561 };
562 let path = entry.path();
563 if path.extension().and_then(|e| e.to_str()) != Some("bin") {
564 continue;
565 }
566 let meta = match entry.metadata() {
567 Ok(m) => m,
568 Err(_) => continue,
569 };
570 if !meta.is_file() {
571 continue;
572 }
573 let size = meta.len();
574 let mtime = meta.modified().unwrap_or(std::time::UNIX_EPOCH);
575 total_size += size;
576 files.push((path, size, mtime));
577 }
578
579 if total_size <= max_bytes {
580 return Ok(());
581 }
582
583 files.sort_by_key(|&(_, _, mtime)| mtime);
585
586 for (path, size, _) in &files {
587 if total_size <= max_bytes {
588 break;
589 }
590 if fs::remove_file(path).is_ok() {
592 total_size = total_size.saturating_sub(*size);
593 }
594 }
595
596 Ok(())
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602 use std::sync::atomic::{AtomicU64, Ordering};
603
604 static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
606
607 fn test_cache_dir() -> PathBuf {
608 let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
609 let pid = std::process::id();
610 let dir = std::env::temp_dir()
611 .join("xlog_disk_cache_test")
612 .join(format!("{}_{}", pid, id));
613 fs::create_dir_all(&dir).expect("create test cache dir");
614 dir
615 }
616
617 fn make_key(cnf_hash: u64) -> CircuitCacheKey {
618 CircuitCacheKey {
619 cnf_hash,
620 config_hash: 0xDEADBEEF,
621 random_vars_hash: 0xCAFEBABE,
622 sm: 89,
623 }
624 }
625
626 fn make_artifact() -> CircuitArtifact {
627 CircuitArtifact {
629 num_nodes: 4,
630 num_edges: 3,
631 num_levels: 2,
632 root: 0,
633 max_var: 2,
634 has_free_var_mask: true,
635 node_type: vec![1, 2, 3, 4],
636 child_offsets: vec![0, 1, 2, 3, 3], child_indices: vec![1, 2, 3], lit: vec![0, 1, -1, 2], decision_var: vec![0, 1, 2, 0],
640 decision_child_false: vec![0, 2, 3, 0],
641 decision_child_true: vec![0, 1, 3, 0],
642 level_nodes: vec![0, 1, 2, 3], level_offsets: vec![0, 1, 4], free_var_mask: vec![0, 1, 0], }
646 }
647
648 #[test]
649 fn test_roundtrip() {
650 let dir = test_cache_dir();
651
652 let key = make_key(0x1234);
653 let artifact = make_artifact();
654
655 write_artifact_to(&dir, &key, &artifact).expect("write should succeed");
656
657 let loaded = read_artifact_from(&dir, &key)
658 .expect("read should not error")
659 .expect("read should return Some");
660
661 assert_eq!(loaded.num_nodes, artifact.num_nodes);
662 assert_eq!(loaded.num_edges, artifact.num_edges);
663 assert_eq!(loaded.num_levels, artifact.num_levels);
664 assert_eq!(loaded.root, artifact.root);
665 assert_eq!(loaded.max_var, artifact.max_var);
666 assert_eq!(loaded.has_free_var_mask, artifact.has_free_var_mask);
667 assert_eq!(loaded.node_type, artifact.node_type);
668 assert_eq!(loaded.child_offsets, artifact.child_offsets);
669 assert_eq!(loaded.child_indices, artifact.child_indices);
670 assert_eq!(loaded.lit, artifact.lit);
671 assert_eq!(loaded.decision_var, artifact.decision_var);
672 assert_eq!(loaded.decision_child_false, artifact.decision_child_false);
673 assert_eq!(loaded.decision_child_true, artifact.decision_child_true);
674 assert_eq!(loaded.level_nodes, artifact.level_nodes);
675 assert_eq!(loaded.level_offsets, artifact.level_offsets);
676 assert_eq!(loaded.free_var_mask, artifact.free_var_mask);
677
678 let _ = fs::remove_dir_all(&dir);
679 }
680
681 #[test]
685 fn test_roundtrip_unaligned_num_nodes() {
686 let dir = test_cache_dir();
687
688 let key = make_key(0x5555);
689 let artifact = CircuitArtifact {
691 num_nodes: 5,
692 num_edges: 4,
693 num_levels: 3,
694 root: 0,
695 max_var: 3,
696 has_free_var_mask: false,
697 node_type: vec![1, 2, 3, 4, 5],
698 child_offsets: vec![0, 1, 2, 3, 4, 4], child_indices: vec![1, 2, 3, 4], lit: vec![0, 1, -1, 2, -2], decision_var: vec![0, 1, 2, 3, 0],
702 decision_child_false: vec![0, 2, 3, 4, 0],
703 decision_child_true: vec![0, 1, 3, 4, 0],
704 level_nodes: vec![0, 1, 2, 3, 4], level_offsets: vec![0, 1, 3, 5], free_var_mask: vec![0, 1, 0, 1], };
708
709 write_artifact_to(&dir, &key, &artifact).expect("write should succeed");
710
711 let loaded = read_artifact_from(&dir, &key)
712 .expect("read should not error")
713 .expect("read should return Some");
714
715 assert_eq!(loaded.num_nodes, artifact.num_nodes);
716 assert_eq!(loaded.num_edges, artifact.num_edges);
717 assert_eq!(loaded.num_levels, artifact.num_levels);
718 assert_eq!(loaded.root, artifact.root);
719 assert_eq!(loaded.max_var, artifact.max_var);
720 assert_eq!(loaded.has_free_var_mask, artifact.has_free_var_mask);
721 assert_eq!(loaded.node_type, artifact.node_type);
722 assert_eq!(loaded.child_offsets, artifact.child_offsets);
723 assert_eq!(loaded.child_indices, artifact.child_indices);
724 assert_eq!(loaded.lit, artifact.lit);
725 assert_eq!(loaded.decision_var, artifact.decision_var);
726 assert_eq!(loaded.decision_child_false, artifact.decision_child_false);
727 assert_eq!(loaded.decision_child_true, artifact.decision_child_true);
728 assert_eq!(loaded.level_nodes, artifact.level_nodes);
729 assert_eq!(loaded.level_offsets, artifact.level_offsets);
730 assert_eq!(loaded.free_var_mask, artifact.free_var_mask);
731
732 let _ = fs::remove_dir_all(&dir);
733 }
734
735 #[test]
736 fn test_mismatched_key_returns_none() {
737 let dir = test_cache_dir();
738
739 let key = make_key(0xAAAA);
740 let artifact = make_artifact();
741
742 write_artifact_to(&dir, &key, &artifact).expect("write should succeed");
743
744 let different_key = make_key(0xBBBB);
746 let result = read_artifact_from(&dir, &different_key).expect("read should not error");
747 assert!(result.is_none(), "mismatched key should return None");
748
749 let _ = fs::remove_dir_all(&dir);
750 }
751
752 #[test]
753 fn test_missing_file_returns_none() {
754 let dir = test_cache_dir();
755
756 let key = make_key(0x9999);
757 let result = read_artifact_from(&dir, &key).expect("read should not error");
758 assert!(result.is_none(), "missing file should return None");
759
760 let _ = fs::remove_dir_all(&dir);
761 }
762
763 #[test]
764 fn test_truncated_file_returns_none() {
765 let dir = test_cache_dir();
766
767 let key = make_key(0x7777);
768 let artifact = make_artifact();
769 write_artifact_to(&dir, &key, &artifact).expect("write should succeed");
770
771 let path = dir.join(artifact_filename(&key));
773 let data = fs::read(&path).unwrap();
774 fs::write(&path, &data[..HEADER_SIZE / 2]).unwrap();
775
776 let result = read_artifact_from(&dir, &key).expect("read should not error");
777 assert!(result.is_none(), "truncated file should return None");
778
779 let _ = fs::remove_dir_all(&dir);
780 }
781
782 #[test]
783 fn test_corrupted_magic_returns_none() {
784 let dir = test_cache_dir();
785
786 let key = make_key(0x6666);
787 let artifact = make_artifact();
788 write_artifact_to(&dir, &key, &artifact).expect("write should succeed");
789
790 let path = dir.join(artifact_filename(&key));
792 let mut data = fs::read(&path).unwrap();
793 data[0] = 0xFF;
794 data[1] = 0xFF;
795 fs::write(&path, &data).unwrap();
796
797 let result = read_artifact_from(&dir, &key).expect("read should not error");
798 assert!(result.is_none(), "corrupted magic should return None");
799
800 let _ = fs::remove_dir_all(&dir);
801 }
802
803 #[test]
804 fn test_eviction() {
805 let dir = test_cache_dir();
806
807 let key1 = make_key(0x1111);
808 let key2 = make_key(0x2222);
809 let artifact = make_artifact();
810
811 write_artifact_to(&dir, &key1, &artifact).expect("write 1 should succeed");
814 write_artifact_to(&dir, &key2, &artifact).expect("write 2 should succeed");
815
816 assert!(read_artifact_from(&dir, &key1).unwrap().is_some());
818 assert!(read_artifact_from(&dir, &key2).unwrap().is_some());
819
820 evict_if_needed_in_with_limit(&dir, 0).expect("eviction should succeed");
822
823 let r1 = read_artifact_from(&dir, &key1).unwrap();
825 let r2 = read_artifact_from(&dir, &key2).unwrap();
826 assert!(r1.is_none(), "key1 should have been evicted");
827 assert!(r2.is_none(), "key2 should have been evicted");
828
829 let _ = fs::remove_dir_all(&dir);
830 }
831
832 #[test]
833 fn test_evict_artifact_removes_stale_entry() {
834 let dir = test_cache_dir();
840 let key = make_key(0xA11CE);
841 let artifact = make_artifact();
842
843 write_artifact_to(&dir, &key, &artifact).expect("write should succeed");
844 assert!(read_artifact_from(&dir, &key).expect("read ok").is_some());
845
846 evict_artifact_from(&dir, &key);
847 assert!(
848 read_artifact_from(&dir, &key).expect("read ok").is_none(),
849 "evicted entry must read as a cache miss"
850 );
851 evict_artifact_from(&dir, &key);
853
854 let _ = fs::remove_dir_all(&dir);
855 }
856}