haematite 0.6.2

Content-addressed, branchable, actor-native storage engine
Documentation
//! The durable metadata manifest, read side (§3 ⟨r2, B1⟩ / ⟨r4, B4⟩).
//!
//! `<data_dir>/metadata-manifest.json` is the reservation ledger the future
//! facade wiring writes (reserve-before-publish, the full
//! Reserved→Publishing→Published⇄Updating→Decommissioning lifecycle). The
//! vacuum only READS it: stats reports each entry's state; sweep requires
//! every entry settled. The write side (the serialized manifest authority and
//! the four `haem manifest` operations) ships with the sweep unit.
//!
//! SCHEMA STATUS: this concrete shape implements the §3 protocol and is
//! routed to the certifying pair with the implementation review (§9.1a names
//! the schema details — path normalization, database identity binding,
//! duplicate handling, association cardinality — as the pair's sign-off).

use std::fs;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use super::error::VacuumError;

/// The manifest file name inside a database data directory.
pub const MANIFEST_FILE: &str = "metadata-manifest.json";

/// Current manifest format version. Reads accept exactly this version; a
/// newer stamp refuses loudly (an old binary must never misread a newer
/// ledger it cannot know).
pub const MANIFEST_FORMAT_VERSION: u32 = 1;

/// Per-entry durable state (§3 ⟨r6⟩⟨r7⟩⟨r12⟩). `Reserved` mechanically MEANS
/// no record was ever written — the reserve-before-publish ordering
/// guarantees it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ManifestEntryState {
    Reserved,
    Publishing,
    Published,
    Updating { nonce: u64 },
    Decommissioning,
}

impl ManifestEntryState {
    /// Sweep eligibility (§3 ⟨r7, B9⟩): only `Published` is settled; every
    /// non-terminal state is a typed refusal regardless of filesystem shape.
    pub const fn is_settled(&self) -> bool {
        matches!(self, Self::Published)
    }

    /// The state name as shown in reports and refusals.
    pub fn display_name(&self) -> String {
        match self {
            Self::Reserved => "Reserved".to_owned(),
            Self::Publishing => "Publishing".to_owned(),
            Self::Published => "Published".to_owned(),
            Self::Updating { nonce } => format!("Updating {{ nonce: {nonce} }}"),
            Self::Decommissioning => "Decommissioning".to_owned(),
        }
    }
}

/// What kind of metadata source an entry lists.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ManifestSourceKind {
    RefsDir,
    SnapshotRegistry,
}

/// One manifest entry: a metadata source bound to this database.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestEntry {
    pub kind: ManifestSourceKind,
    /// Absolute path, or relative to the data dir (resolved at read time).
    pub path: PathBuf,
    pub state: ManifestEntryState,
    /// The reservation nonce this entry was committed under (token validity
    /// is nonce-exact, §3 ⟨r6⟩).
    pub reservation_nonce: u64,
    /// Store/shard association for snapshot registries (§2 M3: a
    /// manifest-bound registry's roots are walked ONLY in the declared
    /// store(s)). `None` for refs dirs — HBR1 records carry their own
    /// per-record shard association.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub shards: Option<Vec<usize>>,
}

/// The decoded manifest.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetadataManifest {
    pub format_version: u32,
    /// Monotonic generation stamped by the serialized manifest authority
    /// (§3 ⟨r5, B6⟩).
    pub generation: u64,
    pub entries: Vec<ManifestEntry>,
}

impl MetadataManifest {
    /// Resolve an entry's path against the data dir when relative.
    pub fn resolved_path(data_dir: &Path, entry: &ManifestEntry) -> PathBuf {
        if entry.path.is_absolute() {
            entry.path.clone()
        } else {
            data_dir.join(&entry.path)
        }
    }
}

/// Read the manifest if one exists.
///
/// Absent file ⇒ `Ok(None)`; an existing manifest that fails to decode fails
/// LOUD — it is the trust root, and a partial read of it could silently
/// reintroduce the B1 omission.
pub fn read_manifest(data_dir: &Path) -> Result<Option<MetadataManifest>, VacuumError> {
    let path = data_dir.join(MANIFEST_FILE);
    let bytes = match fs::read(&path) {
        Ok(bytes) => bytes,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(error) => return Err(VacuumError::Io { path, error }),
    };
    let manifest: MetadataManifest =
        serde_json::from_slice(&bytes).map_err(|error| VacuumError::MetadataOpen {
            path: path.clone(),
            reason: error.to_string(),
        })?;
    if manifest.format_version != MANIFEST_FORMAT_VERSION {
        return Err(VacuumError::MetadataOpen {
            path,
            reason: format!(
                "manifest format_version {} is not supported (this binary reads {})",
                manifest.format_version, MANIFEST_FORMAT_VERSION
            ),
        });
    }
    Ok(Some(manifest))
}