Skip to main content

mediagit_compression/
zlib_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//! Zlib compression implementation for Git compatibility
15//!
16//! Git uses zlib (deflate) compression for all objects in the object database.
17//! This implementation provides Git-compatible compression and decompression.
18
19use crate::error::{CompressionError, CompressionResult};
20use crate::{CompressionLevel, Compressor};
21use flate2::read::{ZlibDecoder, ZlibEncoder};
22use flate2::Compression;
23use std::fmt;
24use std::io::Read;
25
26/// Zlib compressor implementation
27///
28/// Uses the zlib (deflate) compression algorithm for Git compatibility.
29/// Supports configurable compression levels.
30#[derive(Clone)]
31pub struct ZlibCompressor {
32    level: CompressionLevel,
33}
34
35impl ZlibCompressor {
36    /// Create a new Zlib compressor with the given compression level
37    pub fn new(level: CompressionLevel) -> Self {
38        ZlibCompressor { level }
39    }
40
41    /// Create a Zlib compressor with fast compression
42    pub fn fast() -> Self {
43        ZlibCompressor::new(CompressionLevel::Fast)
44    }
45
46    /// Create a Zlib compressor with default compression (Git default: level 6)
47    pub fn default_level() -> Self {
48        ZlibCompressor::new(CompressionLevel::Default)
49    }
50
51    /// Create a Zlib compressor with best compression
52    pub fn best() -> Self {
53        ZlibCompressor::new(CompressionLevel::Best)
54    }
55
56    /// Get the flate2 compression level
57    fn get_compression(&self) -> Compression {
58        match self.level {
59            CompressionLevel::Fast => Compression::fast(), // Level 1
60            CompressionLevel::Default => Compression::new(6), // Git default
61            CompressionLevel::Best => Compression::best(), // Level 9
62        }
63    }
64}
65
66impl fmt::Debug for ZlibCompressor {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        f.debug_struct("ZlibCompressor")
69            .field("level", &self.level)
70            .finish()
71    }
72}
73
74impl Compressor for ZlibCompressor {
75    fn compress(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
76        if data.is_empty() {
77            return Ok(Vec::new());
78        }
79
80        let mut encoder = ZlibEncoder::new(data, self.get_compression());
81        let mut compressed = Vec::new();
82
83        encoder
84            .read_to_end(&mut compressed)
85            .map_err(|e| CompressionError::zstd_error(format!("zlib compression failed: {}", e)))?;
86
87        Ok(compressed)
88    }
89
90    fn decompress(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
91        if data.is_empty() {
92            return Ok(Vec::new());
93        }
94
95        // Check if this looks like zlib compressed data
96        // Zlib header: CMF (0x78) + FLG where (CMF * 256 + FLG) % 31 == 0
97        // Valid headers: 0x789C (default), 0x78DA (best), 0x7801 (no compression)
98        let is_zlib = data.len() >= 2 && data[0] == 0x78 && {
99            let cmf = data[0] as u16;
100            let flg = data[1] as u16;
101            (cmf * 256 + flg).is_multiple_of(31)
102        };
103
104        if is_zlib {
105            let mut decoder = ZlibDecoder::new(data);
106            let mut decompressed = Vec::new();
107
108            decoder.read_to_end(&mut decompressed).map_err(|e| {
109                CompressionError::decompression_failed(format!("zlib decompression failed: {}", e))
110            })?;
111
112            Ok(decompressed)
113        } else {
114            // Data is not zlib compressed, return as-is
115            // This handles backward compatibility with uncompressed data
116            Ok(data.to_vec())
117        }
118    }
119}
120
121#[cfg(test)]
122#[allow(clippy::unwrap_used)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_zlib_compress_decompress() {
128        let compressor = ZlibCompressor::new(CompressionLevel::Default);
129        let original = b"Hello, World! This is a test of zlib compression.";
130
131        let compressed = compressor.compress(original).unwrap();
132        let decompressed = compressor.decompress(&compressed).unwrap();
133
134        assert_eq!(original, &decompressed[..]);
135    }
136
137    #[test]
138    fn test_zlib_compress_empty() {
139        let compressor = ZlibCompressor::new(CompressionLevel::Default);
140        let compressed = compressor.compress(b"").unwrap();
141        assert_eq!(compressed.len(), 0);
142    }
143
144    #[test]
145    fn test_zlib_decompress_empty() {
146        let compressor = ZlibCompressor::new(CompressionLevel::Default);
147        let decompressed = compressor.decompress(b"").unwrap();
148        assert_eq!(decompressed.len(), 0);
149    }
150
151    #[test]
152    fn test_zlib_decompress_uncompressed_data() {
153        let compressor = ZlibCompressor::new(CompressionLevel::Default);
154        let data = b"This is not compressed";
155        let result = compressor.decompress(data).unwrap();
156        // Should return as-is since it doesn't have zlib magic bytes
157        assert_eq!(result, data);
158    }
159
160    #[test]
161    fn test_zlib_compression_levels() {
162        let data = b"This is test data that should compress. ".repeat(100);
163
164        let fast = ZlibCompressor::fast().compress(&data).unwrap();
165        let default = ZlibCompressor::default_level().compress(&data).unwrap();
166        let best = ZlibCompressor::best().compress(&data).unwrap();
167
168        // Best should compress better than default, which should be better than fast
169        assert!(best.len() <= default.len());
170        assert!(default.len() <= fast.len() + 50); // some tolerance
171
172        // All should decompress correctly
173        let decompressor = ZlibCompressor::default_level();
174        let fast_dec = decompressor.decompress(&fast).unwrap();
175        let default_dec = decompressor.decompress(&default).unwrap();
176        let best_dec = decompressor.decompress(&best).unwrap();
177
178        assert_eq!(fast_dec, data);
179        assert_eq!(default_dec, data);
180        assert_eq!(best_dec, data);
181    }
182
183    #[test]
184    fn test_zlib_large_data() {
185        let compressor = ZlibCompressor::new(CompressionLevel::Default);
186        let original = vec![0x42u8; 1024 * 1024]; // 1MB of repeated data
187
188        let compressed = compressor.compress(&original).unwrap();
189        let decompressed = compressor.decompress(&compressed).unwrap();
190
191        assert_eq!(original, decompressed);
192        // Repeated data should compress very well
193        assert!(compressed.len() < original.len() / 100);
194    }
195
196    #[test]
197    fn test_zlib_random_data() {
198        let compressor = ZlibCompressor::new(CompressionLevel::Default);
199        // Semi-random data
200        let original = (0..1000).map(|i| (i ^ 0xAA) as u8).collect::<Vec<_>>();
201
202        let compressed = compressor.compress(&original).unwrap();
203        let decompressed = compressor.decompress(&compressed).unwrap();
204
205        assert_eq!(original, decompressed);
206    }
207
208    #[test]
209    fn test_zlib_debug_format() {
210        let compressor = ZlibCompressor::new(CompressionLevel::Default);
211        let debug_str = format!("{:?}", compressor);
212        assert!(debug_str.contains("ZlibCompressor"));
213        assert!(debug_str.contains("Default"));
214    }
215
216    #[test]
217    fn test_zlib_git_compatibility() {
218        // Git uses zlib compression with level 6 by default
219        let compressor = ZlibCompressor::default_level();
220        let git_blob = b"blob 13\0Hello, World!";
221
222        let compressed = compressor.compress(git_blob).unwrap();
223
224        // Verify zlib header (0x78 + compression level bits)
225        assert_eq!(compressed[0], 0x78);
226
227        // Decompress and verify
228        let decompressed = compressor.decompress(&compressed).unwrap();
229        assert_eq!(decompressed, git_blob);
230    }
231}