Skip to main content

ipfrs_core/
compression.rs

1//! Compression support for block data
2//!
3//! This module provides compression and decompression capabilities for block data,
4//! supporting multiple algorithms for different use cases.
5//!
6//! # Supported Algorithms
7//!
8//! - **None**: No compression (passthrough)
9//! - **Zstd**: Zstandard compression (high ratio, good speed)
10//! - **Lz4**: LZ4 compression (very fast, moderate ratio)
11//!
12//! # Example
13//!
14//! ```rust
15//! use ipfrs_core::compression::{CompressionAlgorithm, compress, decompress};
16//! use bytes::Bytes;
17//!
18//! let data = Bytes::from_static(b"Hello, World! This is some data to compress.");
19//! let level = 3;
20//!
21//! // Compress with Zstd
22//! let compressed = compress(&data, CompressionAlgorithm::Zstd, level).unwrap();
23//! println!("Original: {} bytes, Compressed: {} bytes", data.len(), compressed.len());
24//!
25//! // Decompress
26//! let decompressed = decompress(&compressed, CompressionAlgorithm::Zstd).unwrap();
27//! assert_eq!(data, decompressed);
28//! ```
29
30use crate::error::{Error, Result};
31use bytes::Bytes;
32
33/// Compression algorithms supported by ipfrs-core
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
35pub enum CompressionAlgorithm {
36    /// No compression (passthrough)
37    #[default]
38    None,
39    /// Zstandard compression (high ratio, good speed)
40    Zstd,
41    /// LZ4 compression (very fast, moderate ratio)
42    Lz4,
43}
44
45impl CompressionAlgorithm {
46    /// Get the name of the compression algorithm
47    #[inline]
48    pub fn name(&self) -> &'static str {
49        match self {
50            CompressionAlgorithm::None => "none",
51            CompressionAlgorithm::Zstd => "zstd",
52            CompressionAlgorithm::Lz4 => "lz4",
53        }
54    }
55
56    /// Check if this algorithm actually compresses data
57    #[inline]
58    pub fn is_compressed(&self) -> bool {
59        !matches!(self, CompressionAlgorithm::None)
60    }
61
62    /// Get all available compression algorithms
63    pub fn all() -> &'static [CompressionAlgorithm] {
64        &[
65            CompressionAlgorithm::None,
66            CompressionAlgorithm::Zstd,
67            CompressionAlgorithm::Lz4,
68        ]
69    }
70}
71
72impl std::fmt::Display for CompressionAlgorithm {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        write!(f, "{}", self.name())
75    }
76}
77
78/// Compress data using the specified algorithm and level
79///
80/// # Arguments
81///
82/// * `data` - The data to compress
83/// * `algorithm` - The compression algorithm to use
84/// * `level` - Compression level (0-9, where 0 is fastest and 9 is best compression)
85///
86/// # Returns
87///
88/// Compressed data as `Bytes`, or the original data if algorithm is `None`
89///
90/// # Example
91///
92/// ```rust
93/// use ipfrs_core::compression::{CompressionAlgorithm, compress};
94/// use bytes::Bytes;
95///
96/// let data = Bytes::from_static(b"Hello, World!");
97/// let compressed = compress(&data, CompressionAlgorithm::Zstd, 3).unwrap();
98/// ```
99pub fn compress(data: &Bytes, algorithm: CompressionAlgorithm, level: u8) -> Result<Bytes> {
100    if level > 9 {
101        return Err(Error::InvalidInput(format!(
102            "Invalid compression level {}, must be 0-9",
103            level
104        )));
105    }
106
107    match algorithm {
108        CompressionAlgorithm::None => Ok(data.clone()),
109        CompressionAlgorithm::Zstd => compress_zstd(data, level),
110        CompressionAlgorithm::Lz4 => compress_lz4(data, level),
111    }
112}
113
114/// Decompress data using the specified algorithm
115///
116/// # Arguments
117///
118/// * `data` - The compressed data
119/// * `algorithm` - The compression algorithm that was used
120///
121/// # Returns
122///
123/// Decompressed data as `Bytes`
124///
125/// # Example
126///
127/// ```rust
128/// use ipfrs_core::compression::{CompressionAlgorithm, compress, decompress};
129/// use bytes::Bytes;
130///
131/// let data = Bytes::from_static(b"Hello, World!");
132/// let compressed = compress(&data, CompressionAlgorithm::Lz4, 3).unwrap();
133/// let decompressed = decompress(&compressed, CompressionAlgorithm::Lz4).unwrap();
134/// assert_eq!(data, decompressed);
135/// ```
136pub fn decompress(data: &Bytes, algorithm: CompressionAlgorithm) -> Result<Bytes> {
137    match algorithm {
138        CompressionAlgorithm::None => Ok(data.clone()),
139        CompressionAlgorithm::Zstd => decompress_zstd(data),
140        CompressionAlgorithm::Lz4 => decompress_lz4(data),
141    }
142}
143
144/// Compress data using Zstd
145fn compress_zstd(data: &Bytes, level: u8) -> Result<Bytes> {
146    // Convert level 0-9 to zstd level range (-7 to 22)
147    // 0 -> 1 (fastest), 9 -> 22 (best compression)
148    let zstd_level = if level == 0 {
149        1
150    } else {
151        1 + (level as i32 * 21 / 9)
152    };
153
154    let compressed = oxiarc_zstd::compress_with_level(data, zstd_level)
155        .map_err(|e| Error::Internal(format!("Zstd compression failed: {}", e)))?;
156    Ok(Bytes::from(compressed))
157}
158
159/// Decompress data using Zstd
160fn decompress_zstd(data: &Bytes) -> Result<Bytes> {
161    // OxiARC zstd frames are self-describing, so no output capacity hint is needed.
162    let decompressed = oxiarc_zstd::decompress(data)
163        .map_err(|e| Error::Internal(format!("Zstd decompression failed: {}", e)))?;
164    Ok(Bytes::from(decompressed))
165}
166
167/// Compress data using LZ4
168///
169/// The output is a self-describing block: a 4-byte little-endian original size
170/// header followed by an OxiARC LZ4 frame. The size header lets [`decompress_lz4`]
171/// allocate an accurate output buffer without an external length hint.
172fn compress_lz4(data: &Bytes, _level: u8) -> Result<Bytes> {
173    // OxiARC LZ4 does not expose compression levels in the simple frame API.
174    let payload = oxiarc_lz4::compress(data)
175        .map_err(|e| Error::Internal(format!("LZ4 compression failed: {}", e)))?;
176    let orig_len = data.len() as u32;
177    let mut out = Vec::with_capacity(4 + payload.len());
178    out.extend_from_slice(&orig_len.to_le_bytes());
179    out.extend_from_slice(&payload);
180    Ok(Bytes::from(out))
181}
182
183/// Decompress data using LZ4
184fn decompress_lz4(data: &Bytes) -> Result<Bytes> {
185    if data.len() < 4 {
186        return Err(Error::Internal(
187            "LZ4 block too short (missing original size header)".to_string(),
188        ));
189    }
190    let orig_size = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
191    // Generous upper bound: 2x original size avoids truncation for edge cases.
192    let max_output = orig_size.saturating_mul(2).max(orig_size + 64);
193    let decompressed = oxiarc_lz4::decompress(&data[4..], max_output)
194        .map_err(|e| Error::Internal(format!("LZ4 decompression failed: {}", e)))?;
195    Ok(Bytes::from(decompressed))
196}
197
198/// Estimate compression ratio for given data
199///
200/// Returns a value between 0.0 and 1.0, where lower values indicate better compression.
201/// A value of 0.5 means the data compressed to 50% of its original size.
202///
203/// # Example
204///
205/// ```rust
206/// use ipfrs_core::compression::{CompressionAlgorithm, compression_ratio};
207/// use bytes::Bytes;
208///
209/// let data = Bytes::from_static(b"Hello, World! Hello, World! Hello, World!");
210/// let ratio = compression_ratio(&data, CompressionAlgorithm::Zstd, 5).unwrap();
211/// assert!(ratio < 1.0); // Should compress well due to repetition
212/// ```
213pub fn compression_ratio(data: &Bytes, algorithm: CompressionAlgorithm, level: u8) -> Result<f64> {
214    if data.is_empty() {
215        return Ok(0.0);
216    }
217
218    let compressed = compress(data, algorithm, level)?;
219    Ok(compressed.len() as f64 / data.len() as f64)
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_compression_none() {
228        let data = Bytes::from_static(b"Hello, World!");
229        let compressed = compress(&data, CompressionAlgorithm::None, 5).unwrap();
230        assert_eq!(data, compressed);
231
232        let decompressed = decompress(&compressed, CompressionAlgorithm::None).unwrap();
233        assert_eq!(data, decompressed);
234    }
235
236    #[test]
237    fn test_compression_zstd() {
238        // Use highly compressible data (repetitive pattern)
239        let data = Bytes::from("Hello, World! ".repeat(100));
240        let compressed = compress(&data, CompressionAlgorithm::Zstd, 5).unwrap();
241
242        // Should compress well with repetitive data
243        assert!(compressed.len() < data.len());
244
245        let decompressed = decompress(&compressed, CompressionAlgorithm::Zstd).unwrap();
246        assert_eq!(data, decompressed);
247    }
248
249    #[test]
250    fn test_compression_lz4() {
251        // Use highly compressible data (repetitive pattern)
252        let data = Bytes::from("Hello, World! ".repeat(100));
253        let compressed = compress(&data, CompressionAlgorithm::Lz4, 5).unwrap();
254
255        // Should compress well with repetitive data
256        assert!(compressed.len() < data.len());
257
258        let decompressed = decompress(&compressed, CompressionAlgorithm::Lz4).unwrap();
259        assert_eq!(data, decompressed);
260    }
261
262    #[test]
263    fn test_compression_levels() {
264        let data = Bytes::from_static(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); // Highly compressible
265
266        // Level 0 (fastest)
267        let compressed_0 = compress(&data, CompressionAlgorithm::Zstd, 0).unwrap();
268
269        // Level 9 (best compression)
270        let compressed_9 = compress(&data, CompressionAlgorithm::Zstd, 9).unwrap();
271
272        // Both should decompress correctly
273        let decompressed_0 = decompress(&compressed_0, CompressionAlgorithm::Zstd).unwrap();
274        let decompressed_9 = decompress(&compressed_9, CompressionAlgorithm::Zstd).unwrap();
275
276        assert_eq!(data, decompressed_0);
277        assert_eq!(data, decompressed_9);
278
279        // Higher level should generally compress better
280        assert!(compressed_9.len() <= compressed_0.len());
281    }
282
283    #[test]
284    fn test_invalid_compression_level() {
285        let data = Bytes::from_static(b"Hello");
286        let result = compress(&data, CompressionAlgorithm::Zstd, 10);
287        assert!(result.is_err());
288    }
289
290    #[test]
291    fn test_compression_ratio() {
292        let data = Bytes::from("a".repeat(1000));
293        let ratio = compression_ratio(&data, CompressionAlgorithm::Zstd, 5).unwrap();
294        assert!(ratio < 0.1); // Should compress very well
295    }
296
297    #[test]
298    fn test_compression_algorithm_name() {
299        assert_eq!(CompressionAlgorithm::None.name(), "none");
300        assert_eq!(CompressionAlgorithm::Zstd.name(), "zstd");
301        assert_eq!(CompressionAlgorithm::Lz4.name(), "lz4");
302    }
303
304    #[test]
305    fn test_compression_algorithm_is_compressed() {
306        assert!(!CompressionAlgorithm::None.is_compressed());
307        assert!(CompressionAlgorithm::Zstd.is_compressed());
308        assert!(CompressionAlgorithm::Lz4.is_compressed());
309    }
310
311    #[test]
312    fn test_compression_algorithm_all() {
313        let all = CompressionAlgorithm::all();
314        assert_eq!(all.len(), 3);
315        assert!(all.contains(&CompressionAlgorithm::None));
316        assert!(all.contains(&CompressionAlgorithm::Zstd));
317        assert!(all.contains(&CompressionAlgorithm::Lz4));
318    }
319
320    #[test]
321    fn test_empty_data() {
322        let data = Bytes::new();
323        let compressed = compress(&data, CompressionAlgorithm::Zstd, 5).unwrap();
324        let decompressed = decompress(&compressed, CompressionAlgorithm::Zstd).unwrap();
325        assert_eq!(data, decompressed);
326    }
327
328    #[test]
329    fn test_large_data() {
330        let data = Bytes::from(vec![42u8; 1_000_000]); // 1MB of same data
331        let compressed = compress(&data, CompressionAlgorithm::Zstd, 5).unwrap();
332
333        // Should compress very well (highly redundant data)
334        assert!(compressed.len() < data.len() / 100);
335
336        let decompressed = decompress(&compressed, CompressionAlgorithm::Zstd).unwrap();
337        assert_eq!(data, decompressed);
338    }
339
340    #[test]
341    fn test_algorithm_display() {
342        assert_eq!(CompressionAlgorithm::None.to_string(), "none");
343        assert_eq!(CompressionAlgorithm::Zstd.to_string(), "zstd");
344        assert_eq!(CompressionAlgorithm::Lz4.to_string(), "lz4");
345    }
346
347    #[test]
348    fn test_all_algorithms_roundtrip() {
349        let data = Bytes::from_static(b"The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.");
350
351        for algorithm in CompressionAlgorithm::all() {
352            for level in 0..=9 {
353                let compressed = compress(&data, *algorithm, level).unwrap();
354                let decompressed = decompress(&compressed, *algorithm).unwrap();
355                assert_eq!(
356                    data, decompressed,
357                    "Failed for {:?} at level {}",
358                    algorithm, level
359                );
360            }
361        }
362    }
363}