Skip to main content

pamoja_codec/
cbor.rs

1//! A compact CBOR codec built on [`ciborium`].
2
3use alloc::format;
4use alloc::string::ToString;
5use alloc::vec::Vec;
6
7use serde::de::DeserializeOwned;
8use serde::Serialize;
9
10use pamoja_core::{Error, Result};
11
12use crate::Codec;
13
14/// A codec that serializes values to and from CBOR.
15///
16/// CBOR is a compact, self-describing binary format, which makes it the default
17/// choice for constrained devices and metered radio links where every byte costs
18/// power or money. The codec works for any type that implements [`serde::Serialize`]
19/// and [`serde::de::DeserializeOwned`].
20///
21/// # Examples
22///
23/// ```
24/// use pamoja_codec::{CborCodec, Codec};
25///
26/// let codec = CborCodec;
27/// let value = (1u8, "ok".to_owned());
28/// let encoded = codec.encode(&value).unwrap();
29/// let decoded: (u8, String) = codec.decode(&encoded).unwrap();
30/// assert_eq!(decoded, value);
31/// ```
32#[derive(Clone, Copy, Debug, Default)]
33pub struct CborCodec;
34
35impl<T> Codec<T> for CborCodec
36where
37    T: Serialize + DeserializeOwned,
38{
39    fn encode(&self, value: &T) -> Result<Vec<u8>> {
40        let mut buffer = Vec::new();
41        ciborium::into_writer(value, &mut buffer).map_err(write_error)?;
42        Ok(buffer)
43    }
44
45    fn decode(&self, bytes: &[u8]) -> Result<T> {
46        ciborium::from_reader(bytes).map_err(read_error)
47    }
48}
49
50/// Puts a CBOR decoding failure into words; the library displays its errors in their
51/// debug form.
52pub(crate) fn read_error<T>(error: ciborium::de::Error<T>) -> Error {
53    Error::Codec(match error {
54        ciborium::de::Error::Io(_) => "the CBOR ends part-way through a value".to_string(),
55        ciborium::de::Error::Syntax(offset) => format!("the CBOR is malformed at byte {offset}"),
56        ciborium::de::Error::Semantic(Some(offset), message) => {
57            format!("{message}, at byte {offset}")
58        }
59        ciborium::de::Error::Semantic(None, message) => message,
60        ciborium::de::Error::RecursionLimitExceeded => {
61            "the CBOR nests too deeply to read".to_string()
62        }
63    })
64}
65
66/// Puts a CBOR encoding failure into words; the library displays its errors in their
67/// debug form.
68pub(crate) fn write_error<T>(error: ciborium::ser::Error<T>) -> Error {
69    Error::Codec(match error {
70        ciborium::ser::Error::Io(_) => "the CBOR could not be written".to_string(),
71        ciborium::ser::Error::Value(message) => message,
72    })
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn round_trips_a_tuple() {
81        let codec = CborCodec;
82        let value = (42u32, true, vec![1u8, 2, 3]);
83        let encoded = codec.encode(&value).expect("encode");
84        let decoded: (u32, bool, Vec<u8>) = codec.decode(&encoded).expect("decode");
85        assert_eq!(decoded, value);
86    }
87
88    #[test]
89    fn decoding_truncated_input_is_a_codec_error() {
90        let codec = CborCodec;
91        let result: Result<(u32, u32)> = codec.decode(&[]);
92        assert!(matches!(result, Err(Error::Codec(_))));
93    }
94}