1use crate::error::{Result, TdbError};
18use serde::{Deserialize, Serialize};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum CompressionStrategy {
23 Speed,
25 Balanced,
27 Ratio,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[repr(u8)]
34pub enum CompressionAlgorithm {
35 None = 0,
37 Lz4 = 1,
39 Zstd = 2,
41 Brotli = 3,
43 Snappy = 4,
45 Prefix = 5,
47 Delta = 6,
49 RunLength = 7,
51 Bitmap = 8,
53}
54
55#[derive(Debug, Clone, Copy)]
57pub struct CompressionLevel(u8);
58
59impl CompressionLevel {
60 pub const FAST: Self = Self(1);
62 pub const DEFAULT: Self = Self(5);
64 pub const BEST: Self = Self(9);
66
67 pub fn new(level: u8) -> Self {
69 Self(level.min(9))
70 }
71
72 pub fn value(&self) -> u8 {
74 self.0
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct CompressionStats {
81 pub algorithm: CompressionAlgorithm,
83 pub original_size: usize,
85 pub compressed_size: usize,
87 pub ratio: f64,
89 pub compression_time_us: u64,
91 pub decompression_time_us: Option<u64>,
93}
94
95impl CompressionStats {
96 pub fn new(
98 algorithm: CompressionAlgorithm,
99 original_size: usize,
100 compressed_size: usize,
101 compression_time_us: u64,
102 ) -> Self {
103 let ratio = if original_size > 0 {
104 compressed_size as f64 / original_size as f64
105 } else {
106 1.0
107 };
108
109 Self {
110 algorithm,
111 original_size,
112 compressed_size,
113 ratio,
114 compression_time_us,
115 decompression_time_us: None,
116 }
117 }
118
119 pub fn savings_percent(&self) -> f64 {
121 (1.0 - self.ratio) * 100.0
122 }
123}
124
125pub struct UnifiedCompression {
127 default_algorithm: CompressionAlgorithm,
129 default_level: CompressionLevel,
131 strategy: CompressionStrategy,
133}
134
135impl UnifiedCompression {
136 pub fn new() -> Self {
138 Self {
139 default_algorithm: CompressionAlgorithm::Zstd,
140 default_level: CompressionLevel::DEFAULT,
141 strategy: CompressionStrategy::Balanced,
142 }
143 }
144
145 pub fn with_algorithm(mut self, algorithm: CompressionAlgorithm) -> Self {
147 self.default_algorithm = algorithm;
148 self
149 }
150
151 pub fn with_level(mut self, level: CompressionLevel) -> Self {
153 self.default_level = level;
154 self
155 }
156
157 pub fn with_strategy(mut self, strategy: CompressionStrategy) -> Self {
159 self.strategy = strategy;
160 self
161 }
162
163 pub fn compress(&self, data: &[u8]) -> Result<Vec<u8>> {
165 self.compress_with(data, self.default_algorithm, self.default_level)
166 }
167
168 pub fn compress_with(
170 &self,
171 data: &[u8],
172 algorithm: CompressionAlgorithm,
173 level: CompressionLevel,
174 ) -> Result<Vec<u8>> {
175 use std::time::Instant;
176
177 let start = Instant::now();
178
179 let compressed = match algorithm {
180 CompressionAlgorithm::None => data.to_vec(),
181 CompressionAlgorithm::Lz4 => self.compress_lz4(data)?,
182 CompressionAlgorithm::Zstd => self.compress_zstd(data, level.value() as i32)?,
183 CompressionAlgorithm::Brotli => self.compress_brotli(data, level.value() as u32)?,
184 CompressionAlgorithm::Snappy => self.compress_snappy(data)?,
185 CompressionAlgorithm::Prefix => self.compress_prefix(data)?,
186 CompressionAlgorithm::Delta => self.compress_delta(data)?,
187 CompressionAlgorithm::RunLength => self.compress_run_length(data)?,
188 CompressionAlgorithm::Bitmap => self.compress_bitmap(data)?,
189 };
190
191 Ok(compressed)
192 }
193
194 pub fn decompress(&self, data: &[u8], algorithm: CompressionAlgorithm) -> Result<Vec<u8>> {
196 match algorithm {
197 CompressionAlgorithm::None => Ok(data.to_vec()),
198 CompressionAlgorithm::Lz4 => self.decompress_lz4(data),
199 CompressionAlgorithm::Zstd => self.decompress_zstd(data),
200 CompressionAlgorithm::Brotli => self.decompress_brotli(data),
201 CompressionAlgorithm::Snappy => self.decompress_snappy(data),
202 CompressionAlgorithm::Prefix => self.decompress_prefix(data),
203 CompressionAlgorithm::Delta => self.decompress_delta(data),
204 CompressionAlgorithm::RunLength => self.decompress_run_length(data),
205 CompressionAlgorithm::Bitmap => self.decompress_bitmap(data),
206 }
207 }
208
209 pub fn select_algorithm(&self, data: &[u8]) -> CompressionAlgorithm {
211 match self.strategy {
213 CompressionStrategy::Speed => CompressionAlgorithm::Snappy,
214 CompressionStrategy::Balanced => {
215 if data.len() < 1024 {
216 CompressionAlgorithm::Lz4
217 } else {
218 CompressionAlgorithm::Zstd
219 }
220 }
221 CompressionStrategy::Ratio => CompressionAlgorithm::Brotli,
222 }
223 }
224
225 fn compress_lz4(&self, data: &[u8]) -> Result<Vec<u8>> {
228 oxiarc_lz4::compress(data)
229 .map_err(|e| TdbError::Other(format!("LZ4 compression failed: {:?}", e)))
230 }
231
232 fn decompress_lz4(&self, data: &[u8]) -> Result<Vec<u8>> {
233 oxiarc_lz4::decompress(data, 100 * 1024 * 1024)
234 .map_err(|e| TdbError::Other(format!("LZ4 decompression failed: {:?}", e)))
235 }
236
237 fn compress_zstd(&self, data: &[u8], level: i32) -> Result<Vec<u8>> {
238 oxiarc_zstd::encode_all(data, level)
239 .map_err(|e| TdbError::Other(format!("Zstd compression failed: {}", e)))
240 }
241
242 fn decompress_zstd(&self, data: &[u8]) -> Result<Vec<u8>> {
243 oxiarc_zstd::decode_all(data)
244 .map_err(|e| TdbError::Other(format!("Zstd decompression failed: {}", e)))
245 }
246
247 fn compress_brotli(&self, data: &[u8], level: u32) -> Result<Vec<u8>> {
248 oxiarc_brotli::compress(data, level)
251 .map_err(|e| TdbError::Other(format!("Brotli compression failed: {}", e)))
252 }
253
254 fn decompress_brotli(&self, data: &[u8]) -> Result<Vec<u8>> {
255 oxiarc_brotli::decompress(data)
256 .map_err(|e| TdbError::Other(format!("Brotli decompression failed: {}", e)))
257 }
258
259 fn compress_snappy(&self, data: &[u8]) -> Result<Vec<u8>> {
260 Ok(oxiarc_snappy::compress(data))
262 }
263
264 fn decompress_snappy(&self, data: &[u8]) -> Result<Vec<u8>> {
265 oxiarc_snappy::decompress(data)
266 .map_err(|e| TdbError::Other(format!("Snappy decompression failed: {}", e)))
267 }
268
269 fn compress_prefix(&self, data: &[u8]) -> Result<Vec<u8>> {
282 let s = std::str::from_utf8(data).map_err(|e| {
283 TdbError::Other(format!(
284 "Prefix compression requires valid UTF-8 (URI/string) input: {e}"
285 ))
286 })?;
287
288 let mut compressor = crate::compression::prefix::PrefixCompressor::new(4);
289 let compressed = compressor
290 .compress(s)
291 .map_err(|e| TdbError::Other(format!("Prefix compression failed: {e}")))?;
292
293 let prefix_text: &str = if compressed.prefix_id == 0 {
294 ""
295 } else {
296 compressor
297 .get_prefix(compressed.prefix_id)
298 .map(|s| s.as_str())
299 .ok_or_else(|| {
300 TdbError::Other(
301 "Prefix compression produced an unresolvable prefix id".to_string(),
302 )
303 })?
304 };
305
306 let prefix_bytes = prefix_text.as_bytes();
307 let suffix_bytes = compressed.suffix.as_bytes();
308 let mut out = Vec::with_capacity(4 + prefix_bytes.len() + suffix_bytes.len());
309 out.extend_from_slice(&(prefix_bytes.len() as u32).to_le_bytes());
310 out.extend_from_slice(prefix_bytes);
311 out.extend_from_slice(suffix_bytes);
312 Ok(out)
313 }
314
315 fn decompress_prefix(&self, data: &[u8]) -> Result<Vec<u8>> {
316 if data.len() < 4 {
317 return Err(TdbError::Other(
318 "Prefix-compressed data truncated: missing length header".to_string(),
319 ));
320 }
321 let prefix_len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
322 let suffix_start = 4usize.checked_add(prefix_len).ok_or_else(|| {
323 TdbError::Other("Prefix-compressed data: length overflow".to_string())
324 })?;
325 if data.len() < suffix_start {
326 return Err(TdbError::Other(
327 "Prefix-compressed data truncated: prefix shorter than declared length".to_string(),
328 ));
329 }
330
331 let mut out = Vec::with_capacity(data.len() - 4);
332 out.extend_from_slice(&data[4..suffix_start]);
333 out.extend_from_slice(&data[suffix_start..]);
334 Ok(out)
335 }
336
337 fn compress_delta(&self, data: &[u8]) -> Result<Vec<u8>> {
341 crate::compression::delta::DeltaEncoder::encode_byte_sequence(data)
342 .map_err(|e| TdbError::Other(format!("Delta compression failed: {e}")))
343 }
344
345 fn decompress_delta(&self, data: &[u8]) -> Result<Vec<u8>> {
346 crate::compression::delta::DeltaEncoder::decode_byte_sequence(data)
347 .map_err(|e| TdbError::Other(format!("Delta decompression failed: {e}")))
348 }
349
350 fn compress_run_length(&self, data: &[u8]) -> Result<Vec<u8>> {
354 crate::compression::run_length::RunLengthEncoder::encode(data)
355 .map_err(|e| TdbError::Other(format!("Run-length compression failed: {e}")))
356 }
357
358 fn decompress_run_length(&self, data: &[u8]) -> Result<Vec<u8>> {
359 crate::compression::run_length::RunLengthEncoder::decode(data)
360 .map_err(|e| TdbError::Other(format!("Run-length decompression failed: {e}")))
361 }
362
363 fn compress_bitmap(&self, data: &[u8]) -> Result<Vec<u8>> {
367 crate::compression::bitmap::BitmapWAHEncoder::encode(data)
368 .map_err(|e| TdbError::Other(format!("Bitmap compression failed: {e}")))
369 }
370
371 fn decompress_bitmap(&self, data: &[u8]) -> Result<Vec<u8>> {
372 crate::compression::bitmap::BitmapWAHEncoder::decode(data)
373 .map_err(|e| TdbError::Other(format!("Bitmap decompression failed: {e}")))
374 }
375
376 pub fn benchmark(&self, data: &[u8]) -> Vec<CompressionStats> {
378 let mut results = Vec::new();
379
380 let algorithms = vec![
381 CompressionAlgorithm::Lz4,
382 CompressionAlgorithm::Zstd,
383 CompressionAlgorithm::Brotli,
384 CompressionAlgorithm::Snappy,
385 ];
386
387 for algo in algorithms {
388 if let Ok(compressed) = self.compress_with(data, algo, CompressionLevel::DEFAULT) {
389 let stats = CompressionStats::new(
390 algo,
391 data.len(),
392 compressed.len(),
393 0, );
395 results.push(stats);
396 }
397 }
398
399 results
400 }
401}
402
403impl Default for UnifiedCompression {
404 fn default() -> Self {
405 Self::new()
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn test_unified_compression_creation() {
415 let _compression = UnifiedCompression::new();
416 }
417
418 #[test]
419 fn test_compression_level() {
420 assert_eq!(CompressionLevel::FAST.value(), 1);
421 assert_eq!(CompressionLevel::DEFAULT.value(), 5);
422 assert_eq!(CompressionLevel::BEST.value(), 9);
423
424 let custom = CompressionLevel::new(7);
425 assert_eq!(custom.value(), 7);
426
427 let clamped = CompressionLevel::new(15);
429 assert_eq!(clamped.value(), 9);
430 }
431
432 #[test]
433 fn test_lz4_roundtrip() {
434 let compression = UnifiedCompression::new();
435 let data = b"Hello, World! This is a test of LZ4 compression.";
436
437 let compressed = compression
438 .compress_with(data, CompressionAlgorithm::Lz4, CompressionLevel::DEFAULT)
439 .unwrap();
440
441 let decompressed = compression
445 .decompress(&compressed, CompressionAlgorithm::Lz4)
446 .unwrap();
447
448 assert_eq!(decompressed, data);
449 }
450
451 #[test]
452 fn test_zstd_roundtrip() {
453 let compression = UnifiedCompression::new();
454 let data = b"Zstandard compression test data with some repetition repetition repetition";
455
456 let compressed = compression
457 .compress_with(data, CompressionAlgorithm::Zstd, CompressionLevel::DEFAULT)
458 .unwrap();
459
460 let decompressed = compression
461 .decompress(&compressed, CompressionAlgorithm::Zstd)
462 .unwrap();
463
464 assert_eq!(decompressed, data);
465 }
466
467 #[test]
468 fn test_snappy_roundtrip() {
469 let compression = UnifiedCompression::new();
470 let data = b"Snappy is designed for speed rather than maximum compression";
471
472 let compressed = compression
473 .compress_with(
474 data,
475 CompressionAlgorithm::Snappy,
476 CompressionLevel::DEFAULT,
477 )
478 .unwrap();
479
480 let decompressed = compression
481 .decompress(&compressed, CompressionAlgorithm::Snappy)
482 .unwrap();
483
484 assert_eq!(decompressed, data);
485 }
486
487 #[test]
488 fn test_brotli_roundtrip() {
489 let compression = UnifiedCompression::new();
490 let data = b"Brotli excels at compressing text and structured web content. ".repeat(20);
491
492 let compressed = compression
493 .compress_with(
494 &data,
495 CompressionAlgorithm::Brotli,
496 CompressionLevel::DEFAULT,
497 )
498 .unwrap();
499
500 let decompressed = compression
501 .decompress(&compressed, CompressionAlgorithm::Brotli)
502 .unwrap();
503 assert_eq!(decompressed, data);
504 }
505
506 #[test]
507 fn test_brotli_roundtrip_incompressible() {
508 use scirs2_core::random::rng;
511 use scirs2_core::RngExt;
512 let mut r = rng();
513 let data: Vec<u8> = (0..1500).map(|_| r.random_range(0..256) as u8).collect();
514
515 let compression = UnifiedCompression::new();
516 let compressed = compression
517 .compress_with(
518 &data,
519 CompressionAlgorithm::Brotli,
520 CompressionLevel::DEFAULT,
521 )
522 .unwrap();
523 let decompressed = compression
524 .decompress(&compressed, CompressionAlgorithm::Brotli)
525 .unwrap();
526 assert_eq!(decompressed, data);
527 }
528
529 #[test]
530 fn test_algorithm_selection() {
531 let compression = UnifiedCompression::new().with_strategy(CompressionStrategy::Speed);
532 assert_eq!(
533 compression.select_algorithm(b"test"),
534 CompressionAlgorithm::Snappy
535 );
536
537 let compression = UnifiedCompression::new().with_strategy(CompressionStrategy::Ratio);
538 assert_eq!(
539 compression.select_algorithm(b"test"),
540 CompressionAlgorithm::Brotli
541 );
542 }
543
544 #[test]
545 fn test_compression_stats() {
546 let stats = CompressionStats::new(CompressionAlgorithm::Zstd, 1000, 500, 100);
547
548 assert_eq!(stats.ratio, 0.5);
549 assert_eq!(stats.savings_percent(), 50.0);
550 }
551
552 #[test]
553 fn test_benchmark() {
554 let compression = UnifiedCompression::new();
555 let data = b"Sample data for benchmarking compression algorithms. ".repeat(10);
556
557 let results = compression.benchmark(&data);
558
559 assert!(results.len() >= 4); for result in &results {
561 assert!(result.compressed_size > 0);
562 assert!(result.compressed_size <= result.original_size);
563 }
564 }
565
566 #[test]
567 fn test_none_algorithm() {
568 let compression = UnifiedCompression::new();
569 let data = b"Test data";
570
571 let compressed = compression
572 .compress_with(data, CompressionAlgorithm::None, CompressionLevel::DEFAULT)
573 .unwrap();
574
575 assert_eq!(compressed, data);
576
577 let decompressed = compression
578 .decompress(&compressed, CompressionAlgorithm::None)
579 .unwrap();
580
581 assert_eq!(decompressed, data);
582 }
583
584 #[test]
589 fn test_prefix_algorithm_round_trips() {
590 let compression = UnifiedCompression::new();
591 let data = b"http://example.org/ontology#Person";
592
593 let compressed = compression
594 .compress_with(
595 data,
596 CompressionAlgorithm::Prefix,
597 CompressionLevel::DEFAULT,
598 )
599 .expect("prefix compression should now be implemented");
600 let decompressed = compression
601 .decompress(&compressed, CompressionAlgorithm::Prefix)
602 .expect("prefix decompression should now be implemented");
603 assert_eq!(decompressed, data);
604 }
605
606 #[test]
607 fn test_prefix_algorithm_rejects_non_utf8() {
608 let compression = UnifiedCompression::new();
609 let data: &[u8] = &[0xFF, 0xFE, 0xFD];
610
611 let result = compression.compress_with(
612 data,
613 CompressionAlgorithm::Prefix,
614 CompressionLevel::DEFAULT,
615 );
616 assert!(
617 result.is_err(),
618 "non-UTF-8 input must be a clear error, not silently mangled"
619 );
620 }
621
622 #[test]
623 fn test_delta_algorithm_round_trips() {
624 let compression = UnifiedCompression::new();
625 let data: Vec<u8> = (0..=255u8).collect();
626
627 let compressed = compression
628 .compress_with(
629 &data,
630 CompressionAlgorithm::Delta,
631 CompressionLevel::DEFAULT,
632 )
633 .expect("delta compression should now be implemented");
634 let decompressed = compression
635 .decompress(&compressed, CompressionAlgorithm::Delta)
636 .expect("delta decompression should now be implemented");
637 assert_eq!(decompressed, data);
638 }
639
640 #[test]
641 fn test_run_length_algorithm_round_trips() {
642 let compression = UnifiedCompression::new();
643 let data: Vec<u8> = b"aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd".to_vec();
644
645 let compressed = compression
646 .compress_with(
647 &data,
648 CompressionAlgorithm::RunLength,
649 CompressionLevel::DEFAULT,
650 )
651 .expect("run-length compression should now be implemented");
652 let decompressed = compression
653 .decompress(&compressed, CompressionAlgorithm::RunLength)
654 .expect("run-length decompression should now be implemented");
655 assert_eq!(decompressed, data);
656 }
657
658 #[test]
659 fn test_bitmap_algorithm_round_trips() {
660 let compression = UnifiedCompression::new();
661 let data: Vec<u8> = vec![0u8, 0, 0, 0xFF, 0, 0, 0, 0xFF, 0, 0];
662
663 let compressed = compression
664 .compress_with(
665 &data,
666 CompressionAlgorithm::Bitmap,
667 CompressionLevel::DEFAULT,
668 )
669 .expect("bitmap compression should now be implemented");
670 let decompressed = compression
671 .decompress(&compressed, CompressionAlgorithm::Bitmap)
672 .expect("bitmap decompression should now be implemented");
673 assert_eq!(decompressed, data);
674 }
675}