Skip to main content

aleph_cid/
cid.rs

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