use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::schema::ModelSchema;
pub const CATALOG_CACHE_FILE: &str = "catalog-cache.json";
const MAX_CATALOG_BYTES: u64 = 8 * 1024 * 1024;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CatalogDoc {
pub version: u64,
#[serde(default)]
pub models: Vec<ModelSchema>,
}
pub fn cache_path(models_dir: &Path) -> PathBuf {
models_dir
.parent()
.unwrap_or(models_dir)
.join(CATALOG_CACHE_FILE)
}
pub fn load_cache(path: &Path) -> Vec<ModelSchema> {
load_doc(path).map(|d| d.models).unwrap_or_default()
}
pub fn load_doc(path: &Path) -> Option<CatalogDoc> {
let s = std::fs::read_to_string(path).ok()?;
serde_json::from_str(&s).ok()
}
pub async fn fetch_and_verify(
http: &reqwest::Client,
url: &str,
public_key_b64: &str,
) -> Result<CatalogDoc, String> {
let resp = http
.get(url)
.send()
.await
.map_err(|e| format!("fetch catalog: {e}"))?
.error_for_status()
.map_err(|e| format!("fetch catalog: {e}"))?;
if let Some(len) = resp.content_length() {
if len > MAX_CATALOG_BYTES {
return Err(format!("catalog too large ({len} bytes)"));
}
}
let bytes = resp
.bytes()
.await
.map_err(|e| format!("read catalog: {e}"))?;
if bytes.len() as u64 > MAX_CATALOG_BYTES {
return Err(format!("catalog too large ({} bytes)", bytes.len()));
}
let sig = http
.get(format!("{url}.sig"))
.send()
.await
.map_err(|e| format!("fetch signature: {e}"))?
.error_for_status()
.map_err(|e| format!("fetch signature: {e}"))?
.text()
.await
.map_err(|e| format!("read signature: {e}"))?;
car_bundle::verify_detached(&bytes, sig.trim(), public_key_b64)
.map_err(|e| format!("catalog signature verification failed: {e}"))?;
serde_json::from_slice(&bytes).map_err(|e| format!("parse catalog: {e}"))
}
pub fn save_doc(path: &Path, doc: &CatalogDoc) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(doc)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
let tmp = path.with_extension("json.tmp");
std::fs::write(&tmp, json)?;
std::fs::rename(&tmp, path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_cache_is_empty() {
let p = std::env::temp_dir().join("car-catalog-none-xyz.json");
let _ = std::fs::remove_file(&p);
assert!(load_cache(&p).is_empty());
}
#[test]
fn cache_path_is_sibling_of_models_dir() {
let p = cache_path(Path::new("/home/u/.car/models"));
assert_eq!(p, Path::new("/home/u/.car/catalog-cache.json"));
}
}