Skip to main content

haagenti_brotli/
lib.rs

1//! # Haagenti Brotli
2//!
3//! Brotli compression implementation (RFC 7932).
4//!
5//! Brotli achieves high compression ratios, especially for text and web
6//! content, at the cost of slower compression speed.
7//!
8//! ## Features
9//!
10//! - **High Ratio**: Excellent compression for text/web content
11//! - **Dictionary**: Built-in and custom dictionary support
12//! - **Streaming**: Incremental compression/decompression
13//! - **Quality Levels**: 0-11 quality settings
14//!
15//! ## Example
16//!
17//! ```ignore
18//! use haagenti_brotli::BrotliCodec;
19//! use haagenti_core::{Codec, Compressor, Decompressor};
20//!
21//! let codec = BrotliCodec::new();
22//! let compressed = codec.compress(data)?;
23//! let original = codec.decompress(&compressed)?;
24//! ```
25//!
26//! ## Implementation
27//!
28//! This crate wraps the `brotli` crate to provide Haagenti trait implementations.
29//! A native Rust implementation may be added in the future.
30
31use std::io::{Read, Write};
32
33use haagenti_core::{
34    Algorithm, Codec, CompressionLevel, CompressionStats, Compressor, Decompressor, Error, Result,
35};
36
37/// Default buffer size for Brotli operations.
38const BUFFER_SIZE: usize = 4096;
39
40/// Default window size (log2) for Brotli compression (22 = 4MB window).
41const DEFAULT_LG_WIN: u32 = 22;
42
43/// Map CompressionLevel to Brotli quality (0-11).
44fn map_quality(level: CompressionLevel) -> u32 {
45    match level {
46        CompressionLevel::None => 0,
47        CompressionLevel::Fast => 1,
48        CompressionLevel::Default => 6,
49        CompressionLevel::Best => 10,
50        CompressionLevel::Ultra => 11,
51        CompressionLevel::Custom(l) => (l as u32).clamp(0, 11),
52    }
53}
54
55/// Brotli compressor.
56#[derive(Debug, Clone)]
57pub struct BrotliCompressor {
58    level: CompressionLevel,
59}
60
61impl BrotliCompressor {
62    /// Create a new Brotli compressor with default settings.
63    pub fn new() -> Self {
64        Self {
65            level: CompressionLevel::Default,
66        }
67    }
68
69    /// Create with compression level.
70    pub fn with_level(level: CompressionLevel) -> Self {
71        Self { level }
72    }
73}
74
75impl Default for BrotliCompressor {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81impl Compressor for BrotliCompressor {
82    fn algorithm(&self) -> Algorithm {
83        Algorithm::Brotli
84    }
85
86    fn level(&self) -> CompressionLevel {
87        self.level
88    }
89
90    fn compress(&self, input: &[u8]) -> Result<Vec<u8>> {
91        let quality = map_quality(self.level);
92        let mut output = Vec::new();
93
94        {
95            let mut writer =
96                brotli::CompressorWriter::new(&mut output, BUFFER_SIZE, quality, DEFAULT_LG_WIN);
97            writer
98                .write_all(input)
99                .map_err(|e| Error::algorithm("brotli", e.to_string()))?;
100        }
101
102        Ok(output)
103    }
104
105    fn compress_to(&self, input: &[u8], output: &mut [u8]) -> Result<usize> {
106        let compressed = self.compress(input)?;
107        if compressed.len() > output.len() {
108            return Err(Error::buffer_too_small(compressed.len(), output.len()));
109        }
110        output[..compressed.len()].copy_from_slice(&compressed);
111        Ok(compressed.len())
112    }
113
114    fn max_compressed_size(&self, input_len: usize) -> usize {
115        // Brotli worst case: slightly larger than input
116        input_len + (input_len >> 2) + 128
117    }
118
119    fn stats(&self) -> Option<CompressionStats> {
120        None
121    }
122}
123
124/// Brotli decompressor.
125#[derive(Debug, Clone, Default)]
126pub struct BrotliDecompressor;
127
128impl BrotliDecompressor {
129    /// Create a new Brotli decompressor.
130    pub fn new() -> Self {
131        Self
132    }
133}
134
135impl Decompressor for BrotliDecompressor {
136    fn algorithm(&self) -> Algorithm {
137        Algorithm::Brotli
138    }
139
140    fn decompress(&self, input: &[u8]) -> Result<Vec<u8>> {
141        let mut output = Vec::new();
142
143        {
144            let mut reader = brotli::Decompressor::new(input, BUFFER_SIZE);
145            reader
146                .read_to_end(&mut output)
147                .map_err(|e| Error::algorithm("brotli", e.to_string()))?;
148        }
149
150        Ok(output)
151    }
152
153    fn decompress_to(&self, input: &[u8], output: &mut [u8]) -> Result<usize> {
154        let decompressed = self.decompress(input)?;
155        if decompressed.len() > output.len() {
156            return Err(Error::buffer_too_small(decompressed.len(), output.len()));
157        }
158        output[..decompressed.len()].copy_from_slice(&decompressed);
159        Ok(decompressed.len())
160    }
161
162    fn stats(&self) -> Option<CompressionStats> {
163        None
164    }
165}
166
167/// Brotli codec combining compression and decompression.
168#[derive(Debug, Clone)]
169pub struct BrotliCodec {
170    level: CompressionLevel,
171}
172
173impl BrotliCodec {
174    /// Create a new Brotli codec with default settings.
175    pub fn new() -> Self {
176        Self {
177            level: CompressionLevel::Default,
178        }
179    }
180
181    /// Create with compression level.
182    pub fn with_level(level: CompressionLevel) -> Self {
183        Self { level }
184    }
185}
186
187impl Default for BrotliCodec {
188    fn default() -> Self {
189        Self::new()
190    }
191}
192
193impl Compressor for BrotliCodec {
194    fn algorithm(&self) -> Algorithm {
195        Algorithm::Brotli
196    }
197
198    fn level(&self) -> CompressionLevel {
199        self.level
200    }
201
202    fn compress(&self, input: &[u8]) -> Result<Vec<u8>> {
203        let quality = map_quality(self.level);
204        let mut output = Vec::new();
205
206        {
207            let mut writer =
208                brotli::CompressorWriter::new(&mut output, BUFFER_SIZE, quality, DEFAULT_LG_WIN);
209            writer
210                .write_all(input)
211                .map_err(|e| Error::algorithm("brotli", e.to_string()))?;
212        }
213
214        Ok(output)
215    }
216
217    fn compress_to(&self, input: &[u8], output: &mut [u8]) -> Result<usize> {
218        let compressed = self.compress(input)?;
219        if compressed.len() > output.len() {
220            return Err(Error::buffer_too_small(compressed.len(), output.len()));
221        }
222        output[..compressed.len()].copy_from_slice(&compressed);
223        Ok(compressed.len())
224    }
225
226    fn max_compressed_size(&self, input_len: usize) -> usize {
227        input_len + (input_len >> 2) + 128
228    }
229
230    fn stats(&self) -> Option<CompressionStats> {
231        None
232    }
233}
234
235impl Decompressor for BrotliCodec {
236    fn algorithm(&self) -> Algorithm {
237        Algorithm::Brotli
238    }
239
240    fn decompress(&self, input: &[u8]) -> Result<Vec<u8>> {
241        let mut output = Vec::new();
242
243        {
244            let mut reader = brotli::Decompressor::new(input, BUFFER_SIZE);
245            reader
246                .read_to_end(&mut output)
247                .map_err(|e| Error::algorithm("brotli", e.to_string()))?;
248        }
249
250        Ok(output)
251    }
252
253    fn decompress_to(&self, input: &[u8], output: &mut [u8]) -> Result<usize> {
254        let decompressed = self.decompress(input)?;
255        if decompressed.len() > output.len() {
256            return Err(Error::buffer_too_small(decompressed.len(), output.len()));
257        }
258        output[..decompressed.len()].copy_from_slice(&decompressed);
259        Ok(decompressed.len())
260    }
261
262    fn stats(&self) -> Option<CompressionStats> {
263        None
264    }
265}
266
267impl Codec for BrotliCodec {
268    fn new() -> Self {
269        BrotliCodec::new()
270    }
271
272    fn with_level(level: CompressionLevel) -> Self {
273        BrotliCodec::with_level(level)
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_roundtrip_empty() {
283        let codec = BrotliCodec::new();
284        let input = b"";
285
286        let compressed = codec.compress(input).unwrap();
287        let decompressed = codec.decompress(&compressed).unwrap();
288
289        assert_eq!(decompressed.as_slice(), input);
290    }
291
292    #[test]
293    fn test_roundtrip_small() {
294        let codec = BrotliCodec::new();
295        let input = b"Hello, Brotli!";
296
297        let compressed = codec.compress(input).unwrap();
298        let decompressed = codec.decompress(&compressed).unwrap();
299
300        assert_eq!(decompressed.as_slice(), input);
301    }
302
303    #[test]
304    fn test_roundtrip_large() {
305        let codec = BrotliCodec::new();
306        let pattern = b"The quick brown fox jumps over the lazy dog. ";
307        let input: Vec<u8> = pattern.iter().cycle().take(100_000).copied().collect();
308
309        let compressed = codec.compress(&input).unwrap();
310
311        // Should compress well
312        assert!(compressed.len() < input.len());
313
314        let decompressed = codec.decompress(&compressed).unwrap();
315        assert_eq!(decompressed, input);
316    }
317
318    #[test]
319    fn test_compression_levels() {
320        let input =
321            b"Testing compression levels for Brotli algorithm with some repetitive content.";
322
323        for level in [
324            CompressionLevel::None,
325            CompressionLevel::Fast,
326            CompressionLevel::Default,
327            CompressionLevel::Best,
328        ] {
329            let codec = BrotliCodec::with_level(level);
330            let compressed = codec.compress(input).unwrap();
331            let decompressed = codec.decompress(&compressed).unwrap();
332            assert_eq!(decompressed.as_slice(), input);
333        }
334    }
335
336    #[test]
337    fn test_verify_roundtrip() {
338        let codec = BrotliCodec::new();
339        let input = b"Verify roundtrip functionality for Brotli.";
340
341        assert!(codec.verify_roundtrip(input).unwrap());
342    }
343
344    #[test]
345    fn test_repetitive_data() {
346        let codec = BrotliCodec::new();
347        let input: Vec<u8> = vec![b'A'; 10_000];
348
349        let compressed = codec.compress(&input).unwrap();
350
351        // Highly repetitive data should compress very well
352        assert!(compressed.len() < input.len() / 10);
353
354        let decompressed = codec.decompress(&compressed).unwrap();
355        assert_eq!(decompressed, input);
356    }
357
358    #[test]
359    fn test_web_content() {
360        // Brotli excels at web content - test with HTML-like data
361        let codec = BrotliCodec::with_level(CompressionLevel::Best);
362        let input = br#"
363            <!DOCTYPE html>
364            <html lang="en">
365            <head>
366                <meta charset="UTF-8">
367                <title>Test Page</title>
368                <style>
369                    body { font-family: Arial, sans-serif; }
370                    .container { max-width: 1200px; margin: 0 auto; }
371                </style>
372            </head>
373            <body>
374                <div class="container">
375                    <h1>Hello, World!</h1>
376                    <p>This is a test of Brotli compression on web content.</p>
377                </div>
378            </body>
379            </html>
380        "#;
381
382        let compressed = codec.compress(input).unwrap();
383
384        // Web content should compress very well
385        assert!(compressed.len() < input.len() / 2);
386
387        let decompressed = codec.decompress(&compressed).unwrap();
388        assert_eq!(decompressed.as_slice(), input);
389    }
390
391    #[test]
392    fn test_compressor_decompressor_separate() {
393        let compressor = BrotliCompressor::with_level(CompressionLevel::Fast);
394        let decompressor = BrotliDecompressor::new();
395
396        let input = b"Testing separate compressor and decompressor.";
397
398        let compressed = compressor.compress(input).unwrap();
399        let decompressed = decompressor.decompress(&compressed).unwrap();
400
401        assert_eq!(decompressed.as_slice(), input);
402    }
403}