1use serde::{Deserialize, Serialize};
31use std::fmt;
32
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
43pub enum CompressionType {
44 #[default]
46 None,
47 #[cfg(feature = "gzip")]
49 Gzip,
50 #[cfg(feature = "zlib")]
52 Zlib,
53 #[cfg(feature = "zstd-compression")]
55 Zstd,
56}
57
58impl CompressionType {
59 #[inline]
61 pub fn as_encoding(&self) -> &'static str {
62 match self {
63 CompressionType::None => "utf-8",
64 #[cfg(feature = "gzip")]
65 CompressionType::Gzip => "gzip",
66 #[cfg(feature = "zlib")]
67 CompressionType::Zlib => "zlib",
68 #[cfg(feature = "zstd-compression")]
69 CompressionType::Zstd => "zstd",
70 }
71 }
72
73 pub fn from_encoding(encoding: &str) -> Option<Self> {
75 match encoding.to_lowercase().as_str() {
76 "utf-8" | "identity" | "" => Some(CompressionType::None),
77 #[cfg(feature = "gzip")]
78 "gzip" | "x-gzip" => Some(CompressionType::Gzip),
79 #[cfg(feature = "zlib")]
80 "zlib" | "deflate" => Some(CompressionType::Zlib),
81 #[cfg(feature = "zstd-compression")]
82 "zstd" | "zstandard" => Some(CompressionType::Zstd),
83 _ => None,
84 }
85 }
86
87 pub fn available() -> Vec<CompressionType> {
89 vec![
90 CompressionType::None,
91 #[cfg(feature = "gzip")]
92 CompressionType::Gzip,
93 #[cfg(feature = "zlib")]
94 CompressionType::Zlib,
95 #[cfg(feature = "zstd-compression")]
96 CompressionType::Zstd,
97 ]
98 }
99
100 pub fn id(&self) -> u8 {
105 match self {
106 CompressionType::None => 0,
107 #[cfg(feature = "gzip")]
108 CompressionType::Gzip => 1,
109 #[cfg(feature = "zlib")]
110 CompressionType::Zlib => 2,
111 #[cfg(feature = "zstd-compression")]
112 CompressionType::Zstd => 3,
113 }
114 }
115
116 pub fn from_id(id: u8) -> Option<Self> {
119 match id {
120 0 => Some(CompressionType::None),
121 #[cfg(feature = "gzip")]
122 1 => Some(CompressionType::Gzip),
123 #[cfg(feature = "zlib")]
124 2 => Some(CompressionType::Zlib),
125 #[cfg(feature = "zstd-compression")]
126 3 => Some(CompressionType::Zstd),
127 _ => None,
128 }
129 }
130
131 pub fn name(&self) -> &'static str {
133 match self {
134 CompressionType::None => "none",
135 #[cfg(feature = "gzip")]
136 CompressionType::Gzip => "gzip",
137 #[cfg(feature = "zlib")]
138 CompressionType::Zlib => "zlib",
139 #[cfg(feature = "zstd-compression")]
140 CompressionType::Zstd => "zstd",
141 }
142 }
143
144 pub fn is_enabled(&self) -> bool {
147 !matches!(self, CompressionType::None)
148 }
149
150 pub fn from_name(s: &str) -> Option<Self> {
152 match s.to_lowercase().as_str() {
153 "none" => Some(CompressionType::None),
154 #[cfg(feature = "gzip")]
155 "gzip" => Some(CompressionType::Gzip),
156 #[cfg(feature = "zlib")]
157 "zlib" | "deflate" => Some(CompressionType::Zlib),
158 #[cfg(feature = "zstd-compression")]
159 "zstd" | "zstandard" => Some(CompressionType::Zstd),
160 _ => None,
161 }
162 }
163}
164
165impl fmt::Display for CompressionType {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 write!(f, "{}", self.as_encoding())
168 }
169}
170
171impl TryFrom<&str> for CompressionType {
172 type Error = String;
173
174 fn try_from(s: &str) -> Result<Self, Self::Error> {
175 Self::from_encoding(s).ok_or_else(|| format!("Unknown compression type: {}", s))
176 }
177}
178
179#[derive(Debug, Clone)]
186pub struct CompressionRegistry {
187 default: CompressionType,
188 available: Vec<CompressionType>,
189}
190
191impl CompressionRegistry {
192 pub fn new() -> Self {
195 Self {
196 default: CompressionType::None,
197 available: CompressionType::available(),
198 }
199 }
200
201 pub fn with_default(algo: CompressionType) -> Result<Self, String> {
206 let available = CompressionType::available();
207 if !available.contains(&algo) {
208 return Err(format!(
209 "Compression type {:?} is not available (enabled features: {:?})",
210 algo, available
211 ));
212 }
213 Ok(Self {
214 default: algo,
215 available,
216 })
217 }
218
219 pub fn default_type(&self) -> CompressionType {
221 self.default
222 }
223
224 pub fn available_types(&self) -> &[CompressionType] {
226 &self.available
227 }
228
229 pub fn is_available(&self, algo: &CompressionType) -> bool {
231 self.available.contains(algo)
232 }
233}
234
235impl Default for CompressionRegistry {
236 fn default() -> Self {
237 Self::new()
238 }
239}
240
241#[derive(Debug, Clone, Default, Serialize, Deserialize)]
250pub struct CompressionStats {
251 pub original_bytes: u64,
253 pub compressed_bytes: u64,
255 pub operations: u64,
257 pub failures: u64,
259}
260
261impl CompressionStats {
262 pub fn ratio(&self) -> f64 {
266 if self.original_bytes == 0 {
267 return 0.0;
268 }
269 self.compressed_bytes as f64 / self.original_bytes as f64
270 }
271
272 pub fn record(&mut self, original: usize, compressed: usize) {
274 self.original_bytes += original as u64;
275 self.compressed_bytes += compressed as u64;
276 self.operations += 1;
277 }
278
279 pub fn record_failure(&mut self) {
281 self.failures += 1;
282 }
283
284 pub fn savings_percent(&self) -> f64 {
286 if self.original_bytes == 0 {
287 return 0.0;
288 }
289 (1.0 - self.ratio()) * 100.0
290 }
291}
292
293#[derive(Debug)]
299pub enum CompressionError {
300 Compress(String),
302 Decompress(String),
304 UnsupportedType(String),
306}
307
308impl fmt::Display for CompressionError {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 match self {
311 CompressionError::Compress(msg) => write!(f, "Compression error: {}", msg),
312 CompressionError::Decompress(msg) => write!(f, "Decompression error: {}", msg),
313 CompressionError::UnsupportedType(t) => {
314 write!(f, "Unsupported compression type: {}", t)
315 }
316 }
317 }
318}
319
320impl std::error::Error for CompressionError {}
321
322pub type CompressionResult<T> = Result<T, CompressionError>;
324
325#[derive(Debug, Clone)]
331pub struct Compressor {
332 pub compression_type: CompressionType,
334 pub level: u32,
336}
337
338impl Default for Compressor {
339 fn default() -> Self {
340 Self {
341 compression_type: CompressionType::None,
342 level: 6,
343 }
344 }
345}
346
347impl Compressor {
348 pub fn new(compression_type: CompressionType) -> Self {
350 Self {
351 compression_type,
352 level: 6,
353 }
354 }
355
356 #[must_use]
358 pub fn with_level(mut self, level: u32) -> Self {
359 self.level = level;
360 self
361 }
362
363 pub fn compress(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
365 match self.compression_type {
366 CompressionType::None => Ok(data.to_vec()),
367 #[cfg(feature = "gzip")]
368 CompressionType::Gzip => self.compress_gzip(data),
369 #[cfg(feature = "zlib")]
370 CompressionType::Zlib => self.compress_zlib(data),
371 #[cfg(feature = "zstd-compression")]
372 CompressionType::Zstd => self.compress_zstd(data),
373 }
374 }
375
376 pub fn decompress(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
378 match self.compression_type {
379 CompressionType::None => Ok(data.to_vec()),
380 #[cfg(feature = "gzip")]
381 CompressionType::Gzip => self.decompress_gzip(data),
382 #[cfg(feature = "zlib")]
383 CompressionType::Zlib => self.decompress_zlib(data),
384 #[cfg(feature = "zstd-compression")]
385 CompressionType::Zstd => self.decompress_zstd(data),
386 }
387 }
388
389 pub fn content_encoding(&self) -> &'static str {
391 self.compression_type.as_encoding()
392 }
393
394 #[cfg(feature = "gzip")]
395 fn compress_gzip(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
396 let level = self.level.min(9) as u8;
397 oxiarc_deflate::gzip_compress(data, level)
398 .map_err(|e| CompressionError::Compress(e.to_string()))
399 }
400
401 #[cfg(feature = "gzip")]
402 fn decompress_gzip(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
403 oxiarc_deflate::gzip_decompress(data)
404 .map_err(|e| CompressionError::Decompress(e.to_string()))
405 }
406
407 #[cfg(feature = "zlib")]
408 fn compress_zlib(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
409 let level = self.level.min(9) as u8;
410 oxiarc_deflate::zlib_compress(data, level)
411 .map_err(|e| CompressionError::Compress(e.to_string()))
412 }
413
414 #[cfg(feature = "zlib")]
415 fn decompress_zlib(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
416 oxiarc_deflate::zlib_decompress(data)
417 .map_err(|e| CompressionError::Decompress(e.to_string()))
418 }
419
420 #[cfg(feature = "zstd-compression")]
421 fn compress_zstd(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
422 let level = self.level.min(22) as i32;
423 oxiarc_zstd::encode_all(data, level).map_err(|e| CompressionError::Compress(e.to_string()))
424 }
425
426 #[cfg(feature = "zstd-compression")]
427 fn decompress_zstd(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
428 oxiarc_zstd::decode_all(data).map_err(|e| CompressionError::Decompress(e.to_string()))
429 }
430}
431
432pub fn detect_compression(data: &[u8]) -> CompressionType {
434 if data.len() < 2 {
435 return CompressionType::None;
436 }
437
438 #[cfg(feature = "gzip")]
440 if data[0] == 0x1f && data[1] == 0x8b {
441 return CompressionType::Gzip;
442 }
443
444 #[cfg(feature = "zlib")]
447 if data[0] == 0x78 && (data[1] == 0x01 || data[1] == 0x5E || data[1] == 0x9C || data[1] == 0xDA)
448 {
449 return CompressionType::Zlib;
450 }
451
452 #[cfg(feature = "zstd-compression")]
454 if data.len() >= 4 && data[0] == 0x28 && data[1] == 0xb5 && data[2] == 0x2f && data[3] == 0xfd {
455 return CompressionType::Zstd;
456 }
457
458 CompressionType::None
459}
460
461pub fn auto_decompress(data: &[u8]) -> CompressionResult<Vec<u8>> {
463 let compression_type = detect_compression(data);
464 Compressor::new(compression_type).decompress(data)
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 #[test]
472 fn test_compression_type_as_encoding() {
473 assert_eq!(CompressionType::None.as_encoding(), "utf-8");
474 #[cfg(feature = "gzip")]
475 assert_eq!(CompressionType::Gzip.as_encoding(), "gzip");
476 #[cfg(feature = "zlib")]
477 assert_eq!(CompressionType::Zlib.as_encoding(), "zlib");
478 #[cfg(feature = "zstd-compression")]
479 assert_eq!(CompressionType::Zstd.as_encoding(), "zstd");
480 }
481
482 #[test]
483 fn test_compression_type_from_encoding() {
484 assert_eq!(
485 CompressionType::from_encoding("utf-8"),
486 Some(CompressionType::None)
487 );
488 assert_eq!(
489 CompressionType::from_encoding("identity"),
490 Some(CompressionType::None)
491 );
492 #[cfg(feature = "gzip")]
493 assert_eq!(
494 CompressionType::from_encoding("gzip"),
495 Some(CompressionType::Gzip)
496 );
497 #[cfg(feature = "zlib")]
498 assert_eq!(
499 CompressionType::from_encoding("zlib"),
500 Some(CompressionType::Zlib)
501 );
502 #[cfg(feature = "zstd-compression")]
503 assert_eq!(
504 CompressionType::from_encoding("zstd"),
505 Some(CompressionType::Zstd)
506 );
507 assert_eq!(CompressionType::from_encoding("unknown"), None);
508 }
509
510 #[test]
511 fn test_compression_type_default() {
512 assert_eq!(CompressionType::default(), CompressionType::None);
513 }
514
515 #[test]
516 fn test_compression_type_display() {
517 assert_eq!(CompressionType::None.to_string(), "utf-8");
518 }
519
520 #[test]
521 fn test_compression_type_id_roundtrip() {
522 assert_eq!(
523 CompressionType::from_id(CompressionType::None.id()),
524 Some(CompressionType::None)
525 );
526 #[cfg(feature = "gzip")]
527 assert_eq!(
528 CompressionType::from_id(CompressionType::Gzip.id()),
529 Some(CompressionType::Gzip)
530 );
531 #[cfg(feature = "zlib")]
532 assert_eq!(
533 CompressionType::from_id(CompressionType::Zlib.id()),
534 Some(CompressionType::Zlib)
535 );
536 #[cfg(feature = "zstd-compression")]
537 assert_eq!(
538 CompressionType::from_id(CompressionType::Zstd.id()),
539 Some(CompressionType::Zstd)
540 );
541 assert_eq!(CompressionType::from_id(255), None);
542 }
543
544 #[test]
545 fn test_compression_type_name() {
546 assert_eq!(CompressionType::None.name(), "none");
547 #[cfg(feature = "gzip")]
548 assert_eq!(CompressionType::Gzip.name(), "gzip");
549 #[cfg(feature = "zlib")]
550 assert_eq!(CompressionType::Zlib.name(), "zlib");
551 #[cfg(feature = "zstd-compression")]
552 assert_eq!(CompressionType::Zstd.name(), "zstd");
553 }
554
555 #[test]
556 fn test_compression_type_is_enabled() {
557 assert!(!CompressionType::None.is_enabled());
558 #[cfg(feature = "gzip")]
559 assert!(CompressionType::Gzip.is_enabled());
560 #[cfg(feature = "zlib")]
561 assert!(CompressionType::Zlib.is_enabled());
562 #[cfg(feature = "zstd-compression")]
563 assert!(CompressionType::Zstd.is_enabled());
564 }
565
566 #[test]
567 fn test_compression_type_from_name() {
568 assert_eq!(
569 CompressionType::from_name("none"),
570 Some(CompressionType::None)
571 );
572 #[cfg(feature = "gzip")]
573 assert_eq!(
574 CompressionType::from_name("gzip"),
575 Some(CompressionType::Gzip)
576 );
577 #[cfg(feature = "zlib")]
578 assert_eq!(
579 CompressionType::from_name("zlib"),
580 Some(CompressionType::Zlib)
581 );
582 #[cfg(feature = "zstd-compression")]
583 assert_eq!(
584 CompressionType::from_name("zstd"),
585 Some(CompressionType::Zstd)
586 );
587 assert_eq!(CompressionType::from_name("invalid"), None);
588 }
589
590 #[test]
591 fn test_compressor_no_compression() {
592 let compressor = Compressor::new(CompressionType::None);
593 let data = b"Hello, World!";
594
595 let compressed = compressor.compress(data).expect("compress should succeed");
596 assert_eq!(compressed, data);
597
598 let decompressed = compressor
599 .decompress(&compressed)
600 .expect("decompress should succeed");
601 assert_eq!(decompressed, data);
602 }
603
604 #[cfg(feature = "gzip")]
605 #[test]
606 fn test_compressor_gzip() {
607 let compressor = Compressor::new(CompressionType::Gzip).with_level(6);
608 let data = b"Hello, World!".repeat(100);
609
610 let compressed = compressor
611 .compress(&data)
612 .expect("gzip compress should succeed");
613 assert!(compressed.len() < data.len());
615
616 let decompressed = compressor
617 .decompress(&compressed)
618 .expect("gzip decompress should succeed");
619 assert_eq!(decompressed, data);
620 }
621
622 #[cfg(feature = "zlib")]
623 #[test]
624 fn test_compressor_zlib() {
625 let compressor = Compressor::new(CompressionType::Zlib).with_level(6);
626 let data = b"Hello, World!".repeat(100);
627
628 let compressed = compressor
629 .compress(&data)
630 .expect("zlib compress should succeed");
631 assert!(compressed.len() < data.len());
632
633 let decompressed = compressor
634 .decompress(&compressed)
635 .expect("zlib decompress should succeed");
636 assert_eq!(decompressed, data);
637 }
638
639 #[cfg(feature = "gzip")]
640 #[test]
641 fn test_detect_gzip() {
642 let compressor = Compressor::new(CompressionType::Gzip);
643 let data = b"Test data";
644 let compressed = compressor.compress(data).expect("compress should succeed");
645
646 assert_eq!(detect_compression(&compressed), CompressionType::Gzip);
647 }
648
649 #[cfg(feature = "zstd-compression")]
650 #[test]
651 fn test_compressor_zstd() {
652 let compressor = Compressor::new(CompressionType::Zstd).with_level(3);
653 let data = b"Hello, World!".repeat(100);
654
655 let compressed = compressor
656 .compress(&data)
657 .expect("zstd compress should succeed");
658 assert!(compressed.len() < data.len());
659
660 let decompressed = compressor
661 .decompress(&compressed)
662 .expect("zstd decompress should succeed");
663 assert_eq!(decompressed, data);
664 }
665
666 #[cfg(feature = "zstd-compression")]
667 #[test]
668 fn test_detect_zstd() {
669 let compressor = Compressor::new(CompressionType::Zstd);
670 let data = b"Test data";
671 let compressed = compressor.compress(data).expect("compress should succeed");
672
673 assert_eq!(detect_compression(&compressed), CompressionType::Zstd);
674 }
675
676 #[test]
677 fn test_detect_no_compression() {
678 let data = b"Plain text data";
679 assert_eq!(detect_compression(data), CompressionType::None);
680 }
681
682 #[test]
683 fn test_auto_decompress_plain() {
684 let data = b"Plain text";
685 let result = auto_decompress(data).expect("auto_decompress should succeed");
686 assert_eq!(result, data);
687 }
688
689 #[cfg(feature = "gzip")]
690 #[test]
691 fn test_auto_decompress_gzip() {
692 let compressor = Compressor::new(CompressionType::Gzip);
693 let original = b"Test data for auto-decompress";
694 let compressed = compressor
695 .compress(original)
696 .expect("compress should succeed");
697
698 let decompressed = auto_decompress(&compressed).expect("auto_decompress should succeed");
699 assert_eq!(decompressed, original);
700 }
701
702 #[test]
703 fn test_compression_error_display() {
704 let err = CompressionError::Compress("test error".to_string());
705 assert_eq!(err.to_string(), "Compression error: test error");
706
707 let err = CompressionError::Decompress("decode failed".to_string());
708 assert_eq!(err.to_string(), "Decompression error: decode failed");
709
710 let err = CompressionError::UnsupportedType("lz4".to_string());
711 assert_eq!(err.to_string(), "Unsupported compression type: lz4");
712 }
713
714 #[test]
715 fn test_compression_type_available() {
716 let available = CompressionType::available();
717 assert!(available.contains(&CompressionType::None));
718 }
719
720 #[test]
721 fn test_compression_type_try_from() {
722 use std::convert::TryFrom;
723
724 assert_eq!(
725 CompressionType::try_from("utf-8").expect("should parse utf-8"),
726 CompressionType::None
727 );
728 assert_eq!(
729 CompressionType::try_from("identity").expect("should parse identity"),
730 CompressionType::None
731 );
732
733 #[cfg(feature = "gzip")]
734 assert_eq!(
735 CompressionType::try_from("gzip").expect("should parse gzip"),
736 CompressionType::Gzip
737 );
738
739 #[cfg(feature = "zstd-compression")]
740 assert_eq!(
741 CompressionType::try_from("zstd").expect("should parse zstd"),
742 CompressionType::Zstd
743 );
744
745 assert!(CompressionType::try_from("unknown").is_err());
747 assert!(CompressionType::try_from("lz4").is_err());
748 }
749
750 #[test]
753 fn test_registry_new() {
754 let registry = CompressionRegistry::new();
755 assert_eq!(registry.default_type(), CompressionType::None);
756 assert!(registry.is_available(&CompressionType::None));
757 assert!(!registry.available_types().is_empty());
758 }
759
760 #[test]
761 fn test_registry_with_default_none() {
762 let registry = CompressionRegistry::with_default(CompressionType::None)
763 .expect("None is always available");
764 assert_eq!(registry.default_type(), CompressionType::None);
765 }
766
767 #[cfg(feature = "gzip")]
768 #[test]
769 fn test_registry_with_default_gzip() {
770 let registry = CompressionRegistry::with_default(CompressionType::Gzip)
771 .expect("gzip should be available");
772 assert_eq!(registry.default_type(), CompressionType::Gzip);
773 assert!(registry.is_available(&CompressionType::Gzip));
774 }
775
776 #[test]
777 fn test_registry_default_impl() {
778 let registry = CompressionRegistry::default();
779 assert_eq!(registry.default_type(), CompressionType::None);
780 }
781
782 #[test]
785 fn test_stats_default() {
786 let stats = CompressionStats::default();
787 assert_eq!(stats.original_bytes, 0);
788 assert_eq!(stats.compressed_bytes, 0);
789 assert_eq!(stats.operations, 0);
790 assert_eq!(stats.failures, 0);
791 assert_eq!(stats.ratio(), 0.0);
792 assert_eq!(stats.savings_percent(), 0.0);
793 }
794
795 #[test]
796 fn test_stats_record() {
797 let mut stats = CompressionStats::default();
798 stats.record(1000, 500);
799
800 assert_eq!(stats.original_bytes, 1000);
801 assert_eq!(stats.compressed_bytes, 500);
802 assert_eq!(stats.operations, 1);
803 assert_eq!(stats.failures, 0);
804 assert_eq!(stats.ratio(), 0.5);
805 assert_eq!(stats.savings_percent(), 50.0);
806 }
807
808 #[test]
809 fn test_stats_multiple_records() {
810 let mut stats = CompressionStats::default();
811 stats.record(1000, 500);
812 stats.record(2000, 1000);
813
814 assert_eq!(stats.original_bytes, 3000);
815 assert_eq!(stats.compressed_bytes, 1500);
816 assert_eq!(stats.operations, 2);
817 assert_eq!(stats.ratio(), 0.5);
818 }
819
820 #[test]
821 fn test_stats_record_failure() {
822 let mut stats = CompressionStats::default();
823 stats.record_failure();
824 stats.record_failure();
825
826 assert_eq!(stats.failures, 2);
827 assert_eq!(stats.operations, 0);
828 }
829
830 #[test]
831 fn test_stats_serde_roundtrip() {
832 let mut stats = CompressionStats::default();
833 stats.record(1000, 400);
834 stats.record_failure();
835
836 let json = serde_json::to_string(&stats).expect("serialize should succeed");
837 let deserialized: CompressionStats =
838 serde_json::from_str(&json).expect("deserialize should succeed");
839
840 assert_eq!(deserialized.original_bytes, stats.original_bytes);
841 assert_eq!(deserialized.compressed_bytes, stats.compressed_bytes);
842 assert_eq!(deserialized.operations, stats.operations);
843 assert_eq!(deserialized.failures, stats.failures);
844 }
845
846 #[test]
847 fn test_compression_type_serde_roundtrip() {
848 let ct = CompressionType::None;
849 let json = serde_json::to_string(&ct).expect("serialize should succeed");
850 let deserialized: CompressionType =
851 serde_json::from_str(&json).expect("deserialize should succeed");
852 assert_eq!(deserialized, ct);
853 }
854}