Skip to main content

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}