oxirs_tdb/compression/
lz4.rs1use super::{
20 AdvancedCompressionType, CompressedData, CompressionAlgorithm, CompressionMetadata,
21 MAX_COMPRESSION_INPUT_SIZE,
22};
23use anyhow::{bail, Result};
24use std::collections::HashMap;
25use std::time::Instant;
26
27#[derive(Debug, Clone)]
32pub struct Lz4Compressor {
33 level: u32,
35}
36
37impl Lz4Compressor {
38 pub fn new() -> Self {
40 Self { level: 1 } }
42
43 pub fn with_level(level: u32) -> Self {
50 Self {
51 level: level.min(16),
52 }
53 }
54
55 pub fn fast() -> Self {
57 Self::with_level(1)
58 }
59
60 pub fn balanced() -> Self {
62 Self::with_level(4)
63 }
64
65 pub fn high() -> Self {
67 Self::with_level(9)
68 }
69
70 pub fn max() -> Self {
72 Self::with_level(16)
73 }
74}
75
76impl Default for Lz4Compressor {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82impl CompressionAlgorithm for Lz4Compressor {
83 fn compress(&self, data: &[u8]) -> Result<CompressedData> {
84 if data.is_empty() {
85 return Ok(CompressedData {
86 data: Vec::new(),
87 metadata: CompressionMetadata {
88 algorithm: AdvancedCompressionType::Adaptive, original_size: 0,
90 compressed_size: 0,
91 compression_time_us: 0,
92 metadata: HashMap::new(),
93 },
94 });
95 }
96
97 if data.len() > MAX_COMPRESSION_INPUT_SIZE {
98 bail!(
99 "Input data too large: {} bytes (max: {})",
100 data.len(),
101 MAX_COMPRESSION_INPUT_SIZE
102 );
103 }
104
105 let start = Instant::now();
106
107 let compressed = if self.level == 0 {
109 data.to_vec()
111 } else {
112 oxiarc_lz4::compress(data)
114 .map_err(|e| anyhow::anyhow!("LZ4 compression failed: {}", e))?
115 };
116
117 let compression_time = start.elapsed();
118
119 let mut metadata_map = HashMap::new();
120 metadata_map.insert("level".to_string(), self.level.to_string());
121 metadata_map.insert(
122 "compression_ratio".to_string(),
123 format!("{:.2}", compressed.len() as f64 / data.len() as f64),
124 );
125
126 let compressed_size = compressed.len() as u64;
127
128 Ok(CompressedData {
129 data: compressed,
130 metadata: CompressionMetadata {
131 algorithm: AdvancedCompressionType::Adaptive, original_size: data.len() as u64,
133 compressed_size,
134 compression_time_us: compression_time.as_micros() as u64,
135 metadata: metadata_map,
136 },
137 })
138 }
139
140 fn decompress(&self, compressed: &CompressedData) -> Result<Vec<u8>> {
141 if compressed.data.is_empty() {
142 return Ok(Vec::new());
143 }
144
145 let start = Instant::now();
146
147 let decompressed =
149 if compressed.metadata.original_size == compressed.metadata.compressed_size {
150 compressed.data.clone()
152 } else {
153 oxiarc_lz4::decompress(&compressed.data, compressed.metadata.original_size as usize)
154 .map_err(|e| anyhow::anyhow!("LZ4 decompression failed: {}", e))?
155 };
156
157 let decompression_time = start.elapsed();
158
159 if decompressed.len() != compressed.metadata.original_size as usize {
161 bail!(
162 "Decompressed size mismatch: expected {}, got {}",
163 compressed.metadata.original_size,
164 decompressed.len()
165 );
166 }
167
168 log::trace!(
169 "LZ4 decompression: {} bytes -> {} bytes in {:?}",
170 compressed.data.len(),
171 decompressed.len(),
172 decompression_time
173 );
174
175 Ok(decompressed)
176 }
177
178 fn algorithm_type(&self) -> AdvancedCompressionType {
179 AdvancedCompressionType::Adaptive }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_lz4_compress_empty() {
189 let compressor = Lz4Compressor::new();
190 let result = compressor.compress(&[]).unwrap();
191 assert_eq!(result.data.len(), 0);
192 assert_eq!(result.metadata.original_size, 0);
193 assert_eq!(result.metadata.compressed_size, 0);
194 }
195
196 #[test]
197 fn test_lz4_compress_decompress() {
198 let compressor = Lz4Compressor::new();
199 let original = b"Hello, World! This is a test of LZ4 compression. ".repeat(100);
200
201 let compressed = compressor.compress(&original).unwrap();
202 assert!(compressed.data.len() < original.len());
203 assert_eq!(compressed.metadata.original_size, original.len() as u64);
204
205 let decompressed = compressor.decompress(&compressed).unwrap();
206 assert_eq!(decompressed, original);
207 }
208
209 #[test]
210 fn test_lz4_compression_levels() {
211 let data = b"The quick brown fox jumps over the lazy dog. ".repeat(50);
212
213 let fast = Lz4Compressor::fast();
214 let balanced = Lz4Compressor::balanced();
215 let high = Lz4Compressor::high();
216
217 let fast_result = fast.compress(&data).unwrap();
218 let balanced_result = balanced.compress(&data).unwrap();
219 let high_result = high.compress(&data).unwrap();
220
221 assert_eq!(fast.decompress(&fast_result).unwrap(), data);
223 assert_eq!(balanced.decompress(&balanced_result).unwrap(), data);
224 assert_eq!(high.decompress(&high_result).unwrap(), data);
225
226 println!("Fast: {} bytes", fast_result.data.len());
229 println!("Balanced: {} bytes", balanced_result.data.len());
230 println!("High: {} bytes", high_result.data.len());
231 }
232
233 #[test]
234 fn test_lz4_highly_compressible_data() {
235 let compressor = Lz4Compressor::new();
236 let data = vec![b'A'; 10000]; let compressed = compressor.compress(&data).unwrap();
239 let decompressed = compressor.decompress(&compressed).unwrap();
240
241 assert_eq!(decompressed, data);
242 assert!(compressed.data.len() < data.len() / 10);
244 }
245
246 #[test]
247 fn test_lz4_incompressible_data() {
248 let compressor = Lz4Compressor::new();
249 use scirs2_core::random::rng;
251 use scirs2_core::RngExt;
252
253 let mut rng = rng();
254 let data: Vec<u8> = (0..1000).map(|_| rng.random_range(0..256) as u8).collect();
255
256 let compressed = compressor.compress(&data).unwrap();
257 let decompressed = compressor.decompress(&compressed).unwrap();
258
259 assert_eq!(decompressed, data);
260 println!(
262 "Incompressible: {} -> {} bytes",
263 data.len(),
264 compressed.data.len()
265 );
266 }
267
268 #[test]
269 fn test_lz4_too_large_input() {
270 let compressor = Lz4Compressor::new();
271 let data = vec![0u8; MAX_COMPRESSION_INPUT_SIZE + 1];
272
273 let result = compressor.compress(&data);
274 assert!(result.is_err());
275 }
276
277 #[test]
278 fn test_lz4_metadata() {
279 let compressor = Lz4Compressor::with_level(4);
280 let data = b"Testing metadata collection".repeat(10);
281
282 let compressed = compressor.compress(&data).unwrap();
283
284 assert_eq!(compressed.metadata.metadata.get("level").unwrap(), "4");
285 assert!(compressed
286 .metadata
287 .metadata
288 .contains_key("compression_ratio"));
289 assert!(compressed.metadata.compression_time_us > 0);
290 }
291}