Skip to main content

oxirs_tdb/compression/
snappy.rs

1//! Snappy compression algorithm implementation
2//!
3//! Snappy is a compression algorithm developed by Google that prioritizes
4//! speed over compression ratio. It's designed for very fast compression
5//! and decompression with reasonable compression ratios.
6//!
7//! # Performance Characteristics
8//!
9//! - Compression speed: ~250-500 MB/s
10//! - Decompression speed: ~500-1500 MB/s
11//! - Compression ratio: 1.5-2x on typical data
12//!
13//! # Use Cases
14//!
15//! - Real-time data processing
16//! - Network protocols requiring low latency
17//! - Cache compression where speed is paramount
18//! - Google BigTable-compatible storage
19
20use super::{
21    AdvancedCompressionType, CompressedData, CompressionAlgorithm, CompressionMetadata,
22    MAX_COMPRESSION_INPUT_SIZE,
23};
24use anyhow::{bail, Result};
25use std::collections::HashMap;
26use std::time::Instant;
27
28/// Snappy compression algorithm
29///
30/// Uses the oxiarc-snappy crate for pure Rust Snappy compression.
31/// Optimized for speed with reasonable compression ratios.
32#[derive(Debug, Clone)]
33pub struct SnappyCompressor {
34    /// Use framing format (compatible with streaming)
35    use_framing: bool,
36}
37
38impl SnappyCompressor {
39    /// Create a new Snappy compressor with default settings
40    pub fn new() -> Self {
41        Self {
42            use_framing: false, // Raw format by default
43        }
44    }
45
46    /// Create a Snappy compressor with framing format
47    ///
48    /// The framing format is compatible with the Snappy streaming format
49    /// and includes checksums for data integrity.
50    pub fn with_framing() -> Self {
51        Self { use_framing: true }
52    }
53
54    /// Create a Snappy compressor with raw format (no framing)
55    pub fn raw() -> Self {
56        Self { use_framing: false }
57    }
58}
59
60impl Default for SnappyCompressor {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl CompressionAlgorithm for SnappyCompressor {
67    fn compress(&self, data: &[u8]) -> Result<CompressedData> {
68        if data.is_empty() {
69            return Ok(CompressedData {
70                data: Vec::new(),
71                metadata: CompressionMetadata {
72                    algorithm: AdvancedCompressionType::Adaptive, // Will add Snappy type
73                    original_size: 0,
74                    compressed_size: 0,
75                    compression_time_us: 0,
76                    metadata: HashMap::new(),
77                },
78            });
79        }
80
81        if data.len() > MAX_COMPRESSION_INPUT_SIZE {
82            bail!(
83                "Input data too large: {} bytes (max: {})",
84                data.len(),
85                MAX_COMPRESSION_INPUT_SIZE
86            );
87        }
88
89        let start = Instant::now();
90
91        // Compress using oxiarc-snappy (Pure Rust). Both the framed and raw
92        // Snappy formats are standard and preserved across the migration.
93        let compressed = if self.use_framing {
94            // Use framing format with checksums
95            use std::io::Write;
96            let mut encoder = oxiarc_snappy::FrameEncoder::new(Vec::new());
97            encoder.write_all(data)?;
98            encoder
99                .finish()
100                .map_err(|e| anyhow::anyhow!("Snappy frame finish failed: {}", e))?
101        } else {
102            // Use raw format (faster, no checksums). Raw Snappy compression is
103            // infallible in oxiarc-snappy.
104            oxiarc_snappy::compress(data)
105        };
106
107        let compression_time = start.elapsed();
108
109        let mut metadata_map = HashMap::new();
110        metadata_map.insert("framing".to_string(), self.use_framing.to_string());
111        metadata_map.insert(
112            "compression_ratio".to_string(),
113            format!("{:.2}", compressed.len() as f64 / data.len() as f64),
114        );
115
116        let compressed_size = compressed.len() as u64;
117
118        Ok(CompressedData {
119            data: compressed,
120            metadata: CompressionMetadata {
121                algorithm: AdvancedCompressionType::Adaptive, // Will add Snappy type
122                original_size: data.len() as u64,
123                compressed_size,
124                compression_time_us: compression_time.as_micros() as u64,
125                metadata: metadata_map,
126            },
127        })
128    }
129
130    fn decompress(&self, compressed: &CompressedData) -> Result<Vec<u8>> {
131        if compressed.data.is_empty() {
132            return Ok(Vec::new());
133        }
134
135        let start = Instant::now();
136
137        // Determine if data was compressed with framing
138        let use_framing = compressed
139            .metadata
140            .metadata
141            .get("framing")
142            .and_then(|v| v.parse().ok())
143            .unwrap_or(self.use_framing);
144
145        // Decompress using oxiarc-snappy (Pure Rust).
146        let decompressed = if use_framing {
147            // Use framing format with checksums
148            use std::io::Read;
149            let mut decoder = oxiarc_snappy::FrameDecoder::new(&compressed.data[..]);
150            let mut result = Vec::new();
151            decoder.read_to_end(&mut result)?;
152            result
153        } else {
154            // Use raw format
155            oxiarc_snappy::decompress(&compressed.data)
156                .map_err(|e| anyhow::anyhow!("Snappy decompression failed: {}", e))?
157        };
158
159        let decompression_time = start.elapsed();
160
161        // Verify decompressed size matches metadata
162        if decompressed.len() != compressed.metadata.original_size as usize {
163            bail!(
164                "Decompressed size mismatch: expected {}, got {}",
165                compressed.metadata.original_size,
166                decompressed.len()
167            );
168        }
169
170        log::trace!(
171            "Snappy decompression: {} bytes -> {} bytes in {:?}",
172            compressed.data.len(),
173            decompressed.len(),
174            decompression_time
175        );
176
177        Ok(decompressed)
178    }
179
180    fn algorithm_type(&self) -> AdvancedCompressionType {
181        AdvancedCompressionType::Adaptive // Will add Snappy type
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_snappy_compress_empty() {
191        let compressor = SnappyCompressor::new();
192        let result = compressor.compress(&[]).unwrap();
193        assert_eq!(result.data.len(), 0);
194        assert_eq!(result.metadata.original_size, 0);
195        assert_eq!(result.metadata.compressed_size, 0);
196    }
197
198    #[test]
199    fn test_snappy_compress_decompress_raw() {
200        let compressor = SnappyCompressor::raw();
201        let original = b"Hello, World! This is a test of Snappy compression. ".repeat(100);
202
203        let compressed = compressor.compress(&original).unwrap();
204        assert!(compressed.data.len() < original.len());
205        assert_eq!(compressed.metadata.original_size, original.len() as u64);
206
207        let decompressed = compressor.decompress(&compressed).unwrap();
208        assert_eq!(decompressed, original);
209    }
210
211    #[test]
212    fn test_snappy_compress_decompress_framed() {
213        let compressor = SnappyCompressor::with_framing();
214        let original = b"Hello, World! This is a test of Snappy framed compression. ".repeat(100);
215
216        let compressed = compressor.compress(&original).unwrap();
217        assert!(compressed.data.len() < original.len());
218        assert_eq!(compressed.metadata.original_size, original.len() as u64);
219
220        let decompressed = compressor.decompress(&compressed).unwrap();
221        assert_eq!(decompressed, original);
222    }
223
224    #[test]
225    fn test_snappy_raw_format_fidelity() {
226        // The raw compressor must emit a standard raw Snappy block that the
227        // low-level oxiarc-snappy decoder can decompress directly.
228        let compressor = SnappyCompressor::raw();
229        let original = b"oxiarc raw snappy fidelity check ".repeat(40);
230
231        let compressed = compressor.compress(&original).unwrap();
232        let decoded = oxiarc_snappy::decompress(&compressed.data).unwrap();
233        assert_eq!(decoded, original);
234
235        // And a blob produced directly by oxiarc must decode via the compressor.
236        let direct = oxiarc_snappy::compress(&original);
237        let cd = CompressedData {
238            data: direct,
239            metadata: CompressionMetadata {
240                algorithm: AdvancedCompressionType::Adaptive,
241                original_size: original.len() as u64,
242                compressed_size: 0,
243                compression_time_us: 0,
244                metadata: {
245                    let mut m = std::collections::HashMap::new();
246                    m.insert("framing".to_string(), "false".to_string());
247                    m
248                },
249            },
250        };
251        assert_eq!(compressor.decompress(&cd).unwrap(), original);
252    }
253
254    #[test]
255    fn test_snappy_framed_format_fidelity() {
256        // Framed output must begin with the standard Snappy stream identifier
257        // chunk (0xFF followed by length 6 and the ASCII "sNaPpY" magic).
258        let compressor = SnappyCompressor::with_framing();
259        let original = b"oxiarc framed snappy fidelity check ".repeat(40);
260
261        let compressed = compressor.compress(&original).unwrap();
262        assert!(compressed.data.len() >= 10);
263        assert_eq!(compressed.data[0], 0xFF);
264        assert_eq!(&compressed.data[4..10], b"sNaPpY");
265
266        let decompressed = compressor.decompress(&compressed).unwrap();
267        assert_eq!(decompressed, original);
268    }
269
270    #[test]
271    fn test_snappy_format_comparison() {
272        let data = b"The quick brown fox jumps over the lazy dog. ".repeat(50);
273
274        let raw = SnappyCompressor::raw();
275        let framed = SnappyCompressor::with_framing();
276
277        let raw_result = raw.compress(&data).unwrap();
278        let framed_result = framed.compress(&data).unwrap();
279
280        // Both should decompress to the same data
281        assert_eq!(raw.decompress(&raw_result).unwrap(), data);
282        assert_eq!(framed.decompress(&framed_result).unwrap(), data);
283
284        // Raw format should be slightly smaller (no framing overhead)
285        println!("Raw: {} bytes", raw_result.data.len());
286        println!("Framed: {} bytes", framed_result.data.len());
287        assert!(raw_result.data.len() <= framed_result.data.len());
288    }
289
290    #[test]
291    fn test_snappy_highly_compressible_data() {
292        let compressor = SnappyCompressor::new();
293        let data = vec![b'A'; 10000]; // Highly repetitive data
294
295        let compressed = compressor.compress(&data).unwrap();
296        let decompressed = compressor.decompress(&compressed).unwrap();
297
298        assert_eq!(decompressed, data);
299        // Should achieve good compression on repetitive data
300        assert!(compressed.data.len() < data.len() / 5);
301        println!(
302            "Snappy highly compressible: {} -> {} bytes ({:.2}x)",
303            data.len(),
304            compressed.data.len(),
305            data.len() as f64 / compressed.data.len() as f64
306        );
307    }
308
309    #[test]
310    fn test_snappy_incompressible_data() {
311        let compressor = SnappyCompressor::new();
312        // Use scirs2_core::random for random data generation
313        use scirs2_core::random::rng;
314        use scirs2_core::RngExt;
315
316        let mut rng = rng();
317        let data: Vec<u8> = (0..1000).map(|_| rng.random_range(0..256) as u8).collect();
318
319        let compressed = compressor.compress(&data).unwrap();
320        let decompressed = compressor.decompress(&compressed).unwrap();
321
322        assert_eq!(decompressed, data);
323        // Random data should not compress well
324        println!(
325            "Snappy incompressible: {} -> {} bytes",
326            data.len(),
327            compressed.data.len()
328        );
329    }
330
331    #[test]
332    fn test_snappy_too_large_input() {
333        let compressor = SnappyCompressor::new();
334        let data = vec![0u8; MAX_COMPRESSION_INPUT_SIZE + 1];
335
336        let result = compressor.compress(&data);
337        assert!(result.is_err());
338    }
339
340    #[test]
341    fn test_snappy_metadata() {
342        let compressor = SnappyCompressor::with_framing();
343        let data = b"Testing metadata collection with Snappy".repeat(10);
344
345        let compressed = compressor.compress(&data).unwrap();
346
347        assert_eq!(compressed.metadata.metadata.get("framing").unwrap(), "true");
348        assert!(compressed
349            .metadata
350            .metadata
351            .contains_key("compression_ratio"));
352        assert!(compressed.metadata.compression_time_us > 0);
353    }
354
355    #[test]
356    fn test_snappy_small_data() {
357        let compressor = SnappyCompressor::new();
358        let data = b"Small";
359
360        let compressed = compressor.compress(data).unwrap();
361        let decompressed = compressor.decompress(&compressed).unwrap();
362
363        assert_eq!(decompressed, data);
364        // Very small data might not compress
365        println!(
366            "Snappy small data: {} -> {} bytes",
367            data.len(),
368            compressed.data.len()
369        );
370    }
371
372    #[test]
373    fn test_snappy_rdf_data_compression() {
374        let compressor = SnappyCompressor::new();
375        // Simulate RDF triple data with typical patterns
376        let rdf_data = b"<http://example.org/resource/1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/Class> .\n"
377            .repeat(100);
378
379        let compressed = compressor.compress(&rdf_data).unwrap();
380        let decompressed = compressor.decompress(&compressed).unwrap();
381
382        assert_eq!(decompressed, rdf_data);
383
384        // RDF data with repetitive URIs should compress well
385        let ratio = rdf_data.len() as f64 / compressed.data.len() as f64;
386        println!("Snappy RDF compression ratio: {:.2}x", ratio);
387        assert!(ratio > 1.5); // Should achieve >1.5x compression
388    }
389
390    #[test]
391    fn test_snappy_roundtrip_consistency() {
392        let compressor = SnappyCompressor::new();
393        let data = b"Consistency test data".repeat(50);
394
395        // Compress and decompress multiple times
396        let mut current = data.to_vec();
397        for _ in 0..5 {
398            let compressed = compressor.compress(&current).unwrap();
399            current = compressor.decompress(&compressed).unwrap();
400        }
401
402        assert_eq!(current, data);
403    }
404}