1use std::path::Path;
2
3use chrono::Utc;
4use serde::{Deserialize, Serialize};
5
6use super::io::{CacheIoError, load_snapshot_payload, write_snapshot_payload};
7use super::metadata::{CacheInvalidationKey, CacheSnapshotMetadata};
8use super::paths::scan_result_cache_path;
9use crate::models::{
10 Author, Copyright, FileInfo, Holder, LicenseDetection, OutputEmail, OutputURL, PackageData,
11};
12
13const SCAN_CACHE_SCHEMA_VERSION: u32 = 1;
14const SCAN_CACHE_ENGINE_VERSION: &str = "scan-result-cache-v1";
15const SCAN_CACHE_RULES_FINGERPRINT: &str = env!("CARGO_PKG_VERSION");
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct CachedScanFindings {
19 pub package_data: Vec<PackageData>,
20 pub license_expression: Option<String>,
21 pub license_detections: Vec<LicenseDetection>,
22 pub copyrights: Vec<Copyright>,
23 pub holders: Vec<Holder>,
24 pub authors: Vec<Author>,
25 pub emails: Vec<OutputEmail>,
26 pub urls: Vec<OutputURL>,
27 pub programming_language: Option<String>,
28}
29
30impl CachedScanFindings {
31 pub fn from_file_info(file_info: &FileInfo) -> Self {
32 Self {
33 package_data: file_info.package_data.clone(),
34 license_expression: file_info.license_expression.clone(),
35 license_detections: file_info.license_detections.clone(),
36 copyrights: file_info.copyrights.clone(),
37 holders: file_info.holders.clone(),
38 authors: file_info.authors.clone(),
39 emails: file_info.emails.clone(),
40 urls: file_info.urls.clone(),
41 programming_language: file_info.programming_language.clone(),
42 }
43 }
44}
45
46pub fn read_cached_findings(
47 scan_results_dir: &Path,
48 sha256: &str,
49 options_fingerprint: &str,
50) -> Result<Option<CachedScanFindings>, CacheIoError> {
51 let Some(path) = scan_result_cache_path(scan_results_dir, sha256) else {
52 return Ok(None);
53 };
54
55 let key = CacheInvalidationKey {
56 cache_schema_version: SCAN_CACHE_SCHEMA_VERSION,
57 engine_version: SCAN_CACHE_ENGINE_VERSION,
58 rules_fingerprint: SCAN_CACHE_RULES_FINGERPRINT,
59 build_options_fingerprint: options_fingerprint,
60 };
61
62 let Some(payload) = load_snapshot_payload(&path, &key)? else {
63 return Ok(None);
64 };
65
66 match rmp_serde::decode::from_slice::<CachedScanFindings>(&payload) {
67 Ok(findings) => Ok(Some(findings)),
68 Err(_) => Ok(None),
69 }
70}
71
72pub fn write_cached_findings(
73 scan_results_dir: &Path,
74 sha256: &str,
75 options_fingerprint: &str,
76 findings: &CachedScanFindings,
77) -> Result<(), CacheIoError> {
78 let Some(path) = scan_result_cache_path(scan_results_dir, sha256) else {
79 return Ok(());
80 };
81
82 let metadata = CacheSnapshotMetadata {
83 cache_schema_version: SCAN_CACHE_SCHEMA_VERSION,
84 engine_version: SCAN_CACHE_ENGINE_VERSION.to_string(),
85 rules_fingerprint: SCAN_CACHE_RULES_FINGERPRINT.to_string(),
86 build_options_fingerprint: options_fingerprint.to_string(),
87 created_at: Utc::now().to_rfc3339(),
88 };
89
90 let payload = rmp_serde::to_vec(findings).map_err(CacheIoError::Encode)?;
91 write_snapshot_payload(&path, &metadata, &payload)
92}
93
94#[cfg(test)]
95mod tests {
96 use tempfile::TempDir;
97
98 use super::*;
99
100 fn sample_sha256() -> &'static str {
101 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
102 }
103
104 #[test]
105 fn test_write_and_read_cached_findings_roundtrip() {
106 let temp_dir = TempDir::new().expect("create temp dir");
107 let scan_results_dir = temp_dir.path().join("scan-results");
108 let findings = CachedScanFindings {
109 package_data: Vec::new(),
110 license_expression: Some("mit".to_string()),
111 license_detections: Vec::new(),
112 copyrights: Vec::new(),
113 holders: Vec::new(),
114 authors: Vec::new(),
115 emails: Vec::new(),
116 urls: Vec::new(),
117 programming_language: Some("Rust".to_string()),
118 };
119
120 write_cached_findings(
121 &scan_results_dir,
122 sample_sha256(),
123 "cache-options-v1",
124 &findings,
125 )
126 .expect("write cache entry");
127
128 let loaded = read_cached_findings(&scan_results_dir, sample_sha256(), "cache-options-v1")
129 .expect("read cache entry")
130 .expect("cache hit");
131
132 assert_eq!(loaded.license_expression, findings.license_expression);
133 assert_eq!(loaded.programming_language, findings.programming_language);
134 }
135
136 #[test]
137 fn test_read_cached_findings_misses_on_fingerprint_change() {
138 let temp_dir = TempDir::new().expect("create temp dir");
139 let scan_results_dir = temp_dir.path().join("scan-results");
140 let findings = CachedScanFindings {
141 package_data: Vec::new(),
142 license_expression: Some("apache-2.0".to_string()),
143 license_detections: Vec::new(),
144 copyrights: Vec::new(),
145 holders: Vec::new(),
146 authors: Vec::new(),
147 emails: Vec::new(),
148 urls: Vec::new(),
149 programming_language: Some("Rust".to_string()),
150 };
151
152 write_cached_findings(
153 &scan_results_dir,
154 sample_sha256(),
155 "cache-options-v1",
156 &findings,
157 )
158 .expect("write cache entry");
159
160 let loaded = read_cached_findings(&scan_results_dir, sample_sha256(), "cache-options-v2")
161 .expect("read cache entry");
162
163 assert!(loaded.is_none());
164 }
165}