Skip to main content

codec_cbor/
encode_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};
6
7const MT_UINT: u8 = 0;
8const MT_NEGINT: u8 = 1;
9const MT_BYTES: u8 = 2;
10const MT_STRING: u8 = 3;
11const MT_ARRAY: u8 = 4;
12const MT_MAP: u8 = 5;
13
14/// Encode a value using canonical DAG-CBOR.
15///
16/// This encoding:
17/// - uses definite-length, shortest-form (canonical) integer headers only
18/// - orders map keys by RFC 8949 core deterministic rules: shorter encoded
19///   key first, then bytewise lexical order among equal lengths
20/// - contains no floats, tags, or indefinite-length items
21/// - is deterministic and cryptographically stable, so equal values always
22///   encode to identical bytes (a prerequisite for stable content IDs)
23pub fn encode_dag_cbor(value: &CborValue) -> Result<Vec<u8>, CborError> {
24    let mut out = Vec::new();
25    encode_value(value, &mut out, 0)?;
26    Ok(out)
27}
28
29fn encode_value(v: &CborValue, out: &mut Vec<u8>, depth: usize) -> Result<(), CborError> {
30    match v {
31        CborValue::Null => push_byte(out, 0xf6)?,
32        CborValue::Bool(false) => push_byte(out, 0xf4)?,
33        CborValue::Bool(true) => push_byte(out, 0xf5)?,
34
35        CborValue::Int(n) => {
36            if *n >= 0 {
37                write_header(MT_UINT, n.unsigned_abs(), out)?;
38            } else {
39                write_header(MT_NEGINT, n.unsigned_abs() - 1, out)?;
40            }
41        }
42
43        CborValue::Bytes(b) => {
44            write_header(MT_BYTES, len_as_u64(b.len())?, out)?;
45            extend_bytes(out, b)?;
46        }
47
48        CborValue::String(s) => {
49            let bytes = s.as_bytes();
50            write_header(MT_STRING, len_as_u64(bytes.len())?, out)?;
51            extend_bytes(out, bytes)?;
52        }
53
54        CborValue::Array(arr) => {
55            let child_depth = descend(depth)?;
56            ensure_minimum_encoded_len(arr.len(), 1)?;
57            write_header(MT_ARRAY, len_as_u64(arr.len())?, out)?;
58            for v in arr {
59                encode_value(v, out, child_depth)?;
60            }
61        }
62
63        CborValue::Map(entries) => {
64            let child_depth = descend(depth)?;
65            ensure_minimum_encoded_len(entries.len(), 2)?;
66            // RFC 8949 core deterministic ordering sorts text keys by the
67            // length of their encoded bytes first, then by bytewise lexical
68            // order. did:me vectors rely on this exact order for stable CIDs.
69            let mut sorted: Vec<(&String, &CborValue)> =
70                entries.iter().map(|(key, value)| (key, value)).collect();
71            sorted.sort_by(|(ka, _), (kb, _)| {
72                ka.len()
73                    .cmp(&kb.len())
74                    .then_with(|| ka.as_bytes().cmp(kb.as_bytes()))
75            });
76
77            if sorted
78                .windows(2)
79                .any(|pair| pair[0].0.as_bytes() == pair[1].0.as_bytes())
80            {
81                return Err(CborError::DuplicateMapKey);
82            }
83
84            write_header(MT_MAP, len_as_u64(sorted.len())?, out)?;
85
86            for (k, v) in sorted {
87                let kb = k.as_bytes();
88                write_header(MT_STRING, len_as_u64(kb.len())?, out)?;
89                extend_bytes(out, kb)?;
90                encode_value(v, out, child_depth)?;
91            }
92        }
93    }
94    Ok(())
95}
96
97/// Widens a container length to the `u64` argument width CBOR headers use.
98///
99/// This is a widening conversion — `usize` is at most 64 bits on every
100/// supported target — so it never loses information on supported platforms,
101/// while still returning a typed error if that assumption is violated.
102fn len_as_u64(len: usize) -> Result<u64, CborError> {
103    u64::try_from(len).map_err(|_| CborError::LengthTooLarge)
104}
105
106/// Writes a CBOR head byte plus the minimal big-endian argument encoding
107/// for `value`, following canonical (shortest-form) integer rules.
108///
109/// Each branch slices the exact low-order bytes of `value.to_be_bytes()`
110/// that its range guarantees are significant, so no narrowing cast or
111/// truncation is involved.
112fn write_header(mt: u8, value: u64, out: &mut Vec<u8>) -> Result<(), CborError> {
113    let be = value.to_be_bytes();
114    let head = mt << 5;
115    if value < 24 {
116        // The whole argument fits in the low 5 bits of the head byte.
117        push_byte(out, head | be[7])?;
118    } else if value < 0x100 {
119        push_byte(out, head | 24)?;
120        extend_bytes(out, &be[7..8])?;
121    } else if value < 0x1_0000 {
122        push_byte(out, head | 25)?;
123        extend_bytes(out, &be[6..8])?;
124    } else if value < 0x1_0000_0000 {
125        push_byte(out, head | 26)?;
126        extend_bytes(out, &be[4..8])?;
127    } else {
128        push_byte(out, head | 27)?;
129        extend_bytes(out, &be)?;
130    }
131    Ok(())
132}
133
134fn push_byte(out: &mut Vec<u8>, byte: u8) -> Result<(), CborError> {
135    let next_len = out.len().checked_add(1).ok_or(CborError::OffsetOverflow)?;
136    if next_len > MAX_DAG_CBOR_INPUT_LEN {
137        return Err(CborError::OutputTooLarge);
138    }
139    out.push(byte);
140    Ok(())
141}
142
143fn extend_bytes(out: &mut Vec<u8>, bytes: &[u8]) -> Result<(), CborError> {
144    let next_len = out
145        .len()
146        .checked_add(bytes.len())
147        .ok_or(CborError::OffsetOverflow)?;
148    if next_len > MAX_DAG_CBOR_INPUT_LEN {
149        return Err(CborError::OutputTooLarge);
150    }
151    out.extend_from_slice(bytes);
152    Ok(())
153}
154
155fn descend(depth: usize) -> Result<usize, CborError> {
156    let next = depth.checked_add(1).ok_or(CborError::OffsetOverflow)?;
157    if next > MAX_NESTING_DEPTH {
158        return Err(CborError::DepthExceeded);
159    }
160    Ok(next)
161}
162
163fn ensure_minimum_encoded_len(count: usize, min_element_len: usize) -> Result<(), CborError> {
164    let minimum = count
165        .checked_mul(min_element_len)
166        .ok_or(CborError::OffsetOverflow)?;
167    if minimum > MAX_DAG_CBOR_INPUT_LEN {
168        return Err(CborError::OutputTooLarge);
169    }
170    Ok(())
171}