use std::io::Read;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::{ClawError, ClawResult};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotMeta {
pub path: PathBuf,
pub created_at: chrono::DateTime<chrono::Utc>,
pub size_bytes: u64,
pub checksum: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotManifest {
pub version: u32,
pub created_at_ms: u64,
pub source_db: String,
pub size_bytes: u64,
pub blake3: String,
}
pub fn manifest_path_for(snapshot_db_path: &Path) -> PathBuf {
PathBuf::from(format!("{}.manifest.json", snapshot_db_path.display()))
}
pub fn blake3_file_hex(path: &Path) -> ClawResult<String> {
let mut hasher = blake3::Hasher::new();
let mut file = std::fs::File::open(path).map_err(|e| {
ClawError::Snapshot(format!("cannot open '{}' for hashing: {e}", path.display()))
})?;
let mut buf = vec![0_u8; 64 * 1024];
loop {
let n = file.read(&mut buf).map_err(|e| {
ClawError::Snapshot(format!(
"cannot read '{}' while hashing: {e}",
path.display()
))
})?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
Ok(hasher.finalize().to_hex().to_string())
}
pub fn verify_snapshot_integrity(snapshot_db_path: &Path) -> ClawResult<()> {
let manifest_path = manifest_path_for(snapshot_db_path);
if !manifest_path.exists() {
return Err(ClawError::SnapshotCorrupt(format!(
"manifest file missing: {}",
manifest_path.display()
)));
}
let manifest_bytes = std::fs::read(&manifest_path).map_err(|e| {
ClawError::SnapshotCorrupt(format!(
"cannot read manifest '{}': {e}",
manifest_path.display()
))
})?;
let manifest: SnapshotManifest = serde_json::from_slice(&manifest_bytes).map_err(|e| {
ClawError::SnapshotCorrupt(format!(
"cannot parse manifest '{}': {e}",
manifest_path.display()
))
})?;
let expected = blake3::Hash::from_hex(manifest.blake3.as_str())
.map_err(|e| ClawError::SnapshotCorrupt(format!("manifest blake3 is invalid hex: {e}")))?;
let actual_hex = blake3_file_hex(snapshot_db_path)?;
let actual = blake3::Hash::from_hex(actual_hex.as_str())
.map_err(|e| ClawError::SnapshotCorrupt(format!("computed blake3 is invalid hex: {e}")))?;
if !expected.eq(&actual) {
return Err(ClawError::SnapshotCorrupt(format!(
"checksum mismatch for snapshot '{}'",
snapshot_db_path.display()
)));
}
Ok(())
}