#![warn(missing_docs)]
pub mod canonical;
pub mod content_id;
pub mod error;
pub mod trait_def;
pub use content_id::{ContentId, BLAKE3_HASH_CODE, DAG_CBOR_CODEC};
pub use error::ContentError;
pub use trait_def::ContentAddressable;
#[cfg(test)]
mod tests {
use super::*;
use ipld_core::cid::Version;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct Sample {
name: String,
attrs: BTreeMap<String, u64>,
}
impl ContentAddressable for Sample {
fn canonical_form(&self) -> Result<Vec<u8>, ContentError> {
canonical::to_canonical_dagcbor(self)
}
}
fn sample_a() -> Sample {
let mut attrs = BTreeMap::new();
attrs.insert("zeta".to_string(), 26);
attrs.insert("alpha".to_string(), 1);
Sample {
name: "hello".to_string(),
attrs,
}
}
fn sample_b() -> Sample {
Sample {
name: "world".to_string(),
attrs: BTreeMap::new(),
}
}
#[test]
fn same_value_same_content_id() {
let a1 = sample_a().content_id().unwrap();
let a2 = sample_a().content_id().unwrap();
assert_eq!(a1, a2, "equal values must produce equal content ids");
}
#[test]
fn map_insertion_order_does_not_matter() {
let mut one = BTreeMap::new();
one.insert("alpha".to_string(), 1u64);
one.insert("zeta".to_string(), 26u64);
let mut two = BTreeMap::new();
two.insert("zeta".to_string(), 26u64);
two.insert("alpha".to_string(), 1u64);
let s1 = Sample {
name: "x".into(),
attrs: one,
};
let s2 = Sample {
name: "x".into(),
attrs: two,
};
assert_eq!(s1.content_id().unwrap(), s2.content_id().unwrap());
}
#[test]
fn different_value_different_content_id() {
let a = sample_a().content_id().unwrap();
let b = sample_b().content_id().unwrap();
assert_ne!(a, b, "distinct values must produce distinct content ids");
}
#[test]
fn verify_roundtrip_true_and_false() {
let a = sample_a();
let id_a = a.content_id().unwrap();
assert!(
a.verify(&id_a).unwrap(),
"value must verify against its own id"
);
let b = sample_b();
assert!(
!b.verify(&id_a).unwrap(),
"a different value must not verify against another value's id"
);
}
#[test]
fn canonical_dagcbor_roundtrip() {
let a = sample_a();
let bytes = canonical::to_canonical_dagcbor(&a).unwrap();
let back: Sample = canonical::from_canonical_dagcbor(&bytes).unwrap();
assert_eq!(
a, back,
"value must survive a dag-cbor encode/decode roundtrip"
);
}
#[test]
fn content_id_string_roundtrip() {
let id = sample_a().content_id().unwrap();
let s = id.to_string();
let parsed: ContentId = s.parse().unwrap();
assert_eq!(
id, parsed,
"ContentId must roundtrip through its string form"
);
}
#[test]
fn content_id_bytes_roundtrip() {
let id = sample_a().content_id().unwrap();
let bytes = id.to_bytes();
let parsed = ContentId::from_bytes(&bytes).unwrap();
assert_eq!(id, parsed, "ContentId must roundtrip through its byte form");
}
#[test]
fn cid_shape_is_v1_dagcbor_blake3() {
let id = sample_a().content_id().unwrap();
let cid = id.as_cid();
assert_eq!(cid.version(), Version::V1, "must be a CIDv1");
assert_eq!(cid.codec(), DAG_CBOR_CODEC, "codec must be dag-cbor (0x71)");
assert_eq!(
cid.hash().code(),
BLAKE3_HASH_CODE,
"multihash must be BLAKE3 (0x1e)"
);
assert_eq!(
cid.hash().digest().len(),
32,
"BLAKE3 digest must be 32 bytes"
);
}
#[test]
fn content_id_serializes_as_dagcbor_link() {
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct HasLink {
link: ContentId,
}
let id = sample_a().content_id().unwrap();
let value = HasLink { link: id };
let bytes = canonical::to_canonical_dagcbor(&value).unwrap();
let back: HasLink = canonical::from_canonical_dagcbor(&bytes).unwrap();
assert_eq!(value, back, "a ContentId field must roundtrip via dag-cbor");
assert!(
bytes.windows(2).any(|w| w == [0xd8, 0x2a]),
"ContentId must be encoded as a dag-cbor tag-42 link"
);
}
#[test]
fn invalid_cid_string_errors() {
let err = "not-a-cid".parse::<ContentId>().unwrap_err();
assert!(matches!(err, ContentError::InvalidCid { .. }));
}
}