Skip to main content

jmap_cid_types/
cidv1.rs

1//! CIDv1 conversion for [`Sha256`], per draft-atwood-jmap-cid-00
2//! §Mapping to CIDv1 (informative appendix).
3//!
4//! Spec text:
5//!
6//! > A `sha256` value converts losslessly to a CIDv1 as defined by
7//! > {{MULTIFORMATS-CID}}, using the `raw` codec and the `sha2-256`
8//! > multihash.
9//!
10//! The binary CIDv1 is the four prefix bytes `0x01 0x55 0x12 0x20`
11//! (CID version 1, multicodec `raw`, multihash `sha2-256`, digest
12//! length 32) followed by the 32 digest bytes. The string form is the
13//! multibase prefix `b` followed by the binary CIDv1 as lowercase
14//! RFC 4648 base32 without padding.
15//!
16//! Only this one CID shape is accepted on the reverse path: "The
17//! reverse conversion checks the four prefix bytes and hex-encodes
18//! the remaining 32 bytes." Any other version, codec, hash function or
19//! multibase is rejected with [`CidV1Error`].
20
21use crate::Sha256;
22use std::fmt;
23
24/// The four bytes that open every CIDv1 a [`Sha256`] maps to:
25/// version 1, multicodec `raw`, multihash `sha2-256`, length 32.
26const PREFIX: [u8; 4] = [0x01, 0x55, 0x12, 0x20];
27
28/// Length of the binary CIDv1: 4 prefix bytes plus a 32-byte digest.
29const CIDV1_LEN: usize = PREFIX.len() + 32;
30
31/// RFC 4648 §6 base32 alphabet, lowercased as multibase `b` requires.
32const BASE32_LOWER: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
33
34/// Error from [`Sha256::from_cidv1`] and [`Sha256::from_cidv1_bytes`].
35///
36/// `#[non_exhaustive]` at the type level and per variant, so new
37/// variants and new fields stay semver-additive.
38#[non_exhaustive]
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum CidV1Error {
41    /// The string does not start with the multibase prefix `b`
42    /// (lowercase base32, no padding). Other multibase encodings,
43    /// including `B` (uppercase base32), are not accepted.
44    #[non_exhaustive]
45    UnsupportedMultibase,
46    /// A byte after the multibase prefix is outside the lowercase
47    /// base32 alphabet `a-z 2-7`.
48    #[non_exhaustive]
49    InvalidBase32 {
50        /// 0-based byte index in the input string, saturated at
51        /// `u32::MAX`.
52        at: u32,
53        /// The offending byte.
54        byte: u8,
55    },
56    /// The base32 text has a length no byte string encodes to, or its
57    /// unused trailing bits are not zero, so it is not the canonical
58    /// encoding of any binary CID.
59    #[non_exhaustive]
60    NonCanonicalBase32,
61    /// The binary CID is not 36 bytes long.
62    #[non_exhaustive]
63    WrongLength {
64        /// The actual byte length, saturated at `u32::MAX`.
65        got: u32,
66    },
67    /// The first four bytes are not `0x01 0x55 0x12 0x20`, so the CID
68    /// is not a version 1, `raw`, `sha2-256` CID.
69    #[non_exhaustive]
70    UnsupportedPrefix {
71        /// The four bytes found.
72        got: [u8; 4],
73    },
74}
75
76impl fmt::Display for CidV1Error {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            Self::UnsupportedMultibase => {
80                f.write_str("CIDv1 string must start with the multibase prefix 'b'")
81            }
82            Self::InvalidBase32 { at, byte } => write!(
83                f,
84                "CIDv1 string contains a non-base32 byte 0x{byte:02x} at byte {at}"
85            ),
86            Self::NonCanonicalBase32 => f.write_str("CIDv1 string is not canonical base32"),
87            Self::WrongLength { got } => {
88                write!(f, "binary CIDv1 must be {CIDV1_LEN} bytes (got {got})")
89            }
90            Self::UnsupportedPrefix { got } => write!(
91                f,
92                "CIDv1 prefix must be 01 55 12 20 (got {:02x} {:02x} {:02x} {:02x})",
93                got[0], got[1], got[2], got[3]
94            ),
95        }
96    }
97}
98
99impl std::error::Error for CidV1Error {}
100
101impl Sha256 {
102    /// The binary CIDv1 for this digest: `0x01 0x55 0x12 0x20`
103    /// followed by the 32 digest bytes.
104    pub fn to_cidv1_bytes(&self) -> [u8; CIDV1_LEN] {
105        let mut out = [0u8; CIDV1_LEN];
106        out[..PREFIX.len()].copy_from_slice(&PREFIX);
107        out[PREFIX.len()..].copy_from_slice(&self.to_bytes());
108        out
109    }
110
111    /// The CIDv1 string form for this digest: `b` followed by the
112    /// binary CIDv1 as lowercase base32 without padding.
113    pub fn to_cidv1(&self) -> String {
114        let bytes = self.to_cidv1_bytes();
115        let mut out = String::with_capacity(1 + (bytes.len() * 8).div_ceil(5));
116        out.push('b');
117        let mut acc: u32 = 0;
118        let mut bits = 0;
119        for &b in &bytes {
120            acc = (acc << 8) | u32::from(b);
121            bits += 8;
122            while bits >= 5 {
123                bits -= 5;
124                out.push(char::from(BASE32_LOWER[((acc >> bits) & 0x1f) as usize]));
125            }
126        }
127        if bits > 0 {
128            out.push(char::from(
129                BASE32_LOWER[((acc << (5 - bits)) & 0x1f) as usize],
130            ));
131        }
132        out
133    }
134
135    /// Parse a binary CIDv1 back into its [`Sha256`].
136    ///
137    /// Rejects anything that is not exactly 36 bytes starting with
138    /// `0x01 0x55 0x12 0x20`.
139    pub fn from_cidv1_bytes(bytes: &[u8]) -> Result<Self, CidV1Error> {
140        let digest: &[u8; 32] = bytes
141            .strip_prefix(&PREFIX)
142            .and_then(|rest| rest.try_into().ok())
143            .ok_or_else(|| match bytes.first_chunk::<4>() {
144                Some(got) if *got != PREFIX => CidV1Error::UnsupportedPrefix { got: *got },
145                _ => CidV1Error::WrongLength {
146                    got: u32::try_from(bytes.len()).unwrap_or(u32::MAX),
147                },
148            })?;
149        Ok(Self::from(digest))
150    }
151
152    /// Parse a CIDv1 string (`b` + lowercase unpadded base32) back
153    /// into its [`Sha256`].
154    pub fn from_cidv1(s: &str) -> Result<Self, CidV1Error> {
155        let text = s
156            .as_bytes()
157            .strip_prefix(b"b")
158            .ok_or(CidV1Error::UnsupportedMultibase)?;
159        let mut bytes = Vec::with_capacity(text.len() * 5 / 8);
160        let mut acc: u32 = 0;
161        let mut bits = 0;
162        for (i, &c) in text.iter().enumerate() {
163            let value = match c {
164                b'a'..=b'z' => c - b'a',
165                b'2'..=b'7' => c - b'2' + 26,
166                _ => {
167                    return Err(CidV1Error::InvalidBase32 {
168                        at: u32::try_from(i + 1).unwrap_or(u32::MAX),
169                        byte: c,
170                    });
171                }
172            };
173            acc = ((acc << 5) | u32::from(value)) & 0xff_ff;
174            bits += 5;
175            if bits >= 8 {
176                bits -= 8;
177                bytes.push((acc >> bits) as u8);
178            }
179        }
180        // RFC 4648 §6: leftover bits are padding and must be zero; five
181        // or more leftover bits means a character encoded no data.
182        if bits >= 5 || acc & ((1 << bits) - 1) != 0 {
183            return Err(CidV1Error::NonCanonicalBase32);
184        }
185        Self::from_cidv1_bytes(&bytes)
186    }
187}