1use crate::{
23 Error, Result,
24 security::{CompressionBombDetector, CompressionStats},
25};
26#[cfg(feature = "compression")]
27use std::io::Write;
28use std::io::{Cursor, Read};
29use tracing::{debug, info, warn};
30#[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
31use zstd;
32
33#[derive(Debug, Clone, PartialEq, Eq, Default)]
47pub enum ByteCodec {
48 #[default]
50 None,
51 Deflate,
59 Gzip,
63 Brotli,
67 #[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
80 ZstdDict(std::sync::Arc<crate::compression::zstd::ZstdDictionary>),
81}
82
83#[derive(Debug, Clone, Copy, Default)]
87pub enum CompressionQuality {
88 Fast,
90 #[default]
92 Balanced,
93 Best,
95}
96
97impl CompressionQuality {
98 #[cfg(feature = "compression")]
99 fn flate2_level(self) -> flate2::Compression {
100 match self {
101 Self::Fast => flate2::Compression::fast(),
102 Self::Balanced => flate2::Compression::default(),
103 Self::Best => flate2::Compression::best(),
104 }
105 }
106
107 #[cfg(feature = "compression")]
108 fn brotli_quality(self) -> i32 {
109 match self {
110 Self::Fast => 1,
111 Self::Balanced => 5,
112 Self::Best => 11,
113 }
114 }
115}
116
117#[derive(Debug, Clone)]
126pub struct SecureCompressedData {
127 pub data: Vec<u8>,
129 pub original_size: usize,
131 pub compression_ratio: f64,
137 pub codec: ByteCodec,
139}
140
141pub struct SecureCompressor {
157 detector: CompressionBombDetector,
158 codec: ByteCodec,
159 #[cfg_attr(not(feature = "compression"), allow(dead_code))]
160 quality: CompressionQuality,
161}
162
163impl SecureCompressor {
164 pub fn new(detector: CompressionBombDetector, codec: ByteCodec) -> Self {
166 Self {
167 detector,
168 codec,
169 quality: CompressionQuality::default(),
170 }
171 }
172
173 pub fn with_default_security(codec: ByteCodec) -> Self {
175 Self::new(CompressionBombDetector::default(), codec)
176 }
177
178 pub fn with_quality(
180 detector: CompressionBombDetector,
181 codec: ByteCodec,
182 quality: CompressionQuality,
183 ) -> Self {
184 Self {
185 detector,
186 codec,
187 quality,
188 }
189 }
190
191 pub fn compress(&self, data: &[u8]) -> Result<SecureCompressedData> {
195 self.detector.validate_pre_decompression(data.len())?;
196
197 let compressed_bytes = self.encode(data)?;
198
199 let compression_ratio = data.len() as f64 / compressed_bytes.len().max(1) as f64;
200 info!("Compression completed: {:.2}x ratio", compression_ratio);
201
202 Ok(SecureCompressedData {
203 original_size: data.len(),
204 compression_ratio,
205 codec: self.codec.clone(),
206 data: compressed_bytes,
207 })
208 }
209
210 pub fn decompress_protected(&self, compressed: &SecureCompressedData) -> Result<Vec<u8>> {
215 self.detector
216 .validate_pre_decompression(compressed.data.len())?;
217 self.decode_with_protection(&compressed.data, compressed.codec.clone(), None)
218 }
219
220 pub fn decompress_nested(
225 &self,
226 compressed: &SecureCompressedData,
227 depth: usize,
228 ) -> Result<Vec<u8>> {
229 self.detector
230 .validate_pre_decompression(compressed.data.len())?;
231 self.decode_with_protection(&compressed.data, compressed.codec.clone(), Some(depth))
232 }
233
234 fn encode(&self, data: &[u8]) -> Result<Vec<u8>> {
236 match &self.codec {
237 ByteCodec::None => {
238 debug!("No compression applied");
239 Ok(data.to_vec())
240 }
241
242 #[cfg(feature = "compression")]
243 ByteCodec::Deflate => {
244 use flate2::write::DeflateEncoder;
245 let mut enc = DeflateEncoder::new(Vec::new(), self.quality.flate2_level());
246 enc.write_all(data)
247 .map_err(|e| Error::CompressionError(format!("deflate encode: {e}")))?;
248 enc.finish()
249 .map_err(|e| Error::CompressionError(format!("deflate finish: {e}")))
250 }
251
252 #[cfg(feature = "compression")]
253 ByteCodec::Gzip => {
254 use flate2::write::GzEncoder;
255 let mut enc = GzEncoder::new(Vec::new(), self.quality.flate2_level());
256 enc.write_all(data)
257 .map_err(|e| Error::CompressionError(format!("gzip encode: {e}")))?;
258 enc.finish()
259 .map_err(|e| Error::CompressionError(format!("gzip finish: {e}")))
260 }
261
262 #[cfg(feature = "compression")]
263 ByteCodec::Brotli => {
264 let params = brotli::enc::BrotliEncoderParams {
265 quality: self.quality.brotli_quality(),
266 ..Default::default()
267 };
268 let mut out = Vec::new();
269 brotli::BrotliCompress(&mut Cursor::new(data), &mut out, ¶ms)
270 .map_err(|e| Error::CompressionError(format!("brotli encode: {e}")))?;
271 Ok(out)
272 }
273
274 #[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
275 ByteCodec::ZstdDict(dict) => {
276 crate::compression::zstd::ZstdDictCompressor::compress(data, dict.as_ref())
277 }
278
279 #[cfg(not(feature = "compression"))]
280 ByteCodec::Deflate | ByteCodec::Gzip | ByteCodec::Brotli => Err(
281 Error::CompressionError("feature `compression` is not enabled".into()),
282 ),
283 }
284 }
285
286 fn decode_with_protection(
290 &self,
291 data: &[u8],
292 codec: ByteCodec,
293 depth: Option<usize>,
294 ) -> Result<Vec<u8>> {
295 macro_rules! run {
298 ($decoder:expr) => {{
299 let compressed_size = data.len();
300 let mut out = Vec::new();
301 let result = if let Some(d) = depth {
302 let mut protector =
303 self.detector
304 .protect_nested_reader($decoder, compressed_size, d)?;
305 let r = protector.read_to_end(&mut out);
306 let stats = protector.stats();
307 self.log_decompression_stats(&stats);
308 if stats.compression_depth > 0 {
309 warn!(
310 "Nested decompression detected at depth {}",
311 stats.compression_depth
312 );
313 }
314 r
315 } else {
316 let mut protector = self.detector.protect_reader($decoder, compressed_size);
317 let r = protector.read_to_end(&mut out);
318 let stats = protector.stats();
319 self.log_decompression_stats(&stats);
320 r
321 };
322 match result {
323 Ok(_) => {
324 self.detector.validate_result(compressed_size, out.len())?;
325 Ok(out)
326 }
327 Err(e) => {
328 warn!("Decompression failed: {}", e);
329 Err(Error::SecurityError(format!(
330 "Protected decompression failed: {}",
331 e
332 )))
333 }
334 }
335 }};
336 }
337
338 match codec {
339 ByteCodec::None => run!(Cursor::new(data)),
340
341 #[cfg(feature = "compression")]
342 ByteCodec::Deflate => run!(flate2::read::DeflateDecoder::new(Cursor::new(data))),
343
344 #[cfg(feature = "compression")]
345 ByteCodec::Gzip => run!(flate2::read::GzDecoder::new(Cursor::new(data))),
346
347 #[cfg(feature = "compression")]
348 ByteCodec::Brotli => run!(brotli::Decompressor::new(Cursor::new(data), 4096)),
349
350 #[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
355 ByteCodec::ZstdDict(dict) => {
356 let decoder = zstd::stream::read::Decoder::with_dictionary(
357 Cursor::new(data),
358 dict.as_bytes(),
359 )
360 .map_err(|e| Error::CompressionError(format!("zstd decoder init: {e}")))?;
361 run!(decoder)
362 }
363
364 #[cfg(not(feature = "compression"))]
365 ByteCodec::Deflate | ByteCodec::Gzip | ByteCodec::Brotli => Err(
366 Error::CompressionError("feature `compression` is not enabled".into()),
367 ),
368 }
369 }
370
371 fn log_decompression_stats(&self, stats: &CompressionStats) {
372 info!(
373 "Decompression stats: {}B -> {}B (ratio: {:.2}x, depth: {})",
374 stats.compressed_size, stats.decompressed_size, stats.ratio, stats.compression_depth
375 );
376 }
377}
378
379pub struct SecureDecompressionContext {
381 detector: CompressionBombDetector,
382 current_depth: usize,
383 max_concurrent_streams: usize,
384 active_streams: usize,
385}
386
387impl SecureDecompressionContext {
388 pub fn new(detector: CompressionBombDetector, max_concurrent_streams: usize) -> Self {
390 Self {
391 detector,
392 current_depth: 0,
393 max_concurrent_streams,
394 active_streams: 0,
395 }
396 }
397
398 pub fn start_stream(
408 &mut self,
409 compressed_size: usize,
410 ) -> Result<crate::security::CompressionBombProtector<Cursor<Vec<u8>>>> {
411 if self.active_streams >= self.max_concurrent_streams {
412 return Err(Error::SecurityError(format!(
413 "Too many concurrent decompression streams: {}/{}",
414 self.active_streams, self.max_concurrent_streams
415 )));
416 }
417
418 let cursor = Cursor::new(Vec::new());
419 let protector =
420 self.detector
421 .protect_nested_reader(cursor, compressed_size, self.current_depth)?;
422
423 self.active_streams += 1;
424 info!(
425 "Started secure decompression stream (active: {})",
426 self.active_streams
427 );
428
429 Ok(protector)
430 }
431
432 pub fn finish_stream(&mut self) {
434 if self.active_streams > 0 {
435 self.active_streams -= 1;
436 info!(
437 "Finished secure decompression stream (active: {})",
438 self.active_streams
439 );
440 }
441 }
442
443 pub fn stats(&self) -> DecompressionContextStats {
445 DecompressionContextStats {
446 current_depth: self.current_depth,
447 active_streams: self.active_streams,
448 max_concurrent_streams: self.max_concurrent_streams,
449 }
450 }
451}
452
453#[derive(Debug, Clone)]
455pub struct DecompressionContextStats {
456 pub current_depth: usize,
458 pub active_streams: usize,
460 pub max_concurrent_streams: usize,
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 use crate::security::CompressionBombConfig;
468
469 #[test]
470 fn test_secure_compressor_creation() {
471 let detector = CompressionBombDetector::default();
472 let compressor = SecureCompressor::new(detector, ByteCodec::None);
473 assert!(!std::ptr::addr_of!(compressor).cast::<u8>().is_null());
475 }
476
477 #[test]
478 fn test_secure_compression_none() {
479 let compressor = SecureCompressor::with_default_security(ByteCodec::None);
480 let data = b"Hello, world! This is test data for compression.";
481
482 let result = compressor.compress(data);
483 assert!(result.is_ok());
484
485 let compressed = result.unwrap();
486 assert_eq!(compressed.original_size, data.len());
487 assert_eq!(compressed.codec, ByteCodec::None);
488 }
489
490 #[test]
491 fn test_none_roundtrip() {
492 let compressor = SecureCompressor::with_default_security(ByteCodec::None);
493 let data = b"round-trip test";
494
495 let compressed = compressor.compress(data).unwrap();
496 let decompressed = compressor.decompress_protected(&compressed).unwrap();
497 assert_eq!(decompressed, data);
498 }
499
500 #[test]
501 fn test_compression_size_limit() {
502 let config = CompressionBombConfig {
503 max_compressed_size: 100, ..Default::default()
505 };
506 let detector = CompressionBombDetector::new(config);
507 let compressor = SecureCompressor::new(detector, ByteCodec::None);
508
509 let large_data = vec![0u8; 1000]; let result = compressor.compress(&large_data);
511
512 assert!(result.is_err());
514 }
515
516 #[test]
517 fn test_different_codecs_none() {
518 let compressor = SecureCompressor::with_default_security(ByteCodec::None);
519 let data = b"test data";
520
521 let result = compressor.compress(data);
522 assert!(result.is_ok());
523
524 let compressed = result.unwrap();
525 assert_eq!(compressed.compression_ratio, 1.0);
526 assert_eq!(compressed.codec, ByteCodec::None);
527 }
528
529 #[cfg(feature = "compression")]
530 mod compression_tests {
531 use super::*;
532
533 fn repetitive_json() -> Vec<u8> {
535 let item = br#"{"id":1,"name":"test","value":42,"active":true}"#;
536 item.repeat(100)
537 }
538
539 #[test]
540 fn test_deflate_roundtrip() {
541 let compressor = SecureCompressor::with_default_security(ByteCodec::Deflate);
542 let data = repetitive_json();
543
544 let compressed = compressor.compress(&data).unwrap();
545 assert_eq!(compressed.codec, ByteCodec::Deflate);
546 assert!(
547 compressed.data.len() < data.len(),
548 "deflate must reduce size"
549 );
550
551 let decompressed = compressor.decompress_protected(&compressed).unwrap();
552 assert_eq!(decompressed, data);
553 }
554
555 #[test]
556 fn test_gzip_roundtrip() {
557 let compressor = SecureCompressor::with_default_security(ByteCodec::Gzip);
558 let data = repetitive_json();
559
560 let compressed = compressor.compress(&data).unwrap();
561 assert_eq!(compressed.codec, ByteCodec::Gzip);
562 assert!(compressed.data.len() < data.len(), "gzip must reduce size");
563
564 let decompressed = compressor.decompress_protected(&compressed).unwrap();
565 assert_eq!(decompressed, data);
566 }
567
568 #[test]
569 fn test_brotli_roundtrip() {
570 let compressor = SecureCompressor::with_default_security(ByteCodec::Brotli);
571 let data = repetitive_json();
572
573 let compressed = compressor.compress(&data).unwrap();
574 assert_eq!(compressed.codec, ByteCodec::Brotli);
575 assert!(
576 compressed.data.len() < data.len(),
577 "brotli must reduce size"
578 );
579
580 let decompressed = compressor.decompress_protected(&compressed).unwrap();
581 assert_eq!(decompressed, data);
582 }
583
584 #[test]
585 fn test_all_qualities_deflate() {
586 let data = repetitive_json();
587 for quality in [
588 CompressionQuality::Fast,
589 CompressionQuality::Balanced,
590 CompressionQuality::Best,
591 ] {
592 let c = SecureCompressor::with_quality(
593 CompressionBombDetector::default(),
594 ByteCodec::Deflate,
595 quality,
596 );
597 let compressed = c.compress(&data).unwrap();
598 let decompressed = c.decompress_protected(&compressed).unwrap();
599 assert_eq!(decompressed, data);
600 }
601 }
602
603 #[test]
604 fn test_all_qualities_brotli() {
605 let data = repetitive_json();
607 let c = SecureCompressor::with_quality(
608 CompressionBombDetector::default(),
609 ByteCodec::Brotli,
610 CompressionQuality::Fast,
611 );
612 let compressed = c.compress(&data).unwrap();
613 let decompressed = c.decompress_protected(&compressed).unwrap();
614 assert_eq!(decompressed, data);
615 }
616
617 #[test]
618 fn test_codec_mismatch_returns_error() {
619 let c = SecureCompressor::with_default_security(ByteCodec::Brotli);
621 let data = b"codec mismatch test data";
622 let mut compressed = c.compress(data).unwrap();
623 compressed.codec = ByteCodec::Gzip; let result = c.decompress_protected(&compressed);
626 assert!(
627 result.is_err(),
628 "wrong codec must produce an error, not garbage"
629 );
630 }
631
632 #[test]
633 fn test_bomb_detection_on_real_codec() {
634 let config = CompressionBombConfig {
636 max_decompressed_size: 200, max_compressed_size: 10_000, max_ratio: 300.0,
639 check_interval_bytes: 64,
640 ..Default::default()
641 };
642 let detector = CompressionBombDetector::new(config);
643 let compressor =
644 SecureCompressor::new(CompressionBombDetector::default(), ByteCodec::Gzip);
645
646 let data = repetitive_json();
648 let compressed = compressor.compress(&data).unwrap();
649
650 let strict_compressor = SecureCompressor::new(detector, ByteCodec::Gzip);
652 let result = strict_compressor.decompress_protected(&compressed);
653 assert!(
654 result.is_err(),
655 "bomb detector must stop oversized decompression"
656 );
657 }
658 }
659
660 #[test]
661 fn test_secure_decompression_context() {
662 let detector = CompressionBombDetector::default();
663 let mut context = SecureDecompressionContext::new(detector, 2);
664
665 assert!(context.start_stream(1024).is_ok());
666 assert!(context.start_stream(1024).is_ok());
667
668 assert!(context.start_stream(1024).is_err());
670
671 context.finish_stream();
672 assert!(context.start_stream(1024).is_ok());
673 }
674
675 #[test]
676 fn test_context_stats() {
677 let detector = CompressionBombDetector::default();
678 let context = SecureDecompressionContext::new(detector, 5);
679
680 let stats = context.stats();
681 assert_eq!(stats.current_depth, 0);
682 assert_eq!(stats.active_streams, 0);
683 assert_eq!(stats.max_concurrent_streams, 5);
684 }
685
686 #[test]
687 fn test_context_finish_stream_underflow_safe() {
688 let detector = CompressionBombDetector::default();
689 let mut context = SecureDecompressionContext::new(detector, 5);
690
691 context.finish_stream();
693 let stats = context.stats();
694 assert_eq!(stats.active_streams, 0);
695 }
696
697 #[test]
698 fn test_byte_codec_default_is_none() {
699 assert_eq!(ByteCodec::default(), ByteCodec::None);
700 }
701
702 #[test]
703 fn test_byte_codec_clone() {
704 let codec = ByteCodec::None;
705 let cloned = codec.clone();
706 assert_eq!(codec, cloned);
707 }
708
709 #[test]
710 fn test_compression_quality_default_is_balanced() {
711 let c = SecureCompressor::with_default_security(ByteCodec::None);
713 let data = b"quality default test";
714 let compressed = c.compress(data).unwrap();
715 let decompressed = c.decompress_protected(&compressed).unwrap();
716 assert_eq!(decompressed.as_slice(), data);
717 }
718
719 #[test]
720 fn test_secure_compressed_data_clone() {
721 let c = SecureCompressor::with_default_security(ByteCodec::None);
722 let compressed = c.compress(b"clone test").unwrap();
723 let cloned = compressed.clone();
724 assert_eq!(compressed.data, cloned.data);
725 assert_eq!(compressed.original_size, cloned.original_size);
726 assert_eq!(compressed.codec, cloned.codec);
727 }
728
729 #[test]
730 fn test_none_roundtrip_empty_payload() {
731 let c = SecureCompressor::with_default_security(ByteCodec::None);
732 let compressed = c.compress(b"").unwrap();
733 let decompressed = c.decompress_protected(&compressed).unwrap();
734 assert_eq!(decompressed, b"");
735 }
736
737 #[test]
738 fn test_decompress_nested_none() {
739 let c = SecureCompressor::with_default_security(ByteCodec::None);
740 let data = b"nested roundtrip";
741 let compressed = c.compress(data).unwrap();
742 let decompressed = c.decompress_nested(&compressed, 0).unwrap();
743 assert_eq!(decompressed.as_slice(), data);
744 }
745
746 #[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
747 mod zstd_dict_tests {
748 use super::*;
749 use crate::compression::zstd::{MAX_DICT_SIZE, N_TRAIN, ZstdDictCompressor};
750 use crate::security::CompressionBombConfig;
751 use std::sync::Arc;
752
753 fn repetitive_json() -> Vec<u8> {
754 let item = br#"{"id":1,"name":"test","value":42,"active":true}"#;
755 item.repeat(100)
756 }
757
758 fn trained_dict() -> crate::compression::zstd::ZstdDictionary {
759 let samples: Vec<Vec<u8>> = (0..N_TRAIN)
760 .map(|i| {
761 format!(
762 r#"{{"id":{i},"name":"item-{i}","value":{},"active":true}}"#,
763 i * 10
764 )
765 .into_bytes()
766 })
767 .collect();
768 ZstdDictCompressor::train(&samples, MAX_DICT_SIZE).unwrap()
769 }
770
771 #[test]
772 fn test_zstd_dict_roundtrip_via_secure_compressor() {
773 let dict = Arc::new(trained_dict());
774 let compressor =
775 SecureCompressor::with_default_security(ByteCodec::ZstdDict(dict.clone()));
776 let data = repetitive_json();
777
778 let compressed = compressor.compress(&data).unwrap();
779 assert!(matches!(compressed.codec, ByteCodec::ZstdDict(_)));
780 assert!(
781 compressed.data.len() < data.len(),
782 "zstd dict must reduce size on repetitive data"
783 );
784
785 let decompressed = compressor.decompress_protected(&compressed).unwrap();
786 assert_eq!(decompressed, data);
787 }
788
789 #[test]
790 fn test_zstd_dict_bomb_detection() {
791 let dict = Arc::new(trained_dict());
792 let producer =
793 SecureCompressor::with_default_security(ByteCodec::ZstdDict(dict.clone()));
794 let data = repetitive_json();
795 let compressed = producer.compress(&data).unwrap();
796
797 let config = CompressionBombConfig {
798 max_decompressed_size: 200,
799 max_compressed_size: 10_000,
800 max_ratio: 300.0,
801 check_interval_bytes: 64,
802 ..Default::default()
803 };
804 let strict = SecureCompressor::new(
805 crate::security::CompressionBombDetector::new(config),
806 ByteCodec::ZstdDict(dict),
807 );
808 let result = strict.decompress_protected(&compressed);
809 assert!(
810 result.is_err(),
811 "bomb detector must block oversized zstd dict output"
812 );
813 }
814
815 #[test]
816 fn test_zstd_dict_codec_mismatch_errors() {
817 let dict = Arc::new(trained_dict());
818 let c = SecureCompressor::with_default_security(ByteCodec::ZstdDict(dict));
819 let data = b"codec mismatch test data";
820 let mut compressed = c.compress(data).unwrap();
821 compressed.codec = ByteCodec::Gzip;
823 assert!(
824 c.decompress_protected(&compressed).is_err(),
825 "wrong codec must produce an error"
826 );
827 }
828
829 #[test]
830 fn test_zstd_dict_empty_payload_roundtrip() {
831 let dict = Arc::new(trained_dict());
832 let c = SecureCompressor::with_default_security(ByteCodec::ZstdDict(dict));
833 let compressed = c.compress(b"").unwrap();
834 let decompressed = c.decompress_protected(&compressed).unwrap();
835 assert_eq!(decompressed, b"");
836 }
837
838 #[test]
839 fn test_zstd_dict_wrong_dictionary_errors() {
840 let samples_a: Vec<Vec<u8>> = (0..N_TRAIN)
842 .map(|i| format!(r#"{{"corpus":"alpha","id":{i},"score":{}}}"#, i * 7).into_bytes())
843 .collect();
844 let samples_b: Vec<Vec<u8>> = (0..N_TRAIN)
845 .map(|i| format!(r#"{{"corpus":"beta","seq":{i},"label":"x-{i}"}}"#).into_bytes())
846 .collect();
847
848 let dict_a = ZstdDictCompressor::train(&samples_a, MAX_DICT_SIZE).unwrap();
849 let dict_b = ZstdDictCompressor::train(&samples_b, MAX_DICT_SIZE).unwrap();
850
851 let data = b"some representative payload data";
852 let compressed =
853 ZstdDictCompressor::compress(data, &dict_a).expect("compress with dict_a");
854
855 let result = ZstdDictCompressor::decompress(&compressed, &dict_b, data.len() * 4);
857 assert!(
858 result.is_err(),
859 "wrong dictionary must produce a libzstd error"
860 );
861 }
862 }
863
864 #[cfg(feature = "compression")]
865 mod extended_compression_tests {
866 use super::*;
867
868 fn incompressible_payload() -> Vec<u8> {
870 let mut state: u64 = 0x_dead_beef_cafe_babe;
872 (0..512)
873 .map(|_| {
874 state = state
875 .wrapping_mul(6_364_136_223_846_793_005)
876 .wrapping_add(1);
877 (state >> 33) as u8
878 })
879 .collect()
880 }
881
882 #[test]
883 fn test_deflate_roundtrip_incompressible() {
884 let c = SecureCompressor::with_default_security(ByteCodec::Deflate);
885 let data = incompressible_payload();
886 let compressed = c.compress(&data).unwrap();
887 assert_eq!(compressed.codec, ByteCodec::Deflate);
888 let decompressed = c.decompress_protected(&compressed).unwrap();
889 assert_eq!(decompressed, data);
890 }
891
892 #[test]
893 fn test_gzip_roundtrip_incompressible() {
894 let c = SecureCompressor::with_default_security(ByteCodec::Gzip);
895 let data = incompressible_payload();
896 let compressed = c.compress(&data).unwrap();
897 assert_eq!(compressed.codec, ByteCodec::Gzip);
898 let decompressed = c.decompress_protected(&compressed).unwrap();
899 assert_eq!(decompressed, data);
900 }
901
902 #[test]
903 fn test_brotli_roundtrip_incompressible() {
904 let c = SecureCompressor::with_default_security(ByteCodec::Brotli);
905 let data = incompressible_payload();
906 let compressed = c.compress(&data).unwrap();
907 assert_eq!(compressed.codec, ByteCodec::Brotli);
908 let decompressed = c.decompress_protected(&compressed).unwrap();
909 assert_eq!(decompressed, data);
910 }
911
912 #[test]
913 fn test_gzip_all_qualities() {
914 let item = br#"{"id":1,"name":"test","value":42}"#;
915 let data: Vec<u8> = item.repeat(50);
916 for quality in [
917 CompressionQuality::Fast,
918 CompressionQuality::Balanced,
919 CompressionQuality::Best,
920 ] {
921 let c = SecureCompressor::with_quality(
922 CompressionBombDetector::default(),
923 ByteCodec::Gzip,
924 quality,
925 );
926 let compressed = c.compress(&data).unwrap();
927 let decompressed = c.decompress_protected(&compressed).unwrap();
928 assert_eq!(
929 decompressed, data,
930 "gzip quality {quality:?} roundtrip failed"
931 );
932 }
933 }
934
935 #[test]
936 fn test_brotli_balanced_quality() {
937 let item = br#"{"key":"value","n":99}"#;
938 let data: Vec<u8> = item.repeat(80);
939 let c = SecureCompressor::with_quality(
940 CompressionBombDetector::default(),
941 ByteCodec::Brotli,
942 CompressionQuality::Balanced,
943 );
944 let compressed = c.compress(&data).unwrap();
945 let decompressed = c.decompress_protected(&compressed).unwrap();
946 assert_eq!(decompressed, data);
947 }
948
949 #[test]
950 fn test_decompress_nested_with_depth() {
951 let c = SecureCompressor::with_default_security(ByteCodec::Deflate);
952 let item = br#"{"x":1}"#;
953 let data: Vec<u8> = item.repeat(100);
954 let compressed = c.compress(&data).unwrap();
955 let decompressed = c.decompress_nested(&compressed, 1).unwrap();
956 assert_eq!(decompressed, data);
957 }
958
959 #[test]
960 fn test_decompress_nested_depth_exceeded_returns_error() {
961 use crate::security::CompressionBombConfig;
962 let config = CompressionBombConfig {
963 max_compression_depth: 2,
964 ..Default::default()
965 };
966 let c = SecureCompressor::new(CompressionBombDetector::new(config), ByteCodec::Deflate);
967 let item = br#"{"x":1}"#;
968 let data: Vec<u8> = item.repeat(100);
969 let compressed = c.compress(&data).unwrap();
970 let result = c.decompress_nested(&compressed, 3);
972 assert!(result.is_err(), "depth beyond limit must return an error");
973 }
974
975 #[test]
976 fn test_bomb_detection_deflate() {
977 use crate::security::CompressionBombConfig;
978 let config = CompressionBombConfig {
979 max_decompressed_size: 200,
980 max_compressed_size: 10_000,
981 max_ratio: 300.0,
982 check_interval_bytes: 64,
983 ..Default::default()
984 };
985 let item = br#"{"id":1,"name":"test","value":42,"active":true}"#;
986 let data: Vec<u8> = item.repeat(100);
987 let producer = SecureCompressor::with_default_security(ByteCodec::Deflate);
988 let compressed = producer.compress(&data).unwrap();
989
990 let strict =
991 SecureCompressor::new(CompressionBombDetector::new(config), ByteCodec::Deflate);
992 let result = strict.decompress_protected(&compressed);
993 assert!(
994 result.is_err(),
995 "bomb detector must block oversized deflate output"
996 );
997 }
998
999 #[test]
1000 fn test_bomb_detection_brotli() {
1001 use crate::security::CompressionBombConfig;
1002 let config = CompressionBombConfig {
1003 max_decompressed_size: 200,
1004 max_compressed_size: 10_000,
1005 max_ratio: 300.0,
1006 check_interval_bytes: 64,
1007 ..Default::default()
1008 };
1009 let item = br#"{"id":1,"name":"test","value":42,"active":true}"#;
1010 let data: Vec<u8> = item.repeat(100);
1011 let producer = SecureCompressor::with_default_security(ByteCodec::Brotli);
1012 let compressed = producer.compress(&data).unwrap();
1013
1014 let strict =
1015 SecureCompressor::new(CompressionBombDetector::new(config), ByteCodec::Brotli);
1016 let result = strict.decompress_protected(&compressed);
1017 assert!(
1018 result.is_err(),
1019 "bomb detector must block oversized brotli output"
1020 );
1021 }
1022
1023 #[test]
1024 fn test_codec_mismatch_deflate_as_gzip() {
1025 let c = SecureCompressor::with_default_security(ByteCodec::Deflate);
1026 let data = b"deflate mismatch test payload";
1027 let mut compressed = c.compress(data).unwrap();
1028 compressed.codec = ByteCodec::Gzip;
1029 let result = c.decompress_protected(&compressed);
1030 assert!(result.is_err(), "Deflate data decoded as Gzip must fail");
1031 }
1032
1033 #[test]
1034 fn test_empty_payload_all_codecs() {
1035 for codec in [ByteCodec::Deflate, ByteCodec::Gzip, ByteCodec::Brotli] {
1036 let label = format!("{codec:?}");
1037 let c = SecureCompressor::with_default_security(codec);
1038 let compressed = c.compress(b"").unwrap();
1039 let decompressed = c.decompress_protected(&compressed).unwrap();
1040 assert_eq!(decompressed, b"", "empty roundtrip failed for {label}");
1041 }
1042 }
1043 }
1044}