Skip to main content

alopex_core/storage/
compression.rs

1//! 圧縮アルゴリズムの抽象化とユーティリティ。
2//!
3//! Snappy は常に利用可能、Zstd/LZ4 は feature で有効化する。
4
5use crate::storage::format::FormatError;
6
7#[cfg(feature = "compression-zstd")]
8use std::io::Cursor;
9
10/// 圧縮アルゴリズム識別子。
11#[repr(u8)]
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum CompressionAlgorithm {
14    /// 圧縮なし。
15    None = 0,
16    /// Snappy 圧縮。
17    Snappy = 1,
18    /// Zstandard 圧縮(`compression-zstd` feature)。
19    Zstd = 2,
20    /// LZ4 圧縮(`compression-lz4` feature)。
21    Lz4 = 3,
22}
23
24/// データを指定されたアルゴリズムで圧縮する。
25///
26/// Feature が無効なアルゴリズムを要求された場合は
27/// [`FormatError::UnsupportedCompression`] を返す。
28pub fn compress(data: &[u8], algorithm: CompressionAlgorithm) -> Result<Vec<u8>, FormatError> {
29    match algorithm {
30        CompressionAlgorithm::None => Ok(data.to_vec()),
31        CompressionAlgorithm::Snappy => {
32            snap::raw::Encoder::new().compress_vec(data).map_err(|_| {
33                FormatError::CompressionFailed {
34                    algorithm: algorithm as u8,
35                }
36            })
37        }
38        CompressionAlgorithm::Zstd => {
39            #[cfg(feature = "compression-zstd")]
40            {
41                zstd::stream::encode_all(Cursor::new(data), 0).map_err(|_| {
42                    FormatError::CompressionFailed {
43                        algorithm: algorithm as u8,
44                    }
45                })
46            }
47            #[cfg(not(feature = "compression-zstd"))]
48            {
49                Err(FormatError::UnsupportedCompression {
50                    algorithm: algorithm as u8,
51                })
52            }
53        }
54        CompressionAlgorithm::Lz4 => {
55            #[cfg(feature = "compression-lz4")]
56            {
57                // Enable content size in the block header so decompression does not require
58                // the caller to supply the original length.
59                lz4::block::compress(data, None, true).map_err(|_| FormatError::CompressionFailed {
60                    algorithm: algorithm as u8,
61                })
62            }
63            #[cfg(not(feature = "compression-lz4"))]
64            {
65                Err(FormatError::UnsupportedCompression {
66                    algorithm: algorithm as u8,
67                })
68            }
69        }
70    }
71}
72
73/// データを指定されたアルゴリズムで解凍する。
74///
75/// Feature が無効なアルゴリズムを要求された場合は
76/// [`FormatError::UnsupportedCompression`] を返す。
77pub fn decompress(data: &[u8], algorithm: CompressionAlgorithm) -> Result<Vec<u8>, FormatError> {
78    match algorithm {
79        CompressionAlgorithm::None => Ok(data.to_vec()),
80        CompressionAlgorithm::Snappy => {
81            snap::raw::Decoder::new().decompress_vec(data).map_err(|_| {
82                FormatError::DecompressionFailed {
83                    algorithm: algorithm as u8,
84                }
85            })
86        }
87        CompressionAlgorithm::Zstd => {
88            #[cfg(feature = "compression-zstd")]
89            {
90                zstd::stream::decode_all(Cursor::new(data)).map_err(|_| {
91                    FormatError::DecompressionFailed {
92                        algorithm: algorithm as u8,
93                    }
94                })
95            }
96            #[cfg(not(feature = "compression-zstd"))]
97            {
98                Err(FormatError::UnsupportedCompression {
99                    algorithm: algorithm as u8,
100                })
101            }
102        }
103        CompressionAlgorithm::Lz4 => {
104            #[cfg(feature = "compression-lz4")]
105            {
106                lz4::block::decompress(data, None).map_err(|_| FormatError::DecompressionFailed {
107                    algorithm: algorithm as u8,
108                })
109            }
110            #[cfg(not(feature = "compression-lz4"))]
111            {
112                Err(FormatError::UnsupportedCompression {
113                    algorithm: algorithm as u8,
114                })
115            }
116        }
117    }
118}
119
120// =============================================================================
121// CompressionV2 - Columnar Storage Compression Interface
122// =============================================================================
123
124use serde::{Deserialize, Serialize};
125
126/// V2 Compression algorithm with level support.
127#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
128pub enum CompressionV2 {
129    /// No compression.
130    #[default]
131    None,
132    /// LZ4 compression.
133    Lz4,
134    /// Zstandard compression with configurable level (1-22).
135    Zstd {
136        /// Compression level (1-22, higher = better compression but slower).
137        level: i32,
138    },
139}
140
141impl CompressionV2 {
142    /// Check if this compression algorithm is available (feature flag enabled).
143    pub fn is_available(&self) -> bool {
144        match self {
145            CompressionV2::None => true,
146            CompressionV2::Lz4 => cfg!(feature = "compression-lz4"),
147            CompressionV2::Zstd { .. } => cfg!(feature = "compression-zstd"),
148        }
149    }
150
151    /// Get a human-readable name for the compression algorithm.
152    pub fn name(&self) -> &'static str {
153        match self {
154            CompressionV2::None => "none",
155            CompressionV2::Lz4 => "lz4",
156            CompressionV2::Zstd { .. } => "zstd",
157        }
158    }
159}
160
161/// Compressor trait for V2 compression implementations.
162pub trait Compressor: Send + Sync {
163    /// Compress data and return the compressed bytes.
164    fn compress(&self, data: &[u8]) -> Result<Vec<u8>, FormatError>;
165
166    /// Decompress data given the expected uncompressed size.
167    fn decompress(&self, data: &[u8], uncompressed_size: usize) -> Result<Vec<u8>, FormatError>;
168
169    /// Get the compression type.
170    fn compression_type(&self) -> CompressionV2;
171}
172
173/// No-op compressor that passes data through unchanged.
174pub struct NoneCompressor;
175
176impl Compressor for NoneCompressor {
177    fn compress(&self, data: &[u8]) -> Result<Vec<u8>, FormatError> {
178        Ok(data.to_vec())
179    }
180
181    fn decompress(&self, data: &[u8], _uncompressed_size: usize) -> Result<Vec<u8>, FormatError> {
182        Ok(data.to_vec())
183    }
184
185    fn compression_type(&self) -> CompressionV2 {
186        CompressionV2::None
187    }
188}
189
190/// LZ4 compressor using the existing LZ4 implementation.
191#[cfg(feature = "compression-lz4")]
192pub struct Lz4Compressor;
193
194#[cfg(feature = "compression-lz4")]
195impl Compressor for Lz4Compressor {
196    fn compress(&self, data: &[u8]) -> Result<Vec<u8>, FormatError> {
197        lz4::block::compress(data, None, true).map_err(|_| FormatError::CompressionFailed {
198            algorithm: CompressionAlgorithm::Lz4 as u8,
199        })
200    }
201
202    fn decompress(&self, data: &[u8], _uncompressed_size: usize) -> Result<Vec<u8>, FormatError> {
203        lz4::block::decompress(data, None).map_err(|_| FormatError::DecompressionFailed {
204            algorithm: CompressionAlgorithm::Lz4 as u8,
205        })
206    }
207
208    fn compression_type(&self) -> CompressionV2 {
209        CompressionV2::Lz4
210    }
211}
212
213/// Zstandard compressor with configurable compression level.
214#[cfg(feature = "compression-zstd")]
215pub struct ZstdCompressor {
216    level: i32,
217}
218
219#[cfg(feature = "compression-zstd")]
220impl ZstdCompressor {
221    /// Create a new Zstd compressor with the specified level (1-22).
222    /// Level is clamped to valid range.
223    pub fn new(level: i32) -> Self {
224        Self {
225            level: level.clamp(1, 22),
226        }
227    }
228}
229
230#[cfg(feature = "compression-zstd")]
231impl Compressor for ZstdCompressor {
232    fn compress(&self, data: &[u8]) -> Result<Vec<u8>, FormatError> {
233        zstd::stream::encode_all(Cursor::new(data), self.level).map_err(|_| {
234            FormatError::CompressionFailed {
235                algorithm: CompressionAlgorithm::Zstd as u8,
236            }
237        })
238    }
239
240    fn decompress(&self, data: &[u8], _uncompressed_size: usize) -> Result<Vec<u8>, FormatError> {
241        zstd::stream::decode_all(Cursor::new(data)).map_err(|_| FormatError::DecompressionFailed {
242            algorithm: CompressionAlgorithm::Zstd as u8,
243        })
244    }
245
246    fn compression_type(&self) -> CompressionV2 {
247        CompressionV2::Zstd { level: self.level }
248    }
249}
250
251/// Create a compressor for the given compression type.
252///
253/// Returns `FormatError::UnsupportedCompression` if the required feature flag is disabled.
254pub fn create_compressor(compression: CompressionV2) -> Result<Box<dyn Compressor>, FormatError> {
255    match compression {
256        CompressionV2::None => Ok(Box::new(NoneCompressor)),
257
258        CompressionV2::Lz4 => {
259            #[cfg(feature = "compression-lz4")]
260            {
261                Ok(Box::new(Lz4Compressor))
262            }
263            #[cfg(not(feature = "compression-lz4"))]
264            {
265                Err(FormatError::UnsupportedCompression {
266                    algorithm: CompressionAlgorithm::Lz4 as u8,
267                })
268            }
269        }
270
271        CompressionV2::Zstd { level } => {
272            #[cfg(feature = "compression-zstd")]
273            {
274                Ok(Box::new(ZstdCompressor::new(level)))
275            }
276            #[cfg(not(feature = "compression-zstd"))]
277            {
278                let _ = level; // Suppress unused warning
279                Err(FormatError::UnsupportedCompression {
280                    algorithm: CompressionAlgorithm::Zstd as u8,
281                })
282            }
283        }
284    }
285}
286
287#[cfg(all(test, not(target_arch = "wasm32")))]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_none_compressor_roundtrip() {
293        let compressor = NoneCompressor;
294        let data = b"hello, world!";
295
296        let compressed = compressor.compress(data).unwrap();
297        assert_eq!(compressed, data);
298
299        let decompressed = compressor.decompress(&compressed, data.len()).unwrap();
300        assert_eq!(decompressed, data);
301    }
302
303    #[cfg(feature = "compression-lz4")]
304    #[test]
305    fn test_lz4_compress_decompress() {
306        let compressor = Lz4Compressor;
307        let data = b"hello, world! this is a test string for lz4 compression.";
308
309        let compressed = compressor.compress(data).unwrap();
310        let decompressed = compressor.decompress(&compressed, data.len()).unwrap();
311        assert_eq!(decompressed, data);
312    }
313
314    #[cfg(feature = "compression-lz4")]
315    #[test]
316    fn test_lz4_compressor_via_factory() {
317        let compressor = create_compressor(CompressionV2::Lz4).unwrap();
318        let data = b"test data for lz4";
319
320        let compressed = compressor.compress(data).unwrap();
321        let decompressed = compressor.decompress(&compressed, data.len()).unwrap();
322        assert_eq!(decompressed, data);
323        assert_eq!(compressor.compression_type(), CompressionV2::Lz4);
324    }
325
326    #[cfg(feature = "compression-zstd")]
327    #[test]
328    fn test_zstd_compress_decompress_levels() {
329        let data = b"hello, world! this is a test string for zstd compression at various levels.";
330
331        for level in [1, 3, 9, 15, 22] {
332            let compressor = ZstdCompressor::new(level);
333            let compressed = compressor.compress(data).unwrap();
334            let decompressed = compressor.decompress(&compressed, data.len()).unwrap();
335            assert_eq!(decompressed, data, "Failed at level {}", level);
336        }
337    }
338
339    #[cfg(feature = "compression-zstd")]
340    #[test]
341    fn test_zstd_compressor_via_factory() {
342        let compressor = create_compressor(CompressionV2::Zstd { level: 3 }).unwrap();
343        let data = b"test data for zstd";
344
345        let compressed = compressor.compress(data).unwrap();
346        let decompressed = compressor.decompress(&compressed, data.len()).unwrap();
347        assert_eq!(decompressed, data);
348        assert_eq!(
349            compressor.compression_type(),
350            CompressionV2::Zstd { level: 3 }
351        );
352    }
353
354    #[cfg(feature = "compression-zstd")]
355    #[test]
356    fn test_zstd_level_clamping() {
357        // Level below minimum
358        let compressor = ZstdCompressor::new(-5);
359        assert_eq!(
360            compressor.compression_type(),
361            CompressionV2::Zstd { level: 1 }
362        );
363
364        // Level above maximum
365        let compressor = ZstdCompressor::new(100);
366        assert_eq!(
367            compressor.compression_type(),
368            CompressionV2::Zstd { level: 22 }
369        );
370    }
371
372    #[test]
373    fn test_none_compressor_via_factory() {
374        let compressor = create_compressor(CompressionV2::None).unwrap();
375        let data = b"test data";
376
377        let compressed = compressor.compress(data).unwrap();
378        let decompressed = compressor.decompress(&compressed, data.len()).unwrap();
379        assert_eq!(decompressed, data);
380        assert_eq!(compressor.compression_type(), CompressionV2::None);
381    }
382
383    #[cfg(not(feature = "compression-lz4"))]
384    #[test]
385    fn test_unsupported_lz4_compression_error() {
386        let result = create_compressor(CompressionV2::Lz4);
387        assert!(result.is_err());
388        match result {
389            Err(FormatError::UnsupportedCompression { algorithm }) => {
390                assert_eq!(algorithm, CompressionAlgorithm::Lz4 as u8);
391            }
392            _ => panic!("Expected UnsupportedCompression error"),
393        }
394    }
395
396    #[cfg(not(feature = "compression-zstd"))]
397    #[test]
398    fn test_unsupported_zstd_compression_error() {
399        let result = create_compressor(CompressionV2::Zstd { level: 3 });
400        assert!(result.is_err());
401        match result {
402            Err(FormatError::UnsupportedCompression { algorithm }) => {
403                assert_eq!(algorithm, CompressionAlgorithm::Zstd as u8);
404            }
405            _ => panic!("Expected UnsupportedCompression error"),
406        }
407    }
408
409    #[test]
410    fn test_compression_v2_is_available() {
411        // None is always available
412        assert!(CompressionV2::None.is_available());
413
414        // LZ4 depends on feature flag
415        #[cfg(feature = "compression-lz4")]
416        assert!(CompressionV2::Lz4.is_available());
417        #[cfg(not(feature = "compression-lz4"))]
418        assert!(!CompressionV2::Lz4.is_available());
419
420        // Zstd depends on feature flag
421        #[cfg(feature = "compression-zstd")]
422        assert!(CompressionV2::Zstd { level: 3 }.is_available());
423        #[cfg(not(feature = "compression-zstd"))]
424        assert!(!CompressionV2::Zstd { level: 3 }.is_available());
425    }
426
427    #[test]
428    fn test_compression_v2_name() {
429        assert_eq!(CompressionV2::None.name(), "none");
430        assert_eq!(CompressionV2::Lz4.name(), "lz4");
431        assert_eq!(CompressionV2::Zstd { level: 5 }.name(), "zstd");
432    }
433
434    #[test]
435    fn test_compression_v2_default() {
436        assert_eq!(CompressionV2::default(), CompressionV2::None);
437    }
438}