use super::manifest::Manifest;
use sha2::{Digest, Sha256};
use std::io::Write;
use std::path::{Path, PathBuf};
pub fn cache_root() -> std::io::Result<PathBuf> {
let home = dirs_home().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"no home directory; set HOME or pass --library-cache",
)
})?;
let root = home.join(".jarvy").join("library.d");
if !root.exists() {
std::fs::create_dir_all(&root)?;
}
Ok(root)
}
pub fn manifest_cache_path(url: &str) -> std::io::Result<PathBuf> {
let hash = sha256_hex(url.as_bytes());
let dir = cache_root()?.join(&hash);
if !dir.exists() {
std::fs::create_dir_all(&dir)?;
}
Ok(dir.join("manifest.json"))
}
pub fn read_manifest(path: &Path) -> std::io::Result<Option<Manifest>> {
if !path.exists() {
return Ok(None);
}
let bytes = std::fs::read(path)?;
let manifest: Manifest = serde_json::from_slice(&bytes).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("cache manifest parse failed: {e}"),
)
})?;
Ok(Some(manifest))
}
pub fn write_manifest(path: &Path, manifest: &Manifest) -> std::io::Result<()> {
let tmp = path.with_extension("new");
let serialized = serde_json::to_vec_pretty(manifest).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("serialize manifest: {e}"),
)
})?;
{
let mut f = std::fs::File::create(&tmp)?;
f.write_all(&serialized)?;
f.sync_all()?;
}
std::fs::rename(&tmp, path)?;
Ok(())
}
#[allow(dead_code)] pub fn clean_all() -> std::io::Result<()> {
let root = cache_root()?;
if root.exists() {
for entry in std::fs::read_dir(&root)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
std::fs::remove_dir_all(&path)?;
} else {
std::fs::remove_file(&path)?;
}
}
}
Ok(())
}
fn sha256_hex(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
let digest = hasher.finalize();
hex::encode(digest)
}
fn dirs_home() -> Option<PathBuf> {
if let Some(v) = std::env::var_os("JARVY_HOME") {
return Some(PathBuf::from(v));
}
std::env::var_os("HOME").map(PathBuf::from).or_else(|| {
std::env::var_os("USERPROFILE").map(PathBuf::from)
})
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn sha256_hex_stable() {
assert_eq!(
sha256_hex(b"https://example.com/manifest.json"),
sha256_hex(b"https://example.com/manifest.json"),
);
}
#[test]
fn write_then_read_round_trips() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("manifest.json");
let m = Manifest {
schema_version: 1,
publisher: "test".into(),
description: String::new(),
homepage: String::new(),
generated_at: String::new(),
items: vec![],
};
write_manifest(&path, &m).unwrap();
let read = read_manifest(&path).unwrap().unwrap();
assert_eq!(read.publisher, "test");
}
#[test]
fn read_missing_returns_none() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("does-not-exist.json");
let read = read_manifest(&path).unwrap();
assert!(read.is_none());
}
}