1use 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#[derive(Debug, Clone)]
34pub struct BrotliCompressor {
35 level: u32,
37 window_size: u32,
39}
40
41impl BrotliCompressor {
42 pub fn new() -> Self {
44 Self {
45 level: 6,
46 window_size: 22,
47 }
48 }
49
50 pub fn with_level(level: u32) -> Self {
60 Self {
61 level: level.min(11),
62 window_size: 22,
63 }
64 }
65
66 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 pub fn fast() -> Self {
82 Self::with_level(1)
83 }
84
85 pub fn balanced() -> Self {
87 Self::with_level(6)
88 }
89
90 pub fn high() -> Self {
92 Self::with_level(9)
93 }
94
95 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, 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 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, ¶ms)
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, 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 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 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 }
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 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 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 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 assert_eq!(
270 oxiarc_brotli::decompress(&compressed.data).unwrap(),
271 original
272 );
273 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 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 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 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 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]; let compressed = compressor.compress(&data).unwrap();
329 let decompressed = compressor.decompress(&compressed).unwrap();
330
331 assert_eq!(decompressed, data);
332 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::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 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 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 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); }
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}