1use std::path::{Path, PathBuf};
8
9use crate::air::AirBundle;
10
11pub struct BundleCache {
17 cache_dir: PathBuf,
18}
19
20impl BundleCache {
21 #[must_use]
25 pub fn new(cache_dir: impl Into<PathBuf>) -> Self {
26 Self {
27 cache_dir: cache_dir.into(),
28 }
29 }
30
31 #[must_use]
35 pub fn get(&self, fingerprint: &[u8]) -> Option<AirBundle> {
36 let key = hex_encode(fingerprint);
37 let path = self.cache_dir.join(format!("{key}.air.json"));
38 let data = std::fs::read_to_string(path).ok()?;
39 serde_json::from_str(&data).ok()
40 }
41
42 pub fn put(&self, fingerprint: &[u8], bundle: &AirBundle) -> Result<(), std::io::Error> {
51 std::fs::create_dir_all(&self.cache_dir)?;
52 let key = hex_encode(fingerprint);
53 let path = self.cache_dir.join(format!("{key}.air.json"));
54 let data = serde_json::to_string(bundle)
55 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
56 std::fs::write(path, data)
57 }
58
59 pub fn remove(&self, fingerprint: &[u8]) -> Result<bool, std::io::Error> {
67 let key = hex_encode(fingerprint);
68 let path = self.cache_dir.join(format!("{key}.air.json"));
69 if path.exists() {
70 std::fs::remove_file(path)?;
71 Ok(true)
72 } else {
73 Ok(false)
74 }
75 }
76
77 #[must_use]
79 pub fn cache_dir(&self) -> &Path {
80 &self.cache_dir
81 }
82}
83
84fn hex_encode(bytes: &[u8]) -> String {
86 use std::fmt::Write;
87 bytes
88 .iter()
89 .fold(String::with_capacity(bytes.len() * 2), |mut s, b| {
90 let _ = write!(s, "{b:02x}");
91 s
92 })
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::air::AirModule;
99 use crate::ids::ModuleId;
100
101 fn minimal_bundle() -> AirBundle {
103 let module = AirModule::new(ModuleId::derive(b"cache_test"));
104 AirBundle::new("test", module)
105 }
106
107 #[test]
108 fn hex_encode_empty() {
109 assert_eq!(hex_encode(&[]), "");
110 }
111
112 #[test]
113 fn hex_encode_bytes() {
114 assert_eq!(hex_encode(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
115 }
116
117 #[test]
118 fn cache_miss_returns_none() {
119 let dir = tempfile::tempdir().unwrap();
120 let cache = BundleCache::new(dir.path());
121 assert!(cache.get(b"nonexistent").is_none());
122 }
123
124 #[test]
125 fn cache_roundtrip() {
126 let dir = tempfile::tempdir().unwrap();
127 let cache = BundleCache::new(dir.path());
128
129 let bundle = minimal_bundle();
130 let fingerprint = b"test_fingerprint_123";
131
132 cache.put(fingerprint, &bundle).unwrap();
133 let retrieved = cache.get(fingerprint).expect("should find cached bundle");
134 assert_eq!(retrieved, bundle);
135 }
136
137 #[test]
138 fn cache_remove() {
139 let dir = tempfile::tempdir().unwrap();
140 let cache = BundleCache::new(dir.path());
141
142 let bundle = minimal_bundle();
143 let fingerprint = b"removable";
144
145 cache.put(fingerprint, &bundle).unwrap();
146 assert!(cache.get(fingerprint).is_some());
147
148 assert!(cache.remove(fingerprint).unwrap());
149 assert!(cache.get(fingerprint).is_none());
150
151 assert!(!cache.remove(fingerprint).unwrap());
153 }
154
155 #[test]
156 fn cache_creates_directory() {
157 let dir = tempfile::tempdir().unwrap();
158 let cache_dir = dir.path().join("nested").join("cache");
159 let cache = BundleCache::new(&cache_dir);
160
161 let bundle = minimal_bundle();
162 cache.put(b"test", &bundle).unwrap();
163
164 assert!(cache_dir.exists());
165 }
166}