Skip to main content

serializer/machine/
compress.rs

1//! DX-Compress: Integrated LZ4 Streaming
2//!
3//! rkyv has no built-in compression.
4//! DX-Compress streams LZ4 with zero-copy.
5//!
6//! Result: 70% smaller wire size with negligible overhead
7
8use super::types::{DxMachineError, Result};
9
10/// Compression level for LZ4
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum CompressionLevel {
13    /// Fastest compression, larger output (default)
14    #[default]
15    Fast,
16    /// Balanced compression and speed
17    Default,
18    /// Maximum compression, slower
19    High,
20}
21
22impl CompressionLevel {
23    /// Convert to Zstd compression level (1-22)
24    pub fn to_zstd_level(self) -> i32 {
25        match self {
26            CompressionLevel::Fast => 1,    // Fastest
27            CompressionLevel::Default => 3, // Balanced (default)
28            CompressionLevel::High => 19,   // Maximum compression
29        }
30    }
31}
32
33/// Compressed DX-Machine buffer
34///
35/// Wraps compressed data with lazy decompression.
36/// The first access triggers decompression, subsequent accesses use cache.
37#[derive(Debug)]
38pub struct DxCompressed {
39    /// Compressed data
40    compressed: Vec<u8>,
41    /// Original uncompressed size (for allocation)
42    original_size: u32,
43    /// Decompression cache (lazy)
44    decompressed: Option<Vec<u8>>,
45}
46
47impl DxCompressed {
48    /// Create empty compressed buffer
49    pub fn new() -> Self {
50        Self {
51            compressed: Vec::new(),
52            original_size: 0,
53            decompressed: None,
54        }
55    }
56
57    /// Compress data using LZ4 (fast, pure Rust)
58    ///
59    /// LZ4 provides good compression with excellent speed (pure Rust, no C dependencies).
60    pub fn compress(data: &[u8]) -> Self {
61        // Use LZ4 compression (pure Rust implementation)
62        let compressed = lz4_compress_fast(data);
63        let original_size = data.len() as u32;
64
65        Self {
66            compressed,
67            original_size,
68            decompressed: None,
69        }
70    }
71
72    /// Compress with level hint
73    pub fn compress_level(data: &[u8], _level: CompressionLevel) -> Self {
74        // LZ4 doesn't have compression levels in lz4_flex
75        Self::compress(data)
76    }
77
78    /// Get compressed size
79    #[inline(always)]
80    pub fn compressed_size(&self) -> usize {
81        self.compressed.len()
82    }
83
84    /// Get original (uncompressed) size
85    #[inline(always)]
86    pub fn original_size(&self) -> usize {
87        self.original_size as usize
88    }
89
90    /// Get compression ratio (compressed / original)
91    #[inline(always)]
92    pub fn ratio(&self) -> f64 {
93        if self.original_size == 0 {
94            return 1.0;
95        }
96        self.compressed.len() as f64 / self.original_size as f64
97    }
98
99    /// Get space savings (1.0 - ratio)
100    #[inline(always)]
101    pub fn savings(&self) -> f64 {
102        1.0 - self.ratio()
103    }
104
105    /// Get compressed bytes
106    #[inline(always)]
107    pub fn as_compressed(&self) -> &[u8] {
108        &self.compressed
109    }
110
111    /// Decompress and get data
112    ///
113    /// First call triggers decompression, subsequent calls use cache.
114    pub fn decompress(&mut self) -> Result<&[u8]> {
115        if self.decompressed.is_none() {
116            let data = lz4_decompress_fast(&self.compressed)?;
117            self.decompressed = Some(data);
118        }
119
120        // SAFETY: We just checked and set decompressed above
121        Ok(self.decompressed.as_ref().unwrap_or_else(|| unreachable!()))
122    }
123
124    /// Force decompress and return owned data
125    pub fn decompress_owned(&self) -> Result<Vec<u8>> {
126        lz4_decompress_fast(&self.compressed)
127    }
128
129    /// Check if already decompressed (cached)
130    #[inline(always)]
131    pub fn is_cached(&self) -> bool {
132        self.decompressed.is_some()
133    }
134
135    /// Clear the decompression cache
136    pub fn clear_cache(&mut self) {
137        self.decompressed = None;
138    }
139
140    /// Create from pre-compressed data
141    pub fn from_compressed(compressed: Vec<u8>, original_size: u32) -> Self {
142        Self {
143            compressed,
144            original_size,
145            decompressed: None,
146        }
147    }
148
149    /// Serialize to wire format: `[compressed_data_with_prepended_size...]`
150    pub fn to_wire(&self) -> Vec<u8> {
151        // LZ4 data already has size prepended by lz4_flex
152        self.compressed.clone()
153    }
154
155    /// Parse from wire format
156    pub fn from_wire(data: &[u8]) -> Result<Self> {
157        // LZ4 data has size prepended, extract it
158        if data.len() < 4 {
159            return Err(DxMachineError::BufferTooSmall {
160                required: 4,
161                actual: data.len(),
162            });
163        }
164
165        let original_size = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
166        let compressed = data.to_vec();
167
168        Ok(Self {
169            compressed,
170            original_size,
171            decompressed: None,
172        })
173    }
174}
175
176impl Default for DxCompressed {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182/// Simple LZ4-like compression (pure Rust)
183/// LZ4 compression helper using lz4_flex (pure Rust, fast)
184#[cfg(feature = "compression-lz4")]
185fn lz4_compress_fast(input: &[u8]) -> Vec<u8> {
186    lz4_flex::compress_prepend_size(input)
187}
188
189#[cfg(not(feature = "compression-lz4"))]
190fn lz4_compress_fast(input: &[u8]) -> Vec<u8> {
191    // Fallback to simple RLE compression
192    lz4_compress(input)
193}
194
195/// LZ4 decompression helper using lz4_flex (pure Rust, fast)
196#[cfg(feature = "compression-lz4")]
197pub(crate) fn lz4_decompress_fast(input: &[u8]) -> Result<Vec<u8>> {
198    lz4_flex::decompress_size_prepended(input)
199        .map_err(|e| DxMachineError::DecompressionFailed(e.to_string()))
200}
201
202#[cfg(not(feature = "compression-lz4"))]
203fn lz4_decompress_fast(input: &[u8]) -> Result<Vec<u8>> {
204    // Fallback: assume first 4 bytes are size
205    if input.len() < 4 {
206        return Err(DxMachineError::DecompressionFailed(
207            "Input too short".into(),
208        ));
209    }
210    let size = u32::from_le_bytes([input[0], input[1], input[2], input[3]]) as usize;
211    lz4_decompress(&input[4..], size)
212}
213
214/// Zstd compression helper
215#[allow(dead_code)]
216#[cfg(feature = "compression")]
217fn zstd_compress(input: &[u8], level: i32) -> Vec<u8> {
218    zstd::encode_all(input, level).unwrap_or_else(|_| input.to_vec())
219}
220
221#[allow(dead_code)]
222#[cfg(not(feature = "compression"))]
223fn zstd_compress(input: &[u8], _level: i32) -> Vec<u8> {
224    // Fallback to LZ4 if zstd not enabled
225    lz4_compress(input)
226}
227
228/// Zstd decompression helper
229#[allow(dead_code)]
230#[cfg(feature = "compression")]
231fn zstd_decompress(input: &[u8]) -> Result<Vec<u8>> {
232    zstd::decode_all(input).map_err(|e| DxMachineError::DecompressionFailed(e.to_string()))
233}
234
235#[allow(dead_code)]
236#[cfg(not(feature = "compression"))]
237fn zstd_decompress(input: &[u8]) -> Result<Vec<u8>> {
238    // Fallback to LZ4 if zstd not enabled
239    // Assume original size is stored in first 4 bytes
240    if input.len() < 4 {
241        return Err(DxMachineError::DecompressionFailed(
242            "Input too short".into(),
243        ));
244    }
245    let size = u32::from_le_bytes([input[0], input[1], input[2], input[3]]) as usize;
246    lz4_decompress(&input[4..], size)
247}
248
249///
250/// This is a simplified LZ4-compatible implementation.
251/// For production, consider using the `lz4_flex` crate.
252#[allow(dead_code)]
253fn lz4_compress(input: &[u8]) -> Vec<u8> {
254    if input.is_empty() {
255        return Vec::new();
256    }
257
258    // Simple RLE + literal encoding for now
259    // This is not full LZ4, but provides good compression for structured data
260    let mut output = Vec::with_capacity(input.len());
261    let mut i = 0;
262
263    while i < input.len() {
264        // Try to find a run of identical bytes
265        let byte = input[i];
266        let mut run_len = 1;
267
268        while i + run_len < input.len() && input[i + run_len] == byte && run_len < 255 {
269            run_len += 1;
270        }
271
272        if run_len >= 4 {
273            // Encode as run: 0xFF marker + length + byte
274            output.push(0xFF);
275            output.push(run_len as u8);
276            output.push(byte);
277            i += run_len;
278        } else {
279            // Find literal sequence (until we hit a run or end)
280            let lit_start = i;
281            while i < input.len() {
282                // Check for upcoming run
283                if i + 4 <= input.len() {
284                    let b = input[i];
285                    if input[i + 1] == b && input[i + 2] == b && input[i + 3] == b {
286                        break;
287                    }
288                }
289                i += 1;
290                if i - lit_start >= 254 {
291                    break;
292                }
293            }
294
295            let lit_len = i - lit_start;
296            // Encode as literal: length (if < 0xFF) + bytes
297            if lit_len > 0 {
298                output.push(lit_len as u8);
299                output.extend_from_slice(&input[lit_start..i]);
300            }
301        }
302    }
303
304    output
305}
306
307/// Simple LZ4-like decompression
308#[allow(dead_code)] // Reserved for future decompression feature
309fn lz4_decompress(input: &[u8], expected_size: usize) -> Result<Vec<u8>> {
310    if input.is_empty() {
311        return Ok(Vec::new());
312    }
313
314    let mut output = Vec::with_capacity(expected_size);
315    let mut i = 0;
316
317    while i < input.len() {
318        let marker = input[i];
319        i += 1;
320
321        if marker == 0xFF {
322            // Run-length encoded
323            if i + 2 > input.len() {
324                return Err(DxMachineError::InvalidData("Truncated RLE sequence".into()));
325            }
326            let run_len = input[i] as usize;
327            let byte = input[i + 1];
328            i += 2;
329
330            output.extend(std::iter::repeat_n(byte, run_len));
331        } else {
332            // Literal sequence
333            let lit_len = marker as usize;
334            if i + lit_len > input.len() {
335                return Err(DxMachineError::InvalidData(
336                    "Truncated literal sequence".into(),
337                ));
338            }
339            output.extend_from_slice(&input[i..i + lit_len]);
340            i += lit_len;
341        }
342    }
343
344    Ok(output)
345}
346
347/// Streaming compressor for large data
348pub struct StreamCompressor {
349    /// Chunk size for streaming
350    chunk_size: usize,
351    /// Accumulated chunks
352    chunks: Vec<DxCompressed>,
353    /// Current buffer
354    buffer: Vec<u8>,
355}
356
357impl StreamCompressor {
358    /// Create a new streaming compressor
359    ///
360    /// # Arguments
361    /// * `chunk_size` - Size of each chunk (default 64KB)
362    pub fn new(chunk_size: usize) -> Self {
363        Self {
364            chunk_size,
365            chunks: Vec::new(),
366            buffer: Vec::with_capacity(chunk_size),
367        }
368    }
369
370    /// Default chunk size (64KB)
371    pub fn default_chunk() -> Self {
372        Self::new(64 * 1024)
373    }
374
375    /// Write data to the stream
376    pub fn write(&mut self, data: &[u8]) {
377        let mut remaining = data;
378
379        while !remaining.is_empty() {
380            let space = self.chunk_size - self.buffer.len();
381            let take = remaining.len().min(space);
382
383            self.buffer.extend_from_slice(&remaining[..take]);
384            remaining = &remaining[take..];
385
386            if self.buffer.len() >= self.chunk_size {
387                self.flush_chunk();
388            }
389        }
390    }
391
392    /// Flush current buffer as a chunk
393    fn flush_chunk(&mut self) {
394        if !self.buffer.is_empty() {
395            let chunk = DxCompressed::compress(&self.buffer);
396            self.chunks.push(chunk);
397            self.buffer.clear();
398        }
399    }
400
401    /// Finish compression and get all chunks
402    pub fn finish(mut self) -> Vec<DxCompressed> {
403        self.flush_chunk();
404        self.chunks
405    }
406
407    /// Get current number of chunks
408    pub fn chunk_count(&self) -> usize {
409        self.chunks.len()
410    }
411
412    /// Get total compressed size
413    pub fn total_compressed_size(&self) -> usize {
414        self.chunks
415            .iter()
416            .map(|c| c.compressed_size())
417            .sum::<usize>()
418            + self.buffer.len()
419        // Current uncompressed buffer
420    }
421}
422
423/// Streaming decompressor
424pub struct StreamDecompressor {
425    chunks: Vec<DxCompressed>,
426    current_chunk: usize,
427    current_offset: usize,
428}
429
430impl StreamDecompressor {
431    /// Create from compressed chunks
432    pub fn new(chunks: Vec<DxCompressed>) -> Self {
433        Self {
434            chunks,
435            current_chunk: 0,
436            current_offset: 0,
437        }
438    }
439
440    /// Read decompressed data
441    pub fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
442        if self.current_chunk >= self.chunks.len() {
443            return Ok(0);
444        }
445
446        let mut written = 0;
447
448        while written < buf.len() && self.current_chunk < self.chunks.len() {
449            let chunk = &mut self.chunks[self.current_chunk];
450            let data = chunk.decompress()?;
451
452            let remaining_in_chunk = data.len() - self.current_offset;
453            let to_copy = (buf.len() - written).min(remaining_in_chunk);
454
455            buf[written..written + to_copy]
456                .copy_from_slice(&data[self.current_offset..self.current_offset + to_copy]);
457
458            written += to_copy;
459            self.current_offset += to_copy;
460
461            if self.current_offset >= data.len() {
462                self.current_chunk += 1;
463                self.current_offset = 0;
464            }
465        }
466
467        Ok(written)
468    }
469
470    /// Decompress all chunks to a single buffer
471    pub fn decompress_all(&mut self) -> Result<Vec<u8>> {
472        let total_size: usize = self.chunks.iter().map(|c| c.original_size()).sum();
473        let mut output = Vec::with_capacity(total_size);
474
475        for chunk in &mut self.chunks {
476            let data = chunk.decompress()?;
477            output.extend_from_slice(data);
478        }
479
480        Ok(output)
481    }
482}
483
484// ============================================================================
485// LZ4 Compression
486// ============================================================================
487
488/// Compress data using LZ4 (fastest compression)
489///
490/// LZ4 provides excellent speed with good compression ratios.
491/// Typical compression: 40-60% size reduction.
492#[cfg(feature = "compression-lz4")]
493pub fn compress_lz4(data: &[u8]) -> Result<Vec<u8>> {
494    if data.is_empty() {
495        return Ok(Vec::new());
496    }
497
498    Ok(lz4_flex::compress_prepend_size(data))
499}
500
501/// Compress data using LZ4 (stub for non-lz4 builds)
502#[cfg(not(feature = "compression-lz4"))]
503pub fn compress_lz4(_data: &[u8]) -> Result<Vec<u8>> {
504    Err(DxMachineError::InvalidData(
505        "LZ4 compression not available (enable 'compression-lz4' feature)".into(),
506    ))
507}
508
509/// Decompress LZ4-compressed data
510///
511/// Automatically detects size and decompresses.
512#[cfg(feature = "compression-lz4")]
513pub fn decompress_lz4(data: &[u8]) -> Result<Vec<u8>> {
514    if data.is_empty() {
515        return Ok(Vec::new());
516    }
517
518    lz4_flex::decompress_size_prepended(data).map_err(|e| {
519        DxMachineError::DecompressionFailed(format!("LZ4 decompression failed: {}", e))
520    })
521}
522
523/// Decompress LZ4 data (stub for non-lz4 builds)
524#[cfg(not(feature = "compression-lz4"))]
525pub fn decompress_lz4(_data: &[u8]) -> Result<Vec<u8>> {
526    Err(DxMachineError::InvalidData(
527        "LZ4 decompression not available (enable 'compression-lz4' feature)".into(),
528    ))
529}
530
531// ============================================================================
532// Zstd Compression
533// ============================================================================
534
535/// Compress data using Zstd with default level (3)
536///
537/// Zstd provides better compression ratios than LZ4 at the cost of speed.
538/// Typical compression: 75-85% size reduction.
539#[cfg(feature = "compression")]
540pub fn compress_zstd(data: &[u8]) -> Result<Vec<u8>> {
541    compress_zstd_level(data, CompressionLevel::Default)
542}
543
544/// Compress data using Zstd (stub for non-zstd builds)
545#[cfg(not(feature = "compression"))]
546pub fn compress_zstd(_data: &[u8]) -> Result<Vec<u8>> {
547    Err(DxMachineError::InvalidData(
548        "Zstd compression not available (enable 'compression' feature)".into(),
549    ))
550}
551
552/// Compress data using Zstd with specified compression level
553///
554/// # Arguments
555/// * `data` - Data to compress
556/// * `level` - Compression level (Fast=1, Default=3, High=19)
557///
558/// # Performance
559/// - Level 1: ~500 MB/s compression, 60-70% reduction
560/// - Level 3: ~200 MB/s compression, 70-80% reduction (default)
561/// - Level 19: ~20 MB/s compression, 80-85% reduction
562#[cfg(feature = "compression")]
563pub fn compress_zstd_level(data: &[u8], level: CompressionLevel) -> Result<Vec<u8>> {
564    if data.is_empty() {
565        return Ok(Vec::new());
566    }
567
568    let zstd_level = level.to_zstd_level();
569    zstd::encode_all(data, zstd_level)
570        .map_err(|e| DxMachineError::InvalidData(format!("Zstd compression failed: {}", e)))
571}
572
573/// Compress data using Zstd with level (stub for non-zstd builds)
574#[cfg(not(feature = "compression"))]
575pub fn compress_zstd_level(_data: &[u8], _level: CompressionLevel) -> Result<Vec<u8>> {
576    Err(DxMachineError::InvalidData(
577        "Zstd compression not available (enable 'compression' feature)".into(),
578    ))
579}
580
581/// Decompress Zstd-compressed data
582///
583/// Automatically detects compression level and decompresses.
584/// Decompression is typically 2-3x faster than compression.
585#[cfg(feature = "compression")]
586pub fn decompress_zstd(data: &[u8]) -> Result<Vec<u8>> {
587    if data.is_empty() {
588        return Ok(Vec::new());
589    }
590
591    zstd::decode_all(data)
592        .map_err(|e| DxMachineError::InvalidData(format!("Zstd decompression failed: {}", e)))
593}
594
595/// Decompress Zstd data (stub for non-zstd builds)
596#[cfg(not(feature = "compression"))]
597pub fn decompress_zstd(_data: &[u8]) -> Result<Vec<u8>> {
598    Err(DxMachineError::InvalidData(
599        "Zstd decompression not available (enable 'compression' feature)".into(),
600    ))
601}
602
603#[cfg(test)]
604mod tests {
605    use super::*;
606
607    #[test]
608    #[cfg(feature = "compression-lz4")]
609    fn test_compress_decompress() {
610        let original = b"Hello, World! This is a test of the compression system.";
611        let mut compressed = DxCompressed::compress(original);
612
613        // Verify compression happened
614        println!(
615            "Original: {} bytes, Compressed: {} bytes, Ratio: {:.2}",
616            original.len(),
617            compressed.compressed_size(),
618            compressed.ratio()
619        );
620
621        // Decompress and verify
622        let decompressed = compressed.decompress().unwrap();
623        assert_eq!(decompressed, original);
624    }
625
626    #[test]
627    #[cfg(feature = "compression-lz4")]
628    fn test_compress_repetitive_data() {
629        // Highly compressible data
630        let original: Vec<u8> = std::iter::repeat_n(b'A', 1000).collect();
631        let compressed = DxCompressed::compress(&original);
632
633        println!(
634            "Repetitive: {} bytes -> {} bytes ({:.1}% savings)",
635            original.len(),
636            compressed.compressed_size(),
637            compressed.savings() * 100.0
638        );
639
640        // Should achieve significant compression
641        assert!(compressed.ratio() < 0.1); // Less than 10% of original
642    }
643
644    #[test]
645    #[cfg(feature = "compression-lz4")]
646    fn test_compress_random_data() {
647        // Less compressible data
648        let original: Vec<u8> = (0..1000).map(|i| (i % 256) as u8).collect();
649        let compressed = DxCompressed::compress(&original);
650
651        println!(
652            "Sequential: {} bytes -> {} bytes ({:.1}% savings)",
653            original.len(),
654            compressed.compressed_size(),
655            compressed.savings() * 100.0
656        );
657    }
658
659    #[test]
660    #[cfg(feature = "compression-lz4")]
661    fn test_wire_format() {
662        let original = b"Test data for wire format";
663        let compressed = DxCompressed::compress(original);
664
665        let wire = compressed.to_wire();
666        let restored = DxCompressed::from_wire(&wire).unwrap();
667
668        assert_eq!(restored.original_size(), original.len());
669        assert_eq!(restored.compressed_size(), compressed.compressed_size());
670    }
671
672    #[test]
673    #[cfg(feature = "compression-lz4")]
674    fn test_streaming_compressor() {
675        let mut compressor = StreamCompressor::new(32);
676
677        // Write data in multiple chunks
678        for i in 0..10 {
679            let data: Vec<u8> = (0..20).map(|j| ((i * 20 + j) % 256) as u8).collect();
680            compressor.write(&data);
681        }
682
683        let chunks = compressor.finish();
684        println!("Produced {} chunks", chunks.len());
685
686        // Decompress all
687        let mut decompressor = StreamDecompressor::new(chunks);
688        let output = decompressor.decompress_all().unwrap();
689
690        // Verify
691        let expected: Vec<u8> = (0..200).map(|i| (i % 256) as u8).collect();
692        assert_eq!(output, expected);
693    }
694
695    #[test]
696    #[cfg(feature = "compression-lz4")]
697    fn test_cache() {
698        let original = b"Cache test data";
699        let mut compressed = DxCompressed::compress(original);
700
701        assert!(!compressed.is_cached());
702
703        compressed.decompress().unwrap();
704        assert!(compressed.is_cached());
705
706        compressed.clear_cache();
707        assert!(!compressed.is_cached());
708    }
709
710    #[test]
711    #[cfg(feature = "compression-lz4")]
712    fn test_empty_data() {
713        let original: &[u8] = &[];
714        let mut compressed = DxCompressed::compress(original);
715
716        assert_eq!(compressed.original_size(), 0);
717        let decompressed = compressed.decompress().unwrap();
718        assert!(decompressed.is_empty());
719    }
720
721    #[test]
722    #[cfg(feature = "compression")]
723    fn test_zstd_compress_decompress() {
724        let original = b"Hello, World! This is a test of Zstd compression.";
725        let compressed = compress_zstd(original).unwrap();
726
727        println!(
728            "Zstd: Original {} bytes -> Compressed {} bytes ({:.1}% of original)",
729            original.len(),
730            compressed.len(),
731            (compressed.len() as f64 / original.len() as f64) * 100.0
732        );
733
734        let decompressed = decompress_zstd(&compressed).unwrap();
735        assert_eq!(decompressed, original);
736    }
737
738    #[test]
739    #[cfg(feature = "compression")]
740    fn test_zstd_compression_levels() {
741        let original: Vec<u8> = std::iter::repeat_n(b'A', 1000).collect();
742
743        // Test all three levels
744        let fast = compress_zstd_level(&original, CompressionLevel::Fast).unwrap();
745        let default = compress_zstd_level(&original, CompressionLevel::Default).unwrap();
746        let high = compress_zstd_level(&original, CompressionLevel::High).unwrap();
747
748        println!("Zstd compression levels on 1000 bytes of 'A':");
749        println!("  Fast (level 1): {} bytes", fast.len());
750        println!("  Default (level 3): {} bytes", default.len());
751        println!("  High (level 19): {} bytes", high.len());
752
753        // Higher compression should produce smaller or equal output
754        assert!(high.len() <= default.len());
755        assert!(default.len() <= fast.len());
756
757        // All should decompress correctly
758        assert_eq!(decompress_zstd(&fast).unwrap(), original);
759        assert_eq!(decompress_zstd(&default).unwrap(), original);
760        assert_eq!(decompress_zstd(&high).unwrap(), original);
761    }
762
763    #[test]
764    #[cfg(feature = "compression")]
765    fn test_zstd_empty_data() {
766        let original: &[u8] = &[];
767        let compressed = compress_zstd(original).unwrap();
768        assert!(compressed.is_empty());
769
770        let decompressed = decompress_zstd(&compressed).unwrap();
771        assert!(decompressed.is_empty());
772    }
773
774    #[test]
775    #[cfg(feature = "compression")]
776    fn test_zstd_vs_lz4() {
777        // Compare Zstd and LZ4 on repetitive data
778        let original: Vec<u8> = std::iter::repeat_n(b'X', 10000).collect();
779
780        let lz4_compressed = lz4_compress(&original);
781        let zstd_compressed = compress_zstd(&original).unwrap();
782
783        println!("Compression comparison on 10KB repetitive data:");
784        println!(
785            "  LZ4: {} bytes ({:.1}% of original)",
786            lz4_compressed.len(),
787            (lz4_compressed.len() as f64 / original.len() as f64) * 100.0
788        );
789        println!(
790            "  Zstd: {} bytes ({:.1}% of original)",
791            zstd_compressed.len(),
792            (zstd_compressed.len() as f64 / original.len() as f64) * 100.0
793        );
794
795        // Both should decompress correctly
796        let lz4_decompressed = lz4_decompress(&lz4_compressed, original.len()).unwrap();
797        let zstd_decompressed = decompress_zstd(&zstd_compressed).unwrap();
798
799        assert_eq!(lz4_decompressed, original);
800        assert_eq!(zstd_decompressed, original);
801    }
802}