sf_cli/
compression.rs

1//! Compression and decompression utilities
2
3use flate2::{read::GzDecoder, write::GzEncoder, Compression};
4use std::io::{Read, Write};
5use thiserror::Error;
6
7/// Compression errors
8#[derive(Error, Debug)]
9pub enum CompressionError {
10    #[error("Compression failed: {0}")]
11    CompressionFailed(String),
12    #[error("Decompression failed: {0}")]
13    DecompressionFailed(String),
14    #[error("IO error: {0}")]
15    IoError(#[from] std::io::Error),
16}
17
18/// Compression engine for data compression/decompression
19pub struct CompressionEngine {
20    level: Compression,
21}
22
23impl Default for CompressionEngine {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl CompressionEngine {
30    /// Create a new compression engine with default compression level
31    pub fn new() -> Self {
32        Self {
33            level: Compression::default(),
34        }
35    }
36
37    /// Create a new compression engine with specified compression level (0-9)
38    pub fn with_level(level: u32) -> Self {
39        Self {
40            level: Compression::new(level),
41        }
42    }
43
44    /// Compress data using gzip
45    pub fn compress(&self, data: &[u8]) -> Result<Vec<u8>, CompressionError> {
46        let mut encoder = GzEncoder::new(Vec::new(), self.level);
47        encoder
48            .write_all(data)
49            .map_err(|e| CompressionError::CompressionFailed(e.to_string()))?;
50        
51        encoder
52            .finish()
53            .map_err(|e| CompressionError::CompressionFailed(e.to_string()))
54    }
55
56    /// Decompress gzip data
57    pub fn decompress(&self, compressed_data: &[u8]) -> Result<Vec<u8>, CompressionError> {
58        let mut decoder = GzDecoder::new(compressed_data);
59        let mut decompressed = Vec::new();
60        
61        decoder
62            .read_to_end(&mut decompressed)
63            .map_err(|e| CompressionError::DecompressionFailed(e.to_string()))?;
64        
65        Ok(decompressed)
66    }
67
68    /// Estimate compression ratio for given data
69    pub fn estimate_ratio(&self, data: &[u8]) -> Result<f64, CompressionError> {
70        let compressed = self.compress(data)?;
71        Ok(compressed.len() as f64 / data.len() as f64)
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn test_compression_decompression() {
81        let engine = CompressionEngine::new();
82        let data = b"Hello, World! This is a test message that should compress well because it has repeating patterns. Hello, World! This is a test message that should compress well because it has repeating patterns.";
83
84        let compressed = engine.compress(data).unwrap();
85        assert!(compressed.len() < data.len()); // Should be smaller for this repetitive data
86
87        let decompressed = engine.decompress(&compressed).unwrap();
88        assert_eq!(decompressed.as_slice(), data);
89    }
90
91    #[test]
92    fn test_compression_levels() {
93        let data = b"Test data for compression level testing. ".repeat(100);
94        
95        let engine_fast = CompressionEngine::with_level(1);
96        let engine_best = CompressionEngine::with_level(9);
97
98        let compressed_fast = engine_fast.compress(&data).unwrap();
99        let compressed_best = engine_best.compress(&data).unwrap();
100
101        // Best compression should produce smaller output
102        assert!(compressed_best.len() <= compressed_fast.len());
103
104        // Both should decompress to original data
105        let decompressed_fast = engine_fast.decompress(&compressed_fast).unwrap();
106        let decompressed_best = engine_best.decompress(&compressed_best).unwrap();
107        
108        assert_eq!(decompressed_fast, data);
109        assert_eq!(decompressed_best, data);
110    }
111
112    #[test]
113    fn test_empty_data() {
114        let engine = CompressionEngine::new();
115        let empty_data = b"";
116
117        let compressed = engine.compress(empty_data).unwrap();
118        let decompressed = engine.decompress(&compressed).unwrap();
119        
120        assert_eq!(decompressed.as_slice(), empty_data);
121    }
122
123    #[test]
124    fn test_invalid_compressed_data() {
125        let engine = CompressionEngine::new();
126        let invalid_data = b"This is not compressed data";
127
128        let result = engine.decompress(invalid_data);
129        assert!(matches!(result, Err(CompressionError::DecompressionFailed(_))));
130    }
131
132    #[test]
133    fn test_compression_ratio() {
134        let engine = CompressionEngine::new();
135        
136        // Highly compressible data
137        let repetitive_data = b"A".repeat(1000);
138        let ratio1 = engine.estimate_ratio(&repetitive_data).unwrap();
139        
140        // Less compressible data (random-like)
141        let varied_data = (0..1000).map(|i| (i % 256) as u8).collect::<Vec<_>>();
142        let ratio2 = engine.estimate_ratio(&varied_data).unwrap();
143        
144        // Repetitive data should compress better (lower ratio)
145        assert!(ratio1 < ratio2);
146        assert!(ratio1 < 1.0);
147        assert!(ratio2 < 1.0);
148    }
149}