neco-cbor 0.1.1

necosystems series CBOR / DAG-CBOR codec for no_std environments
Documentation
use alloc::boxed::Box;
use alloc::vec::Vec;
use core::fmt;

use crate::CborValue;
use neco_cid::{Cid, CidError};

const CID_TAG: u64 = 42;
const CID_IDENTITY_PREFIX: u8 = 0x00;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CidTagError {
    NotATag,
    WrongTag(u64),
    NotBytes,
    MissingIdentityPrefix,
    InvalidCid(CidError),
    TrailingData,
}

impl fmt::Display for CidTagError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NotATag => f.write_str("expected CBOR tag"),
            Self::WrongTag(tag) => write!(f, "expected tag 42, got tag {tag}"),
            Self::NotBytes => f.write_str("tag 42 payload must be bytes"),
            Self::MissingIdentityPrefix => {
                f.write_str("tag 42 payload missing 0x00 identity prefix")
            }
            Self::InvalidCid(error) => write!(f, "invalid binary CID: {error}"),
            Self::TrailingData => f.write_str("trailing data after CID"),
        }
    }
}

impl core::error::Error for CidTagError {}

pub fn encode_cid_tag(cid: &Cid) -> CborValue {
    let cid_bytes = cid.to_bytes();
    let mut payload = Vec::with_capacity(cid_bytes.len() + 1);
    payload.push(CID_IDENTITY_PREFIX);
    payload.extend_from_slice(&cid_bytes);
    CborValue::Tag(CID_TAG, Box::new(CborValue::Bytes(payload)))
}

pub fn decode_cid_tag(value: &CborValue) -> Result<Cid, CidTagError> {
    let (tag, payload) = value.as_tag().ok_or(CidTagError::NotATag)?;
    if tag != CID_TAG {
        return Err(CidTagError::WrongTag(tag));
    }
    let bytes = payload.as_bytes().ok_or(CidTagError::NotBytes)?;
    let (prefix, rest) = bytes
        .split_first()
        .ok_or(CidTagError::MissingIdentityPrefix)?;
    if *prefix != CID_IDENTITY_PREFIX {
        return Err(CidTagError::MissingIdentityPrefix);
    }
    let (cid, consumed) = Cid::from_bytes(rest).map_err(CidTagError::InvalidCid)?;
    if consumed != rest.len() {
        return Err(CidTagError::TrailingData);
    }
    Ok(cid)
}

pub fn decode_optional_cid_tag(value: &CborValue) -> Result<Option<Cid>, CidTagError> {
    if value.is_null() {
        return Ok(None);
    }
    decode_cid_tag(value).map(Some)
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloc::boxed::Box;
    use alloc::vec;
    use neco_cid::Codec;

    #[test]
    fn roundtrip_dag_cbor_tag() {
        let cid = Cid::compute(Codec::DagCbor, b"hello");
        let tag = encode_cid_tag(&cid);
        let decoded = decode_cid_tag(&tag).expect("decode tag 42");
        assert_eq!(decoded, cid);
    }

    #[test]
    fn roundtrip_raw_tag() {
        let cid = Cid::compute(Codec::Raw, b"bytes");
        let tag = encode_cid_tag(&cid);
        let decoded = decode_cid_tag(&tag).expect("decode tag 42");
        assert_eq!(decoded, cid);
    }

    #[test]
    fn tag_payload_has_identity_prefix() {
        let cid = Cid::compute(Codec::DagCbor, b"prefix");
        let tag = encode_cid_tag(&cid);

        match tag {
            CborValue::Tag(42, inner) => match inner.as_ref() {
                CborValue::Bytes(payload) => {
                    assert_eq!(payload[0], 0x00);
                    assert_eq!(&payload[1..], cid.to_bytes().as_slice());
                }
                other => panic!("expected bytes payload, got {other:?}"),
            },
            other => panic!("expected tag 42, got {other:?}"),
        }
    }

    #[test]
    fn decode_rejects_non_tag() {
        let value = CborValue::Unsigned(42);
        assert_eq!(decode_cid_tag(&value), Err(CidTagError::NotATag));
    }

    #[test]
    fn decode_rejects_wrong_tag() {
        let value = CborValue::Tag(99, Box::new(CborValue::Bytes(vec![0x00])));
        assert_eq!(decode_cid_tag(&value), Err(CidTagError::WrongTag(99)));
    }

    #[test]
    fn decode_rejects_non_bytes_payload() {
        let value = CborValue::Tag(42, Box::new(CborValue::Text("not bytes".into())));
        assert_eq!(decode_cid_tag(&value), Err(CidTagError::NotBytes));
    }

    #[test]
    fn decode_rejects_missing_prefix() {
        let value = CborValue::Tag(42, Box::new(CborValue::Bytes(vec![])));
        assert_eq!(
            decode_cid_tag(&value),
            Err(CidTagError::MissingIdentityPrefix)
        );
    }

    #[test]
    fn decode_rejects_wrong_prefix() {
        let cid = Cid::compute(Codec::DagCbor, b"wrong-prefix");
        let mut payload = vec![0x01];
        payload.extend_from_slice(&cid.to_bytes());
        let value = CborValue::Tag(42, Box::new(CborValue::Bytes(payload)));
        assert_eq!(
            decode_cid_tag(&value),
            Err(CidTagError::MissingIdentityPrefix)
        );
    }

    #[test]
    fn decode_rejects_trailing_data() {
        let cid = Cid::compute(Codec::Raw, b"trailing");
        let mut payload = vec![0x00];
        payload.extend_from_slice(&cid.to_bytes());
        payload.push(0xff);
        let value = CborValue::Tag(42, Box::new(CborValue::Bytes(payload)));
        assert_eq!(decode_cid_tag(&value), Err(CidTagError::TrailingData));
    }

    #[test]
    fn decode_rejects_invalid_cid() {
        let value = CborValue::Tag(42, Box::new(CborValue::Bytes(vec![0x00, 0x02])));
        assert_eq!(
            decode_cid_tag(&value),
            Err(CidTagError::InvalidCid(CidError::InvalidVersion(2)))
        );
    }

    #[test]
    fn decode_optional_null() {
        assert_eq!(decode_optional_cid_tag(&CborValue::Null), Ok(None));
    }

    #[test]
    fn decode_optional_tag() {
        let cid = Cid::compute(Codec::DagCbor, b"optional");
        let tag = encode_cid_tag(&cid);
        assert_eq!(decode_optional_cid_tag(&tag), Ok(Some(cid)));
    }

    #[test]
    fn decode_optional_reuses_tag_validation() {
        let value = CborValue::Unsigned(123);
        assert_eq!(decode_optional_cid_tag(&value), Err(CidTagError::NotATag));
    }
}