1use std::io::Read;
4use std::path::{Path, PathBuf};
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::{ClawError, ClawResult};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SnapshotMeta {
13 pub path: PathBuf,
15 pub created_at: chrono::DateTime<chrono::Utc>,
17 pub size_bytes: u64,
19 pub checksum: String,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct SnapshotManifest {
26 pub version: u32,
28 pub created_at_ms: u64,
30 pub source_db: String,
32 pub size_bytes: u64,
34 pub blake3: String,
36}
37
38pub fn manifest_path_for(snapshot_db_path: &Path) -> PathBuf {
40 PathBuf::from(format!("{}.manifest.json", snapshot_db_path.display()))
41}
42
43pub fn blake3_file_hex(path: &Path) -> ClawResult<String> {
45 let mut hasher = blake3::Hasher::new();
46 let mut file = std::fs::File::open(path).map_err(|e| {
47 ClawError::Snapshot(format!("cannot open '{}' for hashing: {e}", path.display()))
48 })?;
49
50 let mut buf = vec![0_u8; 64 * 1024];
51 loop {
52 let n = file.read(&mut buf).map_err(|e| {
53 ClawError::Snapshot(format!(
54 "cannot read '{}' while hashing: {e}",
55 path.display()
56 ))
57 })?;
58 if n == 0 {
59 break;
60 }
61 hasher.update(&buf[..n]);
62 }
63
64 Ok(hasher.finalize().to_hex().to_string())
65}
66
67pub fn verify_snapshot_integrity(snapshot_db_path: &Path) -> ClawResult<()> {
69 let manifest_path = manifest_path_for(snapshot_db_path);
70 if !manifest_path.exists() {
71 return Err(ClawError::SnapshotCorrupt(format!(
72 "manifest file missing: {}",
73 manifest_path.display()
74 )));
75 }
76
77 let manifest_bytes = std::fs::read(&manifest_path).map_err(|e| {
78 ClawError::SnapshotCorrupt(format!(
79 "cannot read manifest '{}': {e}",
80 manifest_path.display()
81 ))
82 })?;
83
84 let manifest: SnapshotManifest = serde_json::from_slice(&manifest_bytes).map_err(|e| {
85 ClawError::SnapshotCorrupt(format!(
86 "cannot parse manifest '{}': {e}",
87 manifest_path.display()
88 ))
89 })?;
90
91 let expected = blake3::Hash::from_hex(manifest.blake3.as_str())
92 .map_err(|e| ClawError::SnapshotCorrupt(format!("manifest blake3 is invalid hex: {e}")))?;
93
94 let actual_hex = blake3_file_hex(snapshot_db_path)?;
95 let actual = blake3::Hash::from_hex(actual_hex.as_str())
96 .map_err(|e| ClawError::SnapshotCorrupt(format!("computed blake3 is invalid hex: {e}")))?;
97
98 if !expected.eq(&actual) {
99 return Err(ClawError::SnapshotCorrupt(format!(
100 "checksum mismatch for snapshot '{}'",
101 snapshot_db_path.display()
102 )));
103 }
104
105 Ok(())
106}