1mod bcj_composites;
24mod bitshuffle_lz4;
25mod brotli;
26mod bzip2;
27mod composite;
28mod deflate;
29mod deflate64;
30mod flac;
31pub mod fsst_brotli;
32mod glza;
33mod libdeflate;
34mod lz4;
35mod ppmd;
36mod ppmd8;
37mod ricepp;
38mod shuffle_lz4;
39mod shuffle_zstd;
40mod snappy;
41mod store;
42mod xz;
43mod zpaq;
44mod zstd;
45pub mod zstd_dict;
46
47use std::sync::OnceLock;
48
49use crate::error::CoreError;
50
51pub const CODEC_STORE: u8 = 0x00;
53pub const CODEC_LZ4: u8 = 0x01;
55pub const CODEC_ZSTD: u8 = 0x02;
59pub const CODEC_XZ: u8 = 0x03;
61pub const CODEC_BROTLI: u8 = 0x04;
64pub const CODEC_DEFLATE: u8 = 0x05;
67pub const CODEC_SNAPPY: u8 = 0x06;
70pub const CODEC_FLAC: u8 = 0x07;
74pub const CODEC_RICEPP: u8 = 0x08;
77pub const CODEC_FSST_BROTLI: u8 = 0x09;
79pub const CODEC_BLOSC2_SHUFFLE_LZ4: u8 = 0x0A;
81pub const CODEC_ZPAQ: u8 = 0x0B;
83pub const CODEC_PPMD: u8 = 0x0C;
85pub const CODEC_GLZA: u8 = 0x0D;
87pub const CODEC_SHUFFLE_ZSTD: u8 = 0x0E;
89pub const CODEC_BITSHUFFLE_LZ4: u8 = 0x0F;
91pub const CODEC_BZIP2: u8 = 0x10;
93pub const CODEC_DEFLATE64: u8 = 0x11;
95pub const CODEC_PPMD8: u8 = 0x12;
97
98pub const CODEC_LZ4_HC: u8 = 0x13;
101
102pub const CODEC_LIBDEFLATE: u8 = 0x14;
114
115pub const CODEC_BCJ_X86_LZ4: u8 = 0x20;
117pub const CODEC_BCJ_X86_ZSTD: u8 = 0x21;
119pub const CODEC_BCJ_ARM64_LZ4: u8 = 0x23;
121pub const CODEC_BCJ_ARM64_ZSTD: u8 = 0x24;
123
124pub const CODEC_REFERENCED: u8 = 0xFE;
133
134#[derive(Clone, Debug)]
140pub struct CodecTunables {
141 pub quality: u8,
148 pub zstd_quality: u8,
151 pub xz_level: u8,
154 pub ppmd_order: u8,
156 pub ppmd7_budget: usize,
158 pub ppmd8_budget: usize,
160 pub bzip2_block_kb: u32,
162 pub lzma_dict_mb: u32,
166}
167
168impl CodecTunables {
169 #[must_use]
173 pub fn from_quality(quality: u8) -> Self {
174 Self {
175 quality,
176 zstd_quality: quality,
177 xz_level: 0,
178 ppmd_order: 0,
179 ppmd7_budget: 0,
180 ppmd8_budget: 0,
181 bzip2_block_kb: 0,
182 lzma_dict_mb: 0,
183 }
184 }
185}
186
187impl Default for CodecTunables {
188 fn default() -> Self {
189 Self {
190 quality: 0,
191 zstd_quality: 0,
192 xz_level: 0,
193 ppmd_order: 0,
194 ppmd7_budget: 0,
195 ppmd8_budget: 0,
196 bzip2_block_kb: 0,
197 lzma_dict_mb: 0,
198 }
199 }
200}
201
202pub trait Codec: Send + Sync {
206 fn id(&self) -> u8;
208 fn name(&self) -> &'static str;
210 fn compress(&self, plaintext: &[u8]) -> Result<Vec<u8>, CoreError>;
218 fn decompress(&self, compressed: &[u8], expected_len: u32) -> Result<Vec<u8>, CoreError>;
226
227 fn min_compress_size(&self) -> usize {
233 0
234 }
235
236 fn compress_with_tunables(
246 &self,
247 plaintext: &[u8],
248 tunables: &CodecTunables,
249 ) -> Result<Vec<u8>, CoreError> {
250 let _ = tunables;
251 self.compress(plaintext)
252 }
253}
254
255pub trait PerCodecTunables: Codec {
268 type Tunables: Clone + Send + Sync + 'static;
272
273 fn compress_with_owned_tunables(
279 &self,
280 plaintext: &[u8],
281 tunables: &Self::Tunables,
282 ) -> Result<Vec<u8>, CoreError>;
283}
284
285pub struct CodecRegistry {
287 codecs: Vec<Box<dyn Codec>>,
288}
289
290impl CodecRegistry {
291 #[must_use]
293 pub fn new() -> Self {
294 Self { codecs: Vec::new() }
295 }
296
297 pub fn register(&mut self, codec: Box<dyn Codec>) {
305 let id = codec.id();
306 assert!(
307 !self.codecs.iter().any(|c| c.id() == id),
308 "codec id 0x{id:02X} already registered",
309 );
310 self.codecs.push(codec);
311 }
312
313 fn find(&self, id: u8) -> Option<&dyn Codec> {
314 self.codecs.iter().find(|c| c.id() == id).map(Box::as_ref)
315 }
316
317 fn registered_names(&self) -> String {
318 self.codecs
319 .iter()
320 .map(|c| format!("0x{:02X}={}", c.id(), c.name()))
321 .collect::<Vec<_>>()
322 .join(", ")
323 }
324
325 pub fn compress(&self, id: u8, plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
332 match self.find(id) {
333 Some(codec) => codec_call(|| codec.compress(plaintext)),
334 None => Err(CoreError::UnsupportedFeature {
335 feature: format!(
336 "compress codec 0x{id:02X} (registered: {registered})",
337 registered = self.registered_names()
338 ),
339 }),
340 }
341 }
342
343 pub fn decompress(
350 &self,
351 id: u8,
352 compressed: &[u8],
353 expected_len: u32,
354 ) -> Result<Vec<u8>, CoreError> {
355 match self.find(id) {
356 Some(codec) => codec_call(|| codec.decompress(compressed, expected_len)),
357 None => Err(CoreError::UnsupportedFeature {
358 feature: format!(
359 "decompress codec 0x{id:02X} (registered: {registered})",
360 registered = self.registered_names()
361 ),
362 }),
363 }
364 }
365
366 pub fn compress_with_tunables(
373 &self,
374 id: u8,
375 plaintext: &[u8],
376 tunables: &CodecTunables,
377 ) -> Result<Vec<u8>, CoreError> {
378 match self.find(id) {
379 Some(codec) => codec_call(|| codec.compress_with_tunables(plaintext, tunables)),
380 None => Err(CoreError::UnsupportedFeature {
381 feature: format!(
382 "compress_with_tunables codec 0x{id:02X} (registered: {registered})",
383 registered = self.registered_names()
384 ),
385 }),
386 }
387 }
388}
389
390pub(crate) fn codec_call<F>(f: F) -> Result<Vec<u8>, CoreError>
399where
400 F: FnOnce() -> Result<Vec<u8>, CoreError>,
401{
402 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
403 Ok(r) => r,
404 Err(payload) => {
405 let reason = if let Some(s) = payload.downcast_ref::<&str>() {
406 s.to_string()
407 } else if let Some(s) = payload.downcast_ref::<String>() {
408 s.clone()
409 } else {
410 "unknown panic payload".into()
411 };
412 Err(CoreError::Corrupt {
413 reason: format!("codec panicked: {reason}"),
414 })
415 }
416 }
417}
418
419impl CodecRegistry {
420 pub fn codec_name(&self, codec_id: u8) -> Option<&'static str> {
422 self.codecs
423 .iter()
424 .find(|c| c.id() == codec_id)
425 .map(|c| c.name())
426 }
427}
428
429impl Default for CodecRegistry {
430 fn default() -> Self {
431 let mut registry = Self::new();
432 registry.register(Box::new(store::StoreCodec));
433 registry.register(Box::new(lz4::Lz4Codec));
434 registry.register(Box::new(lz4::Lz4HcCodec));
435 registry.register(Box::new(zstd::ZstdCodec));
436 registry.register(Box::new(xz::XzCodec));
437 registry.register(Box::new(brotli::BrotliCodec));
438 registry.register(Box::new(deflate::DeflateCodec));
439 registry.register(Box::new(libdeflate::LibdeflateCodec));
440 registry.register(Box::new(snappy::SnappyCodec));
441 registry.register(Box::new(flac::FlacCodec));
447 registry.register(Box::new(ricepp::RiceppCodec::fits_default()));
448 registry.register(Box::new(fsst_brotli::FsstBrotliCodec));
449 registry.register(Box::new(shuffle_lz4::float32()));
450 registry.register(Box::new(zpaq::ZpaqCodec));
451 registry.register(Box::new(ppmd::PpmdCodec::new()));
452 registry.register(Box::new(ppmd8::Ppmd8Codec::new()));
453 registry.register(Box::new(glza::GlzaCodec));
454 registry.register(Box::new(shuffle_zstd::shuffle_zstd()));
455 registry.register(Box::new(bitshuffle_lz4::bitshuffle_lz4()));
456 registry.register(Box::new(bzip2::Bzip2Codec::new()));
457 registry.register(Box::new(deflate64::Deflate64Codec::new()));
458 registry.register(Box::new(bcj_composites::bcj_x86_lz4()));
462 registry.register(Box::new(bcj_composites::bcj_x86_zstd()));
463 registry.register(Box::new(bcj_composites::bcj_arm64_lz4()));
464 registry.register(Box::new(bcj_composites::bcj_arm64_zstd()));
465 registry
466 }
467}
468
469impl std::fmt::Debug for CodecRegistry {
470 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471 f.debug_struct("CodecRegistry")
472 .field("codecs", &self.registered_names())
473 .finish()
474 }
475}
476
477static DEFAULT_REGISTRY: OnceLock<CodecRegistry> = OnceLock::new();
478
479fn default_registry() -> &'static CodecRegistry {
480 DEFAULT_REGISTRY.get_or_init(CodecRegistry::default)
481}
482
483#[must_use]
489pub fn best_compressible_codec() -> u8 {
490 CODEC_BROTLI
491}
492
493#[must_use]
504pub fn best_binary_codec() -> u8 {
505 CODEC_LZ4
506}
507
508pub fn codec_name(codec_id: u8) -> Option<&'static str> {
511 default_registry().codec_name(codec_id)
512}
513
514pub fn compress(codec_id: u8, plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
522 default_registry().compress(codec_id, plaintext)
523}
524
525pub fn compress_with_options(
542 codec_id: u8,
543 plaintext: &[u8],
544 quality: u8,
545) -> Result<Vec<u8>, CoreError> {
546 let tunables = CodecTunables::from_quality(quality);
547 compress_with_tunables(codec_id, plaintext, &tunables)
548}
549
550pub fn compress_with_tunables(
558 codec_id: u8,
559 plaintext: &[u8],
560 tunables: &CodecTunables,
561) -> Result<Vec<u8>, CoreError> {
562 default_registry().compress_with_tunables(codec_id, plaintext, tunables)
563}
564
565pub fn decompress(
576 codec_id: u8,
577 compressed: &[u8],
578 expected_len: u32,
579) -> Result<Vec<u8>, CoreError> {
580 default_registry().decompress(codec_id, compressed, expected_len)
581}
582
583#[must_use]
587pub fn compress_lz4_with_size(plaintext: &[u8]) -> Vec<u8> {
588 let codec = omnizip_lz4::Lz4FastCodec;
589 omnizip_codecs::Codec::compress(
590 &codec,
591 plaintext,
592 omnizip_codecs::CompressionLevel::default(),
593 )
594 .unwrap_or_else(|_| plaintext.to_vec())
595}
596
597pub fn compress_zstd(plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
605 zstd::compress(plaintext)
606}
607
608pub fn compress_brotli(plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
614 brotli::compress(plaintext, brotli::DEFAULT_QUALITY)
615}
616
617pub fn compress_brotli_with_quality(plaintext: &[u8], quality: i32) -> Result<Vec<u8>, CoreError> {
630 let tunables = CodecTunables::from_quality(quality.clamp(0, 11) as u8);
631 default_registry().compress_with_tunables(CODEC_BROTLI, plaintext, &tunables)
632}
633
634pub fn compress_deflate(plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
642 deflate::compress(plaintext, deflate::DEFAULT_LEVEL)
643}
644
645#[cfg(test)]
646mod tests {
647 use super::*;
648
649 struct PanickingCodec;
650
651 impl Codec for PanickingCodec {
652 fn id(&self) -> u8 {
653 0xEE
654 }
655 fn name(&self) -> &'static str {
656 "panicking-test"
657 }
658 fn compress(&self, _plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
659 panic!("simulated encoder bug");
660 }
661 fn decompress(&self, _compressed: &[u8], _expected_len: u32) -> Result<Vec<u8>, CoreError> {
662 panic!("simulated decoder bug");
663 }
664 }
665
666 #[test]
667 fn panicking_codec_returns_err_not_unwind() {
668 let mut registry = CodecRegistry::new();
669 registry.register(Box::new(PanickingCodec));
670 let err = registry.compress(0xEE, b"data").expect_err("must be Err");
671 assert!(
672 matches!(err, CoreError::Corrupt { ref reason } if reason.contains("panicked")),
673 "got {err:?}"
674 );
675 let err = registry
676 .decompress(0xEE, b"data", 4)
677 .expect_err("must be Err");
678 assert!(
679 matches!(err, CoreError::Corrupt { ref reason } if reason.contains("panicked")),
680 "got {err:?}"
681 );
682 }
683
684 #[test]
685 fn tunables_ppmd7_bigger_budget_helps_ratio() {
686 let mut input = Vec::with_capacity(1 * 1024 * 1024);
690 let paragraph = b"the quick brown fox jumps over the lazy dog. ";
691 while input.len() + paragraph.len() <= 1 * 1024 * 1024 {
692 input.extend_from_slice(paragraph);
693 }
694
695 let small = CodecTunables {
696 quality: 0,
697 zstd_quality: 0,
698 xz_level: 0,
699 ppmd_order: 4,
700 ppmd7_budget: 8 * 1024 * 1024,
701 ppmd8_budget: 0,
702 bzip2_block_kb: 0,
703 lzma_dict_mb: 0,
704 };
705 let big = CodecTunables {
706 ppmd7_budget: 256 * 1024 * 1024,
707 ..small.clone()
708 };
709
710 let small_c = compress_with_tunables(CODEC_PPMD, &input, &small).expect("ppmd7 small");
711 let big_c = compress_with_tunables(CODEC_PPMD, &input, &big).expect("ppmd7 big");
712 assert!(
713 big_c.len() <= small_c.len(),
714 "256MB budget should not be worse than 8MB ({} vs {})",
715 big_c.len(),
716 small_c.len()
717 );
718
719 let recovered = decompress(CODEC_PPMD, &small_c, input.len() as u32).expect("d");
721 assert_eq!(recovered, input);
722 }
723
724 #[test]
725 fn tunables_brotli_quality_flows_through() {
726 let paragraph = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, \
731 sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
732 let mut input = Vec::with_capacity(10_000);
733 let mut i = 0;
734 while input.len() < 10_000 {
735 input.extend_from_slice(format!("{i:04}: {paragraph:?}\n").as_bytes());
736 i += 1;
737 }
738 let q0 = CodecTunables::from_quality(0);
739 let q11 = CodecTunables::from_quality(11);
740 let c0 = compress_with_tunables(CODEC_BROTLI, &input, &q0).expect("brotli q0");
741 let c11 = compress_with_tunables(CODEC_BROTLI, &input, &q11).expect("brotli q11");
742 assert!(!c0.is_empty() && !c11.is_empty());
745 }
746
747 #[test]
748 fn tunables_bzip2_block_size_maps_to_level() {
749 let input = b"the quick brown fox jumps over the lazy dog. ".repeat(2000);
750 let small = CodecTunables {
751 bzip2_block_kb: 100,
752 ..CodecTunables::default()
753 };
754 let big = CodecTunables {
755 bzip2_block_kb: 900,
756 ..CodecTunables::default()
757 };
758 let cs = compress_with_tunables(CODEC_BZIP2, &input, &small).expect("bzip2 100k");
759 let cb = compress_with_tunables(CODEC_BZIP2, &input, &big).expect("bzip2 900k");
760 assert!(
761 cb.len() <= cs.len(),
762 "900k ({}) <= 100k ({})",
763 cb.len(),
764 cs.len()
765 );
766 }
767
768 #[test]
769 fn store_compress_is_identity() {
770 let data = b"hello world";
771 let compressed = compress(CODEC_STORE, data).expect("store compress");
772 assert_eq!(compressed, data);
773 }
774
775 #[test]
776 fn store_decompress_validates_length() {
777 let data = b"hello world";
778 let result = decompress(CODEC_STORE, data, 11).expect("store decompress");
779 assert_eq!(result, data);
780 }
781
782 #[test]
783 fn zstd_higher_levels_compress_better_than_lower() {
784 let input: Vec<u8> = b"The quick brown fox jumps over the lazy dog. ".repeat(2000);
800 let l1 = omnizip_zstd::compress(&input, omnizip_zstd::ZstdLevel::Fastest).expect("zstd L1");
801 let l6 = omnizip_zstd::compress(&input, omnizip_zstd::ZstdLevel::Default).expect("zstd L6");
802 assert!(
803 l6.len() <= l1.len() + 64,
804 "ZSTD L6 ({}) grossly worse than L1 ({}); level differentiation broken",
805 l6.len(),
806 l1.len()
807 );
808 }
809
810 #[test]
811 fn xz_lzma_round_trips_via_lazy_parsing() {
812 let input: Vec<u8> = b"The quick brown fox jumps over the lazy dog. \
817 Lorem ipsum dolor sit amet. \
818 SVG is a vector image format."
819 .repeat(500);
820 let xz = omnizip_lzma::xz_compress(&input).expect("xz encode");
821 let recovered = omnizip_lzma::xz_container::xz_decompress(&xz).expect("xz decode");
822 assert_eq!(recovered, input);
823 assert!(
824 xz.len() < input.len(),
825 "LZMA should compress real-world text; got {} vs {}",
826 xz.len(),
827 input.len()
828 );
829 }
830
831 #[test]
832 fn store_decompress_rejects_length_mismatch() {
833 let data = b"hello world";
834 match decompress(CODEC_STORE, data, 99) {
835 Err(CoreError::Corrupt { reason }) => {
836 assert!(reason.contains("does not match"), "got: {reason}");
837 }
838 other => panic!("expected Corrupt, got {other:?}"),
839 }
840 }
841
842 #[test]
843 fn lz4_round_trips() {
844 let data = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. \
845 Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
846 let compressed = compress(CODEC_LZ4, data).expect("lz4 compress");
847 let decompressed = decompress(
848 CODEC_LZ4,
849 &compressed,
850 u32::try_from(data.len()).expect("fits u32"),
851 )
852 .expect("lz4 decompress");
853 assert_eq!(decompressed, data);
854 }
855
856 #[test]
857 fn lz4_compresses_repetitive_data() {
858 let data = vec![0x41u8; 10_000];
859 let compressed = compress(CODEC_LZ4, &data).expect("lz4 compress");
860 assert!(
861 compressed.len() < data.len(),
862 "lz4 should compress repetitive data: {} vs {}",
863 compressed.len(),
864 data.len()
865 );
866 }
867
868 #[test]
869 fn zstd_round_trips() {
870 let data = b"The quick brown fox jumps over the lazy dog. ".repeat(1000);
871 let compressed = compress_zstd(&data).expect("zstd compress");
872 let decompressed = decompress(
873 CODEC_ZSTD,
874 &compressed,
875 u32::try_from(data.len()).expect("fits u32"),
876 )
877 .expect("zstd decompress");
878 assert_eq!(decompressed, data);
879 }
880
881 #[test]
882 fn zstd_compresses_repetitive_data() {
883 let data = vec![0x41u8; 10_000];
884 let compressed = compress_zstd(&data).expect("zstd compress");
885 assert!(
886 compressed.len() < data.len(),
887 "zstd should compress repetitive data: {} vs {}",
888 compressed.len(),
889 data.len()
890 );
891 }
892
893 #[test]
894 fn zstd_compresses_better_than_lz4_on_text() {
895 let data = b"The quick brown fox. ".repeat(10_000);
896 let lz4 = compress(CODEC_LZ4, &data).expect("lz4");
897 let zstd = compress_zstd(&data).expect("zstd");
898 assert!(
899 zstd.len() < lz4.len(),
900 "zstd ({}) should be smaller than lz4 ({}) on text",
901 zstd.len(),
902 lz4.len()
903 );
904 }
905
906 #[test]
907 fn zstd_compresses_binary_data() {
908 let data: Vec<u8> = (0..100_000u32)
909 .map(|i| u8::try_from(i % 256).expect("fits u8"))
910 .collect();
911 let compressed = compress_zstd(&data).expect("zstd compress");
912 assert!(compressed.len() < data.len());
913 let decompressed = decompress(
914 CODEC_ZSTD,
915 &compressed,
916 u32::try_from(data.len()).expect("fits u32"),
917 )
918 .expect("zstd decompress");
919 assert_eq!(decompressed, data);
920 }
921
922 #[test]
923 fn xz_encode_round_trips() {
924 let plaintext = b"xz round-trip data";
928 let compressed = compress(CODEC_XZ, plaintext).expect("xz encode succeeds");
929 let decompressed =
930 decompress(CODEC_XZ, &compressed, plaintext.len() as u32).expect("xz decode succeeds");
931 assert_eq!(decompressed.as_slice(), plaintext);
932 }
933
934 #[test]
935 fn reject_unknown_codec() {
936 let result = compress(0xFF, b"data");
937 assert!(matches!(result, Err(CoreError::UnsupportedFeature { .. })));
938 }
939
940 #[test]
941 fn brotli_round_trips() {
942 let data = b"The quick brown fox jumps over the lazy dog. ".repeat(1000);
943 let compressed = compress_brotli(&data).expect("brotli compress");
944 let decompressed = decompress(
945 CODEC_BROTLI,
946 &compressed,
947 u32::try_from(data.len()).expect("fits u32"),
948 )
949 .expect("brotli decompress");
950 assert_eq!(decompressed, data);
951 }
952
953 #[test]
954 fn brotli_compresses_repetitive_data() {
955 let data = vec![0x41u8; 10_000];
956 let compressed = compress_brotli(&data).expect("brotli compress");
957 assert!(
958 compressed.len() < data.len(),
959 "brotli should compress repetitive data: {} vs {}",
960 compressed.len(),
961 data.len()
962 );
963 }
964
965 #[test]
966 fn brotli_and_zstd_both_compress_text() {
967 let data = b"The quick brown fox. ".repeat(10_000);
972 let zstd = compress_zstd(&data).expect("zstd");
973 assert!(zstd.len() < data.len(), "zstd should compress text");
974 let _ = compress_brotli(&data).expect("brotli should not error");
975 }
976
977 #[test]
978 fn brotli_decompress_rejects_length_mismatch() {
979 let data = b"hello world";
980 let compressed = compress_brotli(data).expect("brotli compress");
981 match decompress(CODEC_BROTLI, &compressed, 99) {
982 Err(CoreError::Corrupt { reason }) => {
983 assert!(
984 reason.contains("does not match") || reason.contains("mismatch"),
985 "got: {reason}"
986 );
987 }
988 other => panic!("expected Corrupt, got {other:?}"),
989 }
990 }
991
992 #[test]
993 fn deflate_round_trips() {
994 let data = b"The quick brown fox jumps over the lazy dog. ".repeat(1000);
995 let compressed = compress_deflate(&data).expect("deflate compress");
996 let decompressed = decompress(
997 CODEC_DEFLATE,
998 &compressed,
999 u32::try_from(data.len()).expect("fits u32"),
1000 )
1001 .expect("deflate decompress");
1002 assert_eq!(decompressed, data);
1003 }
1004
1005 #[test]
1006 fn deflate_compresses_repetitive_data() {
1007 let data = vec![0x41u8; 10_000];
1008 let compressed = compress_deflate(&data).expect("deflate compress");
1009 assert!(
1010 compressed.len() < data.len(),
1011 "deflate should compress repetitive data: {} vs {}",
1012 compressed.len(),
1013 data.len()
1014 );
1015 }
1016
1017 #[test]
1018 fn deflate_decompress_rejects_length_mismatch() {
1019 let data = b"hello world";
1020 let compressed = compress_deflate(data).expect("deflate compress");
1021 match decompress(CODEC_DEFLATE, &compressed, 99) {
1022 Err(CoreError::Corrupt { reason }) => {
1023 assert!(
1024 reason.contains("does not match") || reason.contains("mismatch"),
1025 "got: {reason}"
1026 );
1027 }
1028 other => panic!("expected Corrupt, got {other:?}"),
1029 }
1030 }
1031
1032 #[test]
1033 fn snappy_round_trips() {
1034 let data = b"The quick brown fox jumps over the lazy dog. ".repeat(100);
1035 let compressed = compress(CODEC_SNAPPY, &data).expect("snappy compress");
1036 let decompressed = decompress(
1037 CODEC_SNAPPY,
1038 &compressed,
1039 u32::try_from(data.len()).expect("fits u32"),
1040 )
1041 .expect("snappy decompress");
1042 assert_eq!(decompressed, data);
1043 }
1044
1045 #[test]
1046 fn snappy_compresses_repetitive_data() {
1047 let data = vec![0x41u8; 10_000];
1048 let compressed = compress(CODEC_SNAPPY, &data).expect("snappy compress");
1049 assert!(
1050 compressed.len() < data.len(),
1051 "snappy should compress repetitive data: {} vs {}",
1052 compressed.len(),
1053 data.len()
1054 );
1055 }
1056
1057 #[test]
1058 fn snappy_decompress_rejects_length_mismatch() {
1059 let data = b"hello world";
1060 let compressed = compress(CODEC_SNAPPY, data).expect("snappy compress");
1061 match decompress(CODEC_SNAPPY, &compressed, 99) {
1062 Err(CoreError::Corrupt { reason }) => {
1063 assert!(
1064 reason.contains("length mismatch") || reason.contains("does not match"),
1065 "got: {reason}"
1066 );
1067 }
1068 other => panic!("expected Corrupt, got {other:?}"),
1069 }
1070 }
1071
1072 #[test]
1073 fn registry_registers_custom_codec_without_changing_dispatch() {
1074 struct NoopCodec;
1075 const NOOP_ID: u8 = 0xFE;
1076 impl Codec for NoopCodec {
1077 fn id(&self) -> u8 {
1078 NOOP_ID
1079 }
1080 fn name(&self) -> &'static str {
1081 "noop"
1082 }
1083 fn compress(&self, plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
1084 Ok(plaintext.to_vec())
1085 }
1086 fn decompress(
1087 &self,
1088 compressed: &[u8],
1089 expected_len: u32,
1090 ) -> Result<Vec<u8>, CoreError> {
1091 let expected = usize::try_from(expected_len).map_err(|_| CoreError::Corrupt {
1092 reason: format!("noop: expected_len {expected_len} exceeds usize"),
1093 })?;
1094 if compressed.len() != expected {
1095 return Err(CoreError::Corrupt {
1096 reason: "noop: length mismatch".into(),
1097 });
1098 }
1099 Ok(compressed.to_vec())
1100 }
1101 }
1102
1103 let mut registry = CodecRegistry::new();
1104 registry.register(Box::new(NoopCodec));
1105 assert_eq!(registry.compress(NOOP_ID, b"abc").expect("noop"), b"abc");
1106 assert_eq!(
1107 registry
1108 .decompress(NOOP_ID, b"abc", 3)
1109 .expect("noop decompress"),
1110 b"abc"
1111 );
1112 }
1113
1114 #[test]
1115 #[should_panic(expected = "codec id 0x00 already registered")]
1116 fn registry_rejects_duplicate_id() {
1117 let mut registry = CodecRegistry::new();
1118 registry.register(Box::new(store::StoreCodec));
1119 registry.register(Box::new(store::StoreCodec));
1120 }
1121
1122 #[test]
1123 fn default_registry_has_all_seven_codecs() {
1124 let registry = default_registry();
1125 assert!(registry.find(CODEC_STORE).is_some());
1126 assert!(registry.find(CODEC_LZ4).is_some());
1127 assert!(registry.find(CODEC_ZSTD).is_some());
1128 assert!(registry.find(CODEC_XZ).is_some());
1129 assert!(registry.find(CODEC_BROTLI).is_some());
1130 assert!(registry.find(CODEC_DEFLATE).is_some());
1131 assert!(registry.find(CODEC_SNAPPY).is_some());
1132 assert!(registry.find(0xFF).is_none());
1133 }
1134}
1135
1136#[cfg(test)]
1137mod per_codec_tunables_ocp_tests {
1138 use super::*;
1147
1148 #[derive(Clone, Debug)]
1151 struct StrideTunables {
1152 stride: usize,
1153 }
1154
1155 struct DeltaStrideCodec;
1156
1157 impl Codec for DeltaStrideCodec {
1158 fn id(&self) -> u8 {
1159 0xFE }
1161 fn name(&self) -> &'static str {
1162 "delta-stride(test)"
1163 }
1164 fn compress(&self, plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
1165 Ok(self.delta(plaintext, 1))
1167 }
1168 fn decompress(&self, compressed: &[u8], _expected_len: u32) -> Result<Vec<u8>, CoreError> {
1169 let mut out = compressed.to_vec();
1170 for i in 1..out.len() {
1171 out[i] = out[i].wrapping_add(out[i - 1]);
1172 }
1173 Ok(out)
1174 }
1175 }
1176
1177 impl DeltaStrideCodec {
1178 fn delta(&self, data: &[u8], stride: usize) -> Vec<u8> {
1179 let mut out = data.to_vec();
1180 let stride = stride.max(1);
1181 for i in (stride..out.len()).rev() {
1182 out[i] = out[i].wrapping_sub(out[i - stride]);
1183 }
1184 out
1185 }
1186 }
1187
1188 impl PerCodecTunables for DeltaStrideCodec {
1189 type Tunables = StrideTunables;
1190
1191 fn compress_with_owned_tunables(
1192 &self,
1193 plaintext: &[u8],
1194 t: &Self::Tunables,
1195 ) -> Result<Vec<u8>, CoreError> {
1196 Ok(self.delta(plaintext, t.stride))
1197 }
1198 }
1199
1200 #[test]
1201 fn new_codec_tunables_require_no_edits_to_shared_struct() {
1202 let codec = DeltaStrideCodec;
1203 let period: [u8; 4] = [0x11, 0x22, 0x33, 0x44];
1206 let payload: Vec<u8> = period.iter().cycle().copied().take(4096).collect();
1207
1208 let s1 = codec
1212 .compress_with_owned_tunables(&payload, &StrideTunables { stride: 1 })
1213 .expect("stride 1");
1214 let s4 = codec
1215 .compress_with_owned_tunables(&payload, &StrideTunables { stride: 4 })
1216 .expect("stride 4");
1217 assert_ne!(s1, s4, "different tunables must change the output");
1218 assert!(
1221 s4.iter().filter(|&&b| b == 0).count() > s1.iter().filter(|&&b| b == 0).count(),
1222 "stride 4 zeroes the periodic pattern; stride 1 does not"
1223 );
1224
1225 let recovered = codec
1229 .decompress(&s1, payload.len() as u32)
1230 .expect("decompress");
1231 assert_eq!(recovered, payload);
1232
1233 let flat = CodecTunables::from_quality(9);
1238 let via_default = codec.compress(&payload).expect("default path ignores flat");
1239 let _ = flat;
1240 assert_eq!(
1241 via_default, s1,
1242 "default compress == owned tunables stride 1"
1243 );
1244 }
1245}