sbom_tools/enrichment/
cache.rs1use super::source::{CacheStats, JsonCache};
9use crate::error::Result;
10use crate::model::VulnerabilityRef;
11use std::path::PathBuf;
12use std::time::Duration;
13
14pub use super::source::CacheKey;
15
16pub struct FileCache {
18 inner: JsonCache<Vec<VulnerabilityRef>>,
19}
20
21impl FileCache {
22 pub fn new(cache_dir: PathBuf, ttl: Duration) -> Result<Self> {
24 Ok(Self {
25 inner: JsonCache::new(cache_dir, ttl)?,
26 })
27 }
28
29 #[must_use]
33 pub fn get(&self, key: &CacheKey) -> Option<Vec<VulnerabilityRef>> {
34 self.inner.get(key)
35 }
36
37 pub fn set(&self, key: &CacheKey, vulns: &[VulnerabilityRef]) -> Result<()> {
39 self.inner.set(key, vulns)
40 }
41
42 pub fn remove(&self, key: &CacheKey) -> Result<()> {
44 self.inner.remove(key)
45 }
46
47 pub fn clear(&self) -> Result<()> {
49 self.inner.clear()
50 }
51
52 #[must_use]
54 pub fn stats(&self) -> CacheStats {
55 self.inner.stats()
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 fn make_key(purl: Option<&str>, name: &str, eco: Option<&str>, ver: Option<&str>) -> CacheKey {
64 CacheKey::new(
65 purl.map(String::from),
66 name.to_string(),
67 eco.map(String::from),
68 ver.map(String::from),
69 )
70 }
71
72 #[test]
73 fn test_cache_key_filename_deterministic() {
74 let key = make_key(Some("pkg:npm/foo@1.0"), "foo", Some("npm"), Some("1.0"));
75 let f1 = key.to_filename();
76 let f2 = key.to_filename();
77 assert_eq!(f1, f2);
78 assert!(f1.ends_with(".json"));
79 }
80
81 #[test]
82 fn test_cache_key_filename_different() {
83 let k1 = make_key(Some("pkg:npm/foo@1.0"), "foo", Some("npm"), Some("1.0"));
84 let k2 = make_key(Some("pkg:npm/bar@1.0"), "bar", Some("npm"), Some("1.0"));
85 assert_ne!(k1.to_filename(), k2.to_filename());
86 }
87
88 #[test]
89 fn test_cache_key_is_queryable_purl() {
90 let key = make_key(Some("pkg:npm/foo@1.0"), "foo", None, None);
91 assert!(key.is_queryable());
92 }
93
94 #[test]
95 fn test_cache_key_is_queryable_eco_ver() {
96 let key = make_key(None, "foo", Some("npm"), Some("1.0"));
97 assert!(key.is_queryable());
98 }
99
100 #[test]
101 fn test_cache_key_is_queryable_name_only() {
102 let key = make_key(None, "foo", None, None);
103 assert!(!key.is_queryable());
104 }
105
106 #[test]
107 fn test_file_cache_new_creates_dir() {
108 let tmp = tempfile::tempdir().unwrap();
109 let cache_dir = tmp.path().join("vuln_cache");
110 assert!(!cache_dir.exists());
111 let _cache = FileCache::new(cache_dir.clone(), Duration::from_secs(3600)).unwrap();
112 assert!(cache_dir.exists());
113 }
114
115 #[test]
116 fn test_file_cache_set_get_roundtrip() {
117 let tmp = tempfile::tempdir().unwrap();
118 let cache = FileCache::new(tmp.path().to_path_buf(), Duration::from_secs(3600)).unwrap();
119 let key = make_key(Some("pkg:npm/foo@1.0"), "foo", Some("npm"), Some("1.0"));
120
121 let vulns = vec![VulnerabilityRef::new(
122 "CVE-2024-0001".to_string(),
123 crate::model::VulnerabilitySource::Osv,
124 )];
125
126 cache.set(&key, &vulns).unwrap();
127 let result = cache.get(&key);
128 assert!(result.is_some());
129 let retrieved = result.unwrap();
130 assert_eq!(retrieved.len(), 1);
131 assert_eq!(retrieved[0].id, "CVE-2024-0001");
132 }
133
134 #[test]
135 fn test_file_cache_get_miss() {
136 let tmp = tempfile::tempdir().unwrap();
137 let cache = FileCache::new(tmp.path().to_path_buf(), Duration::from_secs(3600)).unwrap();
138 let key = make_key(Some("pkg:npm/nope@1.0"), "nope", Some("npm"), Some("1.0"));
139 assert!(cache.get(&key).is_none());
140 }
141
142 #[test]
143 fn test_file_cache_remove() {
144 let tmp = tempfile::tempdir().unwrap();
145 let cache = FileCache::new(tmp.path().to_path_buf(), Duration::from_secs(3600)).unwrap();
146 let key = make_key(Some("pkg:npm/rm@1.0"), "rm", Some("npm"), Some("1.0"));
147
148 cache.set(&key, &[]).unwrap();
149 assert!(cache.get(&key).is_some());
150 cache.remove(&key).unwrap();
151 assert!(cache.get(&key).is_none());
152 }
153
154 #[test]
155 fn test_file_cache_clear() {
156 let tmp = tempfile::tempdir().unwrap();
157 let cache = FileCache::new(tmp.path().to_path_buf(), Duration::from_secs(3600)).unwrap();
158
159 for i in 0..3 {
160 let key = make_key(None, &format!("pkg{i}"), Some("npm"), Some("1.0"));
161 cache.set(&key, &[]).unwrap();
162 }
163
164 assert_eq!(cache.stats().total_entries, 3);
165 cache.clear().unwrap();
166 assert_eq!(cache.stats().total_entries, 0);
167 }
168
169 #[test]
170 fn test_file_cache_stats_counts() {
171 let tmp = tempfile::tempdir().unwrap();
172 let cache = FileCache::new(tmp.path().to_path_buf(), Duration::from_secs(3600)).unwrap();
173
174 for i in 0..3 {
175 let key = make_key(None, &format!("stats{i}"), Some("npm"), Some("1.0"));
176 cache.set(&key, &[]).unwrap();
177 }
178
179 let stats = cache.stats();
180 assert_eq!(stats.total_entries, 3);
181 assert_eq!(stats.expired_entries, 0);
182 }
183}