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 = zstd::bulk::compress(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    let decompressed =
162        zstd::bulk::decompress(data, 10 * 1024 * 1024) // 10MB max
163            .map_err(|e| Error::Internal(format!("Zstd decompression failed: {}", e)))?;
164    Ok(Bytes::from(decompressed))
165}
166
167/// Compress data using LZ4
168fn compress_lz4(data: &Bytes, _level: u8) -> Result<Bytes> {
169    // lz4_flex doesn't expose compression levels in the simple API
170    // Use the standard compress_prepend_size function
171    let compressed = lz4_flex::compress_prepend_size(data);
172    Ok(Bytes::from(compressed))
173}
174
175/// Decompress data using LZ4
176fn decompress_lz4(data: &Bytes) -> Result<Bytes> {
177    let decompressed = lz4_flex::decompress_size_prepended(data)
178        .map_err(|e| Error::Internal(format!("LZ4 decompression failed: {}", e)))?;
179    Ok(Bytes::from(decompressed))
180}
181
182/// Estimate compression ratio for given data
183///
184/// Returns a value between 0.0 and 1.0, where lower values indicate better compression.
185/// A value of 0.5 means the data compressed to 50% of its original size.
186///
187/// # Example
188///
189/// ```rust
190/// use ipfrs_core::compression::{CompressionAlgorithm, compression_ratio};
191/// use bytes::Bytes;
192///
193/// let data = Bytes::from_static(b"Hello, World! Hello, World! Hello, World!");
194/// let ratio = compression_ratio(&data, CompressionAlgorithm::Zstd, 5).unwrap();
195/// assert!(ratio < 1.0); // Should compress well due to repetition
196/// ```
197pub fn compression_ratio(data: &Bytes, algorithm: CompressionAlgorithm, level: u8) -> Result<f64> {
198    if data.is_empty() {
199        return Ok(0.0);
200    }
201
202    let compressed = compress(data, algorithm, level)?;
203    Ok(compressed.len() as f64 / data.len() as f64)
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_compression_none() {
212        let data = Bytes::from_static(b"Hello, World!");
213        let compressed = compress(&data, CompressionAlgorithm::None, 5).unwrap();
214        assert_eq!(data, compressed);
215
216        let decompressed = decompress(&compressed, CompressionAlgorithm::None).unwrap();
217        assert_eq!(data, decompressed);
218    }
219
220    #[test]
221    fn test_compression_zstd() {
222        // Use highly compressible data (repetitive pattern)
223        let data = Bytes::from("Hello, World! ".repeat(100));
224        let compressed = compress(&data, CompressionAlgorithm::Zstd, 5).unwrap();
225
226        // Should compress well with repetitive data
227        assert!(compressed.len() < data.len());
228
229        let decompressed = decompress(&compressed, CompressionAlgorithm::Zstd).unwrap();
230        assert_eq!(data, decompressed);
231    }
232
233    #[test]
234    fn test_compression_lz4() {
235        // Use highly compressible data (repetitive pattern)
236        let data = Bytes::from("Hello, World! ".repeat(100));
237        let compressed = compress(&data, CompressionAlgorithm::Lz4, 5).unwrap();
238
239        // Should compress well with repetitive data
240        assert!(compressed.len() < data.len());
241
242        let decompressed = decompress(&compressed, CompressionAlgorithm::Lz4).unwrap();
243        assert_eq!(data, decompressed);
244    }
245
246    #[test]
247    fn test_compression_levels() {
248        let data = Bytes::from_static(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); // Highly compressible
249
250        // Level 0 (fastest)
251        let compressed_0 = compress(&data, CompressionAlgorithm::Zstd, 0).unwrap();
252
253        // Level 9 (best compression)
254        let compressed_9 = compress(&data, CompressionAlgorithm::Zstd, 9).unwrap();
255
256        // Both should decompress correctly
257        let decompressed_0 = decompress(&compressed_0, CompressionAlgorithm::Zstd).unwrap();
258        let decompressed_9 = decompress(&compressed_9, CompressionAlgorithm::Zstd).unwrap();
259
260        assert_eq!(data, decompressed_0);
261        assert_eq!(data, decompressed_9);
262
263        // Higher level should generally compress better
264        assert!(compressed_9.len() <= compressed_0.len());
265    }
266
267    #[test]
268    fn test_invalid_compression_level() {
269        let data = Bytes::from_static(b"Hello");
270        let result = compress(&data, CompressionAlgorithm::Zstd, 10);
271        assert!(result.is_err());
272    }
273
274    #[test]
275    fn test_compression_ratio() {
276        let data = Bytes::from("a".repeat(1000));
277        let ratio = compression_ratio(&data, CompressionAlgorithm::Zstd, 5).unwrap();
278        assert!(ratio < 0.1); // Should compress very well
279    }
280
281    #[test]
282    fn test_compression_algorithm_name() {
283        assert_eq!(CompressionAlgorithm::None.name(), "none");
284        assert_eq!(CompressionAlgorithm::Zstd.name(), "zstd");
285        assert_eq!(CompressionAlgorithm::Lz4.name(), "lz4");
286    }
287
288    #[test]
289    fn test_compression_algorithm_is_compressed() {
290        assert!(!CompressionAlgorithm::None.is_compressed());
291        assert!(CompressionAlgorithm::Zstd.is_compressed());
292        assert!(CompressionAlgorithm::Lz4.is_compressed());
293    }
294
295    #[test]
296    fn test_compression_algorithm_all() {
297        let all = CompressionAlgorithm::all();
298        assert_eq!(all.len(), 3);
299        assert!(all.contains(&CompressionAlgorithm::None));
300        assert!(all.contains(&CompressionAlgorithm::Zstd));
301        assert!(all.contains(&CompressionAlgorithm::Lz4));
302    }
303
304    #[test]
305    fn test_empty_data() {
306        let data = Bytes::new();
307        let compressed = compress(&data, CompressionAlgorithm::Zstd, 5).unwrap();
308        let decompressed = decompress(&compressed, CompressionAlgorithm::Zstd).unwrap();
309        assert_eq!(data, decompressed);
310    }
311
312    #[test]
313    fn test_large_data() {
314        let data = Bytes::from(vec![42u8; 1_000_000]); // 1MB of same data
315        let compressed = compress(&data, CompressionAlgorithm::Zstd, 5).unwrap();
316
317        // Should compress very well (highly redundant data)
318        assert!(compressed.len() < data.len() / 100);
319
320        let decompressed = decompress(&compressed, CompressionAlgorithm::Zstd).unwrap();
321        assert_eq!(data, decompressed);
322    }
323
324    #[test]
325    fn test_algorithm_display() {
326        assert_eq!(CompressionAlgorithm::None.to_string(), "none");
327        assert_eq!(CompressionAlgorithm::Zstd.to_string(), "zstd");
328        assert_eq!(CompressionAlgorithm::Lz4.to_string(), "lz4");
329    }
330
331    #[test]
332    fn test_all_algorithms_roundtrip() {
333        let data = Bytes::from_static(b"The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.");
334
335        for algorithm in CompressionAlgorithm::all() {
336            for level in 0..=9 {
337                let compressed = compress(&data, *algorithm, level).unwrap();
338                let decompressed = decompress(&compressed, *algorithm).unwrap();
339                assert_eq!(
340                    data, decompressed,
341                    "Failed for {:?} at level {}",
342                    algorithm, level
343                );
344            }
345        }
346    }
347}