didbot_data/dag_cbor/mod.rs
1//! The DAG-CBOR encoding, which is the canonical form a record is hashed in.
2//!
3//! CBOR by itself has many encodings of the same value: an integer can be
4//! written in five widths, an array can be indefinite-length, a map can be in
5//! any order. DAG-CBOR is the profile that removes all of that, and a record's
6//! identity is the sha-256 of these bytes, so "several encodings" would mean
7//! "several identities for one record".
8//!
9//! The rules, all of which are enforced here rather than left to a caller:
10//!
11//! * **Shortest-form counts.** A length or an integer is written in the
12//! smallest of the five widths that holds it. 23 is one byte, never two.
13//! * **Definite lengths only.** Every string, list and map declares its length
14//! up front. The indefinite-length forms are not emitted.
15//! * **Map keys are sorted length-first**, then bytewise within a length. This
16//! is the ordering the atproto fixtures use and it is *not* the ordering a
17//! `BTreeMap<String, _>` iterates in — `integer` sorts before `unicode`
18//! either way, but `bool` sorts before `array` only under this rule. Sorting
19//! is done here, at the encoder, so that no caller can hold a map in the
20//! wrong order to begin with.
21//! * **No floats.** [`Value`] has no float variant; see its documentation.
22//! * **Links are tag 42**, carrying a byte string that is a zero byte — the
23//! identity multibase prefix — followed by the CID's binary form.
24//!
25//! Encoding cannot fail. Everything that could go wrong went wrong earlier, in
26//! [`Value::from_json`], and a [`Value`] that exists has an encoding by
27//! construction.
28//!
29//! # The other direction
30//!
31//! [`decode`] reads a block back, and it is the harder half: an encoder has
32//! to emit the canonical form, while a decoder has to refuse every other one.
33//! It exists because a Merkle search tree that arrived from somewhere else —
34//! in a CAR file, in a firehose frame — is a pile of blocks, and reading it
35//! means reading them. See [`decode`] for what it refuses and why each
36//! refusal matters.
37
38mod read;
39
40pub use read::{decode, CborError, MAX_DEPTH};
41
42use crate::cid::Cid;
43use crate::value::Value;
44
45/// CBOR major type 0: a non-negative integer.
46const MAJOR_UNSIGNED: u8 = 0;
47/// CBOR major type 1: a negative integer, stored as `-1 - n`.
48const MAJOR_NEGATIVE: u8 = 1;
49/// CBOR major type 2: a byte string.
50const MAJOR_BYTES: u8 = 2;
51/// CBOR major type 3: a UTF-8 string.
52const MAJOR_TEXT: u8 = 3;
53/// CBOR major type 4: a list.
54const MAJOR_LIST: u8 = 4;
55/// CBOR major type 5: a map.
56const MAJOR_MAP: u8 = 5;
57/// CBOR major type 6: a tag.
58const MAJOR_TAG: u8 = 6;
59/// CBOR major type 7: the simple values, of which DAG-CBOR uses three.
60const MAJOR_SIMPLE: u8 = 7;
61
62/// The tag that marks a link. 42 is the IPLD CID tag, and it is the only tag
63/// DAG-CBOR permits.
64const TAG_LINK: u64 = 42;
65
66/// The multibase prefix inside a link's byte string: identity, meaning the
67/// bytes are the CID itself and not a text encoding of it.
68const IDENTITY_MULTIBASE: u8 = 0x00;
69
70/// Encodes a value into its one DAG-CBOR form.
71///
72/// ```
73/// use didbot_data::{dag_cbor, Value};
74/// let value = Value::from_json(&serde_json::json!({})).unwrap();
75/// assert_eq!(dag_cbor::encode(&value), vec![0xa0]);
76/// ```
77pub fn encode(value: &Value) -> Vec<u8> {
78 let mut out = Vec::new();
79 write(value, &mut out);
80 out
81}
82
83/// Encodes a value and returns the CID naming that block.
84///
85/// This is the pairing the rest of atproto is built on, and keeping it in one
86/// function is deliberate: a CID over anything but the canonical encoding is
87/// a number that looks like an identity and is not one.
88pub fn cid(value: &Value) -> Cid {
89 Cid::of_dag_cbor(&encode(value))
90}
91
92/// Writes one value.
93fn write(value: &Value, out: &mut Vec<u8>) {
94 match value {
95 Value::Null => out.push(header_byte(MAJOR_SIMPLE, 22)),
96 Value::Bool(false) => out.push(header_byte(MAJOR_SIMPLE, 20)),
97 Value::Bool(true) => out.push(header_byte(MAJOR_SIMPLE, 21)),
98 Value::Integer(number) if *number >= 0 => {
99 header(MAJOR_UNSIGNED, *number as u64, out);
100 }
101 // A negative integer is stored as its magnitude minus one, so that
102 // -1 costs one byte rather than the eight a sign bit would.
103 Value::Integer(number) => header(MAJOR_NEGATIVE, (-1 - *number) as u64, out),
104 Value::String(text) => {
105 header(MAJOR_TEXT, text.len() as u64, out);
106 out.extend_from_slice(text.as_bytes());
107 }
108 Value::Bytes(bytes) => {
109 header(MAJOR_BYTES, bytes.len() as u64, out);
110 out.extend_from_slice(bytes);
111 }
112 Value::Link(cid) => {
113 header(MAJOR_TAG, TAG_LINK, out);
114 header(MAJOR_BYTES, cid.as_bytes().len() as u64 + 1, out);
115 out.push(IDENTITY_MULTIBASE);
116 out.extend_from_slice(cid.as_bytes());
117 }
118 Value::List(items) => {
119 header(MAJOR_LIST, items.len() as u64, out);
120 for item in items {
121 write(item, out);
122 }
123 }
124 Value::Map(entries) => {
125 header(MAJOR_MAP, entries.len() as u64, out);
126 let mut keys: Vec<&String> = entries.keys().collect();
127 keys.sort_unstable_by(|left, right| key_order(left, right));
128 for key in keys {
129 header(MAJOR_TEXT, key.len() as u64, out);
130 out.extend_from_slice(key.as_bytes());
131 write(&entries[key], out);
132 }
133 }
134 }
135}
136
137/// The DAG-CBOR map key ordering: shorter keys first, then bytewise.
138///
139/// Plain bytewise ordering — what `BTreeMap<String, _>` gives — is a
140/// different order and a different set of bytes, and a suite that compared an
141/// encoder against itself would never notice.
142fn key_order(left: &str, right: &str) -> std::cmp::Ordering {
143 left.len()
144 .cmp(&right.len())
145 .then_with(|| left.as_bytes().cmp(right.as_bytes()))
146}
147
148/// A major type and a small argument in one byte.
149fn header_byte(major: u8, argument: u8) -> u8 {
150 (major << 5) | argument
151}
152
153/// Writes a major type and its argument in the shortest form that holds it.
154///
155/// The five widths are the whole of CBOR's ambiguity about numbers: 23 can be
156/// written inline, or in one, two, four or eight following bytes, and all five
157/// decode to 23. Only the first is DAG-CBOR.
158fn header(major: u8, argument: u64, out: &mut Vec<u8>) {
159 if argument < 24 {
160 out.push(header_byte(major, argument as u8));
161 } else if let Ok(byte) = u8::try_from(argument) {
162 out.push(header_byte(major, 24));
163 out.push(byte);
164 } else if let Ok(short) = u16::try_from(argument) {
165 out.push(header_byte(major, 25));
166 out.extend_from_slice(&short.to_be_bytes());
167 } else if let Ok(word) = u32::try_from(argument) {
168 out.push(header_byte(major, 26));
169 out.extend_from_slice(&word.to_be_bytes());
170 } else {
171 out.push(header_byte(major, 27));
172 out.extend_from_slice(&argument.to_be_bytes());
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use serde_json::json;
180
181 fn encode_json(json: serde_json::Value) -> Vec<u8> {
182 encode(&Value::object_from_json(&json).expect("a valid data-model object"))
183 }
184
185 #[test]
186 fn integers_are_written_in_the_shortest_width_that_holds_them() {
187 let cases: [(i64, &[u8]); 8] = [
188 (0, &[0x00]),
189 (23, &[0x17]),
190 (24, &[0x18, 0x18]),
191 (255, &[0x18, 0xff]),
192 (256, &[0x19, 0x01, 0x00]),
193 (65536, &[0x1a, 0x00, 0x01, 0x00, 0x00]),
194 (-1, &[0x20]),
195 (-500, &[0x39, 0x01, 0xf3]),
196 ];
197 for (number, expected) in cases {
198 assert_eq!(encode(&Value::Integer(number)), expected, "{number}");
199 }
200 }
201
202 #[test]
203 fn the_widest_integers_still_round_trip() {
204 assert_eq!(
205 encode(&Value::Integer(i64::MAX)),
206 [0x1b, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]
207 );
208 assert_eq!(
209 encode(&Value::Integer(i64::MIN)),
210 [0x3b, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]
211 );
212 }
213
214 #[test]
215 fn map_keys_are_sorted_by_length_before_they_are_sorted_by_bytes() {
216 // `b` sorts after `aa` bytewise and before it here. This is the rule
217 // most easily got wrong, because a BTreeMap looks like it has already
218 // done the sorting.
219 let bytes = encode_json(json!({"aa": 1, "b": 2}));
220 assert_eq!(bytes, [0xa2, 0x61, b'b', 0x02, 0x62, b'a', b'a', 0x01]);
221 }
222
223 #[test]
224 fn simple_values_are_the_three_the_profile_allows() {
225 assert_eq!(encode(&Value::Null), [0xf6]);
226 assert_eq!(encode(&Value::Bool(false)), [0xf4]);
227 assert_eq!(encode(&Value::Bool(true)), [0xf5]);
228 }
229
230 #[test]
231 fn a_text_length_is_bytes_and_not_characters() {
232 // A length counted in characters would leave the far side reading a
233 // truncated string and then everything after it as garbage.
234 let text = "a~öñ©⽘";
235 assert_ne!(text.len(), text.chars().count());
236 let bytes = encode(&Value::String(text.to_owned()));
237 assert_eq!(bytes[0], header_byte(MAJOR_TEXT, text.len() as u8));
238 assert_eq!(bytes.len(), text.len() + 1);
239 }
240
241 #[test]
242 fn a_link_is_tag_42_over_an_identity_prefixed_cid() {
243 let cid =
244 Cid::parse("bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a").unwrap();
245 let bytes = encode(&Value::Link(cid.clone()));
246 assert_eq!(&bytes[..4], &[0xd8, 0x2a, 0x58, 0x25]);
247 assert_eq!(bytes[4], 0x00);
248 assert_eq!(&bytes[5..], cid.as_bytes());
249 }
250
251 #[test]
252 fn an_empty_list_and_an_empty_map_are_one_byte_each() {
253 assert_eq!(encode(&Value::List(Vec::new())), [0x80]);
254 assert_eq!(encode_json(json!({})), [0xa0]);
255 }
256}