use std::path::PathBuf;
use crate::models::ScannedCLITool;
pub struct ScanCache {
cache_dir: PathBuf,
}
impl ScanCache {
pub fn new(cache_dir: PathBuf) -> Self {
Self { cache_dir }
}
pub fn get(&self, tool_name: &str, tool_version: Option<&str>) -> Option<ScannedCLITool> {
let key = format!(
"{}_{}.scan.json",
tool_name,
tool_version.unwrap_or("unknown")
);
let path = self.cache_dir.join(&key);
let contents = std::fs::read_to_string(&path).ok()?;
serde_json::from_str(&contents).ok()
}
pub fn put(&self, tool: &ScannedCLITool) -> anyhow::Result<()> {
std::fs::create_dir_all(&self.cache_dir)?;
let key = format!(
"{}_{}.scan.json",
tool.name,
tool.version.as_deref().unwrap_or("unknown")
);
let path = self.cache_dir.join(&key);
let json = serde_json::to_string_pretty(tool)?;
std::fs::write(&path, json)?;
Ok(())
}
pub fn invalidate(&self, tool_name: &str) {
if let Ok(entries) = std::fs::read_dir(&self.cache_dir) {
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
let Some(stem) = name_str.strip_suffix(".scan.json") else {
continue;
};
if let Some((cached_name, _version)) = stem.rsplit_once('_') {
if cached_name == tool_name {
let _ = std::fs::remove_file(entry.path());
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::StructuredOutputInfo;
use tempfile::TempDir;
fn make_tool(name: &str, version: Option<&str>) -> ScannedCLITool {
ScannedCLITool {
name: name.into(),
binary_path: format!("/usr/bin/{name}"),
version: version.map(|v| v.to_string()),
subcommands: vec![],
global_flags: vec![],
structured_output: StructuredOutputInfo::default(),
scan_tier: 1,
warnings: vec![],
}
}
#[test]
fn test_cache_put_and_get() {
let tmp = TempDir::new().unwrap();
let cache = ScanCache::new(tmp.path().to_path_buf());
let tool = make_tool("git", Some("2.43.0"));
cache.put(&tool).unwrap();
let cached = cache.get("git", Some("2.43.0"));
assert!(cached.is_some());
let cached = cached.unwrap();
assert_eq!(cached.name, "git");
assert_eq!(cached.version, Some("2.43.0".into()));
}
#[test]
fn test_cache_miss_wrong_version() {
let tmp = TempDir::new().unwrap();
let cache = ScanCache::new(tmp.path().to_path_buf());
let tool = make_tool("git", Some("2.43.0"));
cache.put(&tool).unwrap();
let cached = cache.get("git", Some("2.44.0"));
assert!(cached.is_none());
}
#[test]
fn test_cache_miss_empty() {
let tmp = TempDir::new().unwrap();
let cache = ScanCache::new(tmp.path().to_path_buf());
let cached = cache.get("git", Some("2.43.0"));
assert!(cached.is_none());
}
#[test]
fn test_cache_unknown_version() {
let tmp = TempDir::new().unwrap();
let cache = ScanCache::new(tmp.path().to_path_buf());
let tool = make_tool("mytool", None);
cache.put(&tool).unwrap();
let cached = cache.get("mytool", None);
assert!(cached.is_some());
}
#[test]
fn test_cache_invalidate() {
let tmp = TempDir::new().unwrap();
let cache = ScanCache::new(tmp.path().to_path_buf());
let tool1 = make_tool("git", Some("2.43.0"));
let tool2 = make_tool("git", Some("2.44.0"));
cache.put(&tool1).unwrap();
cache.put(&tool2).unwrap();
assert!(cache.get("git", Some("2.43.0")).is_some());
assert!(cache.get("git", Some("2.44.0")).is_some());
cache.invalidate("git");
assert!(cache.get("git", Some("2.43.0")).is_none());
assert!(cache.get("git", Some("2.44.0")).is_none());
}
#[test]
fn test_cache_invalidate_empty_no_error() {
let tmp = TempDir::new().unwrap();
let cache = ScanCache::new(tmp.path().to_path_buf());
cache.invalidate("nonexistent");
}
#[test]
fn test_cache_invalidate_preserves_other_tools() {
let tmp = TempDir::new().unwrap();
let cache = ScanCache::new(tmp.path().to_path_buf());
let git = make_tool("git", Some("2.43.0"));
let docker = make_tool("docker", Some("24.0.0"));
cache.put(&git).unwrap();
cache.put(&docker).unwrap();
cache.invalidate("git");
assert!(cache.get("git", Some("2.43.0")).is_none());
assert!(cache.get("docker", Some("24.0.0")).is_some());
}
#[test]
fn test_cache_invalidate_does_not_delete_prefix_sibling() {
let tmp = TempDir::new().unwrap();
let cache = ScanCache::new(tmp.path().to_path_buf());
cache.put(&make_tool("foo", Some("1.0"))).unwrap();
cache.put(&make_tool("foo_bar", Some("2.0"))).unwrap();
cache.invalidate("foo");
assert!(
cache.get("foo", Some("1.0")).is_none(),
"foo not invalidated"
);
assert!(
cache.get("foo_bar", Some("2.0")).is_some(),
"sibling foo_bar wrongly deleted by invalidate(foo)"
);
}
}