1use std::convert::TryFrom;
2use std::fmt::{Display, Formatter};
3use thiserror::Error;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8pub struct CidV0(String);
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub struct CidV1(String);
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub enum Cid {
19 V0(CidV0),
21 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 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 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50
51 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 pub fn new(cid: String) -> Self {
83 CidV1(cid)
84 }
85
86 pub fn as_str(&self) -> &str {
88 &self.0
89 }
90
91 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 pub fn v0(cid: CidV0) -> Self {
112 Cid::V0(cid)
113 }
114
115 pub fn v1(cid: CidV1) -> Self {
117 Cid::V1(cid)
118 }
119
120 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 pub fn is_v0(&self) -> bool {
130 matches!(self, Cid::V0(_))
131 }
132
133 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 if value.starts_with("Qm") && value.len() == 46 {
149 return Ok(Cid::V0(CidV0(value)));
150 }
151
152 if value.len() >= 40 {
157 let first_char = value.chars().next().unwrap();
158 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#[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#[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}