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.iter().map(|b| Value::Number(Number::from(*b))).collect(),
225            ))
226        }
227        3 => {
228            let len = reader
229                .read_len(additional)?
230                .ok_or_else(|| CborError("invalid CBOR text string".into()))?
231                as usize;
232            if len > options.max_byte_length {
233                return Err(CborError("CBOR string exceeds max byte length".into()));
234            }
235            let bytes = reader.take(len)?;
236            let text = std::str::from_utf8(bytes)
237                .map_err(|_| CborError("invalid UTF-8 in CBOR text string".into()))?;
238            Ok(Value::String(text.to_string()))
239        }
240        4 => {
241            let len = reader
242                .read_len(additional)?
243                .ok_or_else(|| CborError("invalid CBOR array".into()))?
244                as usize;
245            if len > options.max_container_length {
246                return Err(CborError("CBOR array exceeds max container length".into()));
247            }
248            let mut items = Vec::with_capacity(len.min(4096));
249            for _ in 0..len {
250                items.push(decode_value(reader, options, depth + 1)?);
251            }
252            Ok(Value::Array(items))
253        }
254        5 => {
255            let len = reader
256                .read_len(additional)?
257                .ok_or_else(|| CborError("invalid CBOR map".into()))?
258                as usize;
259            if len > options.max_container_length {
260                return Err(CborError("CBOR map exceeds max container length".into()));
261            }
262            let mut map = Map::new();
263            for _ in 0..len {
264                let key = decode_value(reader, options, depth + 1)?;
265                let Value::String(key) = key else {
266                    return Err(CborError("CBOR map key must be a text string".into()));
267                };
268                let value = decode_value(reader, options, depth + 1)?;
269                map.insert(key, value);
270            }
271            Ok(Value::Object(map))
272        }
273        6 => {
274            // Tag: ignore, decode the tagged value.
275            let _ = reader.read_len(additional)?;
276            decode_value(reader, options, depth)
277        }
278        7 => match additional {
279            20 => Ok(Value::Bool(false)),
280            21 => Ok(Value::Bool(true)),
281            22 => Ok(Value::Null),
282            26 => {
283                let b = reader.take(4)?;
284                let f = f32::from_be_bytes([b[0], b[1], b[2], b[3]]) as f64;
285                Ok(Number::from_f64(f).map(Value::Number).unwrap_or(Value::Null))
286            }
287            27 => {
288                let b = reader.take(8)?;
289                let f = f64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]);
290                Ok(Number::from_f64(f).map(Value::Number).unwrap_or(Value::Null))
291            }
292            _ => Err(CborError("unsupported CBOR simple value".into())),
293        },
294        _ => Err(CborError("unsupported CBOR major type".into())),
295    }
296}
297
298/// Decode CBOR bytes into a JSON value.
299pub fn decode(bytes: &[u8], options: &CborOptions) -> Result<Value, CborError> {
300    if bytes.len() > options.max_byte_length {
301        return Err(CborError("CBOR payload exceeds max byte length".into()));
302    }
303    let mut reader = Reader { bytes, pos: 0 };
304    let value = decode_value(&mut reader, options, 0)?;
305    if reader.pos != bytes.len() {
306        return Err(CborError("trailing bytes after CBOR value".into()));
307    }
308    Ok(value)
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use serde_json::json;
315
316    fn round_trip(value: Value) {
317        let options = CborOptions::default();
318        let bytes = encode(&value, &options).unwrap();
319        let decoded = decode(&bytes, &options).unwrap();
320        assert_eq!(decoded, value);
321    }
322
323    #[test]
324    fn round_trips_scalars() {
325        round_trip(json!(null));
326        round_trip(json!(true));
327        round_trip(json!(false));
328        round_trip(json!(0));
329        round_trip(json!(23));
330        round_trip(json!(24));
331        round_trip(json!(300));
332        round_trip(json!(70000));
333        round_trip(json!(5_000_000_000i64));
334        round_trip(json!(-1));
335        round_trip(json!(-300));
336        round_trip(json!("hello"));
337        round_trip(json!(1.5));
338    }
339
340    #[test]
341    fn round_trips_containers() {
342        round_trip(json!([1, "two", [3, 4], { "a": true }]));
343        round_trip(json!({ "type": "hello", "nested": { "x": [1, 2, 3] } }));
344    }
345
346    #[test]
347    fn rejects_deep_nesting() {
348        let options = CborOptions {
349            max_depth: 3,
350            ..Default::default()
351        };
352        let deep = json!([[[[1]]]]);
353        assert!(encode(&deep, &options).is_err());
354    }
355
356    #[test]
357    fn rejects_trailing_bytes() {
358        let options = CborOptions::default();
359        let mut bytes = encode(&json!(1), &options).unwrap();
360        bytes.push(0);
361        assert!(decode(&bytes, &options).is_err());
362    }
363
364    #[test]
365    fn rejects_truncated() {
366        let options = CborOptions::default();
367        let bytes = encode(&json!("abcdef"), &options).unwrap();
368        assert!(decode(&bytes[..3], &options).is_err());
369    }
370}