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        // Require a minimum length to avoid accepting short preset names (e.g., "ubuntu24")
155        if value.len() >= 40 {
156            let first_char = value.chars().next().unwrap();
157            // Check for common multibase prefixes
158            if matches!(
159                first_char,
160                'b' | 'B' | 'z' | 'f' | 'F' | 'm' | 'M' | 'u' | 'U'
161            ) {
162                return Ok(Cid::V1(CidV1(value)));
163            }
164        }
165
166        Err(CidError::InvalidFormat)
167    }
168}
169
170impl TryFrom<&str> for Cid {
171    type Error = CidError;
172
173    fn try_from(value: &str) -> Result<Self, Self::Error> {
174        Cid::try_from(value.to_string())
175    }
176}
177
178impl Display for Cid {
179    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
180        write!(f, "{}", self.as_str())
181    }
182}
183
184impl From<CidV0> for Cid {
185    fn from(value: CidV0) -> Self {
186        Cid::V0(value)
187    }
188}
189
190impl From<CidV1> for Cid {
191    fn from(value: CidV1) -> Self {
192        Cid::V1(value)
193    }
194}
195
196// Custom serialization for Cid
197impl Serialize for Cid {
198    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
199    where
200        S: Serializer,
201    {
202        serializer.serialize_str(self.as_str())
203    }
204}
205
206// Custom deserialization for Cid that detects the version
207impl<'de> Deserialize<'de> for Cid {
208    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
209    where
210        D: Deserializer<'de>,
211    {
212        let s = String::deserialize(deserializer)?;
213        Cid::try_from(s).map_err(serde::de::Error::custom)
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_cidv0_new() {
223        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8".to_string();
224        let cid = CidV0::new(cid_str.clone()).unwrap();
225        assert_eq!(cid.as_str(), cid_str);
226    }
227
228    #[test]
229    fn test_cidv0_try_from() {
230        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8";
231        let cid = CidV0::try_from(cid_str).unwrap();
232        assert_eq!(cid.as_str(), cid_str);
233    }
234
235    #[test]
236    fn test_cidv0_invalid_length() {
237        let cid_str = "QmYULJo".to_string();
238        let result = CidV0::new(cid_str);
239        assert!(result.is_err());
240        assert!(matches!(result.unwrap_err(), CidError::InvalidV0));
241    }
242
243    #[test]
244    fn test_cidv0_invalid_prefix() {
245        let cid_str = "XmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8".to_string();
246        let result = CidV0::new(cid_str);
247        assert!(result.is_err());
248    }
249
250    #[test]
251    fn test_cidv0_display() {
252        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8";
253        let cid = CidV0::try_from(cid_str).unwrap();
254        assert_eq!(format!("{}", cid), cid_str);
255    }
256
257    #[test]
258    fn test_cidv1_new() {
259        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi".to_string();
260        let cid = CidV1::new(cid_str.clone());
261        assert_eq!(cid.as_str(), cid_str);
262    }
263
264    #[test]
265    fn test_cidv1_from_string() {
266        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi".to_string();
267        let cid = CidV1::from(cid_str.clone());
268        assert_eq!(cid.as_str(), cid_str);
269    }
270
271    #[test]
272    fn test_cidv1_display() {
273        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi";
274        let cid = CidV1::new(cid_str.to_string());
275        assert_eq!(format!("{}", cid), cid_str);
276    }
277
278    #[test]
279    fn test_cid_from_cidv0() {
280        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8";
281        let cidv0 = CidV0::try_from(cid_str).unwrap();
282        let cid = Cid::from(cidv0);
283        assert!(cid.is_v0());
284        assert_eq!(cid.as_str(), cid_str);
285    }
286
287    #[test]
288    fn test_cid_from_cidv1() {
289        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi";
290        let cidv1 = CidV1::new(cid_str.to_string());
291        let cid = Cid::from(cidv1);
292        assert!(cid.is_v1());
293        assert_eq!(cid.as_str(), cid_str);
294    }
295
296    #[test]
297    fn test_cid_try_from_v0_string() {
298        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8";
299        let cid = Cid::try_from(cid_str).unwrap();
300        assert!(cid.is_v0());
301        assert_eq!(cid.as_str(), cid_str);
302    }
303
304    #[test]
305    fn test_cid_try_from_v1_base32() {
306        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi";
307        let cid = Cid::try_from(cid_str).unwrap();
308        assert!(cid.is_v1());
309        assert_eq!(cid.as_str(), cid_str);
310    }
311
312    #[test]
313    fn test_cid_try_from_v1_base58btc() {
314        let cid_str = "zdj7WWeQ43G6JJvLWQWZpyHuAMq6uYWRjkBXFad11vE2LHhQ7";
315        let cid = Cid::try_from(cid_str).unwrap();
316        assert!(cid.is_v1());
317        assert_eq!(cid.as_str(), cid_str);
318    }
319
320    #[test]
321    fn test_cid_empty_string() {
322        let result = Cid::try_from("");
323        assert!(result.is_err());
324        assert!(matches!(result.unwrap_err(), CidError::EmptyString));
325    }
326
327    #[test]
328    fn test_cid_invalid_format() {
329        let result = Cid::try_from("invalid_cid_format");
330        assert!(result.is_err());
331        assert!(matches!(result.unwrap_err(), CidError::InvalidFormat));
332    }
333
334    #[test]
335    fn test_cid_display() {
336        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8";
337        let cid = Cid::try_from(cid_str).unwrap();
338        assert_eq!(format!("{}", cid), cid_str);
339    }
340
341    #[test]
342    fn test_cidv0_serde() {
343        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8";
344        let cid = CidV0::try_from(cid_str).unwrap();
345
346        let json = serde_json::to_string(&cid).unwrap();
347        assert_eq!(json, format!("\"{}\"", cid_str));
348
349        let deserialized: CidV0 = serde_json::from_str(&json).unwrap();
350        assert_eq!(cid, deserialized);
351    }
352
353    #[test]
354    fn test_cidv1_serde() {
355        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi";
356        let cid = CidV1::new(cid_str.to_string());
357
358        let json = serde_json::to_string(&cid).unwrap();
359        assert_eq!(json, format!("\"{}\"", cid_str));
360
361        let deserialized: CidV1 = serde_json::from_str(&json).unwrap();
362        assert_eq!(cid, deserialized);
363    }
364
365    #[test]
366    fn test_cid_serde_v0() {
367        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8";
368        let cid = Cid::try_from(cid_str).unwrap();
369
370        let json = serde_json::to_string(&cid).unwrap();
371        assert_eq!(json, format!("\"{}\"", cid_str));
372
373        let deserialized: Cid = serde_json::from_str(&json).unwrap();
374        assert_eq!(cid, deserialized);
375        assert!(deserialized.is_v0());
376    }
377
378    #[test]
379    fn test_cid_serde_v1() {
380        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi";
381        let cid = Cid::try_from(cid_str).unwrap();
382
383        let json = serde_json::to_string(&cid).unwrap();
384        assert_eq!(json, format!("\"{}\"", cid_str));
385
386        let deserialized: Cid = serde_json::from_str(&json).unwrap();
387        assert_eq!(cid, deserialized);
388        assert!(deserialized.is_v1());
389    }
390
391    #[test]
392    fn test_cidv0_into_inner() {
393        let cid_str = "QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8".to_string();
394        let cid = CidV0::new(cid_str.clone()).unwrap();
395        assert_eq!(cid.into_inner(), cid_str);
396    }
397
398    #[test]
399    fn test_cidv1_into_inner() {
400        let cid_str = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi".to_string();
401        let cid = CidV1::new(cid_str.clone());
402        assert_eq!(cid.into_inner(), cid_str);
403    }
404}