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}