Skip to main content

rpi_cli/remote/
cbor.rs

1//! Strict CBOR codec for protocol values.
2//!
3//! Port of native Pi's `packages/protocol/src/cbor/`. The protocol only ever
4//! carries JSON-shaped values (null, bool, number, string, array, object), so
5//! this codec implements exactly that subset of CBOR (RFC 8949) with the
6//! defensive limits native uses for untrusted payloads:
7//!
8//! * a maximum encoded byte length / string length ([`DEFAULT_MAX_CBOR_BYTE_LENGTH`]),
9//! * a maximum container length ([`DEFAULT_MAX_CBOR_CONTAINER_LENGTH`]), and
10//! * a maximum nesting depth ([`DEFAULT_MAX_CBOR_DEPTH`]).
11
12use std::fmt;
13
14use serde_json::{Map, Number, Value};
15
16/// Native `DEFAULT_MAX_CBOR_BYTE_LENGTH` (16 MiB).
17pub const DEFAULT_MAX_CBOR_BYTE_LENGTH: usize = 16 * 1024 * 1024;
18/// Native `DEFAULT_MAX_CBOR_CONTAINER_LENGTH`.
19pub const DEFAULT_MAX_CBOR_CONTAINER_LENGTH: usize = 1_000_000;
20/// Native `DEFAULT_MAX_CBOR_DEPTH`.
21pub const DEFAULT_MAX_CBOR_DEPTH: usize = 64;
22
23/// CBOR error.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct CborError(pub String);
26
27impl fmt::Display for CborError {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        f.write_str(&self.0)
30    }
31}
32
33impl std::error::Error for CborError {}
34
35/// Codec limits (native `ResolvedCborOptions`).
36#[derive(Debug, Clone, Copy)]
37pub struct CborOptions {
38    pub max_byte_length: usize,
39    pub max_container_length: usize,
40    pub max_depth: usize,
41}
42
43impl Default for CborOptions {
44    fn default() -> Self {
45        Self {
46            max_byte_length: DEFAULT_MAX_CBOR_BYTE_LENGTH,
47            max_container_length: DEFAULT_MAX_CBOR_CONTAINER_LENGTH,
48            max_depth: DEFAULT_MAX_CBOR_DEPTH,
49        }
50    }
51}
52
53fn write_head(out: &mut Vec<u8>, major: u8, value: u64) {
54    let m = major << 5;
55    if value < 24 {
56        out.push(m | value as u8);
57    } else if value <= u8::MAX as u64 {
58        out.push(m | 24);
59        out.push(value as u8);
60    } else if value <= u16::MAX as u64 {
61        out.push(m | 25);
62        out.extend_from_slice(&(value as u16).to_be_bytes());
63    } else if value <= u32::MAX as u64 {
64        out.push(m | 26);
65        out.extend_from_slice(&(value as u32).to_be_bytes());
66    } else {
67        out.push(m | 27);
68        out.extend_from_slice(&value.to_be_bytes());
69    }
70}
71
72fn encode_value(
73    value: &Value,
74    out: &mut Vec<u8>,
75    options: &CborOptions,
76    depth: usize,
77) -> Result<(), CborError> {
78    if depth > options.max_depth {
79        return Err(CborError("CBOR nesting depth exceeded".into()));
80    }
81    match value {
82        Value::Null => out.push(0xf6),
83        Value::Bool(false) => out.push(0xf4),
84        Value::Bool(true) => out.push(0xf5),
85        Value::Number(n) => {
86            if let Some(u) = n.as_u64() {
87                write_head(out, 0, u);
88            } else if let Some(i) = n.as_i64() {
89                // Negative: major 1 encodes -1 - value.
90                write_head(out, 1, (-(i as i128) - 1) as u64);
91            } else if let Some(f) = n.as_f64() {
92                out.push(0xfb);
93                out.extend_from_slice(&f.to_be_bytes());
94            } else {
95                return Err(CborError("unsupported number".into()));
96            }
97        }
98        Value::String(s) => {
99            let bytes = s.as_bytes();
100            if bytes.len() > options.max_byte_length {
101                return Err(CborError("CBOR string exceeds max byte length".into()));
102            }
103            write_head(out, 3, bytes.len() as u64);
104            out.extend_from_slice(bytes);
105        }
106        Value::Array(items) => {
107            if items.len() > options.max_container_length {
108                return Err(CborError("CBOR array exceeds max container length".into()));
109            }
110            write_head(out, 4, items.len() as u64);
111            for item in items {
112                encode_value(item, out, options, depth + 1)?;
113            }
114        }
115        Value::Object(map) => {
116            if map.len() > options.max_container_length {
117                return Err(CborError("CBOR map exceeds max container length".into()));
118            }
119            write_head(out, 5, map.len() as u64);
120            for (key, item) in map {
121                if key.len() > options.max_byte_length {
122                    return Err(CborError("CBOR string exceeds max byte length".into()));
123                }
124                write_head(out, 3, key.len() as u64);
125                out.extend_from_slice(key.as_bytes());
126                encode_value(item, out, options, depth + 1)?;
127            }
128        }
129    }
130    Ok(())
131}
132
133/// Encode a JSON value to CBOR bytes.
134pub fn encode(value: &Value, options: &CborOptions) -> Result<Vec<u8>, CborError> {
135    let mut out = Vec::new();
136    encode_value(value, &mut out, options, 0)?;
137    if out.len() > options.max_byte_length {
138        return Err(CborError("CBOR payload exceeds max byte length".into()));
139    }
140    Ok(out)
141}
142
143struct Reader<'a> {
144    bytes: &'a [u8],
145    pos: usize,
146}
147
148impl<'a> Reader<'a> {
149    fn take(&mut self, n: usize) -> Result<&'a [u8], CborError> {
150        if self.pos + n > self.bytes.len() {
151            return Err(CborError("unexpected end of CBOR input".into()));
152        }
153        let slice = &self.bytes[self.pos..self.pos + n];
154        self.pos += n;
155        Ok(slice)
156    }
157
158    fn u8(&mut self) -> Result<u8, CborError> {
159        Ok(self.take(1)?[0])
160    }
161
162    fn read_len(&mut self, additional: u8) -> Result<Option<u64>, CborError> {
163        match additional {
164            0..=23 => Ok(Some(additional as u64)),
165            24 => Ok(Some(self.u8()? as u64)),
166            25 => {
167                let b = self.take(2)?;
168                Ok(Some(u16::from_be_bytes([b[0], b[1]]) as u64))
169            }
170            26 => {
171                let b = self.take(4)?;
172                Ok(Some(u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as u64))
173            }
174            27 => {
175                let b = self.take(8)?;
176                Ok(Some(u64::from_be_bytes([
177                    b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
178                ])))
179            }
180            _ => Ok(None),
181        }
182    }
183}
184
185fn decode_value(
186    reader: &mut Reader<'_>,
187    options: &CborOptions,
188    depth: usize,
189) -> Result<Value, CborError> {
190    if depth > options.max_depth {
191        return Err(CborError("CBOR nesting depth exceeded".into()));
192    }
193    let initial = reader.u8()?;
194    let major = initial >> 5;
195    let additional = initial & 0x1f;
196    match major {
197        0 => {
198            let n = reader
199                .read_len(additional)?
200                .ok_or_else(|| CborError("invalid CBOR integer".into()))?;
201            Ok(Value::Number(Number::from(n)))
202        }
203        1 => {
204            let n = reader
205                .read_len(additional)?
206                .ok_or_else(|| CborError("invalid CBOR integer".into()))?;
207            if n > i64::MAX as u64 {
208                return Err(CborError("CBOR negative integer out of range".into()));
209            }
210            Ok(Value::Number(Number::from(-(n as i64) - 1)))
211        }
212        2 => {
213            // Byte string: represent as an array of numbers (protocol values
214            // never carry raw bytes; this keeps the codec total).
215            let len = reader
216                .read_len(additional)?
217                .ok_or_else(|| CborError("invalid CBOR byte string".into()))?
218                as usize;
219            if len > options.max_byte_length {
220                return Err(CborError("CBOR string exceeds max byte length".into()));
221            }
222            let bytes = reader.take(len)?;
223            Ok(Value::Array(
224                bytes
225                    .iter()
226                    .map(|b| Value::Number(Number::from(*b)))
227                    .collect(),
228            ))
229        }
230        3 => {
231            let len = reader
232                .read_len(additional)?
233                .ok_or_else(|| CborError("invalid CBOR text string".into()))?
234                as usize;
235            if len > options.max_byte_length {
236                return Err(CborError("CBOR string exceeds max byte length".into()));
237            }
238            let bytes = reader.take(len)?;
239            let text = std::str::from_utf8(bytes)
240                .map_err(|_| CborError("invalid UTF-8 in CBOR text string".into()))?;
241            Ok(Value::String(text.to_string()))
242        }
243        4 => {
244            let len = reader
245                .read_len(additional)?
246                .ok_or_else(|| CborError("invalid CBOR array".into()))?
247                as usize;
248            if len > options.max_container_length {
249                return Err(CborError("CBOR array exceeds max container length".into()));
250            }
251            let mut items = Vec::with_capacity(len.min(4096));
252            for _ in 0..len {
253                items.push(decode_value(reader, options, depth + 1)?);
254            }
255            Ok(Value::Array(items))
256        }
257        5 => {
258            let len = reader
259                .read_len(additional)?
260                .ok_or_else(|| CborError("invalid CBOR map".into()))?
261                as usize;
262            if len > options.max_container_length {
263                return Err(CborError("CBOR map exceeds max container length".into()));
264            }
265            let mut map = Map::new();
266            for _ in 0..len {
267                let key = decode_value(reader, options, depth + 1)?;
268                let Value::String(key) = key else {
269                    return Err(CborError("CBOR map key must be a text string".into()));
270                };
271                let value = decode_value(reader, options, depth + 1)?;
272                map.insert(key, value);
273            }
274            Ok(Value::Object(map))
275        }
276        6 => {
277            // Tag: ignore, decode the tagged value.
278            let _ = reader.read_len(additional)?;
279            decode_value(reader, options, depth)
280        }
281        7 => match additional {
282            20 => Ok(Value::Bool(false)),
283            21 => Ok(Value::Bool(true)),
284            22 => Ok(Value::Null),
285            26 => {
286                let b = reader.take(4)?;
287                let f = f32::from_be_bytes([b[0], b[1], b[2], b[3]]) as f64;
288                Ok(Number::from_f64(f)
289                    .map(Value::Number)
290                    .unwrap_or(Value::Null))
291            }
292            27 => {
293                let b = reader.take(8)?;
294                let f = f64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]);
295                Ok(Number::from_f64(f)
296                    .map(Value::Number)
297                    .unwrap_or(Value::Null))
298            }
299            _ => Err(CborError("unsupported CBOR simple value".into())),
300        },
301        _ => Err(CborError("unsupported CBOR major type".into())),
302    }
303}
304
305/// Decode CBOR bytes into a JSON value.
306pub fn decode(bytes: &[u8], options: &CborOptions) -> Result<Value, CborError> {
307    if bytes.len() > options.max_byte_length {
308        return Err(CborError("CBOR payload exceeds max byte length".into()));
309    }
310    let mut reader = Reader { bytes, pos: 0 };
311    let value = decode_value(&mut reader, options, 0)?;
312    if reader.pos != bytes.len() {
313        return Err(CborError("trailing bytes after CBOR value".into()));
314    }
315    Ok(value)
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use serde_json::json;
322
323    fn round_trip(value: Value) {
324        let options = CborOptions::default();
325        let bytes = encode(&value, &options).unwrap();
326        let decoded = decode(&bytes, &options).unwrap();
327        assert_eq!(decoded, value);
328    }
329
330    #[test]
331    fn round_trips_scalars() {
332        round_trip(json!(null));
333        round_trip(json!(true));
334        round_trip(json!(false));
335        round_trip(json!(0));
336        round_trip(json!(23));
337        round_trip(json!(24));
338        round_trip(json!(300));
339        round_trip(json!(70000));
340        round_trip(json!(5_000_000_000i64));
341        round_trip(json!(-1));
342        round_trip(json!(-300));
343        round_trip(json!("hello"));
344        round_trip(json!(1.5));
345    }
346
347    #[test]
348    fn round_trips_containers() {
349        round_trip(json!([1, "two", [3, 4], { "a": true }]));
350        round_trip(json!({ "type": "hello", "nested": { "x": [1, 2, 3] } }));
351    }
352
353    #[test]
354    fn rejects_deep_nesting() {
355        let options = CborOptions {
356            max_depth: 3,
357            ..Default::default()
358        };
359        let deep = json!([[[[1]]]]);
360        assert!(encode(&deep, &options).is_err());
361    }
362
363    #[test]
364    fn rejects_trailing_bytes() {
365        let options = CborOptions::default();
366        let mut bytes = encode(&json!(1), &options).unwrap();
367        bytes.push(0);
368        assert!(decode(&bytes, &options).is_err());
369    }
370
371    #[test]
372    fn rejects_truncated() {
373        let options = CborOptions::default();
374        let bytes = encode(&json!("abcdef"), &options).unwrap();
375        assert!(decode(&bytes[..3], &options).is_err());
376    }
377}