reallyme-codec-cbor 0.3.1

Canonical CBOR codec support for ReallyMe Codec.
Documentation
// SPDX-FileCopyrightText: 2026 ReallyMe LLC
//
// SPDX-License-Identifier: MIT OR Apache-2.0

#![allow(missing_docs)]
#![allow(
    clippy::expect_used,
    clippy::panic,
    clippy::print_stdout,
    clippy::unwrap_used
)]
use cid::multibase::Base;
use cid::Cid;
use codec_cbor::CborValue;
use codec_cbor::{
    compute_cid_dag_cbor, dag_cbor_multihash, encode_dag_cbor, is_valid_cid_string,
    sha2_256_content_hash, try_parse_cid, verify_dag_cbor_cid, CidVerificationStatus,
    MAX_CID_STRING_LEN,
};

fn enc(value: &CborValue) -> Vec<u8> {
    encode_dag_cbor(value).unwrap()
}

#[test]
fn deterministic_cid_for_same_input() {
    let v = CborValue::Map(vec![
        ("a".into(), CborValue::Int(1)),
        ("b".into(), CborValue::Bool(true)),
    ]);

    let b1 = enc(&v);
    let b2 = enc(&v);

    let cid1 = compute_cid_dag_cbor(&b1);
    let cid2 = compute_cid_dag_cbor(&b2);

    assert_eq!(cid1, cid2);
}

#[test]
fn different_inputs_produce_different_cids() {
    let cid1 = compute_cid_dag_cbor(&enc(&CborValue::Int(1)));
    let cid2 = compute_cid_dag_cbor(&enc(&CborValue::Int(2)));
    assert_ne!(cid1, cid2);
}

#[test]
fn verify_matching_cid() {
    let v = CborValue::Map(vec![("hello".into(), CborValue::String("world".into()))]);
    let bytes = enc(&v);
    let cid = compute_cid_dag_cbor(&bytes);

    let verification = verify_dag_cbor_cid(&cid, &bytes).unwrap();
    assert_eq!(verification.status(), CidVerificationStatus::Match);
}

#[test]
fn verify_rejects_invalid_uppercase_base32_payload() {
    let bytes = enc(&CborValue::String("canonical cid input".into()));
    let cid = compute_cid_dag_cbor(&bytes);
    let mut invalid_upper_payload = cid.clone();
    invalid_upper_payload.replace_range(1.., &cid[1..].to_ascii_uppercase());

    let verification = verify_dag_cbor_cid(&invalid_upper_payload, &bytes).unwrap();

    assert_eq!(verification.status(), CidVerificationStatus::NonCanonical);
    assert_eq!(verification.expected_cid(), cid);
    assert_eq!(verification.actual_cid(), cid);
}

#[test]
fn verify_rejects_base32_upper_cid_string() {
    let bytes = enc(&CborValue::String("uppercase base32 cid".into()));
    let canonical = compute_cid_dag_cbor(&bytes);
    let parsed = Cid::try_from(canonical.as_str()).unwrap();
    let base32_upper = parsed.to_string_of_base(Base::Base32Upper).unwrap();

    let verification = verify_dag_cbor_cid(&base32_upper, &bytes).unwrap();

    assert_eq!(verification.status(), CidVerificationStatus::NonCanonical);
    assert_eq!(verification.expected_cid(), canonical);
    assert_eq!(verification.actual_cid(), canonical);
}

#[test]
fn verify_rejects_valid_alternate_multibase_cid_strings() {
    let bytes = enc(&CborValue::Map(vec![(
        "cid".into(),
        CborValue::String("alternate multibase".into()),
    )]));
    let canonical = compute_cid_dag_cbor(&bytes);
    let parsed = Cid::try_from(canonical.as_str()).unwrap();
    let base58 = parsed.to_string_of_base(Base::Base58Btc).unwrap();
    let base16 = parsed.to_string_of_base(Base::Base16Lower).unwrap();
    let base2 = parsed.to_string_of_base(Base::Base2).unwrap();
    let emoji = parsed.to_string_of_base(Base::Base256Emoji).unwrap();

    for alternate in [base58, base16, base2, emoji] {
        let verification = verify_dag_cbor_cid(&alternate, &bytes).unwrap();
        assert_eq!(verification.status(), CidVerificationStatus::NonCanonical);
        assert_eq!(verification.expected_cid(), canonical);
        assert_eq!(verification.actual_cid(), canonical);
    }
}

#[test]
fn verification_rejects_non_dag_cbor_payloads() {
    for payload in [
        &[][..],
        &[0xff][..],
        &[0xa2, 0x61, 0x62, 0x01, 0x61, 0x61, 0x02][..],
        &[0x18, 0x01][..],
        &[0xf6, 0xf6][..],
    ] {
        let cid = compute_cid_dag_cbor(payload);
        assert!(verify_dag_cbor_cid(&cid, payload).is_err());
    }
}

#[test]
fn detect_cid_mismatch() {
    let b1 = enc(&CborValue::Int(1));
    let b2 = enc(&CborValue::Int(2));

    let cid_wrong = compute_cid_dag_cbor(&b2);
    let verification = verify_dag_cbor_cid(&cid_wrong, &b1).unwrap();
    assert_eq!(verification.status(), CidVerificationStatus::Mismatch);
}

#[test]
fn cid_syntax_validation() {
    let v = enc(&CborValue::Int(123));
    let cid = compute_cid_dag_cbor(&v);

    assert!(is_valid_cid_string(&cid));
    assert!(!is_valid_cid_string("not-a-cid"));
}

#[test]
fn cidv0_is_only_accepted_in_its_bare_base58_form() {
    let hash = dag_cbor_multihash(&enc(&CborValue::Int(42)));
    let cid = Cid::new_v0(multihash::Multihash::from_bytes(&hash.to_bytes()).unwrap()).unwrap();
    let bare = cid.to_string();
    assert!(is_valid_cid_string(&bare));
    assert!(try_parse_cid(&bare).is_some());
    let wrapped = format!("z{bare}");
    assert!(!is_valid_cid_string(&wrapped));
    assert!(try_parse_cid(&wrapped).is_none());
}

#[test]
fn content_hash_matches_dag_cbor_multihash_digest() {
    let bytes = enc(&CborValue::String("dag-cbor hash".into()));
    let hash = sha2_256_content_hash(&bytes);
    let multihash = dag_cbor_multihash(&bytes);

    assert_eq!(multihash.code(), 0x12);
    assert_eq!(multihash.size(), 32);
    assert_eq!(multihash.digest(), hash);
}

#[test]
fn random_payloads_produce_valid_cids() {
    for i in 0..50 {
        let v = CborValue::Map(vec![("data".into(), CborValue::Bytes(vec![i; 32]))]);
        let cid = compute_cid_dag_cbor(&enc(&v));
        assert!(is_valid_cid_string(&cid));
    }
}

#[test]
fn cid_parsers_reject_trailing_binary_data_and_paths() {
    let payload = enc(&CborValue::Null);
    let canonical = compute_cid_dag_cbor(&payload);
    let parsed = Cid::try_from(canonical.as_str()).unwrap();
    let mut suffixed = parsed.to_bytes();
    suffixed.extend_from_slice(b"untrusted suffix");
    for invalid in [
        cid::multibase::encode(Base::Base32Lower, &suffixed),
        cid::multibase::encode(Base::Base58Btc, &suffixed),
        format!("/ipfs/{canonical}"),
        format!("https://example.invalid/ipfs/{canonical}"),
    ] {
        assert!(!is_valid_cid_string(&invalid));
        assert!(try_parse_cid(&invalid).is_none());
        let verification = verify_dag_cbor_cid(&invalid, &payload).unwrap();
        assert_eq!(verification.status(), CidVerificationStatus::InvalidCid);
        assert_eq!(verification.expected_cid(), canonical);
        assert!(verification.actual_cid().is_empty());
    }
}

#[test]
fn cid_parsers_preserve_versions_and_alternate_bases() {
    let hash = dag_cbor_multihash(b"version compatibility");
    let v0 = Cid::new_v0(multihash::Multihash::from_bytes(&hash.to_bytes()).unwrap()).unwrap();
    assert_eq!(
        try_parse_cid(&v0.to_string()).map(|cid| cid.to_string()),
        Some(v0.to_string())
    );
    let v1 = Cid::new_v1(
        0x71,
        multihash::Multihash::from_bytes(&hash.to_bytes()).unwrap(),
    );
    for base in [
        Base::Base2,
        Base::Base8,
        Base::Base10,
        Base::Base16Lower,
        Base::Base32Lower,
        Base::Base32Upper,
        Base::Base36Lower,
        Base::Base58Btc,
        Base::Base64,
        Base::Base64Url,
        Base::Base64Pad,
        Base::Base64UrlPad,
        Base::Base32HexLower,
        Base::Base32PadLower,
        Base::Base32Z,
        Base::Base256Emoji,
    ] {
        let text = v1.to_string_of_base(base).unwrap();
        assert_eq!(
            try_parse_cid(&text).map(|cid| cid.to_string()),
            Some(v1.to_string())
        );
    }
    let mut invalid_v0 = v0.to_bytes();
    invalid_v0.push(0);
    assert!(try_parse_cid(&cid::multibase::encode(Base::Base58Btc, invalid_v0)).is_none());
}

#[test]
fn cid_parsers_reject_nonminimal_varints_and_oversized_text() {
    let cid = Cid::new_v1(
        0x71,
        multihash::Multihash::from_bytes(&dag_cbor_multihash(b"minimal").to_bytes()).unwrap(),
    );
    let canonical = cid.to_bytes();
    for index in 0..4 {
        let mut nonminimal = canonical.clone();
        nonminimal[index] |= 0x80;
        nonminimal.insert(index + 1, 0);
        assert!(try_parse_cid(&cid::multibase::encode(Base::Base16Lower, nonminimal)).is_none());
    }
    for prefix in ['z', 'k', 'b'] {
        let oversized = format!("{prefix}{}", "1".repeat(MAX_CID_STRING_LEN));
        assert!(try_parse_cid(&oversized).is_none());
        assert!(!is_valid_cid_string(&oversized));
        let payload = enc(&CborValue::Null);
        assert_eq!(
            verify_dag_cbor_cid(&oversized, &payload).unwrap().status(),
            CidVerificationStatus::InvalidCid
        );
    }
    let largest = Cid::new_v1(
        u64::MAX,
        multihash::Multihash::wrap(u64::MAX, &[0xff; 64]).unwrap(),
    );
    let base2 = largest.to_string_of_base(Base::Base2).unwrap();
    assert!(base2.len() <= MAX_CID_STRING_LEN);
    assert_eq!(
        try_parse_cid(&base2).map(|cid| cid.to_string()),
        Some(largest.to_string())
    );
}