1use crate::error::{Error, Result};
6pub use ::cid::Cid;
7use multibase::Base;
8use multihash_codetable::{Code, MultihashDigest};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum HashAlgorithm {
14 #[default]
16 Sha256,
17 Sha512,
19 Sha3_256,
21 Sha3_512,
23 Blake2b256,
25 Blake2b512,
27 Blake2s256,
29 Blake3,
31}
32
33impl HashAlgorithm {
34 #[inline]
36 pub fn code(&self) -> Code {
37 match self {
38 HashAlgorithm::Sha256 => Code::Sha2_256,
39 HashAlgorithm::Sha512 => Code::Sha2_512,
40 HashAlgorithm::Sha3_256 => Code::Sha3_256,
41 HashAlgorithm::Sha3_512 => Code::Sha3_512,
42 HashAlgorithm::Blake2b256 => Code::Blake2b256,
43 HashAlgorithm::Blake2b512 => Code::Blake2b512,
44 HashAlgorithm::Blake2s256 => Code::Blake2s256,
45 HashAlgorithm::Blake3 => Code::Blake3_256,
46 }
47 }
48
49 #[inline]
51 pub const fn name(&self) -> &'static str {
52 match self {
53 HashAlgorithm::Sha256 => "SHA2-256",
54 HashAlgorithm::Sha512 => "SHA2-512",
55 HashAlgorithm::Sha3_256 => "SHA3-256",
56 HashAlgorithm::Sha3_512 => "SHA3-512",
57 HashAlgorithm::Blake2b256 => "BLAKE2b-256",
58 HashAlgorithm::Blake2b512 => "BLAKE2b-512",
59 HashAlgorithm::Blake2s256 => "BLAKE2s-256",
60 HashAlgorithm::Blake3 => "BLAKE3",
61 }
62 }
63
64 #[inline]
66 pub const fn hash_size(&self) -> usize {
67 match self {
68 HashAlgorithm::Sha256 => 32,
69 HashAlgorithm::Sha512 => 64,
70 HashAlgorithm::Sha3_256 => 32,
71 HashAlgorithm::Sha3_512 => 64,
72 HashAlgorithm::Blake2b256 => 32,
73 HashAlgorithm::Blake2b512 => 64,
74 HashAlgorithm::Blake2s256 => 32,
75 HashAlgorithm::Blake3 => 32,
76 }
77 }
78
79 #[inline]
81 pub const fn is_sha(&self) -> bool {
82 matches!(
83 self,
84 HashAlgorithm::Sha256
85 | HashAlgorithm::Sha512
86 | HashAlgorithm::Sha3_256
87 | HashAlgorithm::Sha3_512
88 )
89 }
90
91 #[inline]
93 pub const fn is_blake(&self) -> bool {
94 matches!(
95 self,
96 HashAlgorithm::Blake2b256
97 | HashAlgorithm::Blake2b512
98 | HashAlgorithm::Blake2s256
99 | HashAlgorithm::Blake3
100 )
101 }
102
103 #[inline]
105 pub const fn is_256_bit(&self) -> bool {
106 matches!(
107 self,
108 HashAlgorithm::Sha256
109 | HashAlgorithm::Sha3_256
110 | HashAlgorithm::Blake2b256
111 | HashAlgorithm::Blake2s256
112 | HashAlgorithm::Blake3
113 )
114 }
115
116 #[inline]
118 pub const fn is_512_bit(&self) -> bool {
119 matches!(
120 self,
121 HashAlgorithm::Sha512 | HashAlgorithm::Sha3_512 | HashAlgorithm::Blake2b512
122 )
123 }
124
125 #[inline]
127 pub fn has_simd_support(&self) -> bool {
128 match self {
129 HashAlgorithm::Sha256 | HashAlgorithm::Sha512 => {
131 cfg!(target_arch = "x86_64") || cfg!(target_arch = "aarch64")
132 }
133 HashAlgorithm::Sha3_256 | HashAlgorithm::Sha3_512 => true,
135 HashAlgorithm::Blake2b256 | HashAlgorithm::Blake2b512 | HashAlgorithm::Blake2s256 => {
137 true
138 }
139 HashAlgorithm::Blake3 => true,
141 }
142 }
143
144 pub fn all() -> &'static [HashAlgorithm] {
146 &[
147 HashAlgorithm::Sha256,
148 HashAlgorithm::Sha512,
149 HashAlgorithm::Sha3_256,
150 HashAlgorithm::Sha3_512,
151 HashAlgorithm::Blake2b256,
152 HashAlgorithm::Blake2b512,
153 HashAlgorithm::Blake2s256,
154 HashAlgorithm::Blake3,
155 ]
156 }
157}
158
159impl std::fmt::Display for HashAlgorithm {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 write!(f, "{}", self.name())
162 }
163}
164
165impl std::str::FromStr for HashAlgorithm {
166 type Err = Error;
167
168 fn from_str(s: &str) -> Result<Self> {
169 match s.to_uppercase().as_str() {
170 "SHA256" | "SHA2-256" | "SHA-256" => Ok(HashAlgorithm::Sha256),
171 "SHA512" | "SHA2-512" | "SHA-512" => Ok(HashAlgorithm::Sha512),
172 "SHA3-256" | "SHA3_256" => Ok(HashAlgorithm::Sha3_256),
173 "SHA3-512" | "SHA3_512" => Ok(HashAlgorithm::Sha3_512),
174 "BLAKE2B256" | "BLAKE2B-256" | "BLAKE2B_256" => Ok(HashAlgorithm::Blake2b256),
175 "BLAKE2B512" | "BLAKE2B-512" | "BLAKE2B_512" => Ok(HashAlgorithm::Blake2b512),
176 "BLAKE2S256" | "BLAKE2S-256" | "BLAKE2S_256" => Ok(HashAlgorithm::Blake2s256),
177 "BLAKE3" | "BLAKE3-256" | "BLAKE3_256" => Ok(HashAlgorithm::Blake3),
178 _ => Err(Error::InvalidData(format!("Unknown hash algorithm: {}", s))),
179 }
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
185pub enum MultibaseEncoding {
186 #[default]
188 Base32Lower,
189 Base58Btc,
191 Base64,
193 Base64Url,
195 Base32Upper,
197}
198
199impl MultibaseEncoding {
200 #[inline]
202 pub fn base(&self) -> Base {
203 match self {
204 MultibaseEncoding::Base32Lower => Base::Base32Lower,
205 MultibaseEncoding::Base58Btc => Base::Base58Btc,
206 MultibaseEncoding::Base64 => Base::Base64,
207 MultibaseEncoding::Base64Url => Base::Base64Url,
208 MultibaseEncoding::Base32Upper => Base::Base32Upper,
209 }
210 }
211
212 #[inline]
214 pub const fn prefix(&self) -> char {
215 match self {
216 MultibaseEncoding::Base32Lower => 'b',
217 MultibaseEncoding::Base58Btc => 'z',
218 MultibaseEncoding::Base64 => 'm',
219 MultibaseEncoding::Base64Url => 'u',
220 MultibaseEncoding::Base32Upper => 'B',
221 }
222 }
223
224 #[inline]
226 pub const fn name(&self) -> &'static str {
227 match self {
228 MultibaseEncoding::Base32Lower => "base32 (lowercase)",
229 MultibaseEncoding::Base58Btc => "base58btc",
230 MultibaseEncoding::Base64 => "base64",
231 MultibaseEncoding::Base64Url => "base64url",
232 MultibaseEncoding::Base32Upper => "base32 (uppercase)",
233 }
234 }
235
236 #[inline]
238 pub const fn from_prefix(c: char) -> Option<Self> {
239 match c {
240 'b' => Some(MultibaseEncoding::Base32Lower),
241 'z' => Some(MultibaseEncoding::Base58Btc),
242 'm' => Some(MultibaseEncoding::Base64),
243 'u' => Some(MultibaseEncoding::Base64Url),
244 'B' => Some(MultibaseEncoding::Base32Upper),
245 _ => None,
246 }
247 }
248
249 pub const fn all() -> &'static [MultibaseEncoding] {
251 &[
252 MultibaseEncoding::Base32Lower,
253 MultibaseEncoding::Base58Btc,
254 MultibaseEncoding::Base64,
255 MultibaseEncoding::Base64Url,
256 MultibaseEncoding::Base32Upper,
257 ]
258 }
259}
260
261impl std::fmt::Display for MultibaseEncoding {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 write!(f, "{}", self.name())
264 }
265}
266
267#[derive(Debug, Clone)]
269pub struct CidBuilder {
270 version: cid::Version,
271 codec: u64,
272 hash_algorithm: HashAlgorithm,
273}
274
275impl Default for CidBuilder {
276 fn default() -> Self {
277 Self {
278 version: cid::Version::V1,
279 codec: 0x55, hash_algorithm: HashAlgorithm::Sha256,
281 }
282 }
283}
284
285impl CidBuilder {
286 #[inline]
290 pub fn new() -> Self {
291 Self::default()
292 }
293
294 pub fn v0() -> Self {
301 Self {
302 version: cid::Version::V0,
303 codec: 0x70, hash_algorithm: HashAlgorithm::Sha256,
305 }
306 }
307
308 pub fn version(mut self, version: cid::Version) -> Self {
312 self.version = version;
313 self
314 }
315
316 pub fn codec(mut self, codec: u64) -> Self {
324 self.codec = codec;
325 self
326 }
327
328 pub fn hash_algorithm(mut self, algorithm: HashAlgorithm) -> Self {
332 self.hash_algorithm = algorithm;
333 self
334 }
335
336 pub fn build(&self, data: &[u8]) -> Result<Cid> {
338 let hash = self.hash_algorithm.code().digest(data);
339
340 if self.version == cid::Version::V0 {
342 if self.hash_algorithm != HashAlgorithm::Sha256 {
343 return Err(Error::InvalidInput(
344 "CIDv0 requires SHA2-256 hash algorithm".to_string(),
345 ));
346 }
347 return Cid::new_v0(hash)
349 .map_err(|e| Error::Cid(format!("Failed to create CIDv0: {}", e)));
350 }
351
352 Cid::new(self.version, self.codec, hash)
353 .map_err(|e| Error::Cid(format!("Failed to create CID: {}", e)))
354 }
355
356 pub fn build_dag_cbor(&self, data: &[u8]) -> Result<Cid> {
358 let mut builder = self.clone();
359 builder.codec = 0x71; builder.build(data)
361 }
362
363 pub fn build_raw(&self, data: &[u8]) -> Result<Cid> {
365 let mut builder = self.clone();
366 builder.codec = 0x55; builder.build(data)
368 }
369
370 pub fn build_v0(&self, data: &[u8]) -> Result<Cid> {
375 let hash = Code::Sha2_256.digest(data);
376 Cid::new_v0(hash).map_err(|e| Error::Cid(format!("Failed to create CIDv0: {}", e)))
377 }
378}
379
380pub trait CidExt {
382 fn to_string_with_base(&self, base: MultibaseEncoding) -> String;
384
385 fn to_v1(&self) -> Result<Cid>;
389
390 fn to_v0(&self) -> Result<Cid>;
396
397 fn can_be_v0(&self) -> bool;
401
402 fn is_v0(&self) -> bool;
404
405 fn is_v1(&self) -> bool;
407
408 fn codec_code(&self) -> u64;
410
411 fn hash_algorithm_name(&self) -> &'static str;
413
414 fn hash_algorithm_code(&self) -> u64;
416}
417
418impl CidExt for Cid {
419 fn to_string_with_base(&self, base: MultibaseEncoding) -> String {
420 let bytes = self.to_bytes();
421 multibase::encode(base.base(), bytes)
422 }
423
424 fn to_v1(&self) -> Result<Cid> {
425 if self.version() == cid::Version::V1 {
426 return Ok(*self);
427 }
428
429 Ok(Cid::new_v1(codec::DAG_PB, *self.hash()))
431 }
432
433 fn to_v0(&self) -> Result<Cid> {
434 if self.version() == cid::Version::V0 {
435 return Ok(*self);
436 }
437
438 if self.hash().code() != 0x12 {
440 return Err(Error::InvalidInput(
441 "CIDv0 requires SHA2-256 hash algorithm".to_string(),
442 ));
443 }
444
445 if self.codec() != codec::DAG_PB {
447 return Err(Error::InvalidInput(
448 "CIDv0 requires DAG-PB codec (0x70)".to_string(),
449 ));
450 }
451
452 Cid::new_v0(*self.hash()).map_err(|e| Error::Cid(format!("Failed to create CIDv0: {}", e)))
453 }
454
455 fn can_be_v0(&self) -> bool {
456 self.hash().code() == 0x12 && self.codec() == codec::DAG_PB
458 }
459
460 fn is_v0(&self) -> bool {
461 self.version() == cid::Version::V0
462 }
463
464 fn is_v1(&self) -> bool {
465 self.version() == cid::Version::V1
466 }
467
468 fn codec_code(&self) -> u64 {
469 self.codec()
470 }
471
472 fn hash_algorithm_name(&self) -> &'static str {
473 match self.hash().code() {
474 0x12 => "sha2-256",
475 0x14 => "sha3-256", 0x16 => "sha3-256", 0x1b => "keccak-256",
478 0x1e => "blake2b-256",
479 _ => "unknown",
480 }
481 }
482
483 fn hash_algorithm_code(&self) -> u64 {
484 self.hash().code()
485 }
486}
487
488pub fn parse_cid(s: &str) -> Result<Cid> {
490 s.parse()
491 .map_err(|e| Error::Cid(format!("Failed to parse CID: {}", e)))
492}
493
494pub fn parse_cid_with_base(s: &str) -> Result<(Cid, MultibaseEncoding)> {
496 let first_char = s
497 .chars()
498 .next()
499 .ok_or_else(|| Error::Cid("Empty CID string".to_string()))?;
500
501 if s.starts_with("Qm") {
503 let cid: Cid = s
504 .parse()
505 .map_err(|e| Error::Cid(format!("Failed to parse CIDv0: {}", e)))?;
506 return Ok((cid, MultibaseEncoding::Base58Btc));
507 }
508
509 let base = MultibaseEncoding::from_prefix(first_char)
510 .ok_or_else(|| Error::Cid(format!("Unknown multibase prefix: {}", first_char)))?;
511
512 let cid: Cid = s
513 .parse()
514 .map_err(|e| Error::Cid(format!("Failed to parse CID: {}", e)))?;
515
516 Ok((cid, base))
517}
518
519#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
521#[serde(transparent)]
522pub struct SerializableCid(#[serde(with = "cid_serde")] pub Cid);
523
524impl SerializableCid {
525 pub fn new(cid: Cid) -> Self {
527 Self(cid)
528 }
529
530 pub fn inner(&self) -> &Cid {
532 &self.0
533 }
534
535 pub fn to_string_with_base(&self, base: MultibaseEncoding) -> String {
537 self.0.to_string_with_base(base)
538 }
539}
540
541impl Copy for SerializableCid {}
542
543impl From<Cid> for SerializableCid {
544 fn from(cid: Cid) -> Self {
545 Self(cid)
546 }
547}
548
549impl From<SerializableCid> for Cid {
550 fn from(cid: SerializableCid) -> Self {
551 cid.0
552 }
553}
554
555impl std::fmt::Display for SerializableCid {
556 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
557 write!(f, "{}", self.0)
558 }
559}
560
561mod cid_serde {
562 use super::*;
563 use serde::{Deserializer, Serializer};
564
565 pub fn serialize<S>(cid: &Cid, serializer: S) -> std::result::Result<S::Ok, S::Error>
566 where
567 S: Serializer,
568 {
569 serializer.serialize_str(&cid.to_string())
570 }
571
572 pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result<Cid, D::Error>
573 where
574 D: Deserializer<'de>,
575 {
576 let s = String::deserialize(deserializer)?;
577 s.parse().map_err(serde::de::Error::custom)
578 }
579}
580
581pub mod codec {
583 pub const RAW: u64 = 0x55;
585 pub const DAG_CBOR: u64 = 0x71;
587 pub const DAG_JSON: u64 = 0x0129;
589 pub const DAG_PB: u64 = 0x70;
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596
597 #[test]
598 fn test_multibase_encoding() {
599 let cid = CidBuilder::new().build(b"hello world").unwrap();
600
601 let base32 = cid.to_string_with_base(MultibaseEncoding::Base32Lower);
603 let base58 = cid.to_string_with_base(MultibaseEncoding::Base58Btc);
604 let base64 = cid.to_string_with_base(MultibaseEncoding::Base64);
605
606 assert!(base32.starts_with('b'));
607 assert!(base58.starts_with('z'));
608 assert!(base64.starts_with('m'));
609
610 let parsed32: Cid = base32.parse().unwrap();
612 let parsed58: Cid = base58.parse().unwrap();
613 let parsed64: Cid = base64.parse().unwrap();
614
615 assert_eq!(cid, parsed32);
616 assert_eq!(cid, parsed58);
617 assert_eq!(cid, parsed64);
618 }
619
620 #[test]
621 fn test_parse_cid_with_base() {
622 let cid = CidBuilder::new().build(b"test data").unwrap();
623
624 let base32_str = cid.to_string_with_base(MultibaseEncoding::Base32Lower);
626 let (parsed_cid, detected_base) = parse_cid_with_base(&base32_str).unwrap();
627
628 assert_eq!(cid, parsed_cid);
629 assert_eq!(detected_base, MultibaseEncoding::Base32Lower);
630 }
631
632 #[test]
633 fn test_cid_ext_methods() {
634 let cid = CidBuilder::new().build(b"test").unwrap();
635
636 assert!(cid.is_v1());
637 assert!(!cid.is_v0());
638 assert_eq!(cid.codec_code(), codec::RAW);
639 assert_eq!(cid.hash_algorithm_name(), "sha2-256");
640 }
641
642 #[test]
643 fn test_serializable_cid() {
644 let cid = CidBuilder::new().build(b"test").unwrap();
645 let serializable = SerializableCid::new(cid);
646
647 let json = serde_json::to_string(&serializable).unwrap();
649 let deserialized: SerializableCid = serde_json::from_str(&json).unwrap();
650
651 assert_eq!(serializable, deserialized);
652 }
653
654 #[test]
655 fn test_cidv0_creation() {
656 let cid_v0 = CidBuilder::v0().build(b"hello world").unwrap();
658
659 let cid_str = cid_v0.to_string();
661 assert!(cid_str.starts_with("Qm"), "CIDv0 should start with 'Qm'");
662
663 assert!(cid_v0.is_v0());
665 assert!(!cid_v0.is_v1());
666 }
667
668 #[test]
669 fn test_cidv0_build_v0_method() {
670 let cid_v0 = CidBuilder::new().build_v0(b"test data").unwrap();
672
673 assert!(cid_v0.is_v0());
675 assert!(cid_v0.to_string().starts_with("Qm"));
676 }
677
678 #[test]
679 fn test_cidv0_to_v1_conversion() {
680 let cid_v0 = CidBuilder::v0().build(b"test").unwrap();
681 let cid_v1 = cid_v0.to_v1().unwrap();
682
683 assert!(cid_v1.is_v1());
685 assert!(!cid_v1.is_v0());
686
687 assert_eq!(cid_v0.hash(), cid_v1.hash());
689
690 assert_eq!(cid_v1.codec_code(), codec::DAG_PB);
692 }
693
694 #[test]
695 fn test_cidv1_to_v0_conversion() {
696 let cid_v1 = CidBuilder::new()
698 .codec(codec::DAG_PB)
699 .build(b"test")
700 .unwrap();
701
702 let cid_v0 = cid_v1.to_v0().unwrap();
704
705 assert!(cid_v0.is_v0());
706 assert!(cid_v0.to_string().starts_with("Qm"));
707 }
708
709 #[test]
710 fn test_cidv1_to_v0_fails_wrong_codec() {
711 let cid_v1 = CidBuilder::new().build(b"test").unwrap();
713
714 let result = cid_v1.to_v0();
716 assert!(result.is_err());
717 }
718
719 #[test]
720 fn test_can_be_v0() {
721 let cid_compatible = CidBuilder::new()
723 .codec(codec::DAG_PB)
724 .build(b"test")
725 .unwrap();
726 assert!(cid_compatible.can_be_v0());
727
728 let cid_incompatible = CidBuilder::new().build(b"test").unwrap();
730 assert!(!cid_incompatible.can_be_v0());
731
732 let cid_sha3 = CidBuilder::new()
734 .codec(codec::DAG_PB)
735 .hash_algorithm(HashAlgorithm::Sha3_256)
736 .build(b"test")
737 .unwrap();
738 assert!(!cid_sha3.can_be_v0());
739 }
740
741 #[test]
742 fn test_parse_cidv0_string() {
743 let original = CidBuilder::v0().build(b"hello ipfs").unwrap();
745 let cid_str = original.to_string();
746
747 let parsed = parse_cid(&cid_str).unwrap();
749 assert_eq!(original, parsed);
750
751 let (parsed2, base) = parse_cid_with_base(&cid_str).unwrap();
753 assert_eq!(original, parsed2);
754 assert_eq!(base, MultibaseEncoding::Base58Btc);
755 }
756
757 #[test]
758 fn test_cidv0_roundtrip() {
759 let data = b"test content for roundtrip";
760
761 let cid_v0 = CidBuilder::v0().build(data).unwrap();
763
764 let cid_v1 = cid_v0.to_v1().unwrap();
766
767 let cid_v0_again = cid_v1.to_v0().unwrap();
769
770 assert_eq!(cid_v0, cid_v0_again);
772 }
773}