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).map_err(std::io::Error::other)?;
55 std::fs::write(path, data)
56 }
57
58 pub fn remove(&self, fingerprint: &[u8]) -> Result<bool, std::io::Error> {
66 let key = hex_encode(fingerprint);
67 let path = self.cache_dir.join(format!("{key}.air.json"));
68 if path.exists() {
69 std::fs::remove_file(path)?;
70 Ok(true)
71 } else {
72 Ok(false)
73 }
74 }
75
76 #[must_use]
78 pub fn cache_dir(&self) -> &Path {
79 &self.cache_dir
80 }
81}
82
83fn hex_encode(bytes: &[u8]) -> String {
85 use std::fmt::Write;
86 bytes
87 .iter()
88 .fold(String::with_capacity(bytes.len() * 2), |mut s, b| {
89 let _ = write!(s, "{b:02x}");
90 s
91 })
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::air::AirModule;
98 use crate::ids::ModuleId;
99
100 fn minimal_bundle() -> AirBundle {
102 let module = AirModule::new(ModuleId::derive(b"cache_test"));
103 AirBundle::new("test", module)
104 }
105
106 #[test]
107 fn hex_encode_empty() {
108 assert_eq!(hex_encode(&[]), "");
109 }
110
111 #[test]
112 fn hex_encode_bytes() {
113 assert_eq!(hex_encode(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
114 }
115
116 #[test]
117 fn cache_miss_returns_none() {
118 let dir = tempfile::tempdir().unwrap();
119 let cache = BundleCache::new(dir.path());
120 assert!(cache.get(b"nonexistent").is_none());
121 }
122
123 #[test]
124 fn cache_roundtrip() {
125 let dir = tempfile::tempdir().unwrap();
126 let cache = BundleCache::new(dir.path());
127
128 let bundle = minimal_bundle();
129 let fingerprint = b"test_fingerprint_123";
130
131 cache.put(fingerprint, &bundle).unwrap();
132 let retrieved = cache.get(fingerprint).expect("should find cached bundle");
133 assert_eq!(retrieved, bundle);
134 }
135
136 #[test]
137 fn cache_remove() {
138 let dir = tempfile::tempdir().unwrap();
139 let cache = BundleCache::new(dir.path());
140
141 let bundle = minimal_bundle();
142 let fingerprint = b"removable";
143
144 cache.put(fingerprint, &bundle).unwrap();
145 assert!(cache.get(fingerprint).is_some());
146
147 assert!(cache.remove(fingerprint).unwrap());
148 assert!(cache.get(fingerprint).is_none());
149
150 assert!(!cache.remove(fingerprint).unwrap());
152 }
153
154 #[test]
155 fn cache_creates_directory() {
156 let dir = tempfile::tempdir().unwrap();
157 let cache_dir = dir.path().join("nested").join("cache");
158 let cache = BundleCache::new(&cache_dir);
159
160 let bundle = minimal_bundle();
161 cache.put(b"test", &bundle).unwrap();
162
163 assert!(cache_dir.exists());
164 }
165}