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