1use std::io::{Read, Write};
32
33use haagenti_core::{
34 Algorithm, Codec, CompressionLevel, CompressionStats, Compressor, Decompressor, Error, Result,
35};
36
37const BUFFER_SIZE: usize = 4096;
39
40const DEFAULT_LG_WIN: u32 = 22;
42
43fn 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#[derive(Debug, Clone)]
57pub struct BrotliCompressor {
58 level: CompressionLevel,
59}
60
61impl BrotliCompressor {
62 pub fn new() -> Self {
64 Self {
65 level: CompressionLevel::Default,
66 }
67 }
68
69 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 input_len + (input_len >> 2) + 128
117 }
118
119 fn stats(&self) -> Option<CompressionStats> {
120 None
121 }
122}
123
124#[derive(Debug, Clone, Default)]
126pub struct BrotliDecompressor;
127
128impl BrotliDecompressor {
129 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#[derive(Debug, Clone)]
169pub struct BrotliCodec {
170 level: CompressionLevel,
171}
172
173impl BrotliCodec {
174 pub fn new() -> Self {
176 Self {
177 level: CompressionLevel::Default,
178 }
179 }
180
181 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 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 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 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 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}