Skip to main content

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}