1use std::{fmt::Display, str::FromStr};
6
7use sha2::Digest;
8use thiserror::Error;
9
10use crate::base32::BASE32_LOWER;
11
12mod serde;
13
14pub(crate) use self::serde::{BytesToCidVisitor, CID_SERDE_PRIVATE_IDENTIFIER};
15
16const CID_VERSION: u8 = 1;
17const PREFIX_LEN: usize = 4;
18const HASH_LEN: u8 = 32;
20const DATA_LEN: usize = PREFIX_LEN + HASH_LEN as usize;
21const HASH_CODE_SHA2_256: u8 = 0x12;
22const HASH_CODE_BLAKE3: u8 = 0x1e;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
25pub struct Cid {
26 data: [u8; DATA_LEN],
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
35#[non_exhaustive]
36#[repr(u8)]
37pub enum Codec {
38 Raw = 0x55,
39 Drisl = 0x71,
40}
41
42#[derive(Debug, Error)]
43pub enum ParseCodecError {
44 #[error("Unknown codec: 0x{_0:X}")]
45 UnknownCodec(u8),
46}
47
48impl TryFrom<u8> for Codec {
49 type Error = ParseCodecError;
50
51 fn try_from(value: u8) -> Result<Self, Self::Error> {
52 match value {
53 0x55 => Ok(Self::Raw),
54 0x71 => Ok(Self::Drisl),
55 _ => Err(ParseCodecError::UnknownCodec(value)),
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
61#[non_exhaustive]
62#[repr(u8)]
63pub enum Multihash {
64 Sha2256 = 0x12,
65 Blake3 = 0x1e,
66}
67
68impl TryFrom<u8> for Multihash {
69 type Error = MultihashParseError;
70
71 fn try_from(value: u8) -> Result<Self, Self::Error> {
72 match value {
73 HASH_CODE_SHA2_256 => Ok(Self::Sha2256),
74 HASH_CODE_BLAKE3 => Ok(Self::Blake3),
75 _ => Err(MultihashParseError::UnknownHash(value)),
76 }
77 }
78}
79
80#[derive(Debug, Error)]
81pub enum CidParseError {
82 #[error("Invalid encoding")]
83 InvalidEncoding,
84 #[error("Too short")]
85 TooShort,
86 #[error("Invalid CID version: {_0}")]
87 InvalidCidVersion(u8),
88 #[error("Invalid codec: {_0}")]
89 InvalidCodec(ParseCodecError),
90 #[error("Invalid multihash: {_0}")]
91 InvalidMultihash(MultihashParseError),
92}
93
94impl From<ParseCodecError> for CidParseError {
95 fn from(err: ParseCodecError) -> Self {
96 Self::InvalidCodec(err)
97 }
98}
99
100impl From<MultihashParseError> for CidParseError {
101 fn from(err: MultihashParseError) -> Self {
102 Self::InvalidMultihash(err)
103 }
104}
105
106impl FromStr for Cid {
107 type Err = CidParseError;
108
109 fn from_str(s: &str) -> Result<Self, Self::Err> {
110 if !s.starts_with('b') {
111 return Err(CidParseError::InvalidEncoding);
112 }
113
114 let without_prefix = &s.as_bytes()[1..];
116 let bytes = BASE32_LOWER
117 .decode(without_prefix)
118 .map_err(|_e| CidParseError::InvalidEncoding)?;
119
120 Cid::from_bytes_raw(&bytes)
121 }
122}
123
124impl Cid {
125 pub fn hash(&self) -> &[u8] {
127 match self.data[3] {
128 0 => &[][..], HASH_LEN => &self.data[PREFIX_LEN..],
130 _ => unreachable!("invalid construction"),
131 }
132 }
133
134 pub fn multihash_type(&self) -> Multihash {
135 Multihash::try_from(self.data[2]).expect("invalid construction")
136 }
137
138 pub fn codec(&self) -> Codec {
140 Codec::try_from(self.data[1]).expect("invalid construction")
141 }
142
143 pub fn from_bytes(bytes: &[u8]) -> Result<Self, CidParseError> {
145 if bytes.is_empty() {
146 return Err(CidParseError::TooShort);
147 }
148 if bytes[0] != 0x0 {
149 return Err(CidParseError::InvalidEncoding);
150 }
151 Self::from_bytes_raw(&bytes[1..])
152 }
153
154 pub fn from_bytes_raw(bytes: &[u8]) -> Result<Self, CidParseError> {
156 const MIN_LEN: usize = 3;
157
158 if bytes.len() < MIN_LEN {
159 return Err(CidParseError::TooShort);
160 }
161 if bytes.len() > DATA_LEN {
162 return Err(MultihashParseError::InvalidLength(bytes.len()).into());
163 }
164
165 if bytes[0] != CID_VERSION {
166 return Err(CidParseError::InvalidCidVersion(bytes[0]));
167 }
168 let mut data = [0u8; DATA_LEN];
169 let _codec = Codec::try_from(bytes[1])?;
170 let _multihash = Multihash::try_from(bytes[2])?;
171
172 let len = bytes[3];
173 match len {
174 0 => {
175 if bytes.len() > 4 {
176 return Err(MultihashParseError::InvalidLength(bytes.len()).into());
177 }
178 data[..PREFIX_LEN].copy_from_slice(&bytes[..PREFIX_LEN]);
179 }
180 HASH_LEN => {
181 if bytes.len() != DATA_LEN {
182 return Err(MultihashParseError::InvalidLength(bytes.len()).into());
183 }
184 data.copy_from_slice(bytes);
185 }
186 _ => return Err(MultihashParseError::InvalidLengthPrefix.into()),
187 }
188
189 Ok(Cid { data })
190 }
191
192 pub fn as_bytes(&self) -> &[u8] {
194 match self.data[3] {
195 0 => &self.data[..PREFIX_LEN],
196 HASH_LEN => &self.data,
197 _ => unreachable!("invalid construction"),
198 }
199 }
200
201 pub fn digest_sha2(codec: Codec, data: impl AsRef<[u8]>) -> Self {
202 let hash = sha2::Sha256::digest(data);
203 let mut data = [0u8; DATA_LEN];
204 data[0] = CID_VERSION;
205 data[1] = codec as u8;
206 data[2] = HASH_CODE_SHA2_256;
207 data[3] = HASH_LEN;
208 data[PREFIX_LEN..].copy_from_slice(&hash);
209 Self { data }
210 }
211
212 pub fn digest_blake3(codec: Codec, data: impl AsRef<[u8]>) -> Self {
213 let hash = blake3::hash(data.as_ref());
214 let mut data = [0u8; DATA_LEN];
215 data[0] = CID_VERSION;
216 data[1] = codec as u8;
217 data[2] = HASH_CODE_BLAKE3;
218 data[3] = HASH_LEN;
219 data[PREFIX_LEN..].copy_from_slice(hash.as_bytes());
220 Self { data }
221 }
222
223 pub fn empty_sha2_256(codec: Codec) -> Self {
224 let mut data = [0u8; DATA_LEN];
225 data[0] = CID_VERSION;
226 data[1] = codec as u8;
227 data[2] = HASH_CODE_SHA2_256;
228 data[3] = 0;
229 Self { data }
230 }
231
232 pub fn empty_blake3(codec: Codec) -> Self {
233 let mut data = [0u8; DATA_LEN];
234 data[0] = CID_VERSION;
235 data[1] = codec as u8;
236 data[2] = HASH_CODE_BLAKE3;
237 data[3] = 0;
238 Self { data }
239 }
240}
241
242impl Display for Cid {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 write!(f, "b")?;
245 let out = self.as_bytes();
246 BASE32_LOWER.encode_write(out, f)?;
247
248 Ok(())
249 }
250}
251
252#[derive(Debug, Error)]
253#[non_exhaustive]
254pub enum MultihashParseError {
255 #[error("Invalid length: {_0}")]
256 InvalidLength(usize),
257 #[error("Unknown hash: {_0:x}")]
258 UnknownHash(u8),
259 #[error("Invalid length prefix")]
260 InvalidLengthPrefix,
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_base_sha2_256() {
269 let cid_str = "bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy";
271 let parsed: Cid = cid_str.parse().unwrap();
272 assert_eq!(parsed.codec(), Codec::Raw);
273 assert!(matches!(parsed.multihash_type(), Multihash::Sha2256));
274
275 let cid_str_back = parsed.to_string();
276 assert_eq!(cid_str_back, cid_str);
277 }
278
279 #[test]
280 fn test_base_blake3() {
281 let cid_str = "bafkr4iae4c5tt4yldi76xcpvg3etxykqkvec352im5fqbutolj2xo5yc5e";
283 let parsed: Cid = cid_str.parse().unwrap();
284 assert_eq!(parsed.codec(), Codec::Raw);
285 assert!(matches!(parsed.multihash_type(), Multihash::Blake3));
286
287 let cid_str_back = parsed.to_string();
288 assert_eq!(cid_str_back, cid_str);
289 }
290
291 #[test]
292 fn test_digest_sha2_256() {
293 let cid_str = "bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy";
294 assert_eq!(Cid::digest_sha2(Codec::Raw, b"foo").to_string(), cid_str);
295 }
296
297 #[test]
298 fn test_digest_blake3() {
299 let cid_str = "bafkr4iae4c5tt4yldi76xcpvg3etxykqkvec352im5fqbutolj2xo5yc5e";
300 assert_eq!(Cid::digest_blake3(Codec::Raw, b"foo").to_string(), cid_str);
301 }
302}