1use std::collections::HashMap;
15
16#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
22pub enum CompressionError {
23 #[error("decompression failed: {0}")]
25 DecompressionFailed(String),
26 #[error("unknown algorithm: {0}")]
28 UnknownAlgo(String),
29 #[error("corrupted data")]
31 CorruptedData,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub enum CompressionAlgo {
41 None,
43 Lz4,
45 Zstd {
47 level: i32,
49 },
50 Snappy,
52 Rle,
54}
55
56impl CompressionAlgo {
57 pub fn name(&self) -> String {
59 match self {
60 Self::None => "none".to_string(),
61 Self::Lz4 => "lz4".to_string(),
62 Self::Zstd { level } => format!("zstd({})", level),
63 Self::Snappy => "snappy".to_string(),
64 Self::Rle => "rle".to_string(),
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum CompressionHint {
76 Text,
78 Structured,
80 Binary,
82 Unknown,
84}
85
86#[derive(Debug, Clone)]
92pub struct CompressionResult {
93 pub algo: CompressionAlgo,
95 pub original_size: usize,
97 pub compressed_size: usize,
99 pub ratio: f64,
101 pub data: Vec<u8>,
103}
104
105impl CompressionResult {
106 fn new(algo: CompressionAlgo, original_size: usize, data: Vec<u8>) -> Self {
107 let compressed_size = data.len();
108 let ratio = if original_size == 0 {
109 1.0
110 } else {
111 compressed_size as f64 / original_size as f64
112 };
113 Self {
114 algo,
115 original_size,
116 compressed_size,
117 ratio,
118 data,
119 }
120 }
121}
122
123#[derive(Debug, Clone)]
131pub struct PipelineStage {
132 pub algo: CompressionAlgo,
134 pub min_input_size: usize,
136 pub max_ratio: f64,
139}
140
141#[derive(Debug, Clone)]
150pub struct PipelineConfig {
151 pub stages: Vec<PipelineStage>,
153 pub fallback_algo: CompressionAlgo,
155 pub target_ratio: f64,
157 pub enable_ratio_check: bool,
159}
160
161impl Default for PipelineConfig {
162 fn default() -> Self {
163 Self {
164 stages: vec![
165 PipelineStage {
166 algo: CompressionAlgo::Rle,
167 min_input_size: 64,
168 max_ratio: 0.9,
169 },
170 PipelineStage {
171 algo: CompressionAlgo::Lz4,
172 min_input_size: 256,
173 max_ratio: 0.85,
174 },
175 PipelineStage {
176 algo: CompressionAlgo::Zstd { level: 3 },
177 min_input_size: 512,
178 max_ratio: 0.7,
179 },
180 ],
181 fallback_algo: CompressionAlgo::None,
182 target_ratio: 0.75,
183 enable_ratio_check: true,
184 }
185 }
186}
187
188#[derive(Debug, Clone)]
194pub struct PipelineStats {
195 pub total_compressed: u64,
197 pub total_bytes_in: u64,
199 pub total_bytes_out: u64,
201 pub avg_ratio: f64,
203 pub algo_usage: HashMap<String, u64>,
205}
206
207fn rle_compress(data: &[u8]) -> Vec<u8> {
217 if data.is_empty() {
218 return Vec::new();
219 }
220 let mut out = Vec::with_capacity(data.len());
221 let mut i = 0;
222 while i < data.len() {
223 let byte = data[i];
224 let mut count: usize = 1;
225 while i + count < data.len() && data[i + count] == byte && count < 255 {
226 count += 1;
227 }
228 out.push(count as u8);
229 out.push(byte);
230 i += count;
231 }
232 out
233}
234
235fn rle_decompress(data: &[u8]) -> Result<Vec<u8>, CompressionError> {
237 if !data.len().is_multiple_of(2) {
238 return Err(CompressionError::CorruptedData);
239 }
240 let mut out = Vec::with_capacity(data.len() * 2);
241 let mut i = 0;
242 while i + 1 < data.len() {
243 let count = data[i] as usize;
244 let byte = data[i + 1];
245 if count == 0 {
246 return Err(CompressionError::DecompressionFailed(
247 "zero-length RLE run".to_string(),
248 ));
249 }
250 for _ in 0..count {
251 out.push(byte);
252 }
253 i += 2;
254 }
255 Ok(out)
256}
257
258const LZ4_WINDOW: usize = 4096;
262const LZ4_MIN_MATCH: usize = 4;
264
265#[inline]
267fn fnv1a_4(data: &[u8]) -> u32 {
268 const OFFSET: u32 = 2_166_136_261;
269 const PRIME: u32 = 16_777_619;
270 let mut h = OFFSET;
271 for &b in data.iter().take(4) {
272 h ^= u32::from(b);
273 h = h.wrapping_mul(PRIME);
274 }
275 h
276}
277
278fn lz4_style_compress(data: &[u8]) -> Vec<u8> {
284 if data.is_empty() {
285 return Vec::new();
286 }
287
288 const TABLE_SIZE: usize = 4096;
290 let mut table: [usize; TABLE_SIZE] = [usize::MAX; TABLE_SIZE];
291
292 let mut out = Vec::with_capacity(data.len());
293 let mut pos = 0;
294
295 while pos < data.len() {
296 if pos + LZ4_MIN_MATCH <= data.len() {
298 let seq = &data[pos..pos + 4];
299 let idx = fnv1a_4(seq) as usize % TABLE_SIZE;
300 let candidate = table[idx];
301 table[idx] = pos;
302
303 if candidate != usize::MAX && candidate < pos {
304 let offset = pos - candidate;
305 if offset <= LZ4_WINDOW {
306 let max_len = (data.len() - pos).min(255);
308 let mut match_len = 0;
309 while match_len < max_len
310 && data[candidate + match_len] == data[pos + match_len]
311 {
312 match_len += 1;
313 }
314
315 if match_len >= LZ4_MIN_MATCH {
316 let off = offset as u16;
318 out.extend_from_slice(&off.to_le_bytes());
319 out.push(match_len as u8);
320 pos += match_len;
321 continue;
322 }
323 }
324 }
325 }
326
327 out.extend_from_slice(&0u16.to_le_bytes());
329 out.push(0u8);
330 out.push(data[pos]);
331 pos += 1;
332 }
333
334 out
335}
336
337fn lz4_style_decompress(data: &[u8]) -> Result<Vec<u8>, CompressionError> {
339 let mut out: Vec<u8> = Vec::with_capacity(data.len() * 3);
340 let mut i = 0;
341
342 while i + 2 < data.len() {
343 let offset = u16::from_le_bytes(
344 data[i..i + 2]
345 .try_into()
346 .map_err(|_| CompressionError::CorruptedData)?,
347 ) as usize;
348 let length = data[i + 2] as usize;
349 i += 3;
350
351 if length == 0 {
352 if i >= data.len() {
354 return Err(CompressionError::DecompressionFailed(
355 "truncated literal token".to_string(),
356 ));
357 }
358 out.push(data[i]);
359 i += 1;
360 } else {
361 if offset == 0 {
363 return Err(CompressionError::DecompressionFailed(
364 "zero offset in back-reference".to_string(),
365 ));
366 }
367 let start = out
368 .len()
369 .checked_sub(offset)
370 .ok_or(CompressionError::CorruptedData)?;
371 for k in 0..length {
372 let b = out
373 .get(start + k)
374 .copied()
375 .ok_or(CompressionError::CorruptedData)?;
376 out.push(b);
377 }
378 }
379 }
380
381 Ok(out)
385}
386
387#[derive(Debug)]
406pub struct StorageCompressionPipeline {
407 pub config: PipelineConfig,
409 pub total_compressed: u64,
411 pub total_bytes_in: u64,
413 pub total_bytes_out: u64,
415 pub algo_counters: HashMap<String, u64>,
417}
418
419impl StorageCompressionPipeline {
420 pub fn new(config: PipelineConfig) -> Self {
422 Self {
423 config,
424 total_compressed: 0,
425 total_bytes_in: 0,
426 total_bytes_out: 0,
427 algo_counters: HashMap::new(),
428 }
429 }
430
431 pub fn compress(&mut self, data: &[u8], _hint: CompressionHint) -> CompressionResult {
443 let original_size = data.len();
444
445 let result = self.try_stages(data).unwrap_or_else(|| {
446 let compressed = self.compress_with_algo(data, &self.config.fallback_algo.clone());
447 CompressionResult::new(self.config.fallback_algo.clone(), original_size, compressed)
448 });
449
450 self.total_compressed += 1;
452 self.total_bytes_in += original_size as u64;
453 self.total_bytes_out += result.compressed_size as u64;
454 *self.algo_counters.entry(result.algo.name()).or_insert(0) += 1;
455
456 result
457 }
458
459 fn try_stages(&self, data: &[u8]) -> Option<CompressionResult> {
461 for stage in &self.config.stages {
462 if data.len() < stage.min_input_size {
463 continue;
464 }
465 let compressed = self.compress_with_algo(data, &stage.algo);
466 let ratio = if data.is_empty() {
467 1.0
468 } else {
469 compressed.len() as f64 / data.len() as f64
470 };
471 if !self.config.enable_ratio_check || ratio <= stage.max_ratio {
472 return Some(CompressionResult::new(
473 stage.algo.clone(),
474 data.len(),
475 compressed,
476 ));
477 }
478 }
479 None
480 }
481
482 pub fn decompress(
484 &self,
485 data: &[u8],
486 algo: &CompressionAlgo,
487 ) -> Result<Vec<u8>, CompressionError> {
488 match algo {
489 CompressionAlgo::None => Ok(data.to_vec()),
490 CompressionAlgo::Rle => rle_decompress(data),
491 CompressionAlgo::Lz4 | CompressionAlgo::Snappy => lz4_style_decompress(data),
492 CompressionAlgo::Zstd { level } => {
493 if *level <= 3 {
494 rle_decompress(data)
495 } else {
496 lz4_style_decompress(data)
497 }
498 }
499 }
500 }
501
502 pub fn compress_with_algo(&self, data: &[u8], algo: &CompressionAlgo) -> Vec<u8> {
504 match algo {
505 CompressionAlgo::None => data.to_vec(),
506 CompressionAlgo::Rle => rle_compress(data),
507 CompressionAlgo::Lz4 | CompressionAlgo::Snappy => lz4_style_compress(data),
508 CompressionAlgo::Zstd { level } => {
509 if *level <= 3 {
510 rle_compress(data)
511 } else {
512 lz4_style_compress(data)
513 }
514 }
515 }
516 }
517
518 pub fn detect_hint(data: &[u8]) -> CompressionHint {
520 if data.is_empty() {
521 return CompressionHint::Unknown;
522 }
523 if std::str::from_utf8(data).is_ok() {
524 let first = data[0];
525 if first == b'{' || first == b'[' {
526 return CompressionHint::Structured;
527 }
528 return CompressionHint::Text;
529 }
530 CompressionHint::Binary
531 }
532
533 pub fn auto_compress(&mut self, data: &[u8]) -> CompressionResult {
535 let hint = Self::detect_hint(data);
536 self.compress(data, hint)
537 }
538
539 pub fn pipeline_stats(&self) -> PipelineStats {
541 let avg_ratio = if self.total_bytes_in == 0 {
542 1.0
543 } else {
544 self.total_bytes_out as f64 / self.total_bytes_in as f64
545 };
546 PipelineStats {
547 total_compressed: self.total_compressed,
548 total_bytes_in: self.total_bytes_in,
549 total_bytes_out: self.total_bytes_out,
550 avg_ratio,
551 algo_usage: self.algo_counters.clone(),
552 }
553 }
554}
555
556#[cfg(test)]
561mod tests {
562 use std::collections::HashMap;
563
564 use crate::compression_pipeline::{
565 lz4_style_compress, lz4_style_decompress, rle_compress, rle_decompress, CompressionAlgo,
566 CompressionError, CompressionHint, CompressionResult, PipelineConfig, PipelineStage,
567 PipelineStats, StorageCompressionPipeline,
568 };
569
570 fn make_pipeline() -> StorageCompressionPipeline {
573 StorageCompressionPipeline::new(PipelineConfig::default())
574 }
575
576 fn repeated_data(byte: u8, count: usize) -> Vec<u8> {
577 vec![byte; count]
578 }
579
580 fn alternating_data(len: usize) -> Vec<u8> {
581 (0..len).map(|i| (i % 256) as u8).collect()
582 }
583
584 #[test]
587 fn test_rle_empty_input() {
588 let compressed = rle_compress(&[]);
589 assert!(compressed.is_empty());
590 let decompressed = rle_decompress(&compressed).unwrap();
591 assert!(decompressed.is_empty());
592 }
593
594 #[test]
595 fn test_rle_single_byte() {
596 let data = [0xABu8];
597 let c = rle_compress(&data);
598 assert_eq!(c, vec![1, 0xAB]);
600 assert_eq!(rle_decompress(&c).unwrap(), data);
601 }
602
603 #[test]
604 fn test_rle_uniform_run() {
605 let data = repeated_data(0x42, 100);
606 let c = rle_compress(&data);
607 assert_eq!(c.len(), 2);
609 assert_eq!(rle_decompress(&c).unwrap(), data);
610 }
611
612 #[test]
613 fn test_rle_max_run_cap() {
614 let data = repeated_data(0xCC, 256);
616 let c = rle_compress(&data);
617 assert_eq!(c.len(), 4);
618 assert_eq!(c[0], 255);
619 assert_eq!(c[2], 1);
620 assert_eq!(rle_decompress(&c).unwrap(), data);
621 }
622
623 #[test]
624 fn test_rle_round_trip_alternating() {
625 let data: Vec<u8> = (0u8..=127).collect();
626 let c = rle_compress(&data);
627 assert_eq!(rle_decompress(&c).unwrap(), data);
628 }
629
630 #[test]
631 fn test_rle_odd_length_corrupt() {
632 let result = rle_decompress(&[1, 2, 3]);
634 assert!(matches!(result, Err(CompressionError::CorruptedData)));
635 }
636
637 #[test]
638 fn test_rle_mixed_runs() {
639 let data = b"aaabbbccc";
640 let c = rle_compress(data);
641 assert_eq!(c.len(), 6);
643 assert_eq!(rle_decompress(&c).unwrap().as_slice(), data.as_ref());
644 }
645
646 #[test]
649 fn test_lz4_empty_input() {
650 let c = lz4_style_compress(&[]);
651 assert!(c.is_empty());
652 let d = lz4_style_decompress(&c).unwrap();
653 assert!(d.is_empty());
654 }
655
656 #[test]
657 fn test_lz4_single_byte() {
658 let data = [0xFFu8];
659 let c = lz4_style_compress(&data);
660 let d = lz4_style_decompress(&c).unwrap();
661 assert_eq!(d, data);
662 }
663
664 #[test]
665 fn test_lz4_round_trip_short() {
666 let data = b"hello world";
667 let c = lz4_style_compress(data);
668 let d = lz4_style_decompress(&c).unwrap();
669 assert_eq!(d.as_slice(), data.as_ref());
670 }
671
672 #[test]
673 fn test_lz4_round_trip_repeated() {
674 let data = repeated_data(0xAA, 512);
675 let c = lz4_style_compress(&data);
676 let d = lz4_style_decompress(&c).unwrap();
677 assert_eq!(d, data);
678 }
679
680 #[test]
681 fn test_lz4_compresses_repetitive_data() {
682 let data = repeated_data(0xBB, 1024);
683 let c = lz4_style_compress(&data);
684 assert!(c.len() < data.len());
686 }
687
688 #[test]
689 fn test_lz4_round_trip_alternating() {
690 let data = alternating_data(300);
691 let c = lz4_style_compress(&data);
692 let d = lz4_style_decompress(&c).unwrap();
693 assert_eq!(d, data);
694 }
695
696 #[test]
697 fn test_lz4_round_trip_binary() {
698 let data: Vec<u8> = (0..=255u8).cycle().take(800).collect();
699 let c = lz4_style_compress(&data);
700 let d = lz4_style_decompress(&c).unwrap();
701 assert_eq!(d, data);
702 }
703
704 #[test]
707 fn test_detect_hint_empty() {
708 assert_eq!(
709 StorageCompressionPipeline::detect_hint(&[]),
710 CompressionHint::Unknown
711 );
712 }
713
714 #[test]
715 fn test_detect_hint_text() {
716 assert_eq!(
717 StorageCompressionPipeline::detect_hint(b"hello world"),
718 CompressionHint::Text
719 );
720 }
721
722 #[test]
723 fn test_detect_hint_structured_object() {
724 assert_eq!(
725 StorageCompressionPipeline::detect_hint(b"{\"key\":\"value\"}"),
726 CompressionHint::Structured
727 );
728 }
729
730 #[test]
731 fn test_detect_hint_structured_array() {
732 assert_eq!(
733 StorageCompressionPipeline::detect_hint(b"[1,2,3]"),
734 CompressionHint::Structured
735 );
736 }
737
738 #[test]
739 fn test_detect_hint_binary() {
740 let data = vec![0xFF, 0xFE, 0xFD, 0x00, 0x01];
741 assert_eq!(
742 StorageCompressionPipeline::detect_hint(&data),
743 CompressionHint::Binary
744 );
745 }
746
747 #[test]
750 fn test_compress_with_algo_none() {
751 let p = make_pipeline();
752 let data = b"no compression";
753 let c = p.compress_with_algo(data, &CompressionAlgo::None);
754 assert_eq!(c.as_slice(), data.as_ref());
755 }
756
757 #[test]
758 fn test_compress_with_algo_rle() {
759 let p = make_pipeline();
760 let data = repeated_data(0x55, 200);
761 let c = p.compress_with_algo(&data, &CompressionAlgo::Rle);
762 let d = p.decompress(&c, &CompressionAlgo::Rle).unwrap();
763 assert_eq!(d, data);
764 }
765
766 #[test]
767 fn test_compress_with_algo_lz4() {
768 let p = make_pipeline();
769 let data = alternating_data(400);
770 let c = p.compress_with_algo(&data, &CompressionAlgo::Lz4);
771 let d = p.decompress(&c, &CompressionAlgo::Lz4).unwrap();
772 assert_eq!(d, data);
773 }
774
775 #[test]
776 fn test_compress_with_algo_snappy() {
777 let p = make_pipeline();
778 let data = repeated_data(0x11, 512);
779 let c = p.compress_with_algo(&data, &CompressionAlgo::Snappy);
780 let d = p.decompress(&c, &CompressionAlgo::Snappy).unwrap();
781 assert_eq!(d, data);
782 }
783
784 #[test]
785 fn test_compress_with_algo_zstd_low() {
786 let p = make_pipeline();
788 let data = repeated_data(0x22, 300);
789 let c = p.compress_with_algo(&data, &CompressionAlgo::Zstd { level: 2 });
790 let d = p
791 .decompress(&c, &CompressionAlgo::Zstd { level: 2 })
792 .unwrap();
793 assert_eq!(d, data);
794 }
795
796 #[test]
797 fn test_compress_with_algo_zstd_high() {
798 let p = make_pipeline();
800 let data = alternating_data(600);
801 let c = p.compress_with_algo(&data, &CompressionAlgo::Zstd { level: 5 });
802 let d = p
803 .decompress(&c, &CompressionAlgo::Zstd { level: 5 })
804 .unwrap();
805 assert_eq!(d, data);
806 }
807
808 #[test]
811 fn test_compress_small_data_uses_fallback() {
812 let mut p = make_pipeline();
814 let data = b"small";
815 let result = p.compress(data, CompressionHint::Binary);
816 assert_eq!(result.algo, CompressionAlgo::None);
817 assert_eq!(result.original_size, data.len());
818 }
819
820 #[test]
821 fn test_compress_medium_data_rle() {
822 let mut p = make_pipeline();
824 let data = repeated_data(0xAB, 100);
825 let result = p.compress(&data, CompressionHint::Binary);
826 assert_eq!(result.algo, CompressionAlgo::Rle);
828 }
829
830 #[test]
831 fn test_compress_round_trip_pipeline() {
832 let mut p = make_pipeline();
833 let data = repeated_data(0xCD, 200);
834 let result = p.compress(&data, CompressionHint::Binary);
835 let recovered = p.decompress(&result.data, &result.algo).unwrap();
836 assert_eq!(recovered, data);
837 }
838
839 #[test]
840 fn test_auto_compress_json() {
841 let mut p = make_pipeline();
842 let json =
843 br#"{"key":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}"#;
844 let result = p.auto_compress(json);
845 assert_eq!(result.algo, CompressionAlgo::Rle); let recovered = p.decompress(&result.data, &result.algo).unwrap();
847 assert_eq!(recovered.as_slice(), json.as_ref());
848 }
849
850 #[test]
851 fn test_auto_compress_round_trip() {
852 let mut p = make_pipeline();
853 let data = repeated_data(0xEE, 600);
854 let result = p.auto_compress(&data);
855 let recovered = p.decompress(&result.data, &result.algo).unwrap();
856 assert_eq!(recovered, data);
857 }
858
859 #[test]
860 fn test_compress_ratio_field() {
861 let data = repeated_data(0x99, 200);
862 let compressed = rle_compress(&data);
863 let r = CompressionResult::new(CompressionAlgo::Rle, data.len(), compressed.clone());
864 let expected = compressed.len() as f64 / data.len() as f64;
865 assert!((r.ratio - expected).abs() < 1e-12);
866 }
867
868 #[test]
869 fn test_compress_ratio_zero_len() {
870 let r = CompressionResult::new(CompressionAlgo::None, 0, vec![]);
871 assert_eq!(r.ratio, 1.0);
872 }
873
874 #[test]
877 fn test_stats_initial() {
878 let p = make_pipeline();
879 let stats = p.pipeline_stats();
880 assert_eq!(stats.total_compressed, 0);
881 assert_eq!(stats.total_bytes_in, 0);
882 assert_eq!(stats.total_bytes_out, 0);
883 assert_eq!(stats.avg_ratio, 1.0);
884 assert!(stats.algo_usage.is_empty());
885 }
886
887 #[test]
888 fn test_stats_after_compress() {
889 let mut p = make_pipeline();
890 let data = repeated_data(0x77, 200);
891 p.compress(&data, CompressionHint::Binary);
892 let stats = p.pipeline_stats();
893 assert_eq!(stats.total_compressed, 1);
894 assert_eq!(stats.total_bytes_in, 200);
895 assert!(stats.total_bytes_out > 0);
896 assert!(stats.avg_ratio > 0.0);
897 }
898
899 #[test]
900 fn test_stats_algo_counter() {
901 let mut p = make_pipeline();
902 let data = repeated_data(0x33, 200);
904 p.compress(&data, CompressionHint::Binary);
905 p.compress(&data, CompressionHint::Binary);
906 let stats = p.pipeline_stats();
907 let rle_count = stats.algo_usage.get("rle").copied().unwrap_or(0);
909 assert_eq!(rle_count, 2);
910 }
911
912 #[test]
913 fn test_stats_multiple_algos() {
914 let mut p = make_pipeline();
915 p.compress(b"small", CompressionHint::Binary);
917 p.compress(&repeated_data(0x44, 200), CompressionHint::Binary);
919 let stats = p.pipeline_stats();
920 assert_eq!(stats.total_compressed, 2);
921 assert!(stats.algo_usage.contains_key("none"));
922 assert!(stats.algo_usage.contains_key("rle"));
923 }
924
925 #[test]
928 fn test_custom_config_stages() {
929 let config = PipelineConfig {
930 stages: vec![PipelineStage {
931 algo: CompressionAlgo::Lz4,
932 min_input_size: 10,
933 max_ratio: 0.99,
934 }],
935 fallback_algo: CompressionAlgo::None,
936 target_ratio: 0.8,
937 enable_ratio_check: true,
938 };
939 let mut p = StorageCompressionPipeline::new(config);
940 let data = repeated_data(0x55, 512);
941 let result = p.compress(&data, CompressionHint::Binary);
942 let recovered = p.decompress(&result.data, &result.algo).unwrap();
943 assert_eq!(recovered, data);
944 }
945
946 #[test]
947 fn test_ratio_check_disabled() {
948 let config = PipelineConfig {
950 stages: vec![PipelineStage {
951 algo: CompressionAlgo::Rle,
952 min_input_size: 4,
953 max_ratio: 0.01, }],
955 fallback_algo: CompressionAlgo::None,
956 target_ratio: 1.0,
957 enable_ratio_check: false,
958 };
959 let mut p = StorageCompressionPipeline::new(config);
960 let data = b"abcd"; let result = p.compress(data, CompressionHint::Binary);
962 assert_eq!(result.algo, CompressionAlgo::Rle);
964 }
965
966 #[test]
967 fn test_fallback_when_ratio_too_high() {
968 let config = PipelineConfig {
970 stages: vec![PipelineStage {
971 algo: CompressionAlgo::Rle,
972 min_input_size: 1,
973 max_ratio: 0.001, }],
975 fallback_algo: CompressionAlgo::None,
976 target_ratio: 1.0,
977 enable_ratio_check: true,
978 };
979 let mut p = StorageCompressionPipeline::new(config);
980 let data: Vec<u8> = (0u8..=127).collect();
981 let result = p.compress(&data, CompressionHint::Binary);
982 assert_eq!(result.algo, CompressionAlgo::None);
983 }
984
985 #[test]
988 fn test_algo_name_none() {
989 assert_eq!(CompressionAlgo::None.name(), "none");
990 }
991
992 #[test]
993 fn test_algo_name_lz4() {
994 assert_eq!(CompressionAlgo::Lz4.name(), "lz4");
995 }
996
997 #[test]
998 fn test_algo_name_zstd() {
999 assert_eq!(CompressionAlgo::Zstd { level: 7 }.name(), "zstd(7)");
1000 }
1001
1002 #[test]
1003 fn test_algo_name_snappy() {
1004 assert_eq!(CompressionAlgo::Snappy.name(), "snappy");
1005 }
1006
1007 #[test]
1008 fn test_algo_name_rle() {
1009 assert_eq!(CompressionAlgo::Rle.name(), "rle");
1010 }
1011
1012 #[test]
1015 fn test_pipeline_stats_avg_ratio() {
1016 let mut p = make_pipeline();
1017 let data = repeated_data(0xBB, 1000);
1018 p.compress(&data, CompressionHint::Binary);
1019 let stats = p.pipeline_stats();
1020 let expected = stats.total_bytes_out as f64 / stats.total_bytes_in as f64;
1021 assert!((stats.avg_ratio - expected).abs() < 1e-12);
1022 }
1023
1024 #[test]
1025 fn test_pipeline_stats_type() {
1026 let stats = PipelineStats {
1027 total_compressed: 5,
1028 total_bytes_in: 1000,
1029 total_bytes_out: 500,
1030 avg_ratio: 0.5,
1031 algo_usage: HashMap::new(),
1032 };
1033 assert_eq!(stats.total_compressed, 5);
1034 assert_eq!(stats.avg_ratio, 0.5);
1035 }
1036
1037 #[test]
1040 fn test_large_uniform_rle_round_trip() {
1041 let mut p = make_pipeline();
1042 let data = repeated_data(0xDE, 8192);
1043 let result = p.compress(&data, CompressionHint::Binary);
1044 let recovered = p.decompress(&result.data, &result.algo).unwrap();
1045 assert_eq!(recovered, data);
1046 }
1047
1048 #[test]
1049 fn test_large_alternating_lz4_round_trip() {
1050 let p = make_pipeline();
1051 let data: Vec<u8> = (0u8..=255).cycle().take(4096).collect();
1052 let c = p.compress_with_algo(&data, &CompressionAlgo::Lz4);
1053 let d = p.decompress(&c, &CompressionAlgo::Lz4).unwrap();
1054 assert_eq!(d, data);
1055 }
1056
1057 #[test]
1058 fn test_pipeline_three_calls_stats() {
1059 let mut p = make_pipeline();
1060 for _ in 0..3 {
1061 let data = repeated_data(0xAA, 500);
1062 p.compress(&data, CompressionHint::Binary);
1063 }
1064 let stats = p.pipeline_stats();
1065 assert_eq!(stats.total_compressed, 3);
1066 assert_eq!(stats.total_bytes_in, 1500);
1067 }
1068}