oxirs_tdb/compression/
zstd_compression.rs1use super::{
22 AdvancedCompressionType, CompressedData, CompressionAlgorithm, CompressionMetadata,
23 MAX_COMPRESSION_INPUT_SIZE,
24};
25use anyhow::{bail, Result};
26use std::collections::HashMap;
27use std::io::{Read, Write};
28use std::time::Instant;
29
30#[derive(Debug, Clone)]
34pub struct ZstdCompressor {
35 level: i32,
37}
38
39impl ZstdCompressor {
40 pub fn new() -> Self {
42 Self { level: 3 }
43 }
44
45 pub fn with_level(level: i32) -> Self {
55 Self {
56 level: level.clamp(1, 22),
57 }
58 }
59
60 pub fn fast() -> Self {
62 Self::with_level(1)
63 }
64
65 pub fn balanced() -> Self {
67 Self::with_level(3)
68 }
69
70 pub fn high() -> Self {
72 Self::with_level(9)
73 }
74
75 pub fn max() -> Self {
77 Self::with_level(19)
78 }
79
80 pub fn ultra() -> Self {
82 Self::with_level(22)
83 }
84}
85
86impl Default for ZstdCompressor {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl CompressionAlgorithm for ZstdCompressor {
93 fn compress(&self, data: &[u8]) -> Result<CompressedData> {
94 if data.is_empty() {
95 return Ok(CompressedData {
96 data: Vec::new(),
97 metadata: CompressionMetadata {
98 algorithm: AdvancedCompressionType::Adaptive, original_size: 0,
100 compressed_size: 0,
101 compression_time_us: 0,
102 metadata: HashMap::new(),
103 },
104 });
105 }
106
107 if data.len() > MAX_COMPRESSION_INPUT_SIZE {
108 bail!(
109 "Input data too large: {} bytes (max: {})",
110 data.len(),
111 MAX_COMPRESSION_INPUT_SIZE
112 );
113 }
114
115 let start = Instant::now();
116
117 let mut encoder = oxiarc_zstd::ZstdStreamEncoder::new(Vec::new(), self.level);
119 encoder.write_all(data)?;
120 let compressed = encoder.finish()?;
121
122 let compression_time = start.elapsed();
123
124 let mut metadata_map = HashMap::new();
125 metadata_map.insert("level".to_string(), self.level.to_string());
126 metadata_map.insert(
127 "compression_ratio".to_string(),
128 format!("{:.2}", compressed.len() as f64 / data.len() as f64),
129 );
130
131 let compressed_size = compressed.len() as u64;
132
133 Ok(CompressedData {
134 data: compressed,
135 metadata: CompressionMetadata {
136 algorithm: AdvancedCompressionType::Adaptive, original_size: data.len() as u64,
138 compressed_size,
139 compression_time_us: compression_time.as_micros() as u64,
140 metadata: metadata_map,
141 },
142 })
143 }
144
145 fn decompress(&self, compressed: &CompressedData) -> Result<Vec<u8>> {
146 if compressed.data.is_empty() {
147 return Ok(Vec::new());
148 }
149
150 let start = Instant::now();
151
152 let mut decoder = oxiarc_zstd::ZstdStreamDecoder::new(&compressed.data[..]);
154 let mut decompressed = Vec::new();
155 decoder.read_to_end(&mut decompressed)?;
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 "Zstd 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_zstd_compress_empty() {
189 let compressor = ZstdCompressor::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_zstd_compress_decompress() {
198 let compressor = ZstdCompressor::new();
199 let original = b"Hello, World! This is a test of Zstandard 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_zstd_compression_levels() {
211 let data = b"The quick brown fox jumps over the lazy dog. ".repeat(50);
212
213 let fast = ZstdCompressor::fast();
214 let balanced = ZstdCompressor::balanced();
215 let high = ZstdCompressor::high();
216 let max = ZstdCompressor::max();
217
218 let fast_result = fast.compress(&data).unwrap();
219 let balanced_result = balanced.compress(&data).unwrap();
220 let high_result = high.compress(&data).unwrap();
221 let max_result = max.compress(&data).unwrap();
222
223 assert_eq!(fast.decompress(&fast_result).unwrap(), data);
225 assert_eq!(balanced.decompress(&balanced_result).unwrap(), data);
226 assert_eq!(high.decompress(&high_result).unwrap(), data);
227 assert_eq!(max.decompress(&max_result).unwrap(), data);
228
229 println!("Fast (1): {} bytes", fast_result.data.len());
231 println!("Balanced (3): {} bytes", balanced_result.data.len());
232 println!("High (9): {} bytes", high_result.data.len());
233 println!("Max (19): {} bytes", max_result.data.len());
234
235 assert!(balanced_result.data.len() <= fast_result.data.len());
237 assert!(high_result.data.len() <= balanced_result.data.len());
238 assert!(max_result.data.len() <= high_result.data.len());
239 }
240
241 #[test]
242 fn test_zstd_highly_compressible_data() {
243 let compressor = ZstdCompressor::new();
244 let data = vec![b'A'; 10000]; let compressed = compressor.compress(&data).unwrap();
247 let decompressed = compressor.decompress(&compressed).unwrap();
248
249 assert_eq!(decompressed, data);
250 assert!(compressed.data.len() < data.len() / 100);
252 println!(
253 "Zstd highly compressible: {} -> {} bytes ({:.2}x)",
254 data.len(),
255 compressed.data.len(),
256 data.len() as f64 / compressed.data.len() as f64
257 );
258 }
259
260 #[test]
261 fn test_zstd_incompressible_data() {
262 let compressor = ZstdCompressor::new();
263 use scirs2_core::random::rng;
265 use scirs2_core::RngExt;
266
267 let mut rng = rng();
268 let data: Vec<u8> = (0..1000).map(|_| rng.random_range(0..256) as u8).collect();
269
270 let compressed = compressor.compress(&data).unwrap();
271 let decompressed = compressor.decompress(&compressed).unwrap();
272
273 assert_eq!(decompressed, data);
274 println!(
276 "Zstd incompressible: {} -> {} bytes",
277 data.len(),
278 compressed.data.len()
279 );
280 }
281
282 #[test]
283 fn test_zstd_too_large_input() {
284 let compressor = ZstdCompressor::new();
285 let data = vec![0u8; MAX_COMPRESSION_INPUT_SIZE + 1];
286
287 let result = compressor.compress(&data);
288 assert!(result.is_err());
289 }
290
291 #[test]
292 fn test_zstd_metadata() {
293 let compressor = ZstdCompressor::with_level(9);
294 let data = b"Testing metadata collection with Zstandard".repeat(10);
295
296 let compressed = compressor.compress(&data).unwrap();
297
298 assert_eq!(compressed.metadata.metadata.get("level").unwrap(), "9");
299 assert!(compressed
300 .metadata
301 .metadata
302 .contains_key("compression_ratio"));
303 assert!(compressed.metadata.compression_time_us > 0);
304 }
305
306 #[test]
307 fn test_zstd_rdf_data_compression() {
308 let compressor = ZstdCompressor::balanced();
309 let rdf_data = b"<http://example.org/resource/1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/Class> .\n"
311 .repeat(100);
312
313 let compressed = compressor.compress(&rdf_data).unwrap();
314 let decompressed = compressor.decompress(&compressed).unwrap();
315
316 assert_eq!(decompressed, rdf_data);
317
318 let ratio = rdf_data.len() as f64 / compressed.data.len() as f64;
320 println!("RDF compression ratio: {:.2}x", ratio);
321 assert!(ratio > 3.0); }
323
324 #[test]
325 fn test_zstd_level_clamping() {
326 let too_low = ZstdCompressor::with_level(0);
327 let too_high = ZstdCompressor::with_level(100);
328
329 assert_eq!(too_low.level, 1);
330 assert_eq!(too_high.level, 22);
331 }
332}