Skip to main content

provenant/cache/
scan_cache.rs

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}