Skip to main content

pq_map/discover/
chain_cache.rs

1use std::collections::BTreeMap;
2use std::fs;
3use std::path::PathBuf;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8use crate::model::ChainEntry;
9
10use super::cache;
11
12pub const SNAPSHOT_VERSION: u32 = 1;
13
14#[derive(Debug, Serialize, Deserialize)]
15pub struct Snapshot {
16    pub version: u32,
17    pub fetched_at: DateTime<Utc>,
18    pub chains: BTreeMap<u64, Vec<ChainEntry>>,
19}
20
21fn cache_dir() -> PathBuf {
22    if let Ok(dir) = std::env::var("CHAIN_CACHE_DIR") {
23        return PathBuf::from(dir);
24    }
25    let base = std::env::var("XDG_CACHE_HOME")
26        .ok()
27        .filter(|s| !s.is_empty())
28        .map(PathBuf::from)
29        .or_else(|| {
30            std::env::var("HOME")
31                .ok()
32                .map(|h| PathBuf::from(h).join(".cache"))
33        });
34    base.unwrap_or_else(std::env::temp_dir)
35        .join("pq-map")
36        .join("chain")
37}
38
39pub fn load(domain: &str) -> Option<Snapshot> {
40    cache::migrate_legacy_file(&cache_dir(), domain);
41    let file = cache::latest_file(&cache_dir(), domain)?;
42    let body = fs::read_to_string(&file).ok()?;
43    let snap: Snapshot = serde_json::from_str(&body).ok()?;
44    if snap.version != SNAPSHOT_VERSION {
45        return None;
46    }
47    match cache::age(&file) {
48        Some(age) => eprintln!(
49            "CT: loading chain snapshot {} ({domain}, {} min old)",
50            file.display(),
51            age / 60
52        ),
53        None => eprintln!("CT: loading chain snapshot {} ({domain})", file.display()),
54    }
55    Some(snap)
56}
57
58pub fn age(domain: &str) -> Option<u64> {
59    cache::age(&cache::latest_file(&cache_dir(), domain)?)
60}
61
62pub fn store(domain: &str, chains: &BTreeMap<u64, Vec<ChainEntry>>) {
63    let snapshot = Snapshot {
64        version: SNAPSHOT_VERSION,
65        fetched_at: Utc::now(),
66        chains: chains.clone(),
67    };
68    let Ok(body) = serde_json::to_string_pretty(&snapshot) else {
69        eprintln!("CT: chain caching failed: serialization error");
70        return;
71    };
72    cache::migrate_legacy_file(&cache_dir(), domain);
73    match cache::domain_store(&cache_dir(), domain, &body) {
74        Ok(path) => eprintln!("CT: chain snapshot cached: {}", path.display()),
75        Err(e) => eprintln!("CT: chain caching failed: {e}"),
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    fn entry(leaf: u64, depth: u32) -> (u64, ChainEntry) {
84        (
85            leaf,
86            ChainEntry {
87                depth,
88                certificate_id: leaf * 10 + depth as u64,
89                common_name: Some(format!("ca{depth}")),
90                ski: Some("aa".to_string()),
91                aki: None,
92                not_before: None,
93                not_after: None,
94                key_algorithm: Some("RSA".to_string()),
95                key_size: Some(2048),
96                signature_key_algorithm: Some("RSA".to_string()),
97                signature_hash_algorithm: Some("SHA-256".to_string()),
98                issuer_ca_name: None,
99            },
100        )
101    }
102
103    #[test]
104    fn snapshot_roundtrip() {
105        let dir = std::env::temp_dir().join("pq-chain-cache-test");
106        fs::create_dir_all(&dir).unwrap();
107        unsafe { std::env::set_var("CHAIN_CACHE_DIR", &dir) };
108
109        let mut chains = BTreeMap::new();
110        let (leaf, e0) = entry(9612296601, 0);
111        let (_, e1) = entry(9612296601, 1);
112        chains.insert(leaf, vec![e0, e1]);
113
114        store("roundtrip.example.pl", &chains);
115        let loaded = load("roundtrip.example.pl").expect("snapshot loads");
116        let entries = loaded.chains.get(&leaf).unwrap();
117        assert_eq!(entries.len(), 2);
118        assert_eq!(entries[1].depth, 1);
119        assert_eq!(entries[1].common_name.as_deref(), Some("ca1"));
120
121        fs::remove_dir_all(&dir).unwrap();
122        unsafe { std::env::remove_var("CHAIN_CACHE_DIR") };
123    }
124}