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}