From 26dc390f98e582126e828604a3a2082704ade079 Mon Sep 17 00:00:00 2001 From: Hayleigh Thompson Date: Sat, 25 Jul 2026 22:33:29 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20documentation=20for=20the?= =?UTF-8?q?=20cid=20module.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitmodules | 0 src/at/cid.gleam | 132 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/src/at/cid.gleam b/src/at/cid.gleam index baef226..61811fb 100644 --- a/src/at/cid.gleam +++ b/src/at/cid.gleam @@ -2,25 +2,63 @@ import at/internal/base32 import gleam/dynamic/decode.{type Decoder} +import gleam/json.{type Json} +import munch // TYPES ----------------------------------------------------------------------- +/// A `Cid` or _content identifier_ is a self-describing hash of some binary +/// content that allows for content-addressed storage in distributed systems. +/// In atproto, records are stored with a [`Cid`](#Cid) making it possible to +/// [`verify`](#verify) the integrity of a record or to make content-addressed +/// queries to a repository. +/// +/// Atproto supports only a subset of the [IPFS CID specification](https://specs.ipfs.tech/cid/). +/// Specifically, CIDs in atproto always have the following representation: +/// +/// ```gleam +/// <<1:8, codec:8, 0x12:8, 0x20:8, digest:bytes-size(32)>> +/// ``` +/// +/// Where... +/// +/// - the first segment indicates the CID version, which is always `1`. +/// +/// - the `codec` can either be `0x55` to indicate the content is a raw binary +/// blob, or `0x71` to indicate the content is a DAG-CBOR (Drisl) object. +/// +/// - the following two segments indicate the hash function (SHA256) and the size +/// of the hash (32 bytes). +/// +/// - the final `digest` segment is the SHA256 hash of the content being addressed. +/// pub opaque type Cid { Cid( version: Int, - codec: Int, + codec: Codec, hash_type: Int, hash_size: Int, digest: BitArray, ) } +/// The codec used to indicate the type of content being addressed by a [`Cid`](#Cid). +/// In atproto, this can either be a raw binary blob or a DAG-CBOR (Drisl) object. +/// +/// You can use the [`codec`](#codec) function to extract the codec from a given +/// [`Cid`](#Cid). +/// +pub type Codec { + Drisl + Raw +} + // CONSTANTS ------------------------------------------------------------------- @internal pub const zero = Cid( version: 0, - codec: 0, + codec: Raw, hash_type: 0, hash_size: 0, digest: <<>>, @@ -28,11 +66,29 @@ pub const zero = Cid( // CONSTRUCTORS ---------------------------------------------------------------- +/// Construct a new [`Cid`](#Cid) by hashing the contents of a given `BitArray`. +/// Atproto currently only supports sha256 hashes but you may specify which codec +/// used to indicate whether the [`Cid`](#Cid) is a link to a raw blob or a +/// DAG-CBOR (Drisl) object. +/// +/// If you're constructing or working with [`Cbor`](./cbor#Cbor) objects directly, +/// it's preferable to use `[cbor.to_cid](./cbor#to_cid) to ensure the correct +/// codec. +/// +pub fn new(codec: Codec, content: BitArray) -> Cid { + let digest = munch.hash_bits(munch.sha256, content) + + Cid(version: 1, codec:, hash_type: 0x12, hash_size: 0x20, digest:) +} + +/// Parse a [`Cid`](#Cid) from its raw binary representation. +/// pub fn from_bit_array(bits: BitArray) -> Result(Cid, Nil) { case bits { - <<1:8, codec:8, 0x12:8, 32:8, digest:bytes-size(32)>> -> + <<1:8, codec:8, 0x12:8, 0x20:8, digest:bytes-size(32)>> -> case codec { - 0x55 | 0x71 -> Ok(Cid(1, codec, 0x12, 32, digest)) + 0x55 -> Ok(Cid(1, Raw, 0x12, 0x20, digest)) + 0x71 -> Ok(Cid(1, Drisl, 0x12, 0x20, digest)) _ -> Error(Nil) } @@ -40,6 +96,10 @@ pub fn from_bit_array(bits: BitArray) -> Result(Cid, Nil) { } } +/// Construct a [`Cid`](#Cid) from a base32-encoded string that follows the +/// multibase format, meaning the character `"b"` followed by the base32-encoded +/// binary. +/// pub fn from_string(value: String) -> Result(Cid, Nil) { case value { "b" <> remaining -> @@ -52,6 +112,10 @@ pub fn from_string(value: String) -> Result(Cid, Nil) { } } +/// [Decode](https://gleam-stdlib.hexdocs.pm/gleam/dynamic/decode.html#Decoder) +/// a [`Cid`](#Cid) from either its raw binary representation or a JSON `$link` +/// object. +/// pub fn decoder() -> Decoder(Cid) { decode.one_of(bits_decoder(), [string_decoder()]) } @@ -66,26 +130,80 @@ fn bits_decoder() -> Decoder(Cid) { } fn string_decoder() -> Decoder(Cid) { - use string <- decode.then(decode.string) + use link <- decode.field("$link", decode.string) - case from_string(string) { + case from_string(link) { Ok(cid) -> decode.success(cid) Error(_) -> decode.failure(zero, "Cid") } } +// QUERIES --------------------------------------------------------------------- + +/// Query the codec of a [`Cid`](#Cid) to determine the type of content it is +/// addressing. +/// +pub fn codec(cid: Cid) -> Codec { + cid.codec +} + +/// Extract the SHA256 digest from a [`Cid`](#Cid). +/// +pub fn digest(cid: Cid) -> BitArray { + cid.digest +} + +/// Verify some content against a [`Cid`](#Cid) by hashing its content using the +/// same algorithm and comparing it to the CID's own digest. +/// +pub fn verify(cid: Cid, content: BitArray) -> Bool { + let digest = munch.hash_bits(munch.sha256, content) + + cid.digest == digest +} + // CONVERSIONS ----------------------------------------------------------------- +/// Convert a [`Cid`](#Cid) to its raw binary representation. +/// +/// A [`Cid`](#Cid) can be reconstructed from this binary representation using the +/// [`from_bit_array`](#from_bit_array) function or the provided [`decoder`](#decoder). +/// pub fn to_bit_array(cid: Cid) -> BitArray { + let codec = case cid.codec { + Drisl -> 0x71 + Raw -> 0x55 + } + << cid.version:8, - cid.codec:8, + codec:8, cid.hash_type:8, cid.hash_size:8, cid.digest:bits, >> } +/// Convert a [`Cid`](#Cid) to a base32-encoded string using the multibase format. +/// That a prefix `"b"` to indicate the encoding, followed by the encoded string +/// itself. +/// +/// A [`Cid`](#Cid) can be reconstructed from these strings using the +/// [`from_string`](#from_string) function. +/// pub fn to_string(cid: Cid) -> String { "b" <> base32.encode(to_bit_array(cid), True) } + +/// Convert a [`Cid`](#Cid) to a JSON `$link` object matching the atproto Lexicon +/// `cid-link` type. This means an object with a single `$link` field containing +/// the base32-encoded string produced by [`to_string`](#to_string). +/// +/// A [`Cid`](#Cid) can be decoded from these objects using the provided +/// [`decoder`](#decoder). +/// +pub fn to_json(cid: Cid) -> Json { + json.object([ + #("$link", json.string(to_string(cid))), + ]) +} -- 2.51.2