Skip to main content

aleph_types/
cid.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::convert::TryFrom;
3use std::fmt::{Display, Formatter};
4use thiserror::Error;
5
6/// Newtype for IPFS CIDv0 (base58-encoded, starts with "Qm", 46 characters).
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub struct CidV0(String);
9
10/// Newtype for IPFS CIDv1 (multibase-encoded with various encodings).
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct CidV1(String);
13
14/// Represents an IPFS Content Identifier (CID).
15/// Supports both CIDv0 (base58-encoded SHA-256 multihash) and CIDv1 (multibase-encoded).
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub enum Cid {
18    /// CIDv0: Always a base58-encoded multihash starting with "Qm"
19    V0(CidV0),
20    /// CIDv1: Multibase-encoded CID with various encodings (base32, base58btc, etc.)
21    V1(CidV1),
22}
23
24#[derive(Error, Debug)]
25pub enum CidError {
26    #[error("invalid CID: empty string")]
27    EmptyString,
28    #[error("invalid CID format: unrecognized version or encoding")]
29    InvalidFormat,
30    #[error("invalid CIDv0: must start with 'Qm' and be 46 characters")]
31    InvalidV0,
32}
33
34impl CidV0 {
35    /// Creates a new CIDv0 from a string.
36    /// CIDv0 must start with "Qm" and be exactly 46 characters long.
37    pub fn new(cid: String) -> Result<Self, CidError> {
38        if cid.starts_with("Qm") && cid.len() == 46 {
39            Ok(CidV0(cid))
40        } else {
41            Err(CidError::InvalidV0)
42        }
43    }
44
45    /// Returns the CIDv0 as a string slice.
46    pub fn as_str(&self) -> &str {
47        &self.0
48    }
49
50    /// Consumes the CIDv0 and returns the inner string.
51    pub fn into_inner(self) -> String {
52        self.0
53    }
54}
55
56impl TryFrom<String> for CidV0 {
57    type Error = CidError;
58
59    fn try_from(value: String) -> Result<Self, Self::Error> {
60        CidV0::new(value)
61    }
62}
63
64impl TryFrom<&str> for CidV0 {
65    type Error = CidError;
66
67    fn try_from(value: &str) -> Result<Self, Self::Error> {
68        CidV0::new(value.to_string())
69    }
70}
71
72impl Display for CidV0 {
73    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
74        write!(f, "{}", self.0)
75    }
76}
77
78impl CidV1 {
79    /// Creates a new CIDv1 from a string.
80    /// CIDv1 typically starts with 'b' (base32) or 'z' (base58btc), but can have other multibase prefixes.
81    pub fn new(cid: String) -> Self {
82        CidV1(cid)
83    }
84
85    /// Returns the CIDv1 as a string slice.
86    pub fn as_str(&self) -> &str {
87        &self.0
88    }
89
90    /// Consumes the CIDv1 and returns the inner string.
91    pub fn into_inner(self) -> String {
92        self.0
93    }
94}
95
96impl From<String> for CidV1 {
97    fn from(value: String) -> Self {
98        CidV1(value)
99    }
100}
101
102impl Display for CidV1 {
103    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
104        write!(f, "{}", self.0)
105    }
106}
107
108impl Cid {
109    /// Creates a new CIDv0 variant.
110    pub fn v0(cid: CidV0) -> Self {
111        Cid::V0(cid)
112    }
113
114    /// Creates a new CIDv1 variant.
115    pub fn v1(cid: CidV1) -> Self {
116        Cid::V1(cid)
117    }
118
119    /// Returns the CID as a string slice.
120    pub fn as_str(&self) -> &str {
121        match self {
122            Cid::V0(cid) => cid.as_str(),
123            Cid::V1(cid) => cid.as_str(),
124        }
125    }
126
127    /// Checks if this is a CIDv0.
128    pub fn is_v0(&self) -> bool {
129        matches!(self, Cid::V0(_))
130    }
131
132    /// Checks if this is a CIDv1.
133    pub fn is_v1(&self) -> bool {
134        matches!(self, Cid::V1(_))
135    }
136}
137
138impl TryFrom<String> for Cid {
139    type Error = CidError;
140
141    fn try_from(value: String) -> Result<Self, Self::Error> {
142        if value.is_empty() {
143            return Err(CidError::EmptyString);
144        }
145
146        // CIDv0: starts with "Qm" and is 46 characters long
147        if value.starts_with("Qm") && value.len() == 46 {
148            return Ok(Cid::V0(CidV0(value)));
149        }
150
151        // CIDv1: multibase-encoded, typically starts with 'b' (base32) or 'z' (base58btc)
152        // Common prefixes: b (base32), B (base32upper), z (base58btc), f (base16), F (base16upper),
153        // m (base64), M (base64url), u (base64url), U (base64urlpad)
154        if value.len() > 1 {
155            let first_char = value.chars().next().unwrap();
156            // Check for common multibase prefixes
157            if matches!(
158                first_char,
159                'b' | 'B' | 'z' | 'f' | 'F' | 'm' | 'M' | 'u' | 'U'
160            ) {
161                return Ok(Cid::V1(CidV1(value)));
162            }
163        }
164
165        Err(CidError::InvalidFormat)
166    }
167}
168
169impl TryFrom<&str> for Cid {
170    type Error = CidError;
171
172    fn try_from(value: &str) -> Result<Self, Self::Error> {
173        Cid::try_from(value.to_string())
174    }
175}
176
177impl Display for Cid {
178    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
179        write!(f, "{}", self.as_str())
180    }
181}
182
183impl From<CidV0> for Cid {
184    fn from(value: CidV0) -> Self {
185        Cid::V0(value)
186    }
187}
188
189impl From<CidV1> for Cid {
190    fn from(value: CidV1) -> Self {
191        Cid::V1(value)
192    }
193}
194
195// Custom serialization for Cid
196impl Serialize for Cid {
197    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
198    where
199        S: Serializer,
200    {
201        serializer.serialize_str(self.as_str())
202    }
203}
204
205// Custom deserialization for Cid that detects the version
206impl<'de> Deserialize<'de> for Cid {
207    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
208    where
209        D: Deserializer<'de>,
210    {
211        let s = String::deserialize(deserializer)?;
212        Cid::try_from(s).map_err(serde::de::Error::custom)
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_cidv0_new() {
222        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8".to_string();
223        let cid = CidV0::new(cid_str.clone()).unwrap();
224        assert_eq!(cid.as_str(), cid_str);
225    }
226
227    #[test]
228    fn test_cidv0_try_from() {
229        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8";
230        let cid = CidV0::try_from(cid_str).unwrap();
231        assert_eq!(cid.as_str(), cid_str);
232    }
233
234    #[test]
235    fn test_cidv0_invalid_length() {
236        let cid_str = "QmYULJo".to_string();
237        let result = CidV0::new(cid_str);
238        assert!(result.is_err());
239        assert!(matches!(result.unwrap_err(), CidError::InvalidV0));
240    }
241
242    #[test]
243    fn test_cidv0_invalid_prefix() {
244        let cid_str = "XmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8".to_string();
245        let result = CidV0::new(cid_str);
246        assert!(result.is_err());
247    }
248
249    #[test]
250    fn test_cidv0_display() {
251        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8";
252        let cid = CidV0::try_from(cid_str).unwrap();
253        assert_eq!(format!("{}", cid), cid_str);
254    }
255
256    #[test]
257    fn test_cidv1_new() {
258        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi".to_string();
259        let cid = CidV1::new(cid_str.clone());
260        assert_eq!(cid.as_str(), cid_str);
261    }
262
263    #[test]
264    fn test_cidv1_from_string() {
265        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi".to_string();
266        let cid = CidV1::from(cid_str.clone());
267        assert_eq!(cid.as_str(), cid_str);
268    }
269
270    #[test]
271    fn test_cidv1_display() {
272        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi";
273        let cid = CidV1::new(cid_str.to_string());
274        assert_eq!(format!("{}", cid), cid_str);
275    }
276
277    #[test]
278    fn test_cid_from_cidv0() {
279        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8";
280        let cidv0 = CidV0::try_from(cid_str).unwrap();
281        let cid = Cid::from(cidv0);
282        assert!(cid.is_v0());
283        assert_eq!(cid.as_str(), cid_str);
284    }
285
286    #[test]
287    fn test_cid_from_cidv1() {
288        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi";
289        let cidv1 = CidV1::new(cid_str.to_string());
290        let cid = Cid::from(cidv1);
291        assert!(cid.is_v1());
292        assert_eq!(cid.as_str(), cid_str);
293    }
294
295    #[test]
296    fn test_cid_try_from_v0_string() {
297        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8";
298        let cid = Cid::try_from(cid_str).unwrap();
299        assert!(cid.is_v0());
300        assert_eq!(cid.as_str(), cid_str);
301    }
302
303    #[test]
304    fn test_cid_try_from_v1_base32() {
305        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi";
306        let cid = Cid::try_from(cid_str).unwrap();
307        assert!(cid.is_v1());
308        assert_eq!(cid.as_str(), cid_str);
309    }
310
311    #[test]
312    fn test_cid_try_from_v1_base58btc() {
313        let cid_str = "zdj7WWeQ43G6JJvLWQWZpyHuAMq6uYWRjkBXFad11vE2LHhQ7";
314        let cid = Cid::try_from(cid_str).unwrap();
315        assert!(cid.is_v1());
316        assert_eq!(cid.as_str(), cid_str);
317    }
318
319    #[test]
320    fn test_cid_empty_string() {
321        let result = Cid::try_from("");
322        assert!(result.is_err());
323        assert!(matches!(result.unwrap_err(), CidError::EmptyString));
324    }
325
326    #[test]
327    fn test_cid_invalid_format() {
328        let result = Cid::try_from("invalid_cid_format");
329        assert!(result.is_err());
330        assert!(matches!(result.unwrap_err(), CidError::InvalidFormat));
331    }
332
333    #[test]
334    fn test_cid_display() {
335        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8";
336        let cid = Cid::try_from(cid_str).unwrap();
337        assert_eq!(format!("{}", cid), cid_str);
338    }
339
340    #[test]
341    fn test_cidv0_serde() {
342        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8";
343        let cid = CidV0::try_from(cid_str).unwrap();
344
345        let json = serde_json::to_string(&cid).unwrap();
346        assert_eq!(json, format!("\"{}\"", cid_str));
347
348        let deserialized: CidV0 = serde_json::from_str(&json).unwrap();
349        assert_eq!(cid, deserialized);
350    }
351
352    #[test]
353    fn test_cidv1_serde() {
354        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi";
355        let cid = CidV1::new(cid_str.to_string());
356
357        let json = serde_json::to_string(&cid).unwrap();
358        assert_eq!(json, format!("\"{}\"", cid_str));
359
360        let deserialized: CidV1 = serde_json::from_str(&json).unwrap();
361        assert_eq!(cid, deserialized);
362    }
363
364    #[test]
365    fn test_cid_serde_v0() {
366        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8";
367        let cid = Cid::try_from(cid_str).unwrap();
368
369        let json = serde_json::to_string(&cid).unwrap();
370        assert_eq!(json, format!("\"{}\"", cid_str));
371
372        let deserialized: Cid = serde_json::from_str(&json).unwrap();
373        assert_eq!(cid, deserialized);
374        assert!(deserialized.is_v0());
375    }
376
377    #[test]
378    fn test_cid_serde_v1() {
379        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi";
380        let cid = Cid::try_from(cid_str).unwrap();
381
382        let json = serde_json::to_string(&cid).unwrap();
383        assert_eq!(json, format!("\"{}\"", cid_str));
384
385        let deserialized: Cid = serde_json::from_str(&json).unwrap();
386        assert_eq!(cid, deserialized);
387        assert!(deserialized.is_v1());
388    }
389
390    #[test]
391    fn test_cidv0_into_inner() {
392        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8".to_string();
393        let cid = CidV0::new(cid_str.clone()).unwrap();
394        assert_eq!(cid.into_inner(), cid_str);
395    }
396
397    #[test]
398    fn test_cidv1_into_inner() {
399        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi".to_string();
400        let cid = CidV1::new(cid_str.clone());
401        assert_eq!(cid.into_inner(), cid_str);
402    }
403}