Skip to main content

oxirs_tdb/compression/
brotli_compression.rs

1//! Brotli compression algorithm implementation
2//!
3//! Brotli is a compression algorithm developed by Google that provides:
4//! - Excellent compression ratios (often 20-25% better than gzip)
5//! - Good decompression speed
6//! - Tunable compression levels (0-11)
7//! - Built-in dictionary support
8//!
9//! # Performance Characteristics
10//!
11//! - Compression speed: 10-100 MB/s (level dependent)
12//! - Decompression speed: 200-400 MB/s
13//! - Compression ratio: 3-5x on typical data
14//!
15//! # Use Cases
16//!
17//! - Web content compression (HTTP/2, HTTP/3)
18//! - Archival storage requiring maximum compression
19//! - Network transmission where bandwidth is expensive
20//! - RDF data storage with emphasis on space efficiency
21
22use super::{
23    AdvancedCompressionType, CompressedData, CompressionAlgorithm, CompressionMetadata,
24    MAX_COMPRESSION_INPUT_SIZE,
25};
26use anyhow::{bail, Result};
27use std::collections::HashMap;
28use std::time::Instant;
29
30/// Brotli compression algorithm
31///
32/// Uses the oxiarc-brotli crate for high-quality compression with excellent ratios.
33#[derive(Debug, Clone)]
34pub struct BrotliCompressor {
35    /// Compression level (0-11, higher = better compression but slower)
36    level: u32,
37    /// Window size (10-24, higher = better compression but more memory)
38    window_size: u32,
39}
40
41impl BrotliCompressor {
42    /// Create a new Brotli compressor with default settings (level 6, window 22)
43    pub fn new() -> Self {
44        Self {
45            level: 6,
46            window_size: 22,
47        }
48    }
49
50    /// Create a new Brotli compressor with specified compression level
51    ///
52    /// # Arguments
53    ///
54    /// * `level` - Compression level (0-11). Recommended ranges:
55    ///   - 0-3: Fast compression, lower ratios
56    ///   - 4-6: Balanced compression (recommended)
57    ///   - 7-9: High compression, slower
58    ///   - 10-11: Maximum compression, very slow
59    pub fn with_level(level: u32) -> Self {
60        Self {
61            level: level.min(11),
62            window_size: 22,
63        }
64    }
65
66    /// Create a Brotli compressor with custom level and window size
67    ///
68    /// # Arguments
69    ///
70    /// * `level` - Compression level (0-11)
71    /// * `window_size` - Window size (10-24). Larger windows improve compression
72    ///   but require more memory during compression and decompression
73    pub fn with_level_and_window(level: u32, window_size: u32) -> Self {
74        Self {
75            level: level.min(11),
76            window_size: window_size.clamp(10, 24),
77        }
78    }
79
80    /// Create a fast compression instance (level 1)
81    pub fn fast() -> Self {
82        Self::with_level(1)
83    }
84
85    /// Create a balanced compression instance (level 6)
86    pub fn balanced() -> Self {
87        Self::with_level(6)
88    }
89
90    /// Create a high compression instance (level 9)
91    pub fn high() -> Self {
92        Self::with_level(9)
93    }
94
95    /// Create a maximum compression instance (level 11)
96    pub fn max() -> Self {
97        Self::with_level(11)
98    }
99}
100
101impl Default for BrotliCompressor {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107impl CompressionAlgorithm for BrotliCompressor {
108    fn compress(&self, data: &[u8]) -> Result<CompressedData> {
109        if data.is_empty() {
110            return Ok(CompressedData {
111                data: Vec::new(),
112                metadata: CompressionMetadata {
113                    algorithm: AdvancedCompressionType::Adaptive, // Will add Brotli type
114                    original_size: 0,
115                    compressed_size: 0,
116                    compression_time_us: 0,
117                    metadata: HashMap::new(),
118                },
119            });
120        }
121
122        if data.len() > MAX_COMPRESSION_INPUT_SIZE {
123            bail!(
124                "Input data too large: {} bytes (max: {})",
125                data.len(),
126                MAX_COMPRESSION_INPUT_SIZE
127            );
128        }
129
130        let start = Instant::now();
131
132        // Compress using brotli (oxiarc-brotli, Pure Rust). The on-disk payload is a
133        // standard Brotli stream; `quality`/`lgwin` mirror the previous level/window_size.
134        let params = oxiarc_brotli::BrotliParams {
135            quality: self.level,
136            lgwin: self.window_size,
137            ..Default::default()
138        };
139        let compressed = oxiarc_brotli::compress_with_params(data, &params)
140            .map_err(|e| anyhow::anyhow!("Brotli compression failed: {}", e))?;
141
142        let compression_time = start.elapsed();
143
144        let mut metadata_map = HashMap::new();
145        metadata_map.insert("level".to_string(), self.level.to_string());
146        metadata_map.insert("window_size".to_string(), self.window_size.to_string());
147        metadata_map.insert(
148            "compression_ratio".to_string(),
149            format!("{:.2}", compressed.len() as f64 / data.len() as f64),
150        );
151
152        let compressed_size = compressed.len() as u64;
153
154        Ok(CompressedData {
155            data: compressed,
156            metadata: CompressionMetadata {
157                algorithm: AdvancedCompressionType::Adaptive, // Will add Brotli type
158                original_size: data.len() as u64,
159                compressed_size,
160                compression_time_us: compression_time.as_micros() as u64,
161                metadata: metadata_map,
162            },
163        })
164    }
165
166    fn decompress(&self, compressed: &CompressedData) -> Result<Vec<u8>> {
167        if compressed.data.is_empty() {
168            return Ok(Vec::new());
169        }
170
171        let start = Instant::now();
172
173        // Decompress the standard Brotli stream (oxiarc-brotli, Pure Rust).
174        let decompressed = oxiarc_brotli::decompress(&compressed.data)
175            .map_err(|e| anyhow::anyhow!("Brotli decompression failed: {}", e))?;
176
177        let decompression_time = start.elapsed();
178
179        // Verify decompressed size matches metadata
180        if decompressed.len() != compressed.metadata.original_size as usize {
181            bail!(
182                "Decompressed size mismatch: expected {}, got {}",
183                compressed.metadata.original_size,
184                decompressed.len()
185            );
186        }
187
188        log::trace!(
189            "Brotli decompression: {} bytes -> {} bytes in {:?}",
190            compressed.data.len(),
191            decompressed.len(),
192            decompression_time
193        );
194
195        Ok(decompressed)
196    }
197
198    fn algorithm_type(&self) -> AdvancedCompressionType {
199        AdvancedCompressionType::Adaptive // Will add Brotli type
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_brotli_compress_empty() {
209        let compressor = BrotliCompressor::new();
210        let result = compressor.compress(&[]).unwrap();
211        assert_eq!(result.data.len(), 0);
212        assert_eq!(result.metadata.original_size, 0);
213        assert_eq!(result.metadata.compressed_size, 0);
214    }
215
216    #[test]
217    fn test_brotli_compress_decompress() {
218        let compressor = BrotliCompressor::new();
219        let original = b"Hello, World! This is a test of Brotli compression. ".repeat(100);
220
221        let compressed = compressor.compress(&original).unwrap();
222        assert!(compressed.data.len() < original.len());
223        assert_eq!(compressed.metadata.original_size, original.len() as u64);
224
225        let decompressed = compressor.decompress(&compressed).unwrap();
226        assert_eq!(decompressed, original);
227    }
228
229    #[test]
230    fn test_brotli_format_fidelity() {
231        // The on-disk payload is a standard Brotli stream decodable directly by the
232        // low-level oxiarc-brotli decoder, and a blob produced directly by oxiarc
233        // must decode via the compressor — proving a clean, tag-free Brotli format.
234        let compressor = BrotliCompressor::new();
235        let original = b"oxiarc brotli fidelity check ".repeat(50);
236
237        let compressed = compressor.compress(&original).unwrap();
238        let decoded = oxiarc_brotli::decompress(&compressed.data).unwrap();
239        assert_eq!(decoded, original);
240
241        // A blob produced directly by oxiarc must decode via the compressor.
242        let cd = CompressedData {
243            data: oxiarc_brotli::compress(&original, 6).unwrap(),
244            metadata: CompressionMetadata {
245                algorithm: AdvancedCompressionType::Adaptive,
246                original_size: original.len() as u64,
247                compressed_size: 0,
248                compression_time_us: 0,
249                metadata: HashMap::new(),
250            },
251        };
252        assert_eq!(compressor.decompress(&cd).unwrap(), original);
253    }
254
255    #[test]
256    fn test_brotli_incompressible_roundtrip_real_brotli() {
257        // Random/incompressible data must round-trip losslessly through real Brotli
258        // (no stored fallback, no format tag). This proves the historic oxiarc-brotli
259        // incompressible-input workaround is no longer needed.
260        use scirs2_core::random::rng;
261        use scirs2_core::RngExt;
262        let mut r = rng();
263        let original: Vec<u8> = (0..2000).map(|_| r.random_range(0..256) as u8).collect();
264
265        let compressor = BrotliCompressor::new();
266        let compressed = compressor.compress(&original).unwrap();
267        // The payload is a standard Brotli stream: it must decode via the low-level
268        // decoder directly, with no tag byte stripped.
269        assert_eq!(
270            oxiarc_brotli::decompress(&compressed.data).unwrap(),
271            original
272        );
273        // And via the high-level compressor API.
274        let decompressed = compressor.decompress(&compressed).unwrap();
275        assert_eq!(decompressed, original);
276    }
277
278    #[test]
279    fn test_brotli_compression_levels() {
280        let data = b"The quick brown fox jumps over the lazy dog. ".repeat(50);
281
282        let fast = BrotliCompressor::fast();
283        let balanced = BrotliCompressor::balanced();
284        let high = BrotliCompressor::high();
285
286        let fast_result = fast.compress(&data).unwrap();
287        let balanced_result = balanced.compress(&data).unwrap();
288        let high_result = high.compress(&data).unwrap();
289
290        // All should decompress to the same data
291        assert_eq!(fast.decompress(&fast_result).unwrap(), data);
292        assert_eq!(balanced.decompress(&balanced_result).unwrap(), data);
293        assert_eq!(high.decompress(&high_result).unwrap(), data);
294
295        // Higher compression levels should produce smaller or equal output
296        println!("Fast (1): {} bytes", fast_result.data.len());
297        println!("Balanced (6): {} bytes", balanced_result.data.len());
298        println!("High (9): {} bytes", high_result.data.len());
299
300        // Verify compression improves with higher levels
301        assert!(balanced_result.data.len() <= fast_result.data.len());
302        assert!(high_result.data.len() <= balanced_result.data.len());
303    }
304
305    #[test]
306    fn test_brotli_window_sizes() {
307        let data = b"Testing window size effects on compression. ".repeat(100);
308
309        let small_window = BrotliCompressor::with_level_and_window(6, 16);
310        let large_window = BrotliCompressor::with_level_and_window(6, 24);
311
312        let small_result = small_window.compress(&data).unwrap();
313        let large_result = large_window.compress(&data).unwrap();
314
315        // Both should decompress correctly
316        assert_eq!(small_window.decompress(&small_result).unwrap(), data);
317        assert_eq!(large_window.decompress(&large_result).unwrap(), data);
318
319        println!("Window 16: {} bytes", small_result.data.len());
320        println!("Window 24: {} bytes", large_result.data.len());
321    }
322
323    #[test]
324    fn test_brotli_highly_compressible_data() {
325        let compressor = BrotliCompressor::new();
326        let data = vec![b'A'; 10000]; // Highly repetitive data
327
328        let compressed = compressor.compress(&data).unwrap();
329        let decompressed = compressor.decompress(&compressed).unwrap();
330
331        assert_eq!(decompressed, data);
332        // Should achieve strong compression on repetitive data. oxiarc-brotli at the
333        // default quality compresses 10 KB of a single repeated byte to a few hundred
334        // bytes (>10x).
335        assert!(compressed.data.len() < data.len() / 10);
336        println!(
337            "Brotli highly compressible: {} -> {} bytes ({:.2}x)",
338            data.len(),
339            compressed.data.len(),
340            data.len() as f64 / compressed.data.len() as f64
341        );
342    }
343
344    #[test]
345    fn test_brotli_incompressible_data() {
346        let compressor = BrotliCompressor::new();
347        // Use scirs2_core::random for random data generation
348        use scirs2_core::random::rng;
349        use scirs2_core::RngExt;
350
351        let mut rng = rng();
352        let data: Vec<u8> = (0..1000).map(|_| rng.random_range(0..256) as u8).collect();
353
354        let compressed = compressor.compress(&data).unwrap();
355        let decompressed = compressor.decompress(&compressed).unwrap();
356
357        assert_eq!(decompressed, data);
358        // Random data should not compress well
359        println!(
360            "Brotli incompressible: {} -> {} bytes",
361            data.len(),
362            compressed.data.len()
363        );
364    }
365
366    #[test]
367    fn test_brotli_too_large_input() {
368        let compressor = BrotliCompressor::new();
369        let data = vec![0u8; MAX_COMPRESSION_INPUT_SIZE + 1];
370
371        let result = compressor.compress(&data);
372        assert!(result.is_err());
373    }
374
375    #[test]
376    fn test_brotli_metadata() {
377        let compressor = BrotliCompressor::with_level_and_window(9, 20);
378        let data = b"Testing metadata collection with Brotli".repeat(10);
379
380        let compressed = compressor.compress(&data).unwrap();
381
382        assert_eq!(compressed.metadata.metadata.get("level").unwrap(), "9");
383        assert_eq!(
384            compressed.metadata.metadata.get("window_size").unwrap(),
385            "20"
386        );
387        assert!(compressed
388            .metadata
389            .metadata
390            .contains_key("compression_ratio"));
391        assert!(compressed.metadata.compression_time_us > 0);
392    }
393
394    #[test]
395    fn test_brotli_rdf_data_compression() {
396        let compressor = BrotliCompressor::balanced();
397        // Simulate RDF triple data with typical patterns
398        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"
399            .repeat(100);
400
401        let compressed = compressor.compress(&rdf_data).unwrap();
402        let decompressed = compressor.decompress(&compressed).unwrap();
403
404        assert_eq!(decompressed, rdf_data);
405
406        // RDF data with repetitive URIs should compress excellently with Brotli
407        let ratio = rdf_data.len() as f64 / compressed.data.len() as f64;
408        println!("Brotli RDF compression ratio: {:.2}x", ratio);
409        assert!(ratio > 4.0); // Should achieve >4x compression
410    }
411
412    #[test]
413    fn test_brotli_small_data() {
414        let compressor = BrotliCompressor::new();
415        let data = b"Small";
416
417        let compressed = compressor.compress(data).unwrap();
418        let decompressed = compressor.decompress(&compressed).unwrap();
419
420        assert_eq!(decompressed, data);
421        println!(
422            "Brotli small data: {} -> {} bytes",
423            data.len(),
424            compressed.data.len()
425        );
426    }
427
428    #[test]
429    fn test_brotli_level_clamping() {
430        let too_high = BrotliCompressor::with_level(100);
431        assert_eq!(too_high.level, 11);
432    }
433
434    #[test]
435    fn test_brotli_window_clamping() {
436        let too_small = BrotliCompressor::with_level_and_window(6, 5);
437        let too_large = BrotliCompressor::with_level_and_window(6, 30);
438
439        assert_eq!(too_small.window_size, 10);
440        assert_eq!(too_large.window_size, 24);
441    }
442}