sbom_tools/enrichment/
cache.rs1use crate::error::Result;
4use crate::model::VulnerabilityRef;
5use sha2::{Digest, Sha256};
6use std::fs;
7use std::path::PathBuf;
8use std::time::Duration;
9
10#[derive(Debug, Clone, Hash, PartialEq, Eq)]
12pub struct CacheKey {
13 pub purl: Option<String>,
15 pub name: String,
17 pub ecosystem: Option<String>,
19 pub version: Option<String>,
21}
22
23impl CacheKey {
24 pub fn new(
26 purl: Option<String>,
27 name: String,
28 ecosystem: Option<String>,
29 version: Option<String>,
30 ) -> Self {
31 Self {
32 purl,
33 name,
34 ecosystem,
35 version,
36 }
37 }
38
39 pub fn to_filename(&self) -> String {
41 let mut hasher = Sha256::new();
42 hasher.update(format!(
43 "purl:{:?}|name:{}|eco:{:?}|ver:{:?}",
44 self.purl, self.name, self.ecosystem, self.version
45 ));
46 let hash = hasher.finalize();
47 format!("{:x}.json", hash)
48 }
49
50 pub fn is_queryable(&self) -> bool {
52 self.purl.is_some() || (self.ecosystem.is_some() && self.version.is_some())
54 }
55}
56
57pub struct FileCache {
59 cache_dir: PathBuf,
61 ttl: Duration,
63}
64
65impl FileCache {
66 pub fn new(cache_dir: PathBuf, ttl: Duration) -> Result<Self> {
68 if !cache_dir.exists() {
70 fs::create_dir_all(&cache_dir)?;
71 }
72 Ok(Self { cache_dir, ttl })
73 }
74
75 pub fn get(&self, key: &CacheKey) -> Option<Vec<VulnerabilityRef>> {
79 let path = self.cache_dir.join(key.to_filename());
80
81 let metadata = fs::metadata(&path).ok()?;
83
84 let modified = metadata.modified().ok()?;
86 let age = modified.elapsed().ok()?;
87 if age > self.ttl {
88 let _ = fs::remove_file(&path);
90 return None;
91 }
92
93 let data = fs::read_to_string(&path).ok()?;
95 serde_json::from_str(&data).ok()
96 }
97
98 pub fn set(&self, key: &CacheKey, vulns: &[VulnerabilityRef]) -> Result<()> {
100 let path = self.cache_dir.join(key.to_filename());
101 let data = serde_json::to_string(vulns)?;
102 fs::write(path, data)?;
103 Ok(())
104 }
105
106 pub fn remove(&self, key: &CacheKey) -> Result<()> {
108 let path = self.cache_dir.join(key.to_filename());
109 if path.exists() {
110 fs::remove_file(path)?;
111 }
112 Ok(())
113 }
114
115 pub fn clear(&self) -> Result<()> {
117 if self.cache_dir.exists() {
118 for entry in fs::read_dir(&self.cache_dir)? {
119 let entry = entry?;
120 if entry
121 .path()
122 .extension()
123 .map(|e| e == "json")
124 .unwrap_or(false)
125 {
126 let _ = fs::remove_file(entry.path());
127 }
128 }
129 }
130 Ok(())
131 }
132
133 pub fn stats(&self) -> CacheStats {
135 let mut stats = CacheStats::default();
136
137 if let Ok(entries) = fs::read_dir(&self.cache_dir) {
138 for entry in entries.flatten() {
139 if entry
140 .path()
141 .extension()
142 .map(|e| e == "json")
143 .unwrap_or(false)
144 {
145 stats.total_entries += 1;
146 if let Ok(metadata) = entry.metadata() {
147 stats.total_size += metadata.len();
148
149 if let Ok(modified) = metadata.modified() {
151 if let Ok(age) = modified.elapsed() {
152 if age > self.ttl {
153 stats.expired_entries += 1;
154 }
155 }
156 }
157 }
158 }
159 }
160 }
161
162 stats
163 }
164}
165
166#[derive(Debug, Default)]
168pub struct CacheStats {
169 pub total_entries: usize,
171 pub expired_entries: usize,
173 pub total_size: u64,
175}