syntheca 0.3.0

Content-addressable storage on top of apotheca. Bytes go in, BLAKE3 hash comes out; the underlying cella's compare-and-swap pinax namespace is surfaced as a pass-through.
Documentation
use syntheca::{
    Cella, DepositError, GetError, GetPinaxError, Hash, Name, Options, SetPinaxOutcome, StatError,
};

use sha2::Digest;
use tempfile::TempDir;

fn fresh_cella() -> (TempDir, Cella) {
    let dir = TempDir::new().unwrap();
    let cella = Cella::open(dir.path()).unwrap();
    (dir, cella)
}

fn sha256(bytes: &[u8]) -> [u8; 32] {
    let mut h = sha2::Sha256::new();
    h.update(bytes);
    h.finalize().into()
}

#[test]
fn deposit_get_round_trip() {
    let (_dir, cella) = fresh_cella();
    let bytes = b"hello world";
    let h = cella.deposit(bytes).unwrap();
    assert_eq!(h, Hash::of(bytes));
    let got = cella.get(&h).unwrap();
    assert_eq!(got.as_slice(), bytes);
}

#[test]
fn deposit_is_idempotent() {
    let (_dir, cella) = fresh_cella();
    let bytes = b"twice";
    let h1 = cella.deposit(bytes).unwrap();
    let h2 = cella.deposit(bytes).unwrap();
    assert_eq!(h1, h2);
}

#[test]
fn empty_bytes_round_trip() {
    let (_dir, cella) = fresh_cella();
    let h = cella.deposit(b"").unwrap();
    assert_eq!(h, Hash::of(b""));
    assert_eq!(cella.get(&h).unwrap(), Vec::<u8>::new());
    assert_eq!(cella.stat(&h).unwrap().size, 0);
}

#[test]
fn get_unknown_is_not_found() {
    let (_dir, cella) = fresh_cella();
    let h = Hash::of(b"never deposited");
    match cella.get(&h) {
        Err(GetError::NotFound) => {}
        other => panic!("expected NotFound, got {other:?}"),
    }
}

#[test]
fn stat_unknown_is_not_found() {
    let (_dir, cella) = fresh_cella();
    let h = Hash::of(b"never deposited");
    match cella.stat(&h) {
        Err(StatError::NotFound) => {}
        other => panic!("expected NotFound, got {other:?}"),
    }
}

#[test]
fn stat_returns_size_and_sha256() {
    let (_dir, cella) = fresh_cella();
    let bytes = b"abc";
    let h = cella.deposit(bytes).unwrap();
    let s = cella.stat(&h).unwrap();
    assert_eq!(s.size, bytes.len() as u64);
    assert_eq!(s.sha256, sha256(bytes));
}

#[test]
fn hash_collision_surfaces_when_bytes_differ_but_name_matches() {
    // Construct a contrived collision: deposit through apotheca directly under
    // a name = blake3-hex(B2), but with bytes B1 ≠ B2. A subsequent
    // syntheca.deposit(B2) calls apotheca.deposit(name, B2); apotheca compares
    // sha256(B1) vs sha256(B2), they differ, apotheca returns Collision,
    // syntheca surfaces HashCollision.
    let dir = TempDir::new().unwrap();
    let apo = apotheca::Cella::open(dir.path()).unwrap();

    let b2: &[u8] = b"the bytes the caller would deposit";
    let b1: &[u8] = b"different bytes already squatting on that name";

    let name_hex = Hash::of(b2).to_hex();
    let name = apotheca::Name::new(name_hex.as_bytes()).unwrap();
    assert_eq!(
        apo.deposit(&name, b1).unwrap(),
        apotheca::DepositOutcome::Ok,
        "test setup: first deposit should succeed"
    );

    let cella = Cella::from_apotheca(apo, Options::default());
    match cella.deposit(b2) {
        Err(DepositError::HashCollision) => {}
        other => panic!("expected HashCollision, got {other:?}"),
    }
}

#[test]
fn verify_on_read_catches_mismatched_name() {
    // Same setup as the collision test, but on the read side: an apotheca
    // depositum whose name is the blake3 of one byte sequence but whose stored
    // bytes are something else. With verify_on_read on (default), syntheca's
    // get of that hash must surface IntegrityError.
    let dir = TempDir::new().unwrap();
    let apo = apotheca::Cella::open(dir.path()).unwrap();

    let claimed: &[u8] = b"what the hash claims to name";
    let actual: &[u8] = b"what actually got stored";

    let claimed_hash = Hash::of(claimed);
    let name_hex = claimed_hash.to_hex();
    let name = apotheca::Name::new(name_hex.as_bytes()).unwrap();
    apo.deposit(&name, actual).unwrap();

    let cella = Cella::from_apotheca(apo, Options::default());
    match cella.get(&claimed_hash) {
        Err(GetError::IntegrityError) => {}
        other => panic!("expected IntegrityError, got {other:?}"),
    }
}

#[test]
fn verify_on_read_can_be_disabled() {
    // Same misnamed-depositum setup; with verify_on_read off, syntheca returns
    // the bytes (apotheca's sha256 verify still passed because it was
    // computed at deposit time over the actual bytes).
    let dir = TempDir::new().unwrap();
    let apo = apotheca::Cella::open(dir.path()).unwrap();

    let claimed: &[u8] = b"what the hash claims to name";
    let actual: &[u8] = b"what actually got stored";

    let claimed_hash = Hash::of(claimed);
    let name_hex = claimed_hash.to_hex();
    let name = apotheca::Name::new(name_hex.as_bytes()).unwrap();
    apo.deposit(&name, actual).unwrap();

    let cella = Cella::from_apotheca(
        apo,
        Options {
            verify_on_read: false,
        },
    );
    let got = cella.get(&claimed_hash).unwrap();
    assert_eq!(got, actual);
}

#[test]
fn distinct_bytes_get_distinct_hashes() {
    let (_dir, cella) = fresh_cella();
    let h1 = cella.deposit(b"alpha").unwrap();
    let h2 = cella.deposit(b"beta").unwrap();
    assert_ne!(h1, h2);
    assert_eq!(cella.get(&h1).unwrap(), b"alpha");
    assert_eq!(cella.get(&h2).unwrap(), b"beta");
}

#[test]
fn pinax_round_trip() {
    let (_dir, cella) = fresh_cella();
    let name = Name::new(b"head").unwrap();

    match cella.set_pinax(&name, b"v1", None).unwrap() {
        SetPinaxOutcome::Ok => {}
        other => panic!("expected Ok, got {other:?}"),
    }
    assert_eq!(cella.get_pinax(&name).unwrap(), b"v1");

    let v1_digest = sha256(b"v1");
    match cella.set_pinax(&name, b"v2", Some(v1_digest)).unwrap() {
        SetPinaxOutcome::Ok => {}
        other => panic!("expected Ok on cas, got {other:?}"),
    }
    assert_eq!(cella.get_pinax(&name).unwrap(), b"v2");
}

#[test]
fn pinax_get_unknown_is_not_found() {
    let (_dir, cella) = fresh_cella();
    let name = Name::new(b"missing").unwrap();
    match cella.get_pinax(&name) {
        Err(GetPinaxError::NotFound) => {}
        other => panic!("expected NotFound, got {other:?}"),
    }
}

#[test]
fn pinax_set_conflict_when_expected_wrong() {
    let (_dir, cella) = fresh_cella();
    let name = Name::new(b"head").unwrap();

    cella.set_pinax(&name, b"v1", None).unwrap();

    // Wrong expectation: declaring absent when present.
    match cella.set_pinax(&name, b"v2", None).unwrap() {
        SetPinaxOutcome::Conflict { actual } => {
            assert_eq!(actual, Some(sha256(b"v1")));
        }
        other => panic!("expected Conflict, got {other:?}"),
    }

    // Pinax untouched.
    assert_eq!(cella.get_pinax(&name).unwrap(), b"v1");
}

#[test]
fn pinax_namespace_disjoint_from_deposita() {
    // Same caller-chosen name lives in both namespaces without collision.
    let (_dir, cella) = fresh_cella();

    // Choose a depositum name = blake3-hex of some bytes; install via apotheca
    // directly so the depositum exists at that name.
    let bytes = b"sediment";
    let depositum_hash = cella.deposit(bytes).unwrap();
    let name_hex = depositum_hash.to_hex();
    let pinax_name = Name::new(name_hex.as_bytes()).unwrap();

    // Set a pinax under the same name with totally different bytes.
    cella.set_pinax(&pinax_name, b"surface", None).unwrap();

    assert_eq!(cella.get(&depositum_hash).unwrap(), bytes);
    assert_eq!(cella.get_pinax(&pinax_name).unwrap(), b"surface");
}