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::CborValue;
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) -> Vec<u8> {
24    let mut out = Vec::new();
25    encode_value(value, &mut out);
26    out
27}
28
29fn encode_value(v: &CborValue, out: &mut Vec<u8>) {
30    match v {
31        CborValue::Null => out.push(0xf6),
32        CborValue::Bool(false) => out.push(0xf4),
33        CborValue::Bool(true) => out.push(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            out.extend_from_slice(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            out.extend_from_slice(bytes);
52        }
53
54        CborValue::Array(arr) => {
55            write_header(MT_ARRAY, len_as_u64(arr.len()), out);
56            for v in arr {
57                encode_value(v, out);
58            }
59        }
60
61        CborValue::Map(entries) => {
62            // RFC 8949 core deterministic ordering sorts text keys by the
63            // length of their encoded bytes first, then by bytewise lexical
64            // order. did:me vectors rely on this exact order for stable CIDs.
65            let mut sorted = entries.clone();
66            sorted.sort_by(|(ka, _), (kb, _)| {
67                ka.len()
68                    .cmp(&kb.len())
69                    .then_with(|| ka.as_bytes().cmp(kb.as_bytes()))
70            });
71
72            write_header(MT_MAP, len_as_u64(sorted.len()), out);
73
74            for (k, v) in sorted {
75                let kb = k.as_bytes();
76                write_header(MT_STRING, len_as_u64(kb.len()), out);
77                out.extend_from_slice(kb);
78                encode_value(&v, out);
79            }
80        }
81    }
82}
83
84/// Widens a container length to the `u64` argument width CBOR headers use.
85///
86/// This is a widening conversion — `usize` is at most 64 bits on every
87/// supported target — so it never loses information; the saturating
88/// fallback is unreachable and exists only to keep the conversion
89/// total without an `as` cast or a panic.
90fn len_as_u64(len: usize) -> u64 {
91    u64::try_from(len).unwrap_or(u64::MAX)
92}
93
94/// Writes a CBOR head byte plus the minimal big-endian argument encoding
95/// for `value`, following canonical (shortest-form) integer rules.
96///
97/// Each branch slices the exact low-order bytes of `value.to_be_bytes()`
98/// that its range guarantees are significant, so no narrowing cast or
99/// truncation is involved.
100fn write_header(mt: u8, value: u64, out: &mut Vec<u8>) {
101    let be = value.to_be_bytes();
102    let head = mt << 5;
103    if value < 24 {
104        // The whole argument fits in the low 5 bits of the head byte.
105        out.push(head | be[7]);
106    } else if value < 0x100 {
107        out.push(head | 24);
108        out.extend_from_slice(&be[7..8]);
109    } else if value < 0x1_0000 {
110        out.push(head | 25);
111        out.extend_from_slice(&be[6..8]);
112    } else if value < 0x1_0000_0000 {
113        out.push(head | 26);
114        out.extend_from_slice(&be[4..8]);
115    } else {
116        out.push(head | 27);
117        out.extend_from_slice(&be);
118    }
119}