Skip to main content

codec_cbor/
decode_dag_cbor.rs

1// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5use crate::{CborError, CborValue, MAX_DAG_CBOR_INPUT_LEN, MAX_NESTING_DEPTH};
6use std::cmp::Ordering;
7use std::str;
8use zeroize::Zeroizing;
9
10const MT_UINT: u8 = 0;
11const MT_NEGINT: u8 = 1;
12const MT_BYTES: u8 = 2;
13const MT_STRING: u8 = 3;
14const MT_ARRAY: u8 = 4;
15const MT_MAP: u8 = 5;
16const CONTAINER_INITIAL_RESERVE: usize = 8;
17
18/// Smallest possible encoding of one array element or map key/value: a
19/// single header byte (e.g. a small integer, or an empty string/array).
20/// Used to reject a declared container length that the remaining input
21/// could never satisfy, before any capacity is reserved.
22const MIN_ELEMENT_ENCODED_LEN: usize = 1;
23
24/// Decode canonical DAG-CBOR bytes into a CborValue.
25///
26/// This decoder is intentionally strict:
27/// - rejects non-canonical encodings
28/// - rejects floats, tags, and indefinite-length items
29/// - enforces UTF-8 string map keys
30/// - enforces canonical map key ordering
31///
32/// Suitable only for cryptographic / authoritative CBOR.
33pub fn decode_dag_cbor(bytes: &[u8]) -> Result<CborValue, CborError> {
34    if bytes.len() > MAX_DAG_CBOR_INPUT_LEN {
35        return Err(CborError::InputTooLarge);
36    }
37    let (value, offset) = decode_value(bytes, 0, 0)?;
38    let mut value = Zeroizing::new(value);
39    if offset != bytes.len() {
40        return Err(CborError::TrailingBytes);
41    }
42    Ok(core::mem::replace(&mut *value, CborValue::Null))
43}
44
45/// `depth` is the number of array/map containers currently open. It is
46/// checked against [`MAX_NESTING_DEPTH`] before descending so a
47/// pathologically nested input cannot overflow the stack.
48fn decode_value(
49    bytes: &[u8],
50    mut offset: usize,
51    depth: usize,
52) -> Result<(CborValue, usize), CborError> {
53    if offset >= bytes.len() {
54        return Err(CborError::UnexpectedEnd);
55    }
56
57    let first = bytes[offset];
58    offset = offset.checked_add(1).ok_or(CborError::OffsetOverflow)?;
59
60    let major = first >> 5;
61    let ai = first & 0x1f;
62
63    // Floating-point payload bits are caller data, not simple-value codes.
64    // Reject the type before reading them so typed errors cannot retain a
65    // private number or arbitrary eight-byte payload in their metadata.
66    if major == 7 && (25..=27).contains(&ai) {
67        return Err(CborError::DisallowedSimpleValue {
68            value: u64::from(ai),
69        });
70    }
71
72    let (arg, new_offset) = read_argument(bytes, offset, ai)?;
73    offset = new_offset;
74
75    match major {
76        MT_UINT => Ok((
77            CborValue::Int(i64::try_from(arg).map_err(|_| CborError::IntegerOutOfRange)?),
78            offset,
79        )),
80
81        MT_NEGINT => {
82            let magnitude = i128::from(arg);
83            let value = (-1_i128)
84                .checked_sub(magnitude)
85                .ok_or(CborError::IntegerOutOfRange)?;
86            Ok((
87                CborValue::Int(i64::try_from(value).map_err(|_| CborError::IntegerOutOfRange)?),
88                offset,
89            ))
90        }
91
92        MT_BYTES => {
93            let (b, off) = extract_bytes(bytes, offset, arg)?;
94            Ok((CborValue::Bytes(b), off))
95        }
96
97        MT_STRING => {
98            let (s, off) = extract_string(bytes, offset, arg)?;
99            Ok((CborValue::String(s), off))
100        }
101
102        MT_ARRAY => {
103            let child_depth = descend(depth)?;
104            let item_count = usize::try_from(arg).map_err(|_| CborError::LengthTooLarge)?;
105            // Reserve only what the remaining input could actually contain.
106            // Each element occupies at least one byte, so a count larger
107            // than the bytes left is a malformed length prefix and must be
108            // rejected before allocating (prevents OOM from a crafted
109            // header such as `9B 7F FF …`).
110            bounded_capacity(item_count, bytes.len(), offset)?;
111            let capacity = initial_container_capacity(item_count);
112            let mut items = Zeroizing::new(Vec::with_capacity(capacity));
113            let mut off = offset;
114            for _ in 0..item_count {
115                let (v, next) = decode_value(bytes, off, child_depth)?;
116                items.push(v);
117                off = next;
118            }
119            Ok((CborValue::Array(core::mem::take(&mut *items)), off))
120        }
121
122        MT_MAP => {
123            let child_depth = descend(depth)?;
124            let entry_count = usize::try_from(arg).map_err(|_| CborError::LengthTooLarge)?;
125            // Each entry is a key plus a value, so it needs at least two
126            // bytes; bound the reservation against that before allocating.
127            let entry_min = MIN_ELEMENT_ENCODED_LEN
128                .checked_mul(2)
129                .ok_or(CborError::OffsetOverflow)?;
130            bounded_capacity_with_min(entry_count, bytes.len(), offset, entry_min)?;
131            let capacity = initial_container_capacity(entry_count);
132            let mut entries = Zeroizing::new(Vec::with_capacity(capacity));
133            let mut off = offset;
134            let mut last_key_bytes: Option<Zeroizing<Vec<u8>>> = None;
135
136            for _ in 0..entry_count {
137                let (key_val, key_off) = decode_value(bytes, off, child_depth)?;
138                off = key_off;
139
140                let mut key_val = Zeroizing::new(key_val);
141                let mut key = Zeroizing::new(match &mut *key_val {
142                    CborValue::String(s) => core::mem::take(s),
143                    _ => return Err(CborError::MapKeyMustBeString),
144                });
145
146                let key_bytes = Zeroizing::new(key.as_bytes().to_vec());
147                if let Some(prev) = &last_key_bytes {
148                    match compare_bytes(prev, &key_bytes) {
149                        Ordering::Less => {}
150                        Ordering::Equal => return Err(CborError::DuplicateMapKey),
151                        Ordering::Greater => return Err(CborError::MapKeysOutOfOrder),
152                    }
153                }
154                last_key_bytes = Some(key_bytes);
155
156                let (val, val_off) = decode_value(bytes, off, child_depth)?;
157                off = val_off;
158
159                entries.push((core::mem::take(&mut *key), val));
160            }
161
162            Ok((CborValue::Map(core::mem::take(&mut *entries)), off))
163        }
164
165        7 => match arg {
166            20 => Ok((CborValue::Bool(false), offset)),
167            21 => Ok((CborValue::Bool(true), offset)),
168            22 => Ok((CborValue::Null, offset)),
169            _ => Err(CborError::DisallowedSimpleValue { value: arg }),
170        },
171
172        _ => Err(CborError::DisallowedMajorType { major }),
173    }
174}
175
176fn read_argument(bytes: &[u8], offset: usize, ai: u8) -> Result<(u64, usize), CborError> {
177    match ai {
178        n @ 0..=23 => Ok((u64::from(n), offset)),
179
180        24 => {
181            let end = checked_end(offset, 1)?;
182            if end > bytes.len() {
183                return Err(CborError::TruncatedArgument);
184            }
185            let value = u64::from(bytes[offset]);
186            if value < 24 {
187                return Err(CborError::NonCanonicalInteger);
188            }
189            Ok((value, end))
190        }
191
192        25 => {
193            let end = checked_end(offset, 2)?;
194            if end > bytes.len() {
195                return Err(CborError::TruncatedArgument);
196            }
197            let val = u16::from_be_bytes([bytes[offset], bytes[offset + 1]]);
198            if val < 256 {
199                return Err(CborError::NonCanonicalInteger);
200            }
201            Ok((u64::from(val), end))
202        }
203
204        26 => {
205            let end = checked_end(offset, 4)?;
206            if end > bytes.len() {
207                return Err(CborError::TruncatedArgument);
208            }
209            let val = u32::from_be_bytes([
210                bytes[offset],
211                bytes[offset + 1],
212                bytes[offset + 2],
213                bytes[offset + 3],
214            ]);
215            if val < 65536 {
216                return Err(CborError::NonCanonicalInteger);
217            }
218            Ok((u64::from(val), end))
219        }
220
221        27 => {
222            let end = checked_end(offset, 8)?;
223            if end > bytes.len() {
224                return Err(CborError::TruncatedArgument);
225            }
226            let val = u64::from_be_bytes([
227                bytes[offset],
228                bytes[offset + 1],
229                bytes[offset + 2],
230                bytes[offset + 3],
231                bytes[offset + 4],
232                bytes[offset + 5],
233                bytes[offset + 6],
234                bytes[offset + 7],
235            ]);
236            if val < 0x1_0000_0000 {
237                return Err(CborError::NonCanonicalInteger);
238            }
239            Ok((val, end))
240        }
241
242        _ => Err(CborError::UnsupportedAdditionalInfo),
243    }
244}
245
246fn extract_bytes(bytes: &[u8], offset: usize, len: u64) -> Result<(Vec<u8>, usize), CborError> {
247    let len = usize::try_from(len).map_err(|_| CborError::LengthTooLarge)?;
248    let end = checked_end(offset, len)?;
249    if end > bytes.len() {
250        return Err(CborError::TruncatedBytes);
251    }
252    Ok((bytes[offset..end].to_vec(), end))
253}
254
255fn extract_string(bytes: &[u8], offset: usize, len: u64) -> Result<(String, usize), CborError> {
256    let len = usize::try_from(len).map_err(|_| CborError::LengthTooLarge)?;
257    let end = checked_end(offset, len)?;
258    let raw = bytes.get(offset..end).ok_or(CborError::TruncatedBytes)?;
259    let s = str::from_utf8(raw).map_err(|_| CborError::InvalidUtf8)?;
260    Ok((s.to_string(), end))
261}
262
263fn checked_end(offset: usize, len: usize) -> Result<usize, CborError> {
264    offset.checked_add(len).ok_or(CborError::OffsetOverflow)
265}
266
267/// Enters one nesting level, rejecting input that would exceed
268/// [`MAX_NESTING_DEPTH`].
269fn descend(depth: usize) -> Result<usize, CborError> {
270    let next = depth.checked_add(1).ok_or(CborError::OffsetOverflow)?;
271    if next > MAX_NESTING_DEPTH {
272        return Err(CborError::DepthExceeded);
273    }
274    Ok(next)
275}
276
277/// Capacity to reserve for a container of `count` elements, each at least
278/// [`MIN_ELEMENT_ENCODED_LEN`] bytes.
279fn bounded_capacity(count: usize, total_len: usize, offset: usize) -> Result<usize, CborError> {
280    bounded_capacity_with_min(count, total_len, offset, MIN_ELEMENT_ENCODED_LEN)
281}
282
283/// Rejects a declared element `count` that could not fit in the bytes
284/// remaining after `offset`, then returns that count as the reservation
285/// size. Because every element needs at least `min_element_len` bytes, a
286/// count exceeding `remaining / min_element_len` is provably malformed, so
287/// this both prevents OOM aborts and reserves an exact, honest capacity.
288fn bounded_capacity_with_min(
289    count: usize,
290    total_len: usize,
291    offset: usize,
292    min_element_len: usize,
293) -> Result<usize, CborError> {
294    let remaining = total_len.saturating_sub(offset);
295    let max_possible = remaining / min_element_len.max(1);
296    if count > max_possible {
297        return Err(CborError::ContainerLengthExceedsInput);
298    }
299    Ok(count)
300}
301
302fn initial_container_capacity(count: usize) -> usize {
303    // The declared element count is only a promise by untrusted input. Reserving
304    // it eagerly lets each nested ancestor retain a large allocation before the
305    // decoder discovers the promised siblings are absent. Start small and let
306    // Vec grow only for elements that have actually been decoded.
307    count.min(CONTAINER_INITIAL_RESERVE)
308}
309
310fn compare_bytes(a: &[u8], b: &[u8]) -> Ordering {
311    match a.len().cmp(&b.len()) {
312        Ordering::Equal => a.cmp(b),
313        ordering => ordering,
314    }
315}