use content_addressable::{canonical, ContentAddressable, ContentError};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct WireWithExtra {
alpha: u64,
zeta: u64,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct OnlyAlpha {
alpha: u64,
}
fn wire_with_extra_field() -> Vec<u8> {
canonical::to_canonical_dagcbor(&WireWithExtra { alpha: 1, zeta: 26 })
.expect("the fixture must encode")
}
const REORDERED_KEYS: [u8; 8] = [0xa2, 0x62, 0x62, 0x62, 0x01, 0x61, 0x61, 0x02];
const NON_MINIMAL_INT: [u8; 5] = [0xa1, 0x61, 0x61, 0x18, 0x01];
#[test]
fn checked_refuses_a_typed_decode_that_drops_a_field() {
let bytes = wire_with_extra_field();
let err = canonical::from_canonical_dagcbor_checked::<OnlyAlpha>(&bytes)
.expect_err("a decode that drops `zeta` must be refused");
assert!(
matches!(err, ContentError::LossyDecode),
"a typed round-trip mismatch must be LossyDecode (the bytes are canonical, \
so the mismatch is with the TYPE's representation of them), got {err:?}"
);
}
#[test]
#[allow(deprecated)]
fn unchecked_accepts_the_lossy_bytes_the_checked_decoder_refuses() {
let bytes = wire_with_extra_field();
let value: OnlyAlpha =
canonical::from_canonical_dagcbor(&bytes).expect("the unchecked door accepts them");
assert_eq!(
value,
OnlyAlpha { alpha: 1 },
"the unchecked decode succeeds, silently short one field"
);
let reencoded = canonical::to_canonical_dagcbor(&value).expect("re-encode");
assert_ne!(
reencoded, bytes,
"the dropped field is what makes the identity shift"
);
}
#[test]
fn a_lossy_decode_is_canonical_so_the_generic_check_cannot_catch_it() {
let bytes = wire_with_extra_field();
content_addressable::ContentId::from_canonical_bytes_checked(&bytes)
.expect("the bytes ARE canonical dag-cbor — the generic check is blind here");
}
#[test]
fn checked_refuses_reordered_map_keys() {
let err = canonical::from_canonical_dagcbor_checked::<ipld_core::ipld::Ipld>(&REORDERED_KEYS)
.expect_err("non-canonical key order must be refused");
assert!(
matches!(err, ContentError::NonCanonical),
"reordered keys must be NonCanonical (the BYTES are to blame), got {err:?}"
);
}
#[test]
fn checked_refuses_a_non_minimal_integer() {
let err = canonical::from_canonical_dagcbor_checked::<ipld_core::ipld::Ipld>(&NON_MINIMAL_INT)
.expect_err("a non-minimal integer must be refused");
assert!(
matches!(err, ContentError::NonCanonical),
"a non-minimal int must be NonCanonical (the BYTES are to blame), got {err:?}"
);
}
#[test]
#[allow(deprecated)]
fn unchecked_accepts_the_non_canonical_bytes_the_checked_decoder_refuses() {
let reordered: ipld_core::ipld::Ipld = canonical::from_canonical_dagcbor(&REORDERED_KEYS)
.expect("reordered keys are valid CBOR and decode");
assert_ne!(
canonical::to_canonical_dagcbor(&reordered).expect("re-encode"),
REORDERED_KEYS.to_vec(),
"the reordered fixture must genuinely differ from its canonical form"
);
let non_minimal: ipld_core::ipld::Ipld = canonical::from_canonical_dagcbor(&NON_MINIMAL_INT)
.expect("a non-minimal int is valid CBOR and decodes");
assert_ne!(
canonical::to_canonical_dagcbor(&non_minimal).expect("re-encode"),
NON_MINIMAL_INT.to_vec(),
"the non-minimal fixture must genuinely differ from its canonical form"
);
}
#[test]
fn checked_accepts_a_faithful_round_trip() {
let value = OnlyAlpha { alpha: 7 };
let bytes = canonical::to_canonical_dagcbor(&value).expect("encode");
let back: OnlyAlpha =
canonical::from_canonical_dagcbor_checked(&bytes).expect("a faithful decode is accepted");
assert_eq!(back, value);
}
#[test]
fn checked_reports_non_cbor_garbage_as_a_decoding_error() {
let err = canonical::from_canonical_dagcbor_checked::<ipld_core::ipld::Ipld>(&[
0xff, 0xff, 0xff, 0xff,
])
.expect_err("garbage is not dag-cbor");
assert!(
matches!(err, ContentError::DecodingError { .. }),
"garbage must stay a DecodingError, got {err:?}"
);
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Node {
alpha: u64,
}
impl ContentAddressable for Node {
fn canonical_form(&self) -> Result<Vec<u8>, ContentError> {
canonical::to_canonical_dagcbor(self)
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Enveloped {
alpha: u64,
}
impl ContentAddressable for Enveloped {
fn canonical_form(&self) -> Result<Vec<u8>, ContentError> {
canonical::to_canonical_dagcbor(&("envelope", self.alpha))
}
}
#[test]
fn from_canonical_form_refuses_a_decode_that_drops_a_field() {
let bytes = wire_with_extra_field();
let err = Node::from_canonical_form(&bytes)
.expect_err("the trait's decode must refuse a lossy round trip too");
assert!(
matches!(err, ContentError::LossyDecode),
"the ergonomic path must be the safe one, got {err:?}"
);
}
#[test]
fn from_canonical_form_refuses_non_canonical_bytes() {
let err = Node::from_canonical_form(&REORDERED_KEYS)
.expect_err("non-canonical bytes must be refused before any T is trusted");
assert!(
matches!(err, ContentError::NonCanonical),
"the bytes are to blame here, got {err:?}"
);
}
#[test]
fn from_canonical_form_returns_a_value_named_by_the_bytes() {
let bytes = canonical::to_canonical_dagcbor(&Node { alpha: 7 }).expect("encode");
let node = Node::from_canonical_form(&bytes).expect("a faithful round trip is accepted");
assert_eq!(node, Node { alpha: 7 });
assert_eq!(
node.canonical_form().expect("canonical_form"),
bytes,
"the enforced invariant: the decoded value re-encodes to exactly the input"
);
assert_eq!(
node.content_id().expect("content_id"),
content_addressable::ContentId::from_canonical_bytes(&bytes),
"for a lawful impl, byte equality gives identity equality"
);
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Skipped {
alpha: u64,
#[serde(skip)]
hidden: u64,
}
impl ContentAddressable for Skipped {
fn canonical_form(&self) -> Result<Vec<u8>, ContentError> {
canonical::to_canonical_dagcbor(self)
}
}
#[test]
fn the_decoded_value_need_not_equal_the_encoded_one() {
let encoded = Skipped {
alpha: 7,
hidden: 99,
};
let bytes = encoded.canonical_form().expect("canonical_form");
let decoded = Skipped::from_canonical_form(&bytes).expect("accepted — correctly");
assert_ne!(
decoded, encoded,
"the in-memory value is NOT recovered — that is the claim the docs must not make"
);
assert_eq!(
decoded,
Skipped {
alpha: 7,
hidden: 0
}
);
assert_eq!(
decoded.canonical_form().expect("canonical_form"),
bytes,
"byte equality holds even though the value differs"
);
assert_eq!(
decoded.content_id().expect("content_id"),
encoded.content_id().expect("content_id"),
"one canonical form, one identity — the skipped field is outside it"
);
}
#[test]
fn from_canonical_form_re_encodes_through_canonical_form_not_serde() {
let serde_bytes = canonical::to_canonical_dagcbor(&Enveloped { alpha: 7 }).expect("encode");
assert_eq!(
canonical::from_canonical_dagcbor_checked::<Enveloped>(&serde_bytes)
.expect("the serde-level checked decode accepts these bytes"),
Enveloped { alpha: 7 },
);
let err = Enveloped::from_canonical_form(&serde_bytes)
.expect_err("these bytes do not name an Enveloped");
assert!(
matches!(err, ContentError::LossyDecode),
"the trait door must re-encode through canonical_form, got {err:?}"
);
}
#[test]
fn a_custom_canonical_form_is_lawful_but_not_directly_deserializable() {
let node = Enveloped { alpha: 7 };
let own = node.canonical_form().expect("canonical_form");
let id = node.content_id().expect("content_id");
assert_eq!(
id,
content_addressable::ContentId::from_canonical_bytes(&own),
"the type IS lawful — this is not a broken implementation"
);
assert!(node.verify(&id).expect("verify"));
let err = Enveloped::from_canonical_form(&own)
.expect_err("an envelope does not decode as the struct");
assert!(
matches!(err, ContentError::DecodingError { .. }),
"a custom canonical form with no matching Deserialize must fail at the \
TYPED DECODE — not silently, and not as a round-trip mismatch — got {err:?}"
);
}
#[test]
fn content_addressable_stays_dyn_compatible() {
let boxed: Box<dyn ContentAddressable> = Box::new(Node { alpha: 1 });
let id = boxed.content_id().expect("content_id through dyn");
assert!(boxed.verify(&id).expect("verify through dyn"));
}
#[test]
fn the_deprecated_door_points_at_its_successor() {
const DOOR: &str = "pub fn from_canonical_dagcbor<T: DeserializeOwned>(";
let src = include_str!("../src/canonical.rs");
let door = src
.find(DOOR)
.expect("the deprecated door must still exist under this exact signature");
let before = &src[..door];
let start = before
.rfind("#[deprecated(")
.expect("`from_canonical_dagcbor` must carry a deprecation attribute");
let end = before[start..]
.find(")]")
.map(|i| start + i)
.expect("the attribute must terminate");
let between = &before[end + 2..];
assert!(
!between.contains("fn ") && !between.contains("struct ") && !between.contains("enum "),
"the nearest #[deprecated(…) is not attached to {DOOR}; found an item between them: \
{between:?}"
);
let attr = before[start..end]
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
assert!(
attr.contains(r#"since = "0.1.2""#),
"the deprecation must state the release that made it: {attr}"
);
assert!(
attr.contains("from_canonical_dagcbor_checked"),
"the note must name the successor a caller should move to: {attr}"
);
}