Skip to main content

hitbox_backend/
compressor.rs

1//! Compression strategies for cached values.
2//!
3//! This module provides compressors that reduce the size of cached data.
4//!
5//! # Available Compressors
6//!
7//! | Compressor | Ratio | Speed | Feature |
8//! |------------|-------|-------|---------|
9//! | [`PassthroughCompressor`] | None | Fastest | - |
10//! | `GzipCompressor` | Good | Medium | `gzip` |
11//! | `ZstdCompressor` | Best | Fast | `zstd` |
12
13use thiserror::Error;
14
15/// Error type for compression operations.
16#[derive(Debug, Error)]
17pub enum CompressionError {
18    /// Compression operation failed.
19    #[error("Compression failed: {0}")]
20    CompressionFailed(String),
21
22    /// Decompression operation failed.
23    #[error("Decompression failed: {0}")]
24    DecompressionFailed(String),
25}
26
27/// Trait for compressing and decompressing cached values.
28///
29/// Implement this trait to provide custom compression algorithms.
30/// The trait is dyn-compatible with blanket impls for `Box<dyn Compressor>`
31/// and `Arc<dyn Compressor>`.
32pub trait Compressor: Send + Sync + std::fmt::Debug {
33    /// Compress the input data.
34    fn compress(&self, data: &[u8]) -> Result<Vec<u8>, CompressionError>;
35
36    /// Decompress the input data.
37    fn decompress(&self, data: &[u8]) -> Result<Vec<u8>, CompressionError>;
38
39    /// Clone this compressor into a box.
40    fn clone_box(&self) -> Box<dyn Compressor>;
41}
42
43// Blanket implementation for Box<dyn Compressor>
44impl Compressor for Box<dyn Compressor> {
45    fn compress(&self, data: &[u8]) -> Result<Vec<u8>, CompressionError> {
46        (**self).compress(data)
47    }
48
49    fn decompress(&self, data: &[u8]) -> Result<Vec<u8>, CompressionError> {
50        (**self).decompress(data)
51    }
52
53    fn clone_box(&self) -> Box<dyn Compressor> {
54        (**self).clone_box()
55    }
56}
57
58// Blanket implementation for Arc<dyn Compressor>
59impl Compressor for std::sync::Arc<dyn Compressor> {
60    fn compress(&self, data: &[u8]) -> Result<Vec<u8>, CompressionError> {
61        (**self).compress(data)
62    }
63
64    fn decompress(&self, data: &[u8]) -> Result<Vec<u8>, CompressionError> {
65        (**self).decompress(data)
66    }
67
68    fn clone_box(&self) -> Box<dyn Compressor> {
69        (**self).clone_box()
70    }
71}
72
73/// No-op compressor that passes data through unchanged (default)
74#[derive(Debug, Clone, Copy, Default)]
75pub struct PassthroughCompressor;
76
77impl Compressor for PassthroughCompressor {
78    fn compress(&self, data: &[u8]) -> Result<Vec<u8>, CompressionError> {
79        Ok(data.to_vec())
80    }
81
82    fn decompress(&self, data: &[u8]) -> Result<Vec<u8>, CompressionError> {
83        Ok(data.to_vec())
84    }
85
86    fn clone_box(&self) -> Box<dyn Compressor> {
87        Box::new(*self)
88    }
89}
90
91/// Gzip compression with configurable level
92#[cfg(feature = "gzip")]
93#[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
94#[derive(Debug, Clone, Copy)]
95pub struct GzipCompressor {
96    level: u32,
97}
98
99#[cfg(feature = "gzip")]
100impl GzipCompressor {
101    /// Create a new GzipCompressor with default compression level (6)
102    pub fn new() -> Self {
103        Self { level: 6 }
104    }
105
106    /// Create a new GzipCompressor with specified compression level (0-9)
107    pub fn with_level(level: u32) -> Self {
108        Self {
109            level: level.min(9),
110        }
111    }
112}
113
114#[cfg(feature = "gzip")]
115impl Default for GzipCompressor {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121#[cfg(feature = "gzip")]
122impl Compressor for GzipCompressor {
123    fn compress(&self, data: &[u8]) -> Result<Vec<u8>, CompressionError> {
124        use flate2::Compression;
125        use flate2::write::GzEncoder;
126        use std::io::Write;
127
128        let mut encoder = GzEncoder::new(Vec::new(), Compression::new(self.level));
129        encoder
130            .write_all(data)
131            .map_err(|e| CompressionError::CompressionFailed(e.to_string()))?;
132        encoder
133            .finish()
134            .map_err(|e| CompressionError::CompressionFailed(e.to_string()))
135    }
136
137    fn decompress(&self, data: &[u8]) -> Result<Vec<u8>, CompressionError> {
138        use flate2::read::GzDecoder;
139        use std::io::Read;
140
141        let mut decoder = GzDecoder::new(data);
142        let mut decompressed = Vec::new();
143        decoder
144            .read_to_end(&mut decompressed)
145            .map_err(|e| CompressionError::DecompressionFailed(e.to_string()))?;
146        Ok(decompressed)
147    }
148
149    fn clone_box(&self) -> Box<dyn Compressor> {
150        Box::new(*self)
151    }
152}
153
154/// Zstd compression with configurable level
155#[cfg(feature = "zstd")]
156#[cfg_attr(docsrs, doc(cfg(feature = "zstd")))]
157#[derive(Debug, Clone, Copy)]
158pub struct ZstdCompressor {
159    level: i32,
160}
161
162#[cfg(feature = "zstd")]
163impl ZstdCompressor {
164    /// Create a new ZstdCompressor with default compression level (3)
165    pub fn new() -> Self {
166        Self { level: 3 }
167    }
168
169    /// Create a new ZstdCompressor with specified compression level (-7 to 22)
170    /// Lower values = faster but less compression
171    /// Higher values = slower but better compression
172    pub fn with_level(level: i32) -> Self {
173        Self {
174            level: level.clamp(-7, 22),
175        }
176    }
177}
178
179#[cfg(feature = "zstd")]
180impl Default for ZstdCompressor {
181    fn default() -> Self {
182        Self::new()
183    }
184}
185
186#[cfg(feature = "zstd")]
187impl Compressor for ZstdCompressor {
188    fn compress(&self, data: &[u8]) -> Result<Vec<u8>, CompressionError> {
189        zstd::encode_all(data, self.level)
190            .map_err(|e| CompressionError::CompressionFailed(e.to_string()))
191    }
192
193    fn decompress(&self, data: &[u8]) -> Result<Vec<u8>, CompressionError> {
194        zstd::decode_all(data).map_err(|e| CompressionError::DecompressionFailed(e.to_string()))
195    }
196
197    fn clone_box(&self) -> Box<dyn Compressor> {
198        Box::new(*self)
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn test_passthrough_compressor() {
208        let compressor = PassthroughCompressor;
209        let data = b"Hello, World!";
210
211        let compressed = compressor.compress(data).unwrap();
212        assert_eq!(compressed, data);
213
214        let decompressed = compressor.decompress(&compressed).unwrap();
215        assert_eq!(decompressed, data);
216    }
217
218    #[cfg(feature = "gzip")]
219    #[test]
220    fn test_gzip_compressor() {
221        let compressor = GzipCompressor::new();
222        let data = b"Hello, World! This is a test of gzip compression.".repeat(10);
223
224        let compressed = compressor.compress(&data).unwrap();
225        assert!(
226            compressed.len() < data.len(),
227            "Compressed data should be smaller"
228        );
229
230        let decompressed = compressor.decompress(&compressed).unwrap();
231        assert_eq!(decompressed, data);
232    }
233
234    #[cfg(feature = "gzip")]
235    #[test]
236    fn test_gzip_compression_levels() {
237        let data = b"Hello, World! This is a test of gzip compression.".repeat(100);
238
239        let fast = GzipCompressor::with_level(1);
240        let balanced = GzipCompressor::with_level(6);
241        let max = GzipCompressor::with_level(9);
242
243        let fast_compressed = fast.compress(&data).unwrap();
244        let balanced_compressed = balanced.compress(&data).unwrap();
245        let max_compressed = max.compress(&data).unwrap();
246
247        // Higher compression level should produce smaller output
248        assert!(max_compressed.len() <= balanced_compressed.len());
249        assert!(balanced_compressed.len() <= fast_compressed.len());
250
251        // All should decompress to original
252        assert_eq!(fast.decompress(&fast_compressed).unwrap(), data);
253        assert_eq!(balanced.decompress(&balanced_compressed).unwrap(), data);
254        assert_eq!(max.decompress(&max_compressed).unwrap(), data);
255    }
256
257    #[cfg(feature = "zstd")]
258    #[test]
259    fn test_zstd_compressor() {
260        let compressor = ZstdCompressor::new();
261        let data = b"Hello, World! This is a test of zstd compression.".repeat(10);
262
263        let compressed = compressor.compress(&data).unwrap();
264        assert!(
265            compressed.len() < data.len(),
266            "Compressed data should be smaller"
267        );
268
269        let decompressed = compressor.decompress(&compressed).unwrap();
270        assert_eq!(decompressed, data);
271    }
272
273    #[cfg(feature = "zstd")]
274    #[test]
275    fn test_zstd_compression_levels() {
276        let data = b"Hello, World! This is a test of zstd compression.".repeat(100);
277
278        let fast = ZstdCompressor::with_level(-7);
279        let balanced = ZstdCompressor::with_level(3);
280        let max = ZstdCompressor::with_level(22);
281
282        let fast_compressed = fast.compress(&data).unwrap();
283        let balanced_compressed = balanced.compress(&data).unwrap();
284        let max_compressed = max.compress(&data).unwrap();
285
286        // Higher compression level should produce smaller output
287        assert!(max_compressed.len() <= balanced_compressed.len());
288
289        // All should decompress to original
290        assert_eq!(fast.decompress(&fast_compressed).unwrap(), data);
291        assert_eq!(balanced.decompress(&balanced_compressed).unwrap(), data);
292        assert_eq!(max.decompress(&max_compressed).unwrap(), data);
293    }
294}