use bytes::Bytes;
use ipld_core::ipld::Ipld;
use serde::{Serialize, de::DeserializeOwned};
use crate::error::CodecError;
use crate::id::cid::{CODEC_DAG_CBOR, Cid};
use crate::id::multihash::Multihash;
pub fn to_canonical_bytes<T: Serialize>(value: &T) -> Result<Bytes, CodecError> {
serde_ipld_dagcbor::to_vec(value)
.map(Bytes::from)
.map_err(|e| CodecError::Encode(e.to_string()))
}
pub fn from_canonical_bytes<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, CodecError> {
serde_ipld_dagcbor::from_slice(bytes).map_err(|e| CodecError::Decode(e.to_string()))
}
pub fn hash_to_cid<T: Serialize>(value: &T) -> Result<(Bytes, Cid), CodecError> {
let bytes = to_canonical_bytes(value)?;
let hash = Multihash::sha2_256(&bytes);
let cid = Cid::new(CODEC_DAG_CBOR, hash);
Ok((bytes, cid))
}
pub const WALK_IPLD_MAX_DEPTH: usize = 64;
pub fn extract_links(bytes: &[u8]) -> Result<Vec<Cid>, CodecError> {
let root: Ipld = from_canonical_bytes(bytes)?;
let mut out = Vec::new();
walk_ipld(&root, &mut out, 0)?;
Ok(out)
}
fn walk_ipld(node: &Ipld, out: &mut Vec<Cid>, depth: usize) -> Result<(), CodecError> {
if depth >= WALK_IPLD_MAX_DEPTH {
return Err(CodecError::Decode(format!(
"walk_ipld: nesting exceeds depth cap of {WALK_IPLD_MAX_DEPTH}"
)));
}
match node {
Ipld::Null
| Ipld::Bool(_)
| Ipld::Integer(_)
| Ipld::Float(_)
| Ipld::String(_)
| Ipld::Bytes(_) => Ok(()),
Ipld::List(xs) => {
for x in xs {
walk_ipld(x, out, depth + 1)?;
}
Ok(())
}
Ipld::Map(m) => {
for v in m.values() {
walk_ipld(v, out, depth + 1)?;
}
Ok(())
}
Ipld::Link(c) => {
let bytes = c.to_bytes();
let cid =
Cid::from_bytes(&bytes).map_err(|e| CodecError::Decode(format!("link: {e}")))?;
out.push(cid);
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::id::NodeId;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct Fixture {
id: NodeId,
label: String,
number: u64,
}
fn sample() -> Fixture {
Fixture {
id: NodeId::from_bytes_raw([7u8; 16]),
label: "hello".into(),
number: 42,
}
}
#[test]
fn round_trip_byte_identity() {
let original = sample();
let bytes1 = to_canonical_bytes(&original).expect("encode");
let decoded: Fixture = from_canonical_bytes(&bytes1).expect("decode");
assert_eq!(original, decoded, "decode mismatch");
let bytes2 = to_canonical_bytes(&decoded).expect("re-encode");
assert_eq!(bytes1, bytes2, "canonical re-encode must be byte-identical");
}
#[test]
fn hash_to_cid_is_deterministic() {
let sample = sample();
let (bytes1, cid1) = hash_to_cid(&sample).expect("encode+hash");
let (bytes2, cid2) = hash_to_cid(&sample).expect("encode+hash again");
assert_eq!(bytes1, bytes2);
assert_eq!(cid1, cid2);
assert_eq!(cid1.codec(), CODEC_DAG_CBOR);
}
#[test]
fn different_content_different_cid() {
let mut a = sample();
let mut b = sample();
b.number = 43;
let (_, cid_a) = hash_to_cid(&a).expect("encode a");
let (_, cid_b) = hash_to_cid(&b).expect("encode b");
assert_ne!(cid_a, cid_b);
b.number = a.number;
a.label.clear();
a.label.push_str("hello");
let (_, cid_b2) = hash_to_cid(&b).expect("encode b restored");
assert_eq!(cid_a, cid_b2);
}
#[test]
fn stable_id_encodes_as_byte_string() {
let id = NodeId::from_bytes_raw([7u8; 16]);
let bytes = to_canonical_bytes(&id).expect("encode");
assert_eq!(bytes.len(), 17);
assert_eq!(bytes[0], 0x50);
for &b in &bytes[1..] {
assert_eq!(b, 0x07);
}
}
#[test]
fn extract_links_on_leaf_block_is_empty() {
let bytes = to_canonical_bytes(&sample()).expect("encode");
let links = extract_links(&bytes).expect("extract");
assert!(links.is_empty(), "leaf block has no links, got {links:?}");
}
#[test]
fn extract_links_finds_cid_tags() {
use crate::id::{CODEC_RAW, Multihash};
use ipld_core::ipld::Ipld;
use std::collections::BTreeMap;
let make_inner = |seed: u8| -> ipld_core::cid::Cid {
let ours = Cid::new(CODEC_RAW, Multihash::sha2_256(&[seed]));
ipld_core::cid::Cid::try_from(ours.to_bytes().as_slice()).unwrap()
};
let a_inner = make_inner(1);
let b_inner = make_inner(2);
let mut top = BTreeMap::new();
top.insert("direct".to_string(), Ipld::Link(a_inner.clone()));
top.insert(
"nested".to_string(),
Ipld::List(vec![Ipld::Map(
[("x".to_string(), Ipld::Link(b_inner.clone()))]
.into_iter()
.collect(),
)]),
);
let value = Ipld::Map(top);
let bytes = to_canonical_bytes(&value).expect("encode");
let links = extract_links(&bytes).expect("extract");
assert_eq!(links.len(), 2);
let a_ours = Cid::from_bytes(&a_inner.to_bytes()).unwrap();
let b_ours = Cid::from_bytes(&b_inner.to_bytes()).unwrap();
assert_eq!(links[0], a_ours);
assert_eq!(links[1], b_ours);
}
#[test]
fn extract_links_rejects_malformed_bytes() {
let err = extract_links(b"\xff\xff garbage").expect_err("must fail");
assert!(matches!(err, CodecError::Decode(_)));
}
#[test]
fn walk_ipld_rejects_deeply_nested_structure() {
use ipld_core::ipld::Ipld;
let mut v = Ipld::Null;
for _ in 0..(WALK_IPLD_MAX_DEPTH + 4) {
v = Ipld::List(vec![v]);
}
let bytes = to_canonical_bytes(&v).expect("encode");
let err = extract_links(&bytes).expect_err("depth cap must trip");
match err {
CodecError::Decode(msg) => assert!(msg.contains("depth cap"), "got {msg}"),
other => panic!("wrong variant: {other:?}"),
}
}
}