use std::fs;
use std::path::PathBuf;
use std::time::{Duration, SystemTime};
use anyhow::{Context, Result};
use crate::types::AnalysisReport;
const DEFAULT_TTL_SECS: u64 = 24 * 60 * 60;
fn cache_dir() -> Result<PathBuf> {
let home = std::env::var("HOME").context("HOME environment variable not set")?;
Ok(PathBuf::from(home).join(".aegis").join("cache"))
}
fn cache_key(name: &str, version: &str) -> String {
let safe_name = name.replace('/', "__");
format!("{}@{}.json", safe_name, version)
}
pub fn get_cached(name: &str, version: &str) -> Option<AnalysisReport> {
get_cached_with_ttl(name, version, DEFAULT_TTL_SECS)
}
fn get_cached_with_ttl(name: &str, version: &str, ttl_secs: u64) -> Option<AnalysisReport> {
let dir = cache_dir().ok()?;
let path = dir.join(cache_key(name, version));
if !path.exists() {
return None;
}
let metadata = fs::metadata(&path).ok()?;
let modified = metadata.modified().ok()?;
let age = SystemTime::now()
.duration_since(modified)
.unwrap_or(Duration::MAX);
if age > Duration::from_secs(ttl_secs) {
let _ = fs::remove_file(&path);
return None;
}
let content = fs::read_to_string(&path).ok()?;
serde_json::from_str(&content).ok()
}
pub fn save_cache(report: &AnalysisReport) -> Result<()> {
let dir = cache_dir()?;
fs::create_dir_all(&dir).context("failed to create cache directory")?;
let path = dir.join(cache_key(&report.package_name, &report.version));
let json = serde_json::to_string_pretty(report).context("failed to serialize report")?;
fs::write(&path, json).with_context(|| format!("failed to write cache file {:?}", path))?;
Ok(())
}
pub fn clear_cache() -> Result<()> {
let dir = cache_dir()?;
if dir.exists() {
fs::remove_dir_all(&dir).context("failed to remove cache directory")?;
}
println!("Cache cleared.");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{AnalysisReport, RiskLabel};
fn sample_report() -> AnalysisReport {
AnalysisReport {
package_name: "test-pkg".to_string(),
version: "1.0.0".to_string(),
findings: vec![],
risk_score: 0.0,
risk_label: RiskLabel::Clean,
}
}
fn save_to(dir: &std::path::Path, report: &AnalysisReport) {
fs::create_dir_all(dir).unwrap();
let path = dir.join(cache_key(&report.package_name, &report.version));
let json = serde_json::to_string_pretty(report).unwrap();
fs::write(path, json).unwrap();
}
fn read_from(dir: &std::path::Path, name: &str, version: &str) -> Option<AnalysisReport> {
let path = dir.join(cache_key(name, version));
if !path.exists() {
return None;
}
let content = fs::read_to_string(&path).ok()?;
serde_json::from_str(&content).ok()
}
#[test]
fn cache_roundtrip() {
let tmp = tempfile::TempDir::new().unwrap();
let report = sample_report();
save_to(tmp.path(), &report);
let cached = read_from(tmp.path(), "test-pkg", "1.0.0");
assert!(cached.is_some());
let cached = cached.unwrap();
assert_eq!(cached.package_name, "test-pkg");
assert_eq!(cached.version, "1.0.0");
}
#[test]
fn cache_miss_for_unknown() {
let tmp = tempfile::TempDir::new().unwrap();
let cached = read_from(tmp.path(), "nonexistent-pkg-xyz", "0.0.1");
assert!(cached.is_none());
}
#[test]
fn expired_entry_returns_none() {
let tmp = tempfile::TempDir::new().unwrap();
let report = sample_report();
save_to(tmp.path(), &report);
let path = tmp.path().join(cache_key("test-pkg", "1.0.0"));
let content = fs::read_to_string(&path).unwrap();
let parsed: Option<AnalysisReport> = serde_json::from_str(&content).ok();
assert!(parsed.is_some()); }
#[test]
fn scoped_package_cache_key() {
let key = cache_key("@scope/name", "2.0.0");
assert_eq!(key, "@scope__name@2.0.0.json");
}
}