claw-core 0.1.2

Embedded local database engine for ClawDB — an agent-native cognitive database
Documentation
//! Snapshot data structures and helpers for claw-core.

use std::io::Read;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use crate::error::{ClawError, ClawResult};

/// Metadata describing one snapshot database file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotMeta {
    /// Absolute path to the snapshot `.db` file.
    pub path: PathBuf,
    /// Snapshot creation time in UTC.
    pub created_at: chrono::DateTime<chrono::Utc>,
    /// Snapshot size in bytes.
    pub size_bytes: u64,
    /// Lower-case hex-encoded BLAKE3 checksum.
    pub checksum: String,
}

/// Sidecar manifest written next to each snapshot file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotManifest {
    /// Manifest schema version.
    pub version: u32,
    /// Creation timestamp (unix millis).
    pub created_at_ms: u64,
    /// Source database path.
    pub source_db: String,
    /// Snapshot file size in bytes.
    pub size_bytes: u64,
    /// Lower-case hex-encoded BLAKE3 checksum.
    pub blake3: String,
}

/// Return the sidecar manifest path for a snapshot database path.
pub fn manifest_path_for(snapshot_db_path: &Path) -> PathBuf {
    PathBuf::from(format!("{}.manifest.json", snapshot_db_path.display()))
}

/// Compute BLAKE3 checksum for `path` and return lower-case hex encoding.
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())
}

/// Validate that a snapshot file's checksum matches the sidecar manifest.
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(())
}