skp_cache_core/
compression.rs

1//! Compression support for cached values
2//!
3//! Provides zstd compression to reduce memory usage and network bandwidth.
4
5use crate::CacheError;
6
7/// Compression level (1-22, higher = better compression but slower)
8pub const DEFAULT_COMPRESSION_LEVEL: i32 = 3;
9
10/// Minimum size threshold for compression (bytes)
11/// Values smaller than this won't be compressed
12pub const MIN_COMPRESSION_SIZE: usize = 256;
13
14/// Trait for compression implementations
15pub trait Compressor: Send + Sync + Clone + 'static {
16    /// Name of the compressor
17    fn name(&self) -> &str;
18
19    /// Compress data
20    fn compress(&self, data: &[u8]) -> Result<Vec<u8>, CacheError>;
21
22    /// Decompress data
23    fn decompress(&self, data: &[u8]) -> Result<Vec<u8>, CacheError>;
24
25    /// Check if data should be compressed (based on size threshold)
26    fn should_compress(&self, data: &[u8]) -> bool {
27        data.len() >= MIN_COMPRESSION_SIZE
28    }
29}
30
31/// No-op compressor (disabled compression)
32#[derive(Debug, Clone, Copy, Default)]
33pub struct NoopCompressor;
34
35impl Compressor for NoopCompressor {
36    fn name(&self) -> &str {
37        "none"
38    }
39
40    fn compress(&self, data: &[u8]) -> Result<Vec<u8>, CacheError> {
41        Ok(data.to_vec())
42    }
43
44    fn decompress(&self, data: &[u8]) -> Result<Vec<u8>, CacheError> {
45        Ok(data.to_vec())
46    }
47
48    fn should_compress(&self, _data: &[u8]) -> bool {
49        false
50    }
51}
52
53/// Zstd compressor
54#[cfg(feature = "compression")]
55#[derive(Debug, Clone)]
56pub struct ZstdCompressor {
57    level: i32,
58    min_size: usize,
59}
60
61#[cfg(feature = "compression")]
62impl Default for ZstdCompressor {
63    fn default() -> Self {
64        Self::new(DEFAULT_COMPRESSION_LEVEL)
65    }
66}
67
68#[cfg(feature = "compression")]
69impl ZstdCompressor {
70    /// Create a new zstd compressor with the given compression level (1-22)
71    pub fn new(level: i32) -> Self {
72        Self {
73            level: level.clamp(1, 22),
74            min_size: MIN_COMPRESSION_SIZE,
75        }
76    }
77
78    /// Set minimum size for compression
79    pub fn with_min_size(mut self, size: usize) -> Self {
80        self.min_size = size;
81        self
82    }
83
84    /// Get the compression level
85    pub fn level(&self) -> i32 {
86        self.level
87    }
88}
89
90#[cfg(feature = "compression")]
91impl Compressor for ZstdCompressor {
92    fn name(&self) -> &str {
93        "zstd"
94    }
95
96    fn compress(&self, data: &[u8]) -> Result<Vec<u8>, CacheError> {
97        zstd::encode_all(data, self.level)
98            .map_err(|e| CacheError::Compression(e.to_string()))
99    }
100
101    fn decompress(&self, data: &[u8]) -> Result<Vec<u8>, CacheError> {
102        zstd::decode_all(data)
103            .map_err(|e| CacheError::Decompression(e.to_string()))
104    }
105
106    fn should_compress(&self, data: &[u8]) -> bool {
107        data.len() >= self.min_size
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_noop_compressor() {
117        let compressor = NoopCompressor;
118        let data = b"hello world";
119
120        let compressed = compressor.compress(data).unwrap();
121        assert_eq!(compressed, data);
122
123        let decompressed = compressor.decompress(&compressed).unwrap();
124        assert_eq!(decompressed, data);
125
126        assert!(!compressor.should_compress(data));
127    }
128
129    #[cfg(feature = "compression")]
130    #[test]
131    fn test_zstd_compressor() {
132        let compressor = ZstdCompressor::new(3);
133
134        // Large data should compress
135        let data: Vec<u8> = (0..1024).map(|i| (i % 256) as u8).collect();
136
137        let compressed = compressor.compress(&data).unwrap();
138        // Compressed should be smaller (for repetitive data)
139        assert!(compressed.len() < data.len());
140
141        let decompressed = compressor.decompress(&compressed).unwrap();
142        assert_eq!(decompressed, data);
143    }
144
145    #[cfg(feature = "compression")]
146    #[test]
147    fn test_zstd_should_compress() {
148        let compressor = ZstdCompressor::new(3);
149
150        // Small data shouldn't be compressed
151        assert!(!compressor.should_compress(b"small"));
152
153        // Large data should be compressed
154        let large: Vec<u8> = vec![0; 1024];
155        assert!(compressor.should_compress(&large));
156    }
157
158    #[cfg(feature = "compression")]
159    #[test]
160    fn test_zstd_level_clamping() {
161        let low = ZstdCompressor::new(-5);
162        assert_eq!(low.level(), 1);
163
164        let high = ZstdCompressor::new(100);
165        assert_eq!(high.level(), 22);
166    }
167}