mediagit_compression/lib.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#![allow(missing_docs)]
15//! Intelligent compression for MediaGit
16//!
17//! This crate provides compression abstractions and implementations:
18//! - **Smart compression**: Automatic per-object-type strategy selection
19//! - **Zstd compression**: Fast, good compression ratios (default)
20//! - **Brotli compression**: Higher compression, slower (for archival)
21//! - **Auto-detection**: Transparently handle compressed vs uncompressed data
22//! - **Compression levels**: Configurable speed vs compression trade-offs
23//!
24//! # Quick Start
25//!
26//! ## Basic Compression
27//!
28//! ```rust,no_run
29//! use mediagit_compression::{Compressor, CompressionLevel, ZstdCompressor};
30//!
31//! fn main() -> anyhow::Result<()> {
32//! let compressor = ZstdCompressor::new(CompressionLevel::Default);
33//!
34//! let original = b"Hello, World!";
35//! let compressed = compressor.compress(original)?;
36//! let decompressed = compressor.decompress(&compressed)?;
37//!
38//! assert_eq!(original, &decompressed[..]);
39//! println!("Original: {} bytes, Compressed: {} bytes",
40//! original.len(), compressed.len());
41//!
42//! Ok(())
43//! }
44//! ```
45//!
46//! ## Smart Type-Aware Compression
47//!
48//! ```rust,no_run
49//! use mediagit_compression::{SmartCompressor, TypeAwareCompressor, ObjectType};
50//!
51//! fn main() -> anyhow::Result<()> {
52//! let compressor = SmartCompressor::new();
53//!
54//! // Automatically selects best strategy based on file type
55//! let text_data = b"Some text content...";
56//! let compressed = compressor.compress_typed(text_data, ObjectType::Text)?;
57//!
58//! // Already compressed formats stored without recompression (with Store prefix)
59//! let jpeg_data = vec![0xFF, 0xD8, 0xFF, 0xE0];
60//! let stored = compressor.compress_typed(&jpeg_data, ObjectType::Jpeg)?;
61//! assert_eq!(stored[0], 0x00); // Store mode magic byte
62//! assert_eq!(&stored[1..], &jpeg_data[..]); // No recompression overhead
63//!
64//! Ok(())
65//! }
66//! ```
67//!
68//! # Compression Algorithms
69//!
70//! ## Zstd (Default)
71//! - Fast compression and decompression
72//! - Good compression ratios (typical 2-4x for text/structured data)
73//! - Low memory usage
74//! - Ideal for frequently accessed data
75//!
76//! ## Brotli
77//! - Higher compression ratios (typical 3-8x for text/structured data)
78//! - Slower compression but faster decompression than gzip
79//! - Good for archival and infrequently accessed data
80//! - Higher memory usage during compression
81//!
82//! # Per-Object Type Strategies
83//!
84//! `SmartCompressor` automatically selects optimal compression:
85//!
86//! - **Already compressed** (JPEG, PNG, MP4, ZIP): Store without recompression
87//! - **Uncompressed images** (TIFF, BMP, PSD, RAW): Zstd Best compression
88//! - **Text/Code** (TXT, JSON, XML, YAML): Brotli Best for maximum text compression
89//! - **Documents** (PDF, SVG): Zstd Default for balanced performance
90//! - **Unknown/Binary**: Zstd Default as safe fallback
91
92pub mod adaptive;
93pub mod brotli_compressor;
94pub mod delta;
95pub mod error;
96pub mod metrics;
97pub mod per_type_compressor;
98pub mod smart_compressor;
99pub mod zlib_compressor;
100pub mod zstd_compressor;
101
102use std::fmt::Debug;
103
104pub use adaptive::{
105 calculate_entropy, AdaptiveCompressor, CompressionStrategy as AdaptiveStrategy, EntropyClass,
106 FileProfile, PatternClass, PerformanceStats, SizeClass,
107};
108pub use brotli_compressor::BrotliCompressor;
109pub use error::{CompressionError, CompressionResult};
110pub use metrics::{AggregatedStats, CompressionMetrics, MetricsAggregator};
111pub use per_type_compressor::{CompressionProfile, PerObjectTypeCompressor, PerTypeStats};
112pub use smart_compressor::{
113 CompressionStrategy, ObjectCategory, ObjectType, SmartCompressor, TypeAwareCompressor,
114};
115pub use zlib_compressor::ZlibCompressor;
116pub use zstd_compressor::ZstdCompressor;
117
118/// Compression level configuration
119///
120/// Balances compression speed vs compression ratio.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum CompressionLevel {
123 /// Fast compression, larger output (level 1 for zstd, 4 for brotli)
124 Fast,
125 /// Default balance (level 3 for zstd, 9 for brotli)
126 Default,
127 /// Best compression, slower (level 22 for zstd, 11 for brotli)
128 Best,
129}
130
131impl CompressionLevel {
132 /// Convert to zstd compression level (1-22)
133 pub fn to_zstd_level(self) -> i32 {
134 match self {
135 CompressionLevel::Fast => 1,
136 CompressionLevel::Default => 3,
137 CompressionLevel::Best => 22,
138 }
139 }
140
141 /// Convert to brotli compression level (0-11)
142 pub fn to_brotli_level(self) -> u32 {
143 match self {
144 CompressionLevel::Fast => 4,
145 CompressionLevel::Default => 9,
146 CompressionLevel::Best => 11,
147 }
148 }
149}
150
151/// Compression algorithm identifier
152#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
153#[repr(u8)]
154pub enum CompressionAlgorithm {
155 /// No compression (raw data)
156 None = 0,
157 /// Zlib compression (Git-compatible)
158 Zlib = 1,
159 /// Zstd compression
160 Zstd = 2,
161 /// Brotli compression
162 Brotli = 3,
163}
164
165impl CompressionAlgorithm {
166 /// Get magic bytes that identify this algorithm
167 pub fn magic_bytes(self) -> &'static [u8] {
168 match self {
169 CompressionAlgorithm::None => b"",
170 CompressionAlgorithm::Zlib => b"\x78", // Zlib header
171 CompressionAlgorithm::Zstd => b"\x28\xb5\x2f\xfd", // Zstd frame magic
172 CompressionAlgorithm::Brotli => b"BRT\x01", // Custom marker for brotli
173 }
174 }
175
176 /// Detect compression algorithm from data
177 pub fn detect(data: &[u8]) -> Self {
178 if data.is_empty() {
179 return CompressionAlgorithm::None;
180 }
181
182 // Check other formats first (they have more reliable magic bytes)
183 if data.len() >= 4 {
184 // Zstd magic: 0xFD2FB528 (little-endian)
185 if data.starts_with(b"\x28\xb5\x2f\xfd") {
186 return CompressionAlgorithm::Zstd;
187 }
188 // Brotli: No standard magic, but we use "BRT\x01" as custom marker
189 if data.starts_with(b"BRT\x01") {
190 return CompressionAlgorithm::Brotli;
191 }
192 }
193
194 // Check zlib (Git compatibility) - requires proper header validation
195 // Zlib header: CMF (0x78) + FLG where (CMF * 256 + FLG) % 31 == 0
196 if data.len() >= 2 && data[0] == 0x78 {
197 let cmf = data[0] as u16;
198 let flg = data[1] as u16;
199 let header_check = cmf * 256 + flg;
200 // Valid zlib headers: 0x789C (default), 0x78DA (best), 0x7801 (no compression)
201 if header_check.is_multiple_of(31) {
202 return CompressionAlgorithm::Zlib;
203 }
204 }
205
206 // No recognized compression format - treat as uncompressed (Store)
207 CompressionAlgorithm::None
208 }
209}
210
211/// Compressor trait for pluggable compression implementations
212///
213/// Implementations must support transparent compression and decompression
214/// with configurable levels.
215pub trait Compressor: Send + Sync + Debug {
216 /// Compress data
217 ///
218 /// # Arguments
219 ///
220 /// * `data` - Raw data to compress
221 ///
222 /// # Returns
223 ///
224 /// Compressed data with algorithm identification prefix
225 ///
226 /// # Errors
227 ///
228 /// Returns `CompressionError` if compression fails
229 fn compress(&self, data: &[u8]) -> CompressionResult<Vec<u8>>;
230
231 /// Decompress data
232 ///
233 /// Automatically detects compression algorithm from data prefix
234 ///
235 /// # Arguments
236 ///
237 /// * `data` - Compressed data (or raw data without compression)
238 ///
239 /// # Returns
240 ///
241 /// Decompressed data
242 ///
243 /// # Errors
244 ///
245 /// Returns `CompressionError` if decompression fails
246 fn decompress(&self, data: &[u8]) -> CompressionResult<Vec<u8>>;
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn compression_level_conversions() {
255 assert_eq!(CompressionLevel::Fast.to_zstd_level(), 1);
256 assert_eq!(CompressionLevel::Default.to_zstd_level(), 3);
257 assert_eq!(CompressionLevel::Best.to_zstd_level(), 22);
258
259 assert_eq!(CompressionLevel::Fast.to_brotli_level(), 4);
260 assert_eq!(CompressionLevel::Default.to_brotli_level(), 9);
261 assert_eq!(CompressionLevel::Best.to_brotli_level(), 11);
262 }
263
264 #[test]
265 fn algorithm_detection() {
266 // Zlib magic bytes
267 let zlib_data = b"\x78\x9c\x00\x00\x00\x00";
268 assert_eq!(
269 CompressionAlgorithm::detect(zlib_data),
270 CompressionAlgorithm::Zlib
271 );
272
273 // Zstd magic bytes
274 let zstd_data = b"\x28\xb5\x2f\xfd\x00\x00\x00\x00";
275 assert_eq!(
276 CompressionAlgorithm::detect(zstd_data),
277 CompressionAlgorithm::Zstd
278 );
279
280 // Brotli marker
281 let brotli_data = b"BRT\x01some_data";
282 assert_eq!(
283 CompressionAlgorithm::detect(brotli_data),
284 CompressionAlgorithm::Brotli
285 );
286
287 // Uncompressed
288 let raw_data = b"Hello, World!";
289 assert_eq!(
290 CompressionAlgorithm::detect(raw_data),
291 CompressionAlgorithm::None
292 );
293
294 // Empty data
295 let empty_data = b"";
296 assert_eq!(
297 CompressionAlgorithm::detect(empty_data),
298 CompressionAlgorithm::None
299 );
300 }
301
302 #[test]
303 fn compression_level_debug() {
304 assert_eq!(format!("{:?}", CompressionLevel::Fast), "Fast");
305 assert_eq!(format!("{:?}", CompressionLevel::Default), "Default");
306 assert_eq!(format!("{:?}", CompressionLevel::Best), "Best");
307 }
308}