content-addressable 0.1.3

IPLD-native content addressing: data that carries its own proof of integrity, intrinsically (BLAKE3 + CIDv1 + canonical dag-cbor).
Documentation
//! Rust side of the shared byte-parity gate.
//!
//! Parses the committed golden-vector file `tests/vectors.json` (the authority,
//! generated by `tests/gen_vectors.rs`) and asserts, for every case, that the
//! current Rust core reproduces the pinned canonical dag-cbor hex, the pinned
//! `ContentId` string, the pinned `ContentId` bytes, and the pinned
//! `digest_hex` (the bare-digest-hex presentation form, issue #6). The *same*
//! file is consumed by the Python gate (`tests/test_content_addressable.py`),
//! so any future byte drift — a dependency bump, an encoder change — fails
//! loudly in BOTH languages at once.
//!
//! ## What this file pins (and what it does not)
//!
//! Vectors cover only the *currently-stable, JSON-expressible* dag-cbor subset:
//! null, bool, integers (i64 range), strings, arrays, maps with string keys, and
//! byte strings (via the `{"$bytes": "<hex>"}` escape — see `tests/common`).
//! Deliberately excluded: non-integer floats (JSON float ambiguity + dag-cbor
//! float rules), and any value depending on the not-yet-frozen serde
//! representation of `ContentId` (the tag-42 link, README must-fix-gate item 1).
//! Those join the vectors once the corresponding gate item is frozen; until then
//! they live in the per-language suites.

mod common;

use common::{bytes_to_hex, hex_to_bytes, json_value_to_ipld};
use content_addressable::content_id::{BLAKE3_HASH_CODE, DAG_CBOR_CODEC};
use content_addressable::{canonical, ContentId};
use serde::Deserialize;

/// One vector as stored in `tests/vectors.json`.
#[derive(Deserialize)]
struct Vector {
    name: String,
    value: serde_json::Value,
    canonical_dagcbor_hex: String,
    content_id_str: String,
    content_id_bytes_hex: String,
    /// The "bare-digest-hex" presentation form (issue #6): 64-char lower-hex of
    /// the raw 32-byte BLAKE3 digest, no envelope/prefix. Pinned here and in the
    /// Python gate so the named accessor's bytes cannot drift across languages.
    digest_hex: String,
}

fn load_vectors() -> Vec<Vector> {
    let raw = include_str!("vectors.json");
    serde_json::from_str(raw).expect("tests/vectors.json must be valid JSON")
}

#[test]
fn vectors_file_is_nonempty_and_covers_required_cases() {
    let vectors = load_vectors();
    assert!(
        vectors.len() >= 8,
        "the issue requires >= 8 vectors; found {}",
        vectors.len()
    );
    let names: Vec<&str> = vectors.iter().map(|v| v.name.as_str()).collect();
    for required in [
        "empty_map",
        "empty_list",
        "nested_map",
        "map_order_independent",
        "ints_strings_bytes_mix",
        "sample_a",
    ] {
        assert!(
            names.contains(&required),
            "vectors.json must contain the {required:?} case; have {names:?}"
        );
    }
}

#[test]
fn every_vector_matches_the_rust_core() {
    for v in load_vectors() {
        let ipld = json_value_to_ipld(&v.value);

        // 1. Canonical dag-cbor bytes match the pinned hex.
        let bytes = canonical::to_canonical_dagcbor(&ipld)
            .unwrap_or_else(|e| panic!("[{}] encode failed: {e}", v.name));
        assert_eq!(
            bytes_to_hex(&bytes),
            v.canonical_dagcbor_hex,
            "[{}] canonical dag-cbor hex drifted",
            v.name
        );

        // 2. ContentId string (multibase base32, 'b'-prefixed) matches.
        let id = ContentId::from_canonical_bytes(&bytes);
        assert_eq!(
            id.to_string(),
            v.content_id_str,
            "[{}] ContentId string drifted",
            v.name
        );

        // 3. ContentId bytes (the CIDv1 binary form) match.
        assert_eq!(
            bytes_to_hex(&id.to_bytes()),
            v.content_id_bytes_hex,
            "[{}] ContentId bytes drifted",
            v.name
        );

        // 4. The named presentation accessor digest_hex (issue #6): the
        // bare-digest-hex form. It must equal the pinned value, be 64 lower-hex
        // chars with no prefix, and — as a structural cross-check — be exactly
        // the tail of content_id_bytes_hex after the 8-char `01711e20` CID
        // prefix (so the two pinned forms cannot disagree about the digest).
        assert_eq!(
            id.digest_hex(),
            v.digest_hex,
            "[{}] digest_hex drifted",
            v.name
        );
        assert_eq!(
            v.digest_hex.len(),
            64,
            "[{}] digest_hex must be 64 chars",
            v.name
        );
        // No `0x` prefix, lowercase hex only. (A bare-digest-hex can begin with
        // any hex digit including `b`, so do NOT mistake a leading `b` for the
        // base32 multibase prefix — that prefix belongs to content_id_str, not
        // this hex form.)
        assert!(
            !v.digest_hex.starts_with("0x"),
            "[{}] digest_hex must carry no 0x prefix",
            v.name
        );
        assert!(
            v.digest_hex
                .bytes()
                .all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)),
            "[{}] digest_hex must be lowercase hex only",
            v.name
        );
        assert_eq!(
            v.content_id_bytes_hex,
            format!("01711e20{}", v.digest_hex),
            "[{}] digest_hex must be the CID-bytes tail after the 01711e20 prefix",
            v.name
        );
        // digest_bytes() is the raw form of the same thing.
        assert_eq!(
            bytes_to_hex(&id.digest_bytes()),
            v.digest_hex,
            "[{}] digest_bytes() hex must equal the pinned digest_hex",
            v.name
        );

        // The pinned bytes must also parse back to the same id (self-check).
        let expected_cid_bytes = hex_to_bytes(&v.content_id_bytes_hex);
        let parsed = ContentId::from_bytes(&expected_cid_bytes)
            .unwrap_or_else(|e| panic!("[{}] pinned CID bytes do not parse: {e}", v.name));
        assert_eq!(
            parsed, id,
            "[{}] pinned CID bytes name a different id",
            v.name
        );
    }
}

/// Non-circular proof: rebuild the expected CID bytes by hand from the canonical
/// dag-cbor and an *independent* BLAKE3 hash, and assert they equal the pinned
/// `content_id_bytes_hex`. If the vectors were merely the implementation echoing
/// itself this would not constrain anything; because the hand-built form is
/// derived from first principles (CIDv1 prefix + multihash header + raw digest),
/// a match proves the golden bytes are genuinely `BLAKE3(dag-cbor)` wrapped as a
/// CIDv1 dag-cbor link — not a self-referential tautology.
#[test]
fn pinned_cid_bytes_are_independently_reconstructible() {
    // CIDv1 binary prefix for our fixed shape:
    //   0x01 = CID version 1
    //   0x71 = multicodec dag-cbor       (DAG_CBOR_CODEC)
    //   0x1e = multihash code BLAKE3     (BLAKE3_HASH_CODE)
    //   0x20 = digest length 32 (varint) (0x20 == 32)
    // These constants are asserted here so a future codec/hash change trips the
    // hand-reconstruction too, not just the pinned literals.
    assert_eq!(DAG_CBOR_CODEC, 0x71, "this gate hard-codes dag-cbor 0x71");
    assert_eq!(BLAKE3_HASH_CODE, 0x1e, "this gate hard-codes BLAKE3 0x1e");
    const CID_HEADER: [u8; 4] = [0x01, 0x71, 0x1e, 0x20];

    let vectors = load_vectors();
    assert!(!vectors.is_empty());
    for v in &vectors {
        let dagcbor = hex_to_bytes(&v.canonical_dagcbor_hex);
        let digest = blake3::hash(&dagcbor);
        assert_eq!(
            digest.as_bytes().len(),
            32,
            "BLAKE3 digest must be 32 bytes"
        );

        let mut hand_built = Vec::with_capacity(CID_HEADER.len() + 32);
        hand_built.extend_from_slice(&CID_HEADER);
        hand_built.extend_from_slice(digest.as_bytes());

        // The library's own bytes must equal the hand-built form...
        let id = ContentId::from_canonical_bytes(&dagcbor);
        assert_eq!(
            id.to_bytes(),
            hand_built,
            "[{}] hand-reconstructed CID bytes disagree with ContentId::to_bytes() \
             — this would be a real wire-shape bug, not a vector typo",
            v.name
        );
        // ...and the pinned hex must equal both.
        assert_eq!(
            bytes_to_hex(&hand_built),
            v.content_id_bytes_hex,
            "[{}] pinned CID bytes disagree with the independent reconstruction",
            v.name
        );
    }
}

/// Every golden vector survives the **checked** decoder and comes back naming the
/// pinned identity (issue #90).
///
/// This is the positive half of `tests/checked_decode.rs`, which pins the
/// refusals. A gate that refuses bad bytes is only worth having if it accepts
/// every byte string the crate itself calls canonical; running the frozen vector
/// set through it is the strongest available statement that the gate is not
/// over-tight. If a future `serde_ipld_dagcbor` bump changed what round-trips,
/// this fails here — on the frozen bytes — rather than downstream.
///
/// **Scope.** These are the *dag-cbor* vectors. `tests/raw_vectors.json` and
/// `tests/legacy_vectors.json` pin the identities of **opaque byte strings** and
/// of legacy identifier *text*; neither is a dag-cbor document, so a dag-cbor
/// decoder has nothing to say about them. Running them through it would not be a
/// stronger test, it would be a category error — the profile law is that the
/// codec is part of the identity, and `raw` (`0x55`) is not `dag-cbor` (`0x71`).
#[test]
fn every_vector_survives_the_checked_decoder() {
    for v in load_vectors() {
        let bytes = hex_to_bytes(&v.canonical_dagcbor_hex);

        // 1. The checked decoder accepts the pinned bytes.
        let decoded: ipld_core::ipld::Ipld = canonical::from_canonical_dagcbor_checked(&bytes)
            .unwrap_or_else(|e| {
                panic!(
                    "[{}] the checked decoder rejected a GOLDEN vector: {e}",
                    v.name
                )
            });

        // 2. It gave back the value the vector says those bytes hold.
        assert_eq!(
            decoded,
            json_value_to_ipld(&v.value),
            "[{}] the checked decoder returned a different value",
            v.name
        );

        // 3. Re-encoding it reproduces the pinned bytes and therefore the pinned
        //    id — the round trip the checked door exists to guarantee, asserted
        //    against the frozen cross-language authority rather than a fixture.
        let reencoded = canonical::to_canonical_dagcbor(&decoded)
            .unwrap_or_else(|e| panic!("[{}] re-encode failed: {e}", v.name));
        assert_eq!(
            bytes_to_hex(&reencoded),
            v.canonical_dagcbor_hex,
            "[{}] a decoded golden vector re-encoded to different bytes",
            v.name
        );
        assert_eq!(
            ContentId::from_canonical_bytes(&reencoded).to_string(),
            v.content_id_str,
            "[{}] the round trip moved the identity",
            v.name
        );
    }
}