From 05ec30726d72a51b03f6017247e97ff81c2f558a Mon Sep 17 00:00:00 2001 From: Hayleigh Thompson Date: Sat, 25 Jul 2026 23:33:26 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Document=20cbor=20public=20api.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/at/cbor.gleam | 102 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 20 deletions(-) diff --git a/src/at/cbor.gleam b/src/at/cbor.gleam index 49a7d2d..5818151 100644 --- a/src/at/cbor.gleam +++ b/src/at/cbor.gleam @@ -11,14 +11,21 @@ import gleam/string // TYPES ----------------------------------------------------------------------- -/// +/// An encoded CBOR binary value. You can use functions like [`object`](#object) +/// and [`array`](#array) to compose larger data structures, and then finally +/// call [`to_bit_array`](#to_bit_array) to get the raw bytes that you could write +/// to a file or send over hte network. +/// +/// This module implements the just the parts of the [DRISL](https://dasl.ing/drisl.html) +/// subset of CBOR that is used by atproto, and guarantees that the same data will +/// always be byte-identical when encoded. /// pub opaque type Cbor { Cbor(chunk: BitArray) } /// -/// +/// pub type DecodeError { MalformedInput UnableToDecode(List(decode.DecodeError)) @@ -31,7 +38,17 @@ const max_safe_int = 9_007_199_254_740_991 // CONSTRUCTORS ---------------------------------------------------------------- -/// +/// Decode a CBOR-encoded bit array into a structured Gleam value. You might use +/// this to consume an encoded atproto record from a +/// [firehose](https://atproto.com/specs/sync#repository-event-stream), for example. +/// +/// This function is _lenient_ and will accept bitarrays that contain more than +/// the CBOR value being decoded. This is necessary to support CBOR-encoded values +/// that are embedded in a larger structure, but means this function is not +/// spec-compliant. +/// +/// > **Note**: due to limitations of the JavaScript number type, this function +/// > will fail to decode integers larger than 53 bits. /// pub fn parse(cbor: BitArray, decoder: Decoder(a)) -> Result(a, DecodeError) { case parse_dynamic(cbor) { @@ -45,7 +62,35 @@ pub fn parse(cbor: BitArray, decoder: Decoder(a)) -> Result(a, DecodeError) { } } -/// +/// [`parse`](#parse) a CBOR-encoded bit array into a structure Gleam value and +/// ensure that the entire bit array was consumed. +/// +/// > **Note**: due to limitations of the JavaScript number type, this function +/// > will fail to decode integers larger than 53 bits. +/// +pub fn parse_strict( + cbor: BitArray, + decoder: Decoder(a), +) -> Result(a, DecodeError) { + case parse_dynamic(cbor) { + Ok(#(dynamic, <<>>)) -> + case decode.run(dynamic, decoder) { + Ok(value) -> Ok(value) + Error(errors) -> Error(UnableToDecode(errors)) + } + + Ok(_) -> Error(MalformedInput) + + Error(reason) -> Error(reason) + } +} + +/// [`parse`](#parse) a CBOR-encoded value from a bit array and return any remaining +/// bits. This is useful when a single buffer packs multiple CBOR values +/// back-to-back. +/// +/// > **Note**: due to limitations of the JavaScript number type, this function +/// > will fail to decode integers larger than 53 bits. /// pub fn parse_multi( cbor: BitArray, @@ -242,20 +287,28 @@ fn do_parse_dynamic_map( // CONVERSIONS ----------------------------------------------------------------- +/// Convert an encoded [`Cbor`](#Cbor) value into its raw bit array representation. /// -/// pub fn to_bit_array(cbor: Cbor) -> BitArray { cbor.chunk } +/// Hash a [`Cbor`](#Cbor) value and produce a [content id](./cid#Cid) that may +/// be used to reference this value in an atproto record or other [`Cbor`](#Cbor) +/// structure. +/// +pub fn to_cid(cbor: Cbor) -> Cid { + cid.new(cid.Drisl, cbor.chunk) +} + // ENCODING -------------------------------------------------------------------- -/// -/// +/// The [`Cbor`](#Cbor) encoding of `null`. +/// pub const null: Cbor = Cbor(<<7:3, 22:5>>) +/// Encode a Gleam `Bool` as a [`Cbor`](#Cbor) boolean. /// -/// pub fn bool(value: Bool) -> Cbor { case value { True -> Cbor(<<7:3, 21:5>>) @@ -263,9 +316,12 @@ pub fn bool(value: Bool) -> Cbor { } } -/// Encode an integer in the CBOR binary format. Numbers larger than 64 bits will -/// be truncated but numbers up to 53 bits should be preferred to avoid precision -/// loss in JavaScript applications. +/// Encode an integer in the [`Cbor`](#Cbor) binary format. Numbers larger than +/// 64 bits will always be truncated but numbers up to 53 bits should be preferred +/// to avoid precision loss in JavaScript applications. +/// +/// > **Note**: Attempting to round-trip a number larger than 53 bits on the +/// > JavaScript target will fail at decoding time due to integer precision loss. /// pub fn int(value: Int) -> Cbor { case value < 0 { @@ -284,8 +340,8 @@ fn do_int(value: Int) -> BitArray { } } +/// Encode a Gleam `String` as a [`Cbor`](#Cbor) UTF-8 string. /// -/// pub fn string(value: String) -> Cbor { let bytes = bit_array.from_string(value) let length = bit_array.byte_size(bytes) @@ -293,7 +349,8 @@ pub fn string(value: String) -> Cbor { Cbor(<<3:3, do_int(length):bits, bytes:bits>>) } -/// +/// Encode a Gleam `List` as a [`Cbor`](#Cbor) array by providing a function to +/// encode each element. /// pub fn array(values: List(a), encode: fn(a) -> Cbor) -> Cbor { do_array(0, values, <<>>, fn(value) { encode(value).chunk }) @@ -312,8 +369,8 @@ fn do_array( } } -/// Encode an object or map in CBOR binary format. Entries can be provided in -/// any order and will be sorted according to DAG-CBOR canonical key ordering: +/// Encode an object or map in [`Cbor`](#Cbor) binary format. Entries can be +/// provided in any order but will be sorted according to DAG-CBOR canonical key ordering: /// shortest UTF-8 byte length first, then lexicographically. /// pub fn object(entries: List(#(String, Cbor))) -> Cbor { @@ -344,8 +401,9 @@ fn do_object( } } -/// Encode a Gleam `Option` in CBOR binary format. When a value is present it is -/// unwrapped and encoded, otherwise [`null`](#null) is used instead. +/// Encode a Gleam `Option` as a [`Cbor`](#Cbor) value. Either `None` variant is +/// encoded as [`null`](#null) or the `Some` variant is unwrapped and encoded +/// using the provided function. /// pub fn option(value: Option(a), inner: fn(a) -> Cbor) -> Cbor { case value { @@ -354,17 +412,21 @@ pub fn option(value: Option(a), inner: fn(a) -> Cbor) -> Cbor { } } +/// Embed raw bytes as a [`Cbor`](#Cbor) byte string. If you want to /// -/// pub fn bytes(value: BitArray) -> Cbor { let value = bit_array.pad_to_bytes(value) let length = bit_array.byte_size(value) - Cbor(<<2:3, do_int(length):bits, value:bits>>) + Cbor(<<2:3, do_int(length):bits, value:bits-size({ 8 * length })>>) } -/// +/// Encode a [`Cid`](./cid#Cid) as a [`Cbor`](#Cbor) `cid-link`. This is represented +/// as a byte string tagged with the CBOR tag `42` and prefixed with a `0x00` byte. /// +/// Atproto records, commits, and MST nodes all reference each other by [`Cid`](./cid#Cid) +/// content hashes. +/// pub fn cid(value: Cid) -> Cbor { let bits = cid.to_bit_array(value) let length = bit_array.byte_size(bits) + 1 -- 2.51.2