minicbor-adapters 0.0.8

Adapters between `minicbor` and other crates such as `heapless` and `cboritem`
Documentation
//! Adapters between [`minicbor`] and [`cboritem`]

use crate::with::{OurCborLen, OurDecode, OurEncode};
use minicbor::{Decode, Decoder, Encode, Encoder, decode, encode, encode::Write};

impl<'b, C> OurDecode<'b, C> for cboritem::CborItem<'b> {
    fn decode(d: &mut Decoder<'b>, _ctx: &mut C) -> Result<Self, decode::Error> {
        let startpos = d.position();
        d.skip()?;
        let endpos = d.position();
        // SAFETY: minicbor just parsed this successfully.
        Ok(unsafe { cboritem::CborItem::new(&d.input()[startpos..endpos]) })
    }
}

impl<C> OurEncode<C> for cboritem::CborItem<'_> {
    fn encode<W: Write>(
        &self,
        e: &mut Encoder<W>,
        _: &mut C,
    ) -> Result<(), encode::Error<W::Error>> {
        e.writer_mut()
            .write_all(self)
            .map_err(encode::Error::write)?;
        Ok(())
    }
}

impl<C> OurCborLen<C> for cboritem::CborItem<'_> {
    fn cbor_len(&self, _: &mut C) -> usize {
        self.len()
    }
}

/// A wrapper around a CBOR item that gets decoded, but also retains its original encoded slice.
///
/// This allows using the encoded bytes later, for example when a credential that is contained in a
/// CBOR Web Token (CWT) is used as input to a hash function. In particular, these encoded bytes
/// also preserve any uncommon encoding choices (such as encoding a small integer in a larger
/// space). Protocols where this kind of access is encouraged often require input to be encoded
/// deterministically. (They may also put the encoded CBOR in a byte string, in which case this
/// type is not needed).
///
/// When encoding, the opaque item is used instead of re-serializing the parsed item; this might be
/// handy in performance critical situations, or when [`Decode`] is implemented on the item, but
/// [`Encode`] is not.
///
/// # Consistency
///
/// With both items being public, it is possible to construct items where the parsed item is *not*
/// identical to the opaque encoded slice.
///
/// Constructing such values is discouraged, but may be useful at times; if nothing else, APIs that
/// include this type should clarify which part of it they use, or whether they promise consistency.
///
/// Given that this type does not constrain `T` to not have interior mutability, and given that
/// minicbor can not make requirements on encoding and decoding to be deterministic, attempts to
/// uphold equivalence are bound to fail; no attempts are made to work around that.
#[derive(Debug)]
pub struct WithOpaque<'a, T> {
    /// Slice of memory that was decoded into `.parsed`.
    pub opaque: cboritem::CborItem<'a>,
    /// Decoded item in the `.opaque` encoded slice.
    pub parsed: T,
}

impl<'b, C, T> Decode<'b, C> for WithOpaque<'b, T>
where
    T: Decode<'b, C>,
{
    fn decode(d: &mut Decoder<'b>, ctx: &mut C) -> Result<Self, decode::Error> {
        let startpos = d.position();
        let parsed = d.decode_with(ctx)?;
        let endpos = d.position();
        // SAFETY: minicbor just parsed this successfully.
        let opaque = unsafe { cboritem::CborItem::new(&d.input()[startpos..endpos]) };
        Ok(WithOpaque { opaque, parsed })
    }
}

impl<C, T> Encode<C> for WithOpaque<'_, T> {
    fn encode<W: Write>(
        &self,
        e: &mut Encoder<W>,
        _: &mut C,
    ) -> Result<(), encode::Error<W::Error>> {
        e.writer_mut()
            .write_all(&self.opaque)
            .map_err(encode::Error::write)?;
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use super::WithOpaque;
    use cboritem::CborItem;
    use minicbor::{Decode, Encode};

    #[derive(Decode, Debug)]
    struct UsedInWrapper<'a> {
        #[n(0)]
        number: u32,
        #[b(1)]
        text: &'a str,
    }

    #[derive(Decode, Encode, Debug)]
    struct OpaqueItems<'a> {
        #[cbor(b(0), with = "crate")]
        item_a: CborItem<'a>,
        #[cbor(b(1), with = "crate")]
        item_b: CborItem<'a>,
        #[b(2)]
        item_c: Option<WithOpaque<'a, UsedInWrapper<'a>>>,
    }

    const EXAMPLE: &[u8] = &[
        0x83, 0x00, 0x82, 0x01, 0x02, 0x82, 0x18, 0x2a, 0x62, 0x68, 0x69,
    ];

    #[test]
    fn decode_cboritem() {
        let decoded: OpaqueItems = minicbor::decode(EXAMPLE).unwrap();
        let _item_a: usize = minicbor::decode(&decoded.item_a).unwrap();
        let item_b: Result<&str, _> = minicbor::decode(&decoded.item_b);
        item_b.unwrap_err();

        let item_c = decoded.item_c.unwrap();
        assert!((item_c.parsed.number, item_c.parsed.text) == (42, "hi"));
        assert!(matches!(&*item_c.opaque, &[0x82, .., 0x68, 0x69]));
    }

    #[test]
    fn encode_cboritem() {
        let data = [0x82, 0x00, 0x82, 0x01, 0x02];
        let decoded: OpaqueItems = minicbor::decode(data.as_ref()).unwrap();

        let mut buf = [0u8; 20];
        let mut buf = minicbor::encode::write::Cursor::new(buf.as_mut());
        let mut swapped = minicbor::Encoder::new(&mut buf);

        swapped
            .encode(OpaqueItems {
                item_a: decoded.item_b,
                item_b: decoded.item_a,
                // This item is in an inconsistent state -- that is allowed, even though it is
                // discouraged.
                item_c: Some(WithOpaque {
                    opaque: cbor_macro::cbor!([9, ""]),
                    // Actually we couldn't even *check* if it is in an inconsistent state, becasue
                    // this doesn't even (need to) implement Encode.
                    parsed: UsedInWrapper {
                        number: 99,
                        text: "actually the number is 9 and the byte string is empty",
                    },
                }),
            })
            .unwrap();

        assert_eq!(
            &[0x83, 0x82, 0x01, 0x02, 0x00, 0x82, 0x09, 0x60],
            &buf.get_ref()[..buf.position()]
        );
    }

    #[test]
    fn debug() {
        extern crate std;
        let decoded: OpaqueItems = minicbor::decode(EXAMPLE).unwrap();
        // We don't guarantee any precise debug format, but it should work, and let's check before
        // it changes.
        assert_eq!(
            std::format!("{decoded:#?}"),
            "\
OpaqueItems {
    item_a: 00,
    item_b: 82 01 02,
    item_c: Some(
        WithOpaque {
            opaque: 82 18 2a 62 68 69,
            parsed: UsedInWrapper {
                number: 42,
                text: \"hi\",
            },
        },
    ),
}"
        );
    }
}