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