ipfrs_core/
car.rs

1//! CAR (Content Addressable aRchive) format support.
2//!
3//! This module provides utilities for reading and writing CAR files, which are
4//! used to package and transfer IPLD blocks in the IPFS ecosystem.
5//!
6//! CAR (CARv1) format structure:
7//! - Header: CBOR-encoded with version and root CIDs
8//! - Blocks: Sequence of length-prefixed blocks (varint length + CID + data)
9//!
10//! # Examples
11//!
12//! ```rust
13//! use ipfrs_core::{Block, car::{CarWriter, CarReader}};
14//! use bytes::Bytes;
15//!
16//! // Create some blocks
17//! let block1 = Block::new(Bytes::from_static(b"Hello, CAR!")).unwrap();
18//! let block2 = Block::new(Bytes::from_static(b"CAR format test")).unwrap();
19//!
20//! // Write to CAR format
21//! let mut car_data = Vec::new();
22//! let mut writer = CarWriter::new(&mut car_data, vec![*block1.cid()]).unwrap();
23//! writer.write_block(&block1).unwrap();
24//! writer.write_block(&block2).unwrap();
25//! writer.finish().unwrap();
26//!
27//! // Read from CAR format
28//! let reader = CarReader::new(&car_data[..]).unwrap();
29//! let roots = reader.roots();
30//! assert_eq!(roots.len(), 1);
31//! assert_eq!(roots[0], *block1.cid());
32//! ```
33
34use crate::block::Block;
35use crate::cid::{Cid, SerializableCid};
36use crate::compression::CompressionAlgorithm;
37use crate::error::{Error, Result};
38use bytes::Bytes;
39use std::io::{Read, Write};
40
41/// CAR format version 1.
42const CAR_VERSION: u64 = 1;
43
44/// Maximum varint size (10 bytes for 64-bit values).
45const MAX_VARINT_SIZE: usize = 10;
46
47/// CAR file header containing version and root CIDs.
48#[derive(Debug, Clone)]
49pub struct CarHeader {
50    /// CAR format version (always 1 for CARv1).
51    pub version: u64,
52    /// Root CIDs that represent the entry points into the DAG.
53    pub roots: Vec<Cid>,
54}
55
56impl CarHeader {
57    /// Create a new CAR header with the given root CIDs.
58    ///
59    /// # Arguments
60    ///
61    /// * `roots` - Vector of root CIDs
62    ///
63    /// # Examples
64    ///
65    /// ```rust
66    /// use ipfrs_core::{CidBuilder, car::CarHeader};
67    ///
68    /// let cid = CidBuilder::new().build(b"root data").unwrap();
69    /// let header = CarHeader::new(vec![cid]);
70    /// assert_eq!(header.version, 1);
71    /// assert_eq!(header.roots.len(), 1);
72    /// ```
73    pub fn new(roots: Vec<Cid>) -> Self {
74        Self {
75            version: CAR_VERSION,
76            roots,
77        }
78    }
79
80    /// Encode the header to CBOR bytes.
81    fn encode(&self) -> Result<Bytes> {
82        use crate::ipld::Ipld;
83        use std::collections::BTreeMap;
84
85        let mut map = BTreeMap::new();
86        map.insert("version".to_string(), Ipld::Integer(self.version as i128));
87
88        let roots: Vec<Ipld> = self
89            .roots
90            .iter()
91            .map(|cid| Ipld::Link(SerializableCid(*cid)))
92            .collect();
93        map.insert("roots".to_string(), Ipld::List(roots));
94
95        let ipld = Ipld::Map(map);
96        ipld.to_dag_cbor().map(Bytes::from)
97    }
98
99    /// Decode a header from CBOR bytes.
100    fn decode(data: &[u8]) -> Result<Self> {
101        use crate::ipld::Ipld;
102
103        let ipld = Ipld::from_dag_cbor(data)?;
104
105        let map = match ipld {
106            Ipld::Map(m) => m,
107            _ => {
108                return Err(Error::Deserialization(
109                    "CAR header must be a map".to_string(),
110                ))
111            }
112        };
113
114        let version = match map.get("version") {
115            Some(Ipld::Integer(v)) => *v as u64,
116            _ => {
117                return Err(Error::Deserialization(
118                    "CAR header missing version".to_string(),
119                ))
120            }
121        };
122
123        if version != CAR_VERSION {
124            return Err(Error::Deserialization(format!(
125                "Unsupported CAR version: {}",
126                version
127            )));
128        }
129
130        let roots = match map.get("roots") {
131            Some(Ipld::List(list)) => list
132                .iter()
133                .map(|item| match item {
134                    Ipld::Link(SerializableCid(cid)) => Ok(*cid),
135                    _ => Err(Error::Deserialization(
136                        "Invalid root CID in header".to_string(),
137                    )),
138                })
139                .collect::<Result<Vec<Cid>>>()?,
140            _ => {
141                return Err(Error::Deserialization(
142                    "CAR header missing roots".to_string(),
143                ))
144            }
145        };
146
147        Ok(Self { version, roots })
148    }
149}
150
151/// Compression statistics for CAR operations.
152#[derive(Debug, Clone, Default)]
153pub struct CarCompressionStats {
154    /// Total number of blocks processed.
155    pub blocks_processed: usize,
156    /// Total uncompressed bytes written.
157    pub uncompressed_bytes: usize,
158    /// Total compressed bytes written.
159    pub compressed_bytes: usize,
160    /// Number of blocks that were compressed.
161    pub blocks_compressed: usize,
162}
163
164impl CarCompressionStats {
165    /// Create new empty compression statistics.
166    pub fn new() -> Self {
167        Self::default()
168    }
169
170    /// Calculate the compression ratio (compressed / uncompressed).
171    ///
172    /// Returns 1.0 if no compression occurred.
173    pub fn compression_ratio(&self) -> f64 {
174        if self.uncompressed_bytes == 0 {
175            1.0
176        } else {
177            self.compressed_bytes as f64 / self.uncompressed_bytes as f64
178        }
179    }
180
181    /// Calculate bytes saved through compression.
182    pub fn bytes_saved(&self) -> usize {
183        self.uncompressed_bytes
184            .saturating_sub(self.compressed_bytes)
185    }
186
187    /// Calculate compression percentage (0-100).
188    pub fn compression_percentage(&self) -> f64 {
189        if self.uncompressed_bytes == 0 {
190            0.0
191        } else {
192            (self.bytes_saved() as f64 / self.uncompressed_bytes as f64) * 100.0
193        }
194    }
195}
196
197/// Builder for creating a CarWriter with optional compression.
198///
199/// # Examples
200///
201/// ```rust
202/// use ipfrs_core::{CidBuilder, car::CarWriterBuilder, compression::CompressionAlgorithm};
203///
204/// let cid = CidBuilder::new().build(b"root").unwrap();
205/// let mut output = Vec::new();
206/// let writer = CarWriterBuilder::new(vec![cid])
207///     .with_compression(CompressionAlgorithm::Zstd, 3)
208///     .build(&mut output)
209///     .unwrap();
210/// ```
211pub struct CarWriterBuilder {
212    roots: Vec<Cid>,
213    compression: Option<(CompressionAlgorithm, i32)>,
214}
215
216impl CarWriterBuilder {
217    /// Create a new CarWriter builder with the given root CIDs.
218    pub fn new(roots: Vec<Cid>) -> Self {
219        Self {
220            roots,
221            compression: None,
222        }
223    }
224
225    /// Enable compression with the specified algorithm and level.
226    ///
227    /// # Arguments
228    ///
229    /// * `algorithm` - The compression algorithm to use
230    /// * `level` - Compression level (0-9 for Zstd, 0-12 for Lz4)
231    pub fn with_compression(mut self, algorithm: CompressionAlgorithm, level: i32) -> Self {
232        self.compression = Some((algorithm, level));
233        self
234    }
235
236    /// Build the CarWriter with the configured options.
237    pub fn build<W: Write>(self, writer: W) -> Result<CarWriter<W>> {
238        CarWriter::new_with_options(writer, self.roots, self.compression)
239    }
240}
241
242/// Write blocks to CAR format.
243///
244/// CAR files contain a CBOR-encoded header followed by length-prefixed blocks.
245/// Optionally supports block compression for reduced archive sizes.
246pub struct CarWriter<W: Write> {
247    writer: W,
248    header_written: bool,
249    compression: Option<(CompressionAlgorithm, i32)>,
250    stats: CarCompressionStats,
251}
252
253impl<W: Write> CarWriter<W> {
254    /// Create a new CAR writer with the given root CIDs.
255    ///
256    /// For compression support, use `CarWriterBuilder` instead.
257    ///
258    /// # Arguments
259    ///
260    /// * `writer` - The writer to output CAR data to
261    /// * `roots` - Vector of root CIDs for the CAR file
262    ///
263    /// # Examples
264    ///
265    /// ```rust
266    /// use ipfrs_core::{CidBuilder, car::CarWriter};
267    ///
268    /// let cid = CidBuilder::new().build(b"root").unwrap();
269    /// let mut output = Vec::new();
270    /// let writer = CarWriter::new(&mut output, vec![cid]).unwrap();
271    /// ```
272    pub fn new(writer: W, roots: Vec<Cid>) -> Result<Self> {
273        Self::new_with_options(writer, roots, None)
274    }
275
276    /// Create a new CAR writer with optional compression.
277    ///
278    /// This is used internally by `CarWriterBuilder`.
279    ///
280    /// # Arguments
281    ///
282    /// * `writer` - The writer to output CAR data to
283    /// * `roots` - Vector of root CIDs for the CAR file
284    /// * `compression` - Optional compression algorithm and level
285    fn new_with_options(
286        writer: W,
287        roots: Vec<Cid>,
288        compression: Option<(CompressionAlgorithm, i32)>,
289    ) -> Result<Self> {
290        let mut car_writer = Self {
291            writer,
292            header_written: false,
293            compression,
294            stats: CarCompressionStats::new(),
295        };
296        car_writer.write_header(&CarHeader::new(roots))?;
297        Ok(car_writer)
298    }
299
300    /// Write the CAR header.
301    fn write_header(&mut self, header: &CarHeader) -> Result<()> {
302        let header_bytes = header.encode()?;
303        let header_len = header_bytes.len();
304
305        // Write header length as varint
306        write_varint(&mut self.writer, header_len as u64)?;
307
308        // Write header data
309        self.writer.write_all(&header_bytes)?;
310
311        self.header_written = true;
312        Ok(())
313    }
314
315    /// Write a block to the CAR file.
316    ///
317    /// If compression is enabled, the block data will be compressed before writing.
318    ///
319    /// # Arguments
320    ///
321    /// * `block` - The block to write
322    ///
323    /// # Examples
324    ///
325    /// ```rust
326    /// use ipfrs_core::{Block, car::CarWriter};
327    /// use bytes::Bytes;
328    ///
329    /// let block = Block::new(Bytes::from_static(b"test data")).unwrap();
330    /// let mut output = Vec::new();
331    /// let mut writer = CarWriter::new(&mut output, vec![*block.cid()]).unwrap();
332    /// writer.write_block(&block).unwrap();
333    /// ```
334    pub fn write_block(&mut self, block: &Block) -> Result<()> {
335        if !self.header_written {
336            return Err(Error::InvalidData("CAR header not written".to_string()));
337        }
338
339        // Encode CID to bytes
340        let cid_bytes = block.cid().to_bytes();
341        let data = block.data();
342
343        // Update statistics
344        self.stats.blocks_processed += 1;
345        self.stats.uncompressed_bytes += data.len();
346
347        // Compress data if compression is enabled
348        let (final_data, was_compressed) = if let Some((algorithm, level)) = self.compression {
349            let compressed = crate::compression::compress(data, algorithm, level as u8)?;
350            // Only mark as compressed if algorithm is not None
351            let is_compressed = algorithm != CompressionAlgorithm::None;
352            if is_compressed {
353                self.stats.blocks_compressed += 1;
354            }
355            self.stats.compressed_bytes += compressed.len();
356            (compressed, is_compressed)
357        } else {
358            self.stats.compressed_bytes += data.len();
359            (data.clone(), false)
360        };
361
362        // Total length: CID bytes + compression flag (1 byte) + block data
363        let total_len = cid_bytes.len() + 1 + final_data.len();
364
365        // Write length as varint
366        write_varint(&mut self.writer, total_len as u64)?;
367
368        // Write CID
369        self.writer.write_all(&cid_bytes)?;
370
371        // Write compression flag (0 = uncompressed, 1 = compressed)
372        self.writer.write_all(&[was_compressed as u8])?;
373
374        // Write block data (compressed or uncompressed)
375        self.writer.write_all(&final_data)?;
376
377        Ok(())
378    }
379
380    /// Get the compression statistics for this writer.
381    ///
382    /// # Examples
383    ///
384    /// ```rust
385    /// use ipfrs_core::{Block, car::CarWriterBuilder, compression::CompressionAlgorithm};
386    /// use bytes::Bytes;
387    ///
388    /// let block = Block::new(Bytes::from(vec![0u8; 1000])).unwrap();
389    /// let mut output = Vec::new();
390    /// let mut writer = CarWriterBuilder::new(vec![*block.cid()])
391    ///     .with_compression(CompressionAlgorithm::Zstd, 3)
392    ///     .build(&mut output)
393    ///     .unwrap();
394    /// writer.write_block(&block).unwrap();
395    /// let stats = writer.stats();
396    /// assert_eq!(stats.blocks_processed, 1);
397    /// ```
398    pub fn stats(&self) -> &CarCompressionStats {
399        &self.stats
400    }
401
402    /// Finish writing and flush the writer.
403    pub fn finish(mut self) -> Result<()> {
404        self.writer.flush()?;
405        Ok(())
406    }
407}
408
409/// Read blocks from CAR format.
410///
411/// CAR files are read sequentially, yielding blocks one at a time.
412pub struct CarReader<R: Read> {
413    reader: R,
414    header: CarHeader,
415}
416
417impl<R: Read> CarReader<R> {
418    /// Create a new CAR reader.
419    ///
420    /// This reads and parses the CAR header immediately.
421    ///
422    /// # Arguments
423    ///
424    /// * `reader` - The reader to read CAR data from
425    ///
426    /// # Examples
427    ///
428    /// ```rust
429    /// use ipfrs_core::{Block, car::{CarWriter, CarReader}};
430    /// use bytes::Bytes;
431    ///
432    /// let block = Block::new(Bytes::from_static(b"test")).unwrap();
433    /// let mut data = Vec::new();
434    /// let mut writer = CarWriter::new(&mut data, vec![*block.cid()]).unwrap();
435    /// writer.write_block(&block).unwrap();
436    /// writer.finish().unwrap();
437    ///
438    /// let reader = CarReader::new(&data[..]).unwrap();
439    /// assert_eq!(reader.roots().len(), 1);
440    /// ```
441    pub fn new(mut reader: R) -> Result<Self> {
442        // Read header length
443        let header_len = read_varint(&mut reader)?;
444
445        // Read header data
446        let mut header_bytes = vec![0u8; header_len as usize];
447        reader.read_exact(&mut header_bytes)?;
448
449        // Decode header
450        let header = CarHeader::decode(&header_bytes)?;
451
452        Ok(Self { reader, header })
453    }
454
455    /// Get the root CIDs from the CAR header.
456    pub fn roots(&self) -> &[Cid] {
457        &self.header.roots
458    }
459
460    /// Read the next block from the CAR file.
461    ///
462    /// Returns `None` when there are no more blocks.
463    /// Automatically decompresses blocks if they were compressed during writing.
464    ///
465    /// # Examples
466    ///
467    /// ```rust
468    /// use ipfrs_core::{Block, car::{CarWriter, CarReader}};
469    /// use bytes::Bytes;
470    ///
471    /// let block = Block::new(Bytes::from_static(b"data")).unwrap();
472    /// let mut data = Vec::new();
473    /// let mut writer = CarWriter::new(&mut data, vec![*block.cid()]).unwrap();
474    /// writer.write_block(&block).unwrap();
475    /// writer.finish().unwrap();
476    ///
477    /// let mut reader = CarReader::new(&data[..]).unwrap();
478    /// let read_block = reader.read_block().unwrap().unwrap();
479    /// assert_eq!(read_block.cid(), block.cid());
480    /// ```
481    pub fn read_block(&mut self) -> Result<Option<Block>> {
482        // Try to read length varint
483        let total_len = match read_varint_opt(&mut self.reader) {
484            Ok(Some(len)) => len,
485            Ok(None) => return Ok(None), // EOF
486            Err(e) => return Err(e),
487        };
488
489        // Read CID + compression flag + data
490        let mut block_bytes = vec![0u8; total_len as usize];
491        self.reader.read_exact(&mut block_bytes)?;
492
493        let mut cursor = &block_bytes[..];
494
495        // Parse CID
496        let cid = Cid::read_bytes(&mut cursor)
497            .map_err(|e| Error::Cid(format!("Failed to parse CID: {}", e)))?;
498
499        // Read compression flag (may not exist for legacy CAR files)
500        let (is_compressed, data_start) = if !cursor.is_empty() {
501            let flag = cursor[0];
502            if flag == 0 || flag == 1 {
503                // Valid compression flag found
504                (flag == 1, 1)
505            } else {
506                // No compression flag (legacy CAR file)
507                (false, 0)
508            }
509        } else {
510            return Err(Error::Deserialization("Empty block data".to_string()));
511        };
512
513        // Get the block data (after compression flag, if present)
514        let raw_data = &cursor[data_start..];
515
516        // Decompress if necessary
517        let final_data = if is_compressed {
518            let raw_bytes = Bytes::from(raw_data.to_vec());
519            // Try both compression algorithms since we don't store which one was used
520            // Try Zstd first (most common), then Lz4
521            crate::compression::decompress(&raw_bytes, CompressionAlgorithm::Zstd).or_else(
522                |_| crate::compression::decompress(&raw_bytes, CompressionAlgorithm::Lz4),
523            )?
524        } else {
525            Bytes::from(raw_data.to_vec())
526        };
527
528        // Create block
529        let block = Block::new(final_data)?;
530
531        // Verify CID matches
532        if block.cid() != &cid {
533            return Err(Error::InvalidData(format!(
534                "Block CID mismatch: expected {}, got {}",
535                cid,
536                block.cid()
537            )));
538        }
539
540        Ok(Some(block))
541    }
542
543    /// Read all blocks from the CAR file.
544    ///
545    /// # Examples
546    ///
547    /// ```rust
548    /// use ipfrs_core::{Block, car::{CarWriter, CarReader}};
549    /// use bytes::Bytes;
550    ///
551    /// let block1 = Block::new(Bytes::from_static(b"data1")).unwrap();
552    /// let block2 = Block::new(Bytes::from_static(b"data2")).unwrap();
553    /// let mut data = Vec::new();
554    /// let mut writer = CarWriter::new(&mut data, vec![*block1.cid()]).unwrap();
555    /// writer.write_block(&block1).unwrap();
556    /// writer.write_block(&block2).unwrap();
557    /// writer.finish().unwrap();
558    ///
559    /// let mut reader = CarReader::new(&data[..]).unwrap();
560    /// let blocks = reader.read_all_blocks().unwrap();
561    /// assert_eq!(blocks.len(), 2);
562    /// ```
563    pub fn read_all_blocks(&mut self) -> Result<Vec<Block>> {
564        let mut blocks = Vec::new();
565
566        while let Some(block) = self.read_block()? {
567            blocks.push(block);
568        }
569
570        Ok(blocks)
571    }
572}
573
574/// Write a varint-encoded unsigned integer.
575fn write_varint<W: Write>(writer: &mut W, mut value: u64) -> Result<()> {
576    let mut buf = [0u8; MAX_VARINT_SIZE];
577    let mut i = 0;
578
579    loop {
580        let mut byte = (value & 0x7F) as u8;
581        value >>= 7;
582
583        if value != 0 {
584            byte |= 0x80;
585        }
586
587        buf[i] = byte;
588        i += 1;
589
590        if value == 0 {
591            break;
592        }
593    }
594
595    writer.write_all(&buf[..i])?;
596    Ok(())
597}
598
599/// Read a varint-encoded unsigned integer.
600fn read_varint<R: Read>(reader: &mut R) -> Result<u64> {
601    read_varint_opt(reader)?
602        .ok_or_else(|| Error::Deserialization("Unexpected EOF reading varint".to_string()))
603}
604
605/// Read a varint-encoded unsigned integer, returning None on EOF.
606fn read_varint_opt<R: Read>(reader: &mut R) -> Result<Option<u64>> {
607    let mut result = 0u64;
608    let mut shift = 0;
609    let mut buf = [0u8; 1];
610
611    for _ in 0..MAX_VARINT_SIZE {
612        match reader.read_exact(&mut buf) {
613            Ok(()) => {}
614            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof && shift == 0 => {
615                return Ok(None); // EOF at start
616            }
617            Err(e) => return Err(Error::from(e)),
618        }
619
620        let byte = buf[0];
621        result |= ((byte & 0x7F) as u64) << shift;
622
623        if byte & 0x80 == 0 {
624            return Ok(Some(result));
625        }
626
627        shift += 7;
628
629        if shift >= 64 {
630            return Err(Error::Deserialization("Varint too large".to_string()));
631        }
632    }
633
634    Err(Error::Deserialization(
635        "Varint exceeds maximum size".to_string(),
636    ))
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642    use crate::block::Block;
643    use bytes::Bytes;
644
645    #[test]
646    fn test_car_header_encode_decode() {
647        use crate::cid::CidBuilder;
648
649        let cid1 = CidBuilder::new().build(b"test1").unwrap();
650        let cid2 = CidBuilder::new().build(b"test2").unwrap();
651
652        let header = CarHeader::new(vec![cid1, cid2]);
653        let encoded = header.encode().unwrap();
654        let decoded = CarHeader::decode(&encoded).unwrap();
655
656        assert_eq!(decoded.version, 1);
657        assert_eq!(decoded.roots.len(), 2);
658        assert_eq!(decoded.roots[0], cid1);
659        assert_eq!(decoded.roots[1], cid2);
660    }
661
662    #[test]
663    fn test_car_write_read() {
664        let block1 = Block::new(Bytes::from_static(b"Hello, CAR!")).unwrap();
665        let block2 = Block::new(Bytes::from_static(b"CAR format test")).unwrap();
666
667        // Write
668        let mut car_data = Vec::new();
669        let mut writer = CarWriter::new(&mut car_data, vec![*block1.cid()]).unwrap();
670        writer.write_block(&block1).unwrap();
671        writer.write_block(&block2).unwrap();
672        writer.finish().unwrap();
673
674        // Read
675        let mut reader = CarReader::new(&car_data[..]).unwrap();
676
677        assert_eq!(reader.roots().len(), 1);
678        assert_eq!(reader.roots()[0], *block1.cid());
679
680        let read_block1 = reader.read_block().unwrap().unwrap();
681        assert_eq!(read_block1.cid(), block1.cid());
682        assert_eq!(read_block1.data(), block1.data());
683
684        let read_block2 = reader.read_block().unwrap().unwrap();
685        assert_eq!(read_block2.cid(), block2.cid());
686        assert_eq!(read_block2.data(), block2.data());
687
688        assert!(reader.read_block().unwrap().is_none());
689    }
690
691    #[test]
692    fn test_car_read_all_blocks() {
693        let blocks: Vec<Block> = (0..5)
694            .map(|i| Block::new(Bytes::from(format!("Block {}", i))).unwrap())
695            .collect();
696
697        let mut car_data = Vec::new();
698        let mut writer = CarWriter::new(&mut car_data, vec![*blocks[0].cid()]).unwrap();
699
700        for block in &blocks {
701            writer.write_block(block).unwrap();
702        }
703        writer.finish().unwrap();
704
705        let mut reader = CarReader::new(&car_data[..]).unwrap();
706        let read_blocks = reader.read_all_blocks().unwrap();
707
708        assert_eq!(read_blocks.len(), blocks.len());
709
710        for (i, block) in read_blocks.iter().enumerate() {
711            assert_eq!(block.cid(), blocks[i].cid());
712            assert_eq!(block.data(), blocks[i].data());
713        }
714    }
715
716    #[test]
717    fn test_varint_roundtrip() {
718        let test_values = vec![0, 1, 127, 128, 255, 256, 65535, 65536, u64::MAX];
719
720        for value in test_values {
721            let mut buf = Vec::new();
722            write_varint(&mut buf, value).unwrap();
723
724            let mut cursor = &buf[..];
725            let decoded = read_varint(&mut cursor).unwrap();
726
727            assert_eq!(decoded, value);
728        }
729    }
730
731    #[test]
732    fn test_car_empty_roots() {
733        let block = Block::new(Bytes::from_static(b"test")).unwrap();
734
735        let mut car_data = Vec::new();
736        let mut writer = CarWriter::new(&mut car_data, vec![]).unwrap();
737        writer.write_block(&block).unwrap();
738        writer.finish().unwrap();
739
740        let reader = CarReader::new(&car_data[..]).unwrap();
741        assert_eq!(reader.roots().len(), 0);
742    }
743
744    #[test]
745    fn test_car_multiple_roots() {
746        use crate::cid::CidBuilder;
747
748        let cid1 = CidBuilder::new().build(b"root1").unwrap();
749        let cid2 = CidBuilder::new().build(b"root2").unwrap();
750        let cid3 = CidBuilder::new().build(b"root3").unwrap();
751
752        let block = Block::new(Bytes::from_static(b"data")).unwrap();
753
754        let mut car_data = Vec::new();
755        let mut writer = CarWriter::new(&mut car_data, vec![cid1, cid2, cid3]).unwrap();
756        writer.write_block(&block).unwrap();
757        writer.finish().unwrap();
758
759        let reader = CarReader::new(&car_data[..]).unwrap();
760        let roots = reader.roots();
761        assert_eq!(roots.len(), 3);
762        assert_eq!(roots[0], cid1);
763        assert_eq!(roots[1], cid2);
764        assert_eq!(roots[2], cid3);
765    }
766
767    #[test]
768    fn test_car_large_blocks() {
769        // Test with blocks larger than typical sizes
770        let large_data = vec![0x42u8; 1_000_000]; // 1MB block
771        let block = Block::new(Bytes::from(large_data.clone())).unwrap();
772
773        let mut car_data = Vec::new();
774        let mut writer = CarWriter::new(&mut car_data, vec![*block.cid()]).unwrap();
775        writer.write_block(&block).unwrap();
776        writer.finish().unwrap();
777
778        let mut reader = CarReader::new(&car_data[..]).unwrap();
779        let read_block = reader.read_block().unwrap().unwrap();
780
781        assert_eq!(read_block.cid(), block.cid());
782        assert_eq!(read_block.data().len(), large_data.len());
783    }
784
785    #[test]
786    fn test_car_compression_zstd() {
787        use crate::compression::CompressionAlgorithm;
788
789        let block1 = Block::new(Bytes::from(vec![0x42u8; 1000])).unwrap();
790        let block2 = Block::new(Bytes::from(vec![0xAAu8; 2000])).unwrap();
791
792        // Write with Zstd compression
793        let mut car_data = Vec::new();
794        let mut writer = CarWriterBuilder::new(vec![*block1.cid()])
795            .with_compression(CompressionAlgorithm::Zstd, 3)
796            .build(&mut car_data)
797            .unwrap();
798
799        writer.write_block(&block1).unwrap();
800        writer.write_block(&block2).unwrap();
801
802        let stats = writer.stats();
803        assert_eq!(stats.blocks_processed, 2);
804        assert_eq!(stats.blocks_compressed, 2);
805        assert_eq!(stats.uncompressed_bytes, 3000);
806        assert!(stats.compressed_bytes < stats.uncompressed_bytes);
807        assert!(stats.compression_ratio() < 1.0);
808
809        writer.finish().unwrap();
810
811        // Read and verify
812        let mut reader = CarReader::new(&car_data[..]).unwrap();
813        let read_block1 = reader.read_block().unwrap().unwrap();
814        let read_block2 = reader.read_block().unwrap().unwrap();
815
816        assert_eq!(read_block1.cid(), block1.cid());
817        assert_eq!(read_block1.data(), block1.data());
818        assert_eq!(read_block2.cid(), block2.cid());
819        assert_eq!(read_block2.data(), block2.data());
820    }
821
822    #[test]
823    fn test_car_compression_lz4() {
824        use crate::compression::CompressionAlgorithm;
825
826        let block = Block::new(Bytes::from(vec![0x11u8; 5000])).unwrap();
827
828        // Write with LZ4 compression
829        let mut car_data = Vec::new();
830        let mut writer = CarWriterBuilder::new(vec![*block.cid()])
831            .with_compression(CompressionAlgorithm::Lz4, 1)
832            .build(&mut car_data)
833            .unwrap();
834
835        writer.write_block(&block).unwrap();
836
837        let stats = writer.stats();
838        assert_eq!(stats.blocks_processed, 1);
839        assert_eq!(stats.blocks_compressed, 1);
840        assert!(stats.compressed_bytes < stats.uncompressed_bytes);
841
842        writer.finish().unwrap();
843
844        // Read and verify
845        let mut reader = CarReader::new(&car_data[..]).unwrap();
846        let read_block = reader.read_block().unwrap().unwrap();
847
848        assert_eq!(read_block.cid(), block.cid());
849        assert_eq!(read_block.data(), block.data());
850    }
851
852    #[test]
853    fn test_car_compression_none() {
854        use crate::compression::CompressionAlgorithm;
855
856        let block = Block::new(Bytes::from_static(b"test data")).unwrap();
857
858        // Write with None compression (passthrough)
859        let mut car_data = Vec::new();
860        let mut writer = CarWriterBuilder::new(vec![*block.cid()])
861            .with_compression(CompressionAlgorithm::None, 0)
862            .build(&mut car_data)
863            .unwrap();
864
865        writer.write_block(&block).unwrap();
866
867        let stats = writer.stats();
868        assert_eq!(stats.blocks_processed, 1);
869        assert_eq!(stats.blocks_compressed, 0); // None algorithm doesn't count as compressed
870        assert_eq!(stats.uncompressed_bytes, stats.compressed_bytes);
871        assert_eq!(stats.compression_ratio(), 1.0);
872
873        writer.finish().unwrap();
874
875        // Read and verify
876        let mut reader = CarReader::new(&car_data[..]).unwrap();
877        let read_block = reader.read_block().unwrap().unwrap();
878
879        assert_eq!(read_block.cid(), block.cid());
880        assert_eq!(read_block.data(), block.data());
881    }
882
883    #[test]
884    fn test_car_compression_stats() {
885        use crate::compression::CompressionAlgorithm;
886
887        let blocks: Vec<Block> = (0..10)
888            .map(|_| Block::new(Bytes::from(vec![0x42u8; 500])).unwrap())
889            .collect();
890
891        let mut car_data = Vec::new();
892        let mut writer = CarWriterBuilder::new(vec![*blocks[0].cid()])
893            .with_compression(CompressionAlgorithm::Zstd, 5)
894            .build(&mut car_data)
895            .unwrap();
896
897        for block in &blocks {
898            writer.write_block(block).unwrap();
899        }
900
901        let stats = writer.stats();
902        assert_eq!(stats.blocks_processed, 10);
903        assert_eq!(stats.blocks_compressed, 10);
904        assert_eq!(stats.uncompressed_bytes, 5000);
905        assert!(stats.bytes_saved() > 0);
906        assert!(stats.compression_percentage() > 0.0);
907
908        writer.finish().unwrap();
909    }
910
911    #[test]
912    fn test_car_mixed_compression_backward_compat() {
913        // Test that uncompressed CAR files can still be read
914        let block = Block::new(Bytes::from_static(b"legacy data")).unwrap();
915
916        // Write without compression (legacy format)
917        let mut car_data = Vec::new();
918        let mut writer = CarWriter::new(&mut car_data, vec![*block.cid()]).unwrap();
919        writer.write_block(&block).unwrap();
920        writer.finish().unwrap();
921
922        // Read should work fine
923        let mut reader = CarReader::new(&car_data[..]).unwrap();
924        let read_block = reader.read_block().unwrap().unwrap();
925
926        assert_eq!(read_block.cid(), block.cid());
927        assert_eq!(read_block.data(), block.data());
928    }
929
930    #[test]
931    fn test_car_compression_large_file() {
932        use crate::compression::CompressionAlgorithm;
933
934        // Simulate a large file with repetitive data (compresses well)
935        let large_block = Block::new(Bytes::from(vec![0x55u8; 100_000])).unwrap();
936
937        let mut car_data = Vec::new();
938        let mut writer = CarWriterBuilder::new(vec![*large_block.cid()])
939            .with_compression(CompressionAlgorithm::Zstd, 6)
940            .build(&mut car_data)
941            .unwrap();
942
943        writer.write_block(&large_block).unwrap();
944
945        let stats = writer.stats();
946        assert_eq!(stats.uncompressed_bytes, 100_000);
947        // Repetitive data should compress very well
948        assert!(stats.compressed_bytes < 1_000);
949        assert!(stats.compression_ratio() < 0.01);
950        assert!(stats.compression_percentage() > 99.0);
951
952        writer.finish().unwrap();
953
954        // Verify decompression works
955        let mut reader = CarReader::new(&car_data[..]).unwrap();
956        let read_block = reader.read_block().unwrap().unwrap();
957
958        assert_eq!(read_block.cid(), large_block.cid());
959        assert_eq!(read_block.data().len(), 100_000);
960    }
961
962    #[test]
963    fn test_car_builder_without_compression() {
964        let block = Block::new(Bytes::from_static(b"test")).unwrap();
965
966        // Test builder without compression
967        let mut car_data = Vec::new();
968        let mut writer = CarWriterBuilder::new(vec![*block.cid()])
969            .build(&mut car_data)
970            .unwrap();
971
972        writer.write_block(&block).unwrap();
973        writer.finish().unwrap();
974
975        let mut reader = CarReader::new(&car_data[..]).unwrap();
976        let read_block = reader.read_block().unwrap().unwrap();
977
978        assert_eq!(read_block.cid(), block.cid());
979        assert_eq!(read_block.data(), block.data());
980    }
981}