1use std::collections::BTreeMap;
23use std::path::PathBuf;
24
25use crate::hash::ContentHash;
26
27pub fn cache_dir() -> Option<PathBuf> {
35 if let Ok(dir) = std::env::var("TRIDENT_CACHE_DIR") {
36 return Some(PathBuf::from(dir));
37 }
38
39 dirs_home().map(|home| home.join(".trident").join("cache"))
40}
41
42fn dirs_home() -> Option<PathBuf> {
44 std::env::var("HOME").ok().map(PathBuf::from)
45}
46
47fn ensure_cache_subdir(subdir: &str) -> Option<PathBuf> {
49 let dir = cache_dir()?.join(subdir);
50 std::fs::create_dir_all(&dir).ok()?;
51 Some(dir)
52}
53
54#[derive(Clone, Debug)]
58pub struct CachedCompilation {
59 pub tasm: String,
61 pub padded_height: Option<u64>,
63}
64
65pub fn lookup_compilation(source_hash: &ContentHash, target: &str) -> Option<CachedCompilation> {
67 let dir = cache_dir()?.join("compile");
68 let filename = format!("{}.{}.tasm", source_hash.to_hex(), target);
69 let path = dir.join(&filename);
70
71 let tasm = std::fs::read_to_string(&path).ok()?;
72
73 let meta_path = dir.join(format!("{}.{}.meta", source_hash.to_hex(), target));
75 let padded_height = std::fs::read_to_string(&meta_path)
76 .ok()
77 .and_then(|s| s.trim().parse::<u64>().ok());
78
79 Some(CachedCompilation {
80 tasm,
81 padded_height,
82 })
83}
84
85pub fn store_compilation(
87 source_hash: &ContentHash,
88 target: &str,
89 tasm: &str,
90 padded_height: Option<u64>,
91) -> Result<PathBuf, String> {
92 let dir = ensure_cache_subdir("compile")
93 .ok_or_else(|| "cannot create cache directory".to_string())?;
94
95 let filename = format!("{}.{}.tasm", source_hash.to_hex(), target);
96 let path = dir.join(&filename);
97
98 if path.exists() {
100 return Ok(path);
101 }
102
103 std::fs::write(&path, tasm).map_err(|e| format!("cannot write cache file: {}", e))?;
104
105 if let Some(height) = padded_height {
107 let meta_path = dir.join(format!("{}.{}.meta", source_hash.to_hex(), target));
108 let _ = std::fs::write(&meta_path, height.to_string());
109 }
110
111 Ok(path)
112}
113
114#[derive(Clone, Debug)]
118pub struct CachedVerification {
119 pub is_safe: bool,
121 pub constraints: usize,
123 pub variables: u32,
125 pub verdict: String,
127 pub timestamp: String,
129}
130
131impl CachedVerification {
132 fn serialize(&self) -> String {
134 format!(
135 "safe={}\nconstraints={}\nvariables={}\nverdict={}\ntimestamp={}\n",
136 self.is_safe, self.constraints, self.variables, self.verdict, self.timestamp,
137 )
138 }
139
140 fn deserialize(text: &str) -> Option<Self> {
142 let mut map = BTreeMap::new();
143 for line in text.lines() {
144 if let Some((key, value)) = line.split_once('=') {
145 map.insert(key.trim().to_string(), value.trim().to_string());
146 }
147 }
148
149 Some(CachedVerification {
150 is_safe: map.get("safe")?.parse().ok()?,
151 constraints: map.get("constraints")?.parse().ok()?,
152 variables: map.get("variables")?.parse().ok()?,
153 verdict: map.get("verdict")?.clone(),
154 timestamp: map.get("timestamp").cloned().unwrap_or_default(),
155 })
156 }
157}
158
159pub fn lookup_verification(source_hash: &ContentHash) -> Option<CachedVerification> {
161 let dir = cache_dir()?.join("verify");
162 let filename = format!("{}.verify", source_hash.to_hex());
163 let path = dir.join(&filename);
164
165 let text = std::fs::read_to_string(&path).ok()?;
166 CachedVerification::deserialize(&text)
167}
168
169pub fn store_verification(
171 source_hash: &ContentHash,
172 result: &CachedVerification,
173) -> Result<PathBuf, String> {
174 let dir =
175 ensure_cache_subdir("verify").ok_or_else(|| "cannot create cache directory".to_string())?;
176
177 let filename = format!("{}.verify", source_hash.to_hex());
178 let path = dir.join(&filename);
179
180 if path.exists() {
182 return Ok(path);
183 }
184
185 std::fs::write(&path, result.serialize())
186 .map_err(|e| format!("cannot write cache file: {}", e))?;
187
188 Ok(path)
189}
190
191#[derive(Clone, Debug, Default)]
195pub struct CacheStats {
196 pub compilations: usize,
198 pub verifications: usize,
200 pub total_bytes: u64,
202}
203
204pub fn stats() -> CacheStats {
206 let mut stats = CacheStats::default();
207
208 if let Some(base) = cache_dir() {
209 let compile_dir = base.join("compile");
210 if compile_dir.is_dir() {
211 if let Ok(entries) = std::fs::read_dir(&compile_dir) {
212 for entry in entries.flatten() {
213 let path = entry.path();
214 if path.extension().is_some_and(|e| e == "tasm") {
215 stats.compilations += 1;
216 }
217 if let Ok(meta) = std::fs::metadata(&path) {
218 stats.total_bytes += meta.len();
219 }
220 }
221 }
222 }
223
224 let verify_dir = base.join("verify");
225 if verify_dir.is_dir() {
226 if let Ok(entries) = std::fs::read_dir(&verify_dir) {
227 for entry in entries.flatten() {
228 let path = entry.path();
229 if path.extension().is_some_and(|e| e == "verify") {
230 stats.verifications += 1;
231 }
232 if let Ok(meta) = std::fs::metadata(&path) {
233 stats.total_bytes += meta.len();
234 }
235 }
236 }
237 }
238 }
239
240 stats
241}
242
243pub fn clear() -> Result<(), String> {
245 if let Some(base) = cache_dir() {
246 if base.exists() {
247 std::fs::remove_dir_all(&base).map_err(|e| format!("cannot clear cache: {}", e))?;
248 }
249 }
250 Ok(())
251}
252
253pub fn timestamp() -> String {
255 format!("{}", crate::package::unix_timestamp())
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 fn test_hash() -> ContentHash {
263 ContentHash([0xAB; 32])
264 }
265
266 #[test]
267 fn test_cache_dir_resolution() {
268 let _ = cache_dir();
270 }
271
272 #[test]
273 fn test_cached_verification_round_trip() {
274 let result = CachedVerification {
275 is_safe: true,
276 constraints: 42,
277 variables: 10,
278 verdict: "Safe".to_string(),
279 timestamp: "1234567890".to_string(),
280 };
281
282 let serialized = result.serialize();
283 let deserialized = CachedVerification::deserialize(&serialized).unwrap();
284
285 assert_eq!(deserialized.is_safe, true);
286 assert_eq!(deserialized.constraints, 42);
287 assert_eq!(deserialized.variables, 10);
288 assert_eq!(deserialized.verdict, "Safe");
289 }
290
291 fn store_compilation_at(
295 dir: &std::path::Path,
296 hash: &ContentHash,
297 target: &str,
298 tasm: &str,
299 height: Option<u64>,
300 ) {
301 let compile_dir = dir.join("compile");
302 std::fs::create_dir_all(&compile_dir).unwrap();
303 let filename = format!("{}.{}.tasm", hash.to_hex(), target);
304 let path = compile_dir.join(&filename);
305 if !path.exists() {
306 std::fs::write(&path, tasm).unwrap();
307 }
308 if let Some(h) = height {
309 let meta = compile_dir.join(format!("{}.{}.meta", hash.to_hex(), target));
310 std::fs::write(&meta, h.to_string()).unwrap();
311 }
312 }
313
314 fn lookup_compilation_at(
315 dir: &std::path::Path,
316 hash: &ContentHash,
317 target: &str,
318 ) -> Option<CachedCompilation> {
319 let compile_dir = dir.join("compile");
320 let filename = format!("{}.{}.tasm", hash.to_hex(), target);
321 let path = compile_dir.join(&filename);
322 let tasm = std::fs::read_to_string(&path).ok()?;
323 let meta = compile_dir.join(format!("{}.{}.meta", hash.to_hex(), target));
324 let padded_height = std::fs::read_to_string(&meta)
325 .ok()
326 .and_then(|s| s.trim().parse().ok());
327 Some(CachedCompilation {
328 tasm,
329 padded_height,
330 })
331 }
332
333 fn store_verification_at(
334 dir: &std::path::Path,
335 hash: &ContentHash,
336 result: &CachedVerification,
337 ) {
338 let verify_dir = dir.join("verify");
339 std::fs::create_dir_all(&verify_dir).unwrap();
340 let filename = format!("{}.verify", hash.to_hex());
341 let path = verify_dir.join(&filename);
342 if !path.exists() {
343 std::fs::write(&path, result.serialize()).unwrap();
344 }
345 }
346
347 fn lookup_verification_at(
348 dir: &std::path::Path,
349 hash: &ContentHash,
350 ) -> Option<CachedVerification> {
351 let verify_dir = dir.join("verify");
352 let filename = format!("{}.verify", hash.to_hex());
353 let path = verify_dir.join(&filename);
354 let text = std::fs::read_to_string(&path).ok()?;
355 CachedVerification::deserialize(&text)
356 }
357
358 #[test]
359 fn test_store_and_lookup_compilation() {
360 let tmp = tempfile::tempdir().unwrap();
361 let dir = tmp.path();
362 let hash = test_hash();
363 let tasm = "push 1\npush 2\nadd\n";
364
365 store_compilation_at(dir, &hash, "triton", tasm, Some(32));
366
367 let cached = lookup_compilation_at(dir, &hash, "triton").unwrap();
368 assert_eq!(cached.tasm, tasm);
369 assert_eq!(cached.padded_height, Some(32));
370
371 assert!(lookup_compilation_at(dir, &hash, "miden").is_none());
373 }
374
375 #[test]
376 fn test_store_and_lookup_verification() {
377 let tmp = tempfile::tempdir().unwrap();
378 let dir = tmp.path();
379 let hash = test_hash();
380 let result = CachedVerification {
381 is_safe: true,
382 constraints: 5,
383 variables: 3,
384 verdict: "Safe".to_string(),
385 timestamp: "12345".to_string(),
386 };
387
388 store_verification_at(dir, &hash, &result);
389
390 let cached = lookup_verification_at(dir, &hash).unwrap();
391 assert!(cached.is_safe);
392 assert_eq!(cached.constraints, 5);
393 }
394
395 #[test]
396 fn test_cache_stats_direct() {
397 let tmp = tempfile::tempdir().unwrap();
398 let dir = tmp.path();
399 let hash = test_hash();
400
401 store_compilation_at(dir, &hash, "triton", "push 1\n", None);
402 let result = CachedVerification {
403 is_safe: true,
404 constraints: 1,
405 variables: 1,
406 verdict: "Safe".to_string(),
407 timestamp: "12345".to_string(),
408 };
409 store_verification_at(dir, &hash, &result);
410
411 let compile_count = std::fs::read_dir(dir.join("compile"))
413 .unwrap()
414 .filter_map(|e| e.ok())
415 .filter(|e| e.path().extension().is_some_and(|ext| ext == "tasm"))
416 .count();
417 let verify_count = std::fs::read_dir(dir.join("verify"))
418 .unwrap()
419 .filter_map(|e| e.ok())
420 .filter(|e| e.path().extension().is_some_and(|ext| ext == "verify"))
421 .count();
422 assert_eq!(compile_count, 1);
423 assert_eq!(verify_count, 1);
424 }
425
426 #[test]
427 fn test_append_only_semantics() {
428 let tmp = tempfile::tempdir().unwrap();
429 let dir = tmp.path();
430 let hash = test_hash();
431
432 store_compilation_at(dir, &hash, "triton", "push 1\n", None);
434
435 store_compilation_at(dir, &hash, "triton", "push 2\n", None);
437
438 let cached = lookup_compilation_at(dir, &hash, "triton").unwrap();
439 assert_eq!(cached.tasm, "push 1\n", "append-only: first write wins");
440 }
441}