use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::domain::error::{Error, Result};
use crate::util::fs::write_atomic;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ZipFingerprint {
pub hash: u64,
pub mtime: i64,
pub size: u64,
}
pub type ZipCache = HashMap<PathBuf, ZipFingerprint>;
#[derive(Debug, Default, Serialize, Deserialize)]
struct CacheDoc {
#[serde(default)]
entries: Vec<CacheEntry>,
}
#[derive(Debug, Serialize, Deserialize)]
struct CacheEntry {
path: String,
hash: String,
mtime: i64,
size: u64,
}
pub fn load(path: &Path) -> ZipCache {
let Ok(text) = fs::read_to_string(path) else {
return ZipCache::new();
};
let Ok(doc) = toml::from_str::<CacheDoc>(&text) else {
return ZipCache::new();
};
doc.entries
.into_iter()
.filter_map(|entry| {
let hash = u64::from_str_radix(&entry.hash, 16).ok()?;
Some((
PathBuf::from(entry.path),
ZipFingerprint {
hash,
mtime: entry.mtime,
size: entry.size,
},
))
})
.collect()
}
pub fn save(path: &Path, cache: &ZipCache) -> Result<()> {
let mut entries: Vec<CacheEntry> = cache
.iter()
.map(|(dest, fp)| CacheEntry {
path: dest.to_string_lossy().into_owned(),
hash: format!("{:016x}", fp.hash),
mtime: fp.mtime,
size: fp.size,
})
.collect();
entries.sort_by(|a, b| a.path.cmp(&b.path));
let doc = CacheDoc { entries };
let text = toml::to_string_pretty(&doc)
.map_err(|e| Error::invalid(format!("serialise zip cache: {e}")))?;
write_atomic(path, &text, "zip cache")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn save_then_load_round_trips() {
let dir = std::env::temp_dir()
.join(format!("hop-zipcache-test-{}", std::process::id()));
let file = dir.join("zip-manifests.toml");
let mut cache = ZipCache::new();
cache.insert(
PathBuf::from("/backups/rs-hop.zip"),
ZipFingerprint {
hash: 0xdead_beef_0000_1234,
mtime: 1_700_000_000,
size: 4096,
},
);
save(&file, &cache).unwrap();
let loaded = load(&file);
assert_eq!(loaded, cache);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn missing_file_yields_empty_cache() {
let loaded = load(Path::new("/nonexistent/hop-zip-cache.toml"));
assert!(loaded.is_empty());
}
}