Skip to main content

codec_cbor/
decode_dag_cbor.rs

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