haematite 0.6.1

Content-addressed, branchable, actor-native storage engine
Documentation
//! Read-only metadata decode paths (§7 ⟨r2, M2⟩ / ⟨r4, M4⟩).
//!
//! NOT `BranchRefStore::open` / `SnapshotRegistry::open` — the existing opens
//! CREATE missing directories and UNLINK `.branch-*.tmp` debris, which would
//! make "report-only" a lie. These readers decode records from existing files
//! only, through EXACTLY the production codec functions (one parser, two
//! callers), never create, never clean. Temp debris and foreign entries are
//! counted in the report; metadata debris cleanup remains the owning store's
//! explicit operation.

use std::fs;
use std::path::Path;

use crate::branch::refstore::{
    REF_EXTENSION, TEMP_PREFIX, TEMP_SUFFIX, decode_record, ref_file_name,
};
use crate::branch::snapshot::decode_registry;
use crate::branch::{BranchRefRecord, SnapshotEntry};

use super::error::VacuumError;

/// The decoded contents of one refs directory, read-only.
#[derive(Debug)]
pub struct RefsDirScan {
    pub records: Vec<BranchRefRecord>,
    pub temp_debris_files: usize,
    /// Entries the production reader silently ignores (non-`.ref` files,
    /// non-UTF8 names) — counted so growth there is visible.
    pub foreign_entries: usize,
}

/// Decode every HBR1 record in `dir` without creating or cleaning anything.
///
/// Acceptance/rejection matches `BranchRefStore::open` case by case: temp
/// debris skipped (but counted, never unlinked), non-`.ref` entries ignored
/// (but counted), corrupt or misfiled records fail LOUD — a dropped record is
/// a dropped pin.
pub fn read_refs_dir(dir: &Path) -> Result<RefsDirScan, VacuumError> {
    let mut scan = RefsDirScan {
        records: Vec::new(),
        temp_debris_files: 0,
        foreign_entries: 0,
    };
    let entries = fs::read_dir(dir).map_err(|error| VacuumError::Io {
        path: dir.to_path_buf(),
        error,
    })?;
    for entry in entries {
        let entry = entry.map_err(|error| VacuumError::Io {
            path: dir.to_path_buf(),
            error,
        })?;
        let path = entry.path();
        let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
            scan.foreign_entries += 1;
            continue;
        };
        if file_name.starts_with(TEMP_PREFIX) && file_name.ends_with(TEMP_SUFFIX) {
            scan.temp_debris_files += 1;
            continue;
        }
        if path.extension().and_then(|ext| ext.to_str()) != Some(REF_EXTENSION) {
            scan.foreign_entries += 1;
            continue;
        }
        let bytes = fs::read(&path).map_err(|error| VacuumError::Io {
            path: path.clone(),
            error,
        })?;
        let record = decode_record(&bytes).map_err(|error| VacuumError::MetadataOpen {
            path: path.clone(),
            reason: error.to_string(),
        })?;
        if ref_file_name(&record.name) != file_name {
            return Err(VacuumError::MetadataOpen {
                path,
                reason: format!(
                    "ref file holds a record for branch {:?}, whose name hashes elsewhere",
                    record.name
                ),
            });
        }
        scan.records.push(record);
    }
    Ok(scan)
}

/// Decode an HSR1 snapshot registry file, read-only, via the shared codec.
pub fn read_snapshot_registry(path: &Path) -> Result<Vec<SnapshotEntry>, VacuumError> {
    let bytes = fs::read(path).map_err(|error| VacuumError::Io {
        path: path.to_path_buf(),
        error,
    })?;
    decode_registry(&bytes).map_err(|error| VacuumError::MetadataOpen {
        path: path.to_path_buf(),
        reason: error.to_string(),
    })
}