Skip to main content

mediagit_compression/
brotli_compressor.rs

1// MediaGit - Git for Media Files
2// Copyright (C) 2025 MediaGit Contributors
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published
6// by the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13
14//! Brotli compression implementation
15//!
16//! High compression ratios with slower compression speed.
17//! Ideal for archival and infrequently accessed data.
18
19use crate::error::{CompressionError, CompressionResult};
20use crate::{CompressionLevel, Compressor};
21use std::fmt;
22use std::io::Write;
23
24/// Brotli compressor implementation
25///
26/// Uses the Brotli compression algorithm for higher compression ratios
27/// at the cost of slower compression speed.
28#[derive(Clone)]
29pub struct BrotliCompressor {
30    level: CompressionLevel,
31}
32
33impl BrotliCompressor {
34    /// Create a new Brotli compressor with the given compression level
35    pub fn new(level: CompressionLevel) -> Self {
36        BrotliCompressor { level }
37    }
38
39    /// Create a Brotli compressor with fast compression
40    pub fn fast() -> Self {
41        BrotliCompressor::new(CompressionLevel::Fast)
42    }
43
44    /// Create a Brotli compressor with default compression
45    pub fn default_level() -> Self {
46        BrotliCompressor::new(CompressionLevel::Default)
47    }
48
49    /// Create a Brotli compressor with best compression
50    pub fn best() -> Self {
51        BrotliCompressor::new(CompressionLevel::Best)
52    }
53}
54
55impl fmt::Debug for BrotliCompressor {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        f.debug_struct("BrotliCompressor")
58            .field("level", &self.level)
59            .finish()
60    }
61}
62
63impl Compressor for BrotliCompressor {
64    fn compress(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
65        if data.is_empty() {
66            return Ok(Vec::new());
67        }
68
69        let level = self.level.to_brotli_level();
70        let mut output = Vec::with_capacity(data.len() / 2);
71
72        // Add custom marker prefix to identify brotli compressed data
73        output.extend_from_slice(b"BRT\x01");
74
75        // Compress using brotli in a scoped block to drop the writer
76        {
77            let mut compressor = brotli::CompressorWriter::new(
78                &mut output,
79                4096, // buffer size
80                level,
81                22, // window size (larger = better compression but more memory)
82            );
83
84            if let Err(e) = compressor.write_all(data) {
85                return Err(CompressionError::brotli_error(format!(
86                    "brotli compression failed: {}",
87                    e
88                )));
89            }
90
91            if let Err(e) = compressor.flush() {
92                return Err(CompressionError::brotli_error(format!(
93                    "brotli flush failed: {}",
94                    e
95                )));
96            }
97        } // compressor is dropped here, releasing the borrow
98
99        Ok(output)
100    }
101
102    fn decompress(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
103        if data.is_empty() {
104            return Ok(Vec::new());
105        }
106
107        // Check for brotli marker
108        if data.len() >= 4 && data.starts_with(b"BRT\x01") {
109            // Skip the marker prefix
110            let compressed_data = &data[4..];
111            let mut output = Vec::with_capacity(data.len() * 2);
112
113            match brotli::BrotliDecompress(&mut std::io::Cursor::new(compressed_data), &mut output)
114            {
115                Ok(_) => Ok(output),
116                Err(e) => Err(CompressionError::decompression_failed(format!(
117                    "brotli decompression failed: {}",
118                    e
119                ))),
120            }
121        } else {
122            // Data is not brotli compressed, return as-is
123            Ok(data.to_vec())
124        }
125    }
126}
127
128#[cfg(test)]
129#[allow(clippy::unwrap_used)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_brotli_compress_decompress() {
135        let compressor = BrotliCompressor::new(CompressionLevel::Default);
136        let original = b"Hello, World! This is a test of brotli compression.";
137
138        let compressed = compressor.compress(original).unwrap();
139        let decompressed = compressor.decompress(&compressed).unwrap();
140
141        assert_eq!(original, &decompressed[..]);
142    }
143
144    #[test]
145    fn test_brotli_has_marker() {
146        let compressor = BrotliCompressor::new(CompressionLevel::Default);
147        let original = b"Test data for brotli";
148
149        let compressed = compressor.compress(original).unwrap();
150        assert!(compressed.starts_with(b"BRT\x01"));
151    }
152
153    #[test]
154    fn test_brotli_compress_empty() {
155        let compressor = BrotliCompressor::new(CompressionLevel::Default);
156        let compressed = compressor.compress(b"").unwrap();
157        assert_eq!(compressed.len(), 0);
158    }
159
160    #[test]
161    fn test_brotli_decompress_empty() {
162        let compressor = BrotliCompressor::new(CompressionLevel::Default);
163        let decompressed = compressor.decompress(b"").unwrap();
164        assert_eq!(decompressed.len(), 0);
165    }
166
167    #[test]
168    fn test_brotli_decompress_uncompressed_data() {
169        let compressor = BrotliCompressor::new(CompressionLevel::Default);
170        let data = b"This is not brotli compressed";
171        let result = compressor.decompress(data).unwrap();
172        // Should return as-is since it doesn't have brotli marker
173        assert_eq!(result, data);
174    }
175
176    #[test]
177    fn test_brotli_compression_levels() {
178        let data = b"This is test data that should compress. ".repeat(100);
179
180        let fast = BrotliCompressor::fast().compress(&data).unwrap();
181        let default = BrotliCompressor::default_level().compress(&data).unwrap();
182        let best = BrotliCompressor::best().compress(&data).unwrap();
183
184        // Better compression levels should generally produce smaller output
185        // (though this depends on data characteristics)
186        assert!(best.len() <= default.len() || best.len() < 100);
187        assert!(default.len() <= fast.len() || default.len() < 100);
188
189        // All should decompress correctly
190        let decompressor = BrotliCompressor::default_level();
191        let fast_dec = decompressor.decompress(&fast).unwrap();
192        let default_dec = decompressor.decompress(&default).unwrap();
193        let best_dec = decompressor.decompress(&best).unwrap();
194
195        assert_eq!(fast_dec, data);
196        assert_eq!(default_dec, data);
197        assert_eq!(best_dec, data);
198    }
199
200    #[test]
201    fn test_brotli_large_data() {
202        let compressor = BrotliCompressor::new(CompressionLevel::Default);
203        let original = vec![0x42u8; 1024 * 100]; // 100KB of repeated data
204
205        let compressed = compressor.compress(&original).unwrap();
206        let decompressed = compressor.decompress(&compressed).unwrap();
207
208        assert_eq!(original, decompressed);
209        // Repeated data should compress very well
210        assert!(compressed.len() < original.len() / 50);
211    }
212
213    #[test]
214    fn test_brotli_debug_format() {
215        let compressor = BrotliCompressor::new(CompressionLevel::Default);
216        let debug_str = format!("{:?}", compressor);
217        assert!(debug_str.contains("BrotliCompressor"));
218        assert!(debug_str.contains("Default"));
219    }
220}