Skip to main content

claw_core/
snapshot.rs

1//! Snapshot data structures and helpers for claw-core.
2
3use std::io::Read;
4use std::path::{Path, PathBuf};
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::{ClawError, ClawResult};
9
10/// Metadata describing one snapshot database file.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SnapshotMeta {
13    /// Absolute path to the snapshot `.db` file.
14    pub path: PathBuf,
15    /// Snapshot creation time in UTC.
16    pub created_at: chrono::DateTime<chrono::Utc>,
17    /// Snapshot size in bytes.
18    pub size_bytes: u64,
19    /// Lower-case hex-encoded BLAKE3 checksum.
20    pub checksum: String,
21}
22
23/// Sidecar manifest written next to each snapshot file.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct SnapshotManifest {
26    /// Manifest schema version.
27    pub version: u32,
28    /// Creation timestamp (unix millis).
29    pub created_at_ms: u64,
30    /// Source database path.
31    pub source_db: String,
32    /// Snapshot file size in bytes.
33    pub size_bytes: u64,
34    /// Lower-case hex-encoded BLAKE3 checksum.
35    pub blake3: String,
36}
37
38/// Return the sidecar manifest path for a snapshot database path.
39pub fn manifest_path_for(snapshot_db_path: &Path) -> PathBuf {
40    PathBuf::from(format!("{}.manifest.json", snapshot_db_path.display()))
41}
42
43/// Compute BLAKE3 checksum for `path` and return lower-case hex encoding.
44pub 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
67/// Validate that a snapshot file's checksum matches the sidecar manifest.
68pub 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}