ipfrs_core/
cid.rs

1//! Content Identifier (CID) wrapper and utilities
2//!
3//! Provides CID generation, parsing, and multibase encoding/decoding support.
4
5use crate::error::{Error, Result};
6pub use ::cid::Cid;
7use multibase::Base;
8use multihash_codetable::{Code, MultihashDigest};
9use serde::{Deserialize, Serialize};
10
11/// Hash algorithm to use for CID generation
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum HashAlgorithm {
14    /// SHA2-256 (default, most compatible with IPFS)
15    #[default]
16    Sha256,
17    /// SHA2-512 (64-byte hash for enhanced security)
18    Sha512,
19    /// SHA3-256 (Keccak-based, secure)
20    Sha3_256,
21    /// SHA3-512 (Keccak-based, 64-byte hash)
22    Sha3_512,
23    /// BLAKE2b-256 (fast, 32-byte hash, optimized for 64-bit)
24    Blake2b256,
25    /// BLAKE2b-512 (fast, 64-byte hash, maximum security)
26    Blake2b512,
27    /// BLAKE2s-256 (fast, 32-byte hash, optimized for 8-32 bit)
28    Blake2s256,
29    /// BLAKE3 (fastest, 32-byte hash, modern cryptographic design)
30    Blake3,
31}
32
33impl HashAlgorithm {
34    /// Get the multihash code for this algorithm
35    #[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    /// Get the name of the hash algorithm
50    #[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    /// Get the expected hash output size in bytes
65    #[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    /// Check if this is a SHA-family algorithm
80    #[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    /// Check if this is a BLAKE-family algorithm
92    #[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    /// Check if this algorithm produces a 32-byte (256-bit) hash
104    #[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    /// Check if this algorithm produces a 64-byte (512-bit) hash
117    #[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    /// Check if this algorithm has SIMD support on the current platform
126    #[inline]
127    pub fn has_simd_support(&self) -> bool {
128        match self {
129            // SHA2 has SIMD on x86_64 (SHA-NI, AVX2) and ARM (NEON)
130            HashAlgorithm::Sha256 | HashAlgorithm::Sha512 => {
131                cfg!(target_arch = "x86_64") || cfg!(target_arch = "aarch64")
132            }
133            // SHA3 has optimized implementation
134            HashAlgorithm::Sha3_256 | HashAlgorithm::Sha3_512 => true,
135            // BLAKE2 has SIMD support
136            HashAlgorithm::Blake2b256 | HashAlgorithm::Blake2b512 | HashAlgorithm::Blake2s256 => {
137                true
138            }
139            // BLAKE3 has built-in SIMD
140            HashAlgorithm::Blake3 => true,
141        }
142    }
143
144    /// Get all available hash algorithms
145    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/// Multibase encoding options for CID string representation
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
185pub enum MultibaseEncoding {
186    /// Base32 lower case (default for CIDv1, starts with 'b')
187    #[default]
188    Base32Lower,
189    /// Base58 Bitcoin (classic IPFS format, starts with 'z' for CIDv1)
190    Base58Btc,
191    /// Base64 standard (starts with 'm')
192    Base64,
193    /// Base64 URL-safe (starts with 'u')
194    Base64Url,
195    /// Base32 upper case (starts with 'B')
196    Base32Upper,
197}
198
199impl MultibaseEncoding {
200    /// Get the multibase base for this encoding
201    #[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    /// Get the multibase prefix character for this encoding
213    #[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    /// Get the name of this encoding
225    #[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    /// Detect encoding from a CID string prefix
237    #[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    /// Get all available multibase encodings
250    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/// CID builder for creating content identifiers
268#[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, // raw codec
280            hash_algorithm: HashAlgorithm::Sha256,
281        }
282    }
283}
284
285impl CidBuilder {
286    /// Creates a new `CidBuilder` with default settings.
287    ///
288    /// Uses CIDv1, raw codec (0x55), and SHA2-256 hash algorithm by default.
289    #[inline]
290    pub fn new() -> Self {
291        Self::default()
292    }
293
294    /// Create a builder configured for CIDv0 (legacy IPFS format)
295    ///
296    /// CIDv0 uses:
297    /// - SHA2-256 hash algorithm
298    /// - DAG-PB codec (0x70)
299    /// - Base58btc encoding (implicit, starts with "Qm")
300    pub fn v0() -> Self {
301        Self {
302            version: cid::Version::V0,
303            codec: 0x70, // DAG-PB
304            hash_algorithm: HashAlgorithm::Sha256,
305        }
306    }
307
308    /// Sets the CID version (V0 or V1).
309    ///
310    /// Note: CIDv0 has restrictions (must use SHA2-256 and DAG-PB codec).
311    pub fn version(mut self, version: cid::Version) -> Self {
312        self.version = version;
313        self
314    }
315
316    /// Sets the codec for the CID.
317    ///
318    /// Common codecs:
319    /// - `0x55` - raw binary
320    /// - `0x70` - DAG-PB (IPFS DAG protobuf)
321    /// - `0x71` - DAG-CBOR
322    /// - `0x0129` - DAG-JSON
323    pub fn codec(mut self, codec: u64) -> Self {
324        self.codec = codec;
325        self
326    }
327
328    /// Sets the hash algorithm to use for CID generation.
329    ///
330    /// Supported algorithms: SHA2-256, SHA3-256, BLAKE3.
331    pub fn hash_algorithm(mut self, algorithm: HashAlgorithm) -> Self {
332        self.hash_algorithm = algorithm;
333        self
334    }
335
336    /// Build a CID from data using the configured hash algorithm
337    pub fn build(&self, data: &[u8]) -> Result<Cid> {
338        let hash = self.hash_algorithm.code().digest(data);
339
340        // CIDv0 requires SHA2-256 and DAG-PB codec
341        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            // CIDv0 always uses DAG-PB codec implicitly
348            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    /// Build a CID using DAG-CBOR codec
357    pub fn build_dag_cbor(&self, data: &[u8]) -> Result<Cid> {
358        let mut builder = self.clone();
359        builder.codec = 0x71; // DAG-CBOR codec
360        builder.build(data)
361    }
362
363    /// Build a CID using raw codec (default)
364    pub fn build_raw(&self, data: &[u8]) -> Result<Cid> {
365        let mut builder = self.clone();
366        builder.codec = 0x55; // raw codec
367        builder.build(data)
368    }
369
370    /// Build a CIDv0 (legacy format) from data
371    ///
372    /// This is a convenience method that creates a CIDv0 regardless
373    /// of the builder's current configuration.
374    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
380/// Extension trait for CID with additional encoding utilities
381pub trait CidExt {
382    /// Encode the CID to a string with the specified multibase encoding
383    fn to_string_with_base(&self, base: MultibaseEncoding) -> String;
384
385    /// Convert CIDv0 to CIDv1
386    ///
387    /// CIDv0 is converted to CIDv1 with DAG-PB codec (0x70)
388    fn to_v1(&self) -> Result<Cid>;
389
390    /// Try to convert CIDv1 to CIDv0
391    ///
392    /// This only succeeds if:
393    /// - The hash algorithm is SHA2-256
394    /// - The codec is DAG-PB (0x70)
395    fn to_v0(&self) -> Result<Cid>;
396
397    /// Check if this CID can be represented as CIDv0
398    ///
399    /// Returns true if the CID uses SHA2-256 and DAG-PB codec
400    fn can_be_v0(&self) -> bool;
401
402    /// Check if this is a CIDv0
403    fn is_v0(&self) -> bool;
404
405    /// Check if this is a CIDv1
406    fn is_v1(&self) -> bool;
407
408    /// Get the codec code
409    fn codec_code(&self) -> u64;
410
411    /// Get the hash algorithm name
412    fn hash_algorithm_name(&self) -> &'static str;
413
414    /// Get the hash algorithm code
415    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        // CIDv0 is always SHA2-256 with DAG-PB codec
430        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        // CIDv0 requires SHA2-256 hash algorithm
439        if self.hash().code() != 0x12 {
440            return Err(Error::InvalidInput(
441                "CIDv0 requires SHA2-256 hash algorithm".to_string(),
442            ));
443        }
444
445        // CIDv0 requires DAG-PB codec
446        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        // CIDv0 requires SHA2-256 (code 0x12) and DAG-PB codec (0x70)
457        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", // SHA3-256 (Keccak)
476            0x16 => "sha3-256", // SHA3-256 alternative code
477            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
488/// Parse a CID from a multibase-encoded string with automatic base detection
489pub fn parse_cid(s: &str) -> Result<Cid> {
490    s.parse()
491        .map_err(|e| Error::Cid(format!("Failed to parse CID: {}", e)))
492}
493
494/// Parse a CID from a multibase-encoded string and return the detected base
495pub 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    // Check if it looks like CIDv0 (starts with 'Qm')
502    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/// Serializable CID wrapper
520#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
521#[serde(transparent)]
522pub struct SerializableCid(#[serde(with = "cid_serde")] pub Cid);
523
524impl SerializableCid {
525    /// Create a new SerializableCid from a Cid
526    pub fn new(cid: Cid) -> Self {
527        Self(cid)
528    }
529
530    /// Get the inner CID
531    pub fn inner(&self) -> &Cid {
532        &self.0
533    }
534
535    /// Convert to string with specified encoding
536    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
581/// Common IPLD codec constants
582pub mod codec {
583    /// Raw binary data
584    pub const RAW: u64 = 0x55;
585    /// DAG-CBOR (IPLD CBOR)
586    pub const DAG_CBOR: u64 = 0x71;
587    /// DAG-JSON (IPLD JSON)
588    pub const DAG_JSON: u64 = 0x0129;
589    /// DAG-PB (Protocol Buffers, used by IPFS UnixFS)
590    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        // Test different encodings
602        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        // All should parse back to the same CID
611        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        // Encode with base32
625        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        // Test JSON serialization
648        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        // Create CIDv0 using the builder
657        let cid_v0 = CidBuilder::v0().build(b"hello world").unwrap();
658
659        // CIDv0 should start with "Qm"
660        let cid_str = cid_v0.to_string();
661        assert!(cid_str.starts_with("Qm"), "CIDv0 should start with 'Qm'");
662
663        // Verify it's actually v0
664        assert!(cid_v0.is_v0());
665        assert!(!cid_v0.is_v1());
666    }
667
668    #[test]
669    fn test_cidv0_build_v0_method() {
670        // Build CIDv0 using convenience method
671        let cid_v0 = CidBuilder::new().build_v0(b"test data").unwrap();
672
673        // Verify it's v0
674        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        // v1 should be different but represent the same content
684        assert!(cid_v1.is_v1());
685        assert!(!cid_v1.is_v0());
686
687        // The hash should be the same
688        assert_eq!(cid_v0.hash(), cid_v1.hash());
689
690        // v1 should have DAG-PB codec (from v0)
691        assert_eq!(cid_v1.codec_code(), codec::DAG_PB);
692    }
693
694    #[test]
695    fn test_cidv1_to_v0_conversion() {
696        // Create a CIDv1 with DAG-PB codec and SHA2-256 (compatible with v0)
697        let cid_v1 = CidBuilder::new()
698            .codec(codec::DAG_PB)
699            .build(b"test")
700            .unwrap();
701
702        // Convert to v0
703        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        // Create a CIDv1 with RAW codec (not compatible with v0)
712        let cid_v1 = CidBuilder::new().build(b"test").unwrap();
713
714        // Should fail because RAW codec is not compatible with v0
715        let result = cid_v1.to_v0();
716        assert!(result.is_err());
717    }
718
719    #[test]
720    fn test_can_be_v0() {
721        // CIDv1 with DAG-PB and SHA2-256 can be v0
722        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        // CIDv1 with RAW codec cannot be v0
729        let cid_incompatible = CidBuilder::new().build(b"test").unwrap();
730        assert!(!cid_incompatible.can_be_v0());
731
732        // CIDv1 with SHA3-256 cannot be v0
733        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        // Create and stringify a CIDv0
744        let original = CidBuilder::v0().build(b"hello ipfs").unwrap();
745        let cid_str = original.to_string();
746
747        // Parse it back
748        let parsed = parse_cid(&cid_str).unwrap();
749        assert_eq!(original, parsed);
750
751        // Test with parse_cid_with_base
752        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        // Create v0
762        let cid_v0 = CidBuilder::v0().build(data).unwrap();
763
764        // Convert to v1
765        let cid_v1 = cid_v0.to_v1().unwrap();
766
767        // Convert back to v0
768        let cid_v0_again = cid_v1.to_v0().unwrap();
769
770        // Should be equal to original
771        assert_eq!(cid_v0, cid_v0_again);
772    }
773}