prikk-store 0.23.0

Prikk storage crate scaffold.
Documentation
//! DC-55 end-to-end SHA-256 identity evidence, re-targeted for RFC 102 Stage 3 (design-v1.md §12.2).
//!
//! `tests/fixtures/dc55_pre_swap_repo` (kept at `crates/prikk-cli/tests/fixtures/`, not moved --
//! design §12.2's own scope note: this is Stage 3 work because Stage 3 causes it, not a licence to
//! touch other DC-55 material) is a repository written by the pre-DC-55 `prikk` binary, using the
//! first-party SHA-256 implementation `crates/prikk-hash/src/tests/frozen_outgoing.rs` now freezes.
//! Stage 3's format-2 rejection makes it permanently unopenable through `RepositoryLayout::open`, so
//! the original `dc55_sha256_identity_end_to_end.rs` (which drove `prikk verify`/`prikk doctor`
//! against it and asserted a clean pass) was deleted -- the evidence it wanted is in the persisted
//! bytes, not in `verify` returning `Ok`, and `open`/`verify` is the only thing format-3 takes away.
//!
//! **Do not regenerate the fixture.** It is frozen for the same reason
//! `crates/prikk-object/src/vectors/snapshot.txt` is not regenerated by DC-55: it is the evidence,
//! not a convenience. Every assertion below reads its bytes directly and calls the lowest-level
//! production function available for each site, bypassing `RepositoryLayout::open` entirely --
//! `Wal::new` (a bare, layout-free constructor, already used this way by
//! `active/tests/format_transition.rs`), `file_codec::decode_envelope_file` (widened from private to
//! `pub(crate)`), and, for the frozen ref log specifically, a **self-contained decoder local to this
//! file** (`decode_frozen_ref_log_records` below) -- RFC 102 Stage 4 retired `refs/log.rs`'s own
//! per-file `PREFLOG1` codec entirely (ref logs now live in a shared container under a different
//! frame shape, `refs/container.rs`'s `PREFCON1`), but this fixture's bytes were written by the
//! pre-swap binary using the *old* shape and must never be regenerated -- so the *old* decoder is
//! reproduced here, narrowly, for this one frozen file, rather than kept alive as shared
//! infrastructure nothing else needs. Same principle as Stage 3's own G5 (`publish_immutable_file`):
//! evidence for a retired mechanism is kept, not deleted, but scoped to exactly what still needs it.
//!
//! **One site does not hold, corrected from design-v1.md §12.2's claim, not silently absorbed (its
//! own rule 5): the WAL is not observable here.** `queue.wal` in this fixture has been the empty blob
//! (`e69de29...`) since the commit that added the fixture (`01bbefb`, confirmed via `git log -p
//! --follow`) -- a normal consequence of the fixture being frozen *after* both of its seals drained
//! the active WAL, not a fixture defect. `wal.rs`'s `record_checksum` computation is therefore not
//! exercised by any byte in this fixture; the WAL assertion below only proves the empty-file
//! reader-equivalence property (Stage 1's own claim), not the checksum site. See `FINDINGS.md`'s DC-55
//! entry, which this correction updates.

#![allow(clippy::expect_used)]

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

use prikk_hash::{sha256, to_hex};

use crate::file_codec::decode_envelope_file;
use crate::layout::ref_name_storage_key;
use crate::wal::Wal;

const FROZEN_REF_LOG_MAGIC: &[u8; 8] = b"PREFLOG1";
const FROZEN_REF_LOG_VERSION: u16 = 1;
/// magic(8) + version(2) + body_len(8) + checksum(32) -- `refs/log.rs`'s own retired frame shape,
/// reproduced exactly (no `ref_name_key` field: that only exists in the *new* shared-container
/// frame this fixture predates).
const FROZEN_REF_LOG_HEADER_LEN: usize = 8 + 2 + 8 + 32;

struct FrozenRefLogReplay {
    record_count: usize,
    trailing_partial_bytes: usize,
    all_checksums_verified: bool,
}

/// Reproduces `refs/log.rs::decode_log_records`'s exact validation for the one frozen file that
/// still needs it -- see this module's own doc for why the retired codec is not kept as shared
/// infrastructure. No isolate-and-continue resync: the frozen fixture is known-good evidence, not a
/// corruption-recovery scenario, so a first checksum failure here is itself a finding, not something
/// to scan past.
fn decode_frozen_ref_log_records(bytes: &[u8]) -> FrozenRefLogReplay {
    let mut offset = 0_usize;
    let mut record_count = 0_usize;
    let mut all_checksums_verified = true;
    loop {
        let remaining = bytes.len().saturating_sub(offset);
        if remaining < FROZEN_REF_LOG_HEADER_LEN {
            return FrozenRefLogReplay {
                record_count,
                trailing_partial_bytes: remaining,
                all_checksums_verified,
            };
        }
        let header_end = offset + FROZEN_REF_LOG_HEADER_LEN;
        let Some(header) = bytes.get(offset..header_end) else {
            return FrozenRefLogReplay {
                record_count,
                trailing_partial_bytes: remaining,
                all_checksums_verified,
            };
        };
        let Some(magic) = header.get(0..8) else {
            all_checksums_verified = false;
            break;
        };
        if magic != FROZEN_REF_LOG_MAGIC {
            all_checksums_verified = false;
            break;
        }
        let version = u16::from_be_bytes(
            header
                .get(8..10)
                .unwrap_or(&[0, 0])
                .try_into()
                .unwrap_or([0, 0]),
        );
        if version != FROZEN_REF_LOG_VERSION {
            all_checksums_verified = false;
            break;
        }
        let body_len_bytes: [u8; 8] = header
            .get(10..18)
            .unwrap_or(&[0; 8])
            .try_into()
            .unwrap_or([0; 8]);
        let body_len = u64::from_be_bytes(body_len_bytes);
        let Ok(body_len) = usize::try_from(body_len) else {
            all_checksums_verified = false;
            break;
        };
        let checksum: [u8; 32] = header
            .get(18..50)
            .unwrap_or(&[0; 32])
            .try_into()
            .unwrap_or([0; 32]);
        let Some(body_end) = header_end.checked_add(body_len) else {
            all_checksums_verified = false;
            break;
        };
        let Some(body) = bytes.get(header_end..body_end) else {
            return FrozenRefLogReplay {
                record_count,
                trailing_partial_bytes: remaining,
                all_checksums_verified,
            };
        };
        let mut preimage = Vec::new();
        preimage.extend_from_slice(FROZEN_REF_LOG_MAGIC);
        preimage.extend_from_slice(&FROZEN_REF_LOG_VERSION.to_be_bytes());
        preimage.extend_from_slice(&(body_len as u64).to_be_bytes());
        preimage.extend_from_slice(body);
        if sha256(&preimage) != checksum {
            all_checksums_verified = false;
        }
        record_count += 1;
        offset = body_end;
    }
    FrozenRefLogReplay {
        record_count,
        trailing_partial_bytes: 0,
        all_checksums_verified,
    }
}

fn fixture_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../prikk-cli/tests/fixtures/dc55_pre_swap_repo/.prikk")
}

/// `layout.rs`'s ref-name storage key is `to_hex(sha256(ref_name))` (`ref_name_storage_key`, called
/// directly here since it is `pub(crate)`) -- and the frozen fixture's ref pointer/log filenames are
/// exactly that digest for `"heads/main"`, tying the current `sha256` implementation to bytes
/// persisted by the pre-swap binary, not to another constant.
#[test]
fn ref_name_storage_key_matches_the_frozen_ref_filenames() {
    let expected = "c316ccb36a95a977918874d43e722a5a7d9ef74b138f3b76078f6993c14a799f".to_string();
    assert_eq!(ref_name_storage_key("heads/main"), expected);
    assert_eq!(to_hex(sha256(b"heads/main").as_ref()), expected);

    let root = fixture_root();
    assert!(
        root.join(format!("refs/by-id/{expected}.ref")).is_file(),
        "frozen ref pointer filename does not match the recomputed storage key"
    );
    assert!(
        root.join(format!("refs/logs/{expected}.log")).is_file(),
        "frozen ref log filename does not match the recomputed storage key"
    );
}

/// Object ids are content hashes, and a `.pobj` file's own name is its object's id in hex
/// (`layout.rs`'s sharded object path scheme). Decoding every frozen object with
/// `file_codec::decode_envelope_file` (called directly, `pub(crate)`) and recomputing
/// `ObjectEnvelope::object_id()` exercises `id.rs` and `payload/patch.rs`'s canonicalization exactly
/// as the original test's doc claimed, without `RepositoryLayout::open`.
#[test]
fn every_frozen_object_id_matches_its_own_filename() {
    let objects_dir = fixture_root().join("objects");
    let mut checked = 0_usize;
    for path in collect_files(&objects_dir) {
        let Some(extension) = path.extension() else {
            continue;
        };
        if extension != "pobj" {
            continue;
        }
        let bytes = std::fs::read(&path).expect("read frozen object file");
        let envelope = decode_envelope_file(&bytes).expect("decode frozen object envelope");
        let expected_id = path
            .file_stem()
            .and_then(|stem| stem.to_str())
            .expect("object filename is valid UTF-8");
        assert_eq!(
            envelope.object_id().to_hex(),
            expected_id,
            "recomputed object id does not match frozen filename at {}",
            path.display()
        );
        checked += 1;
    }
    assert_eq!(
        checked, 8,
        "expected 8 frozen objects (2 blob, 2 block, 2 patch, 2 ref-state); found {checked} -- \
         update this count deliberately if the fixture ever legitimately changes shape, never to \
         paper over a decode failure"
    );
}

fn collect_files(dir: &Path) -> Vec<PathBuf> {
    let mut out = Vec::new();
    let Ok(entries) = std::fs::read_dir(dir) else {
        return out;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            out.extend(collect_files(&path));
        } else {
            out.push(path);
        }
    }
    out
}

/// The frozen ref log (743 bytes, two records -- matching the original test's "checked ref-log
/// records: 2") carries real per-record SHA-256 checksums, in `refs/log.rs`'s retired per-file frame
/// shape. `decode_frozen_ref_log_records` (this file's own reproduction of that shape, see the
/// module doc) exercises checksum verification on those exact persisted bytes.
#[test]
fn frozen_ref_log_checksums_verify_and_decode_cleanly() {
    let path = fixture_root()
        .join("refs/logs/c316ccb36a95a977918874d43e722a5a7d9ef74b138f3b76078f6993c14a799f.log");
    let bytes = std::fs::read(&path).expect("read frozen ref log");
    let replay = decode_frozen_ref_log_records(&bytes);
    assert!(
        replay.all_checksums_verified,
        "a checksum in the frozen ref log no longer verifies"
    );
    assert_eq!(replay.trailing_partial_bytes, 0);
    assert_eq!(replay.record_count, 2);
}

/// Negative control for `every_frozen_object_id_matches_its_own_filename`, run against a **temporary
/// copy** of one frozen object -- the real fixture is never touched. Proves the id-vs-filename
/// assertion has teeth: a single flipped payload byte changes the recomputed `object_id()` (content
/// addressing), so it no longer matches the original filename, exactly the failure the real test
/// would catch.
#[test]
fn corrupted_object_bytes_no_longer_match_the_original_filename() {
    let original_path = fixture_root().join(
        "objects/blob/1b/1b05e8e870004a5852990d93a5610d80e56507b129e6bc90a82bd050c8a4f878.pobj",
    );
    let original_id = "1b05e8e870004a5852990d93a5610d80e56507b129e6bc90a82bd050c8a4f878";
    let mut bytes = std::fs::read(&original_path).expect("read frozen object for probe copy");
    // Byte 22 is the first `canonical_payload` byte (8 magic + 2 type + 4 schema_version + 8
    // payload-length prefix, per `file_codec::decode_envelope_file`'s own cursor reads) -- inside
    // `object_id()`'s input, unlike a signature byte near the end of the file, which is not.
    let payload_byte = bytes
        .get_mut(22)
        .expect("frozen object payload is non-empty");
    *payload_byte ^= 0x01;

    let envelope =
        decode_envelope_file(&bytes).expect("single-byte-flipped envelope still decodes");
    assert_ne!(
        envelope.object_id().to_hex(),
        original_id,
        "flipping a payload byte must change the recomputed object id"
    );
}

/// **Does not close the WAL gap** (see this module's own doc) -- `queue.wal` is frozen empty, so this
/// only proves the empty-file reader-equivalence property, not `record_checksum`. Kept because it is
/// still a real, if narrower, assertion about the frozen bytes, not removed silently.
#[test]
fn frozen_active_wal_is_empty_and_replays_as_empty() {
    let path = fixture_root().join("active/default/queue.wal");
    assert_eq!(
        std::fs::metadata(&path).expect("stat frozen WAL").len(),
        0,
        "frozen WAL is no longer empty -- if it now carries real records, extend this test to \
         assert on `replay.records`/checksums instead of just emptiness"
    );
    let replay = Wal::new(path).replay().expect("replay frozen WAL");
    assert!(!replay.has_item_failure());
    assert_eq!(replay.trailing_partial_bytes, 0);
    assert!(replay.records.is_empty());
}