Skip to main content

pjson_rs/security/
compression_bomb.rs

1//! Compression bomb protection to prevent memory exhaustion attacks
2
3use crate::{Error, Result};
4use std::io::Read;
5use thiserror::Error;
6
7/// Errors related to compression bomb detection
8#[derive(Error, Debug, Clone)]
9pub enum CompressionBombError {
10    #[error("Compression ratio exceeded: {ratio:.2}x > {max_ratio:.2}x")]
11    RatioExceeded { ratio: f64, max_ratio: f64 },
12
13    #[error("Decompressed size exceeded: {size} bytes > {max_size} bytes")]
14    SizeExceeded { size: usize, max_size: usize },
15
16    #[error("Compression depth exceeded: {depth} > {max_depth}")]
17    DepthExceeded { depth: usize, max_depth: usize },
18}
19
20/// Configuration for compression bomb protection.
21///
22/// `max_ratio` is a security parameter: it limits how much the decompressed output may exceed
23/// the compressed input. Legitimate high-compression workloads (e.g., repetitive JSON with
24/// brotli) can exceed 200x; increase this field if you see false positives. Default is 300.0
25/// which is permissive enough for real brotli/gzip workloads while still catching true bombs.
26#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
27pub struct CompressionBombConfig {
28    /// Maximum allowed compression ratio (decompressed_size / compressed_size).
29    /// This is a security parameter — see struct-level note.
30    pub max_ratio: f64,
31    /// Maximum allowed decompressed size in bytes.
32    pub max_decompressed_size: usize,
33    /// Maximum compressed input size in bytes. Reject inputs larger than this before decoding.
34    /// Defaults to 100 MiB. This is separate from `max_decompressed_size` — a legitimately
35    /// small compressed payload is expected to be far less than the decompressed limit.
36    pub max_compressed_size: usize,
37    /// Maximum nested compression levels.
38    pub max_compression_depth: usize,
39    /// Check interval - how often to check during decompression.
40    pub check_interval_bytes: usize,
41}
42
43impl Default for CompressionBombConfig {
44    fn default() -> Self {
45        Self {
46            // 300x is permissive enough for real brotli on repetitive JSON (200x+ ratios are
47            // normal) while still blocking realistic compression bombs.
48            max_ratio: 300.0,
49            max_decompressed_size: 100 * 1024 * 1024, // 100 MiB
50            max_compressed_size: 100 * 1024 * 1024,   // 100 MiB
51            max_compression_depth: 3,
52            check_interval_bytes: 64 * 1024, // Check every 64 KiB
53        }
54    }
55}
56
57impl CompressionBombConfig {
58    /// Configuration for high-security environments.
59    pub fn high_security() -> Self {
60        Self {
61            max_ratio: 20.0,
62            max_decompressed_size: 10 * 1024 * 1024, // 10 MiB
63            max_compressed_size: 10 * 1024 * 1024,   // 10 MiB
64            max_compression_depth: 2,
65            check_interval_bytes: 32 * 1024, // Check every 32 KiB
66        }
67    }
68
69    /// Configuration for low-memory environments.
70    pub fn low_memory() -> Self {
71        Self {
72            max_ratio: 50.0,
73            max_decompressed_size: 5 * 1024 * 1024, // 5 MiB
74            max_compressed_size: 5 * 1024 * 1024,   // 5 MiB
75            max_compression_depth: 2,
76            check_interval_bytes: 16 * 1024, // Check every 16 KiB
77        }
78    }
79
80    /// Configuration for high-throughput environments.
81    pub fn high_throughput() -> Self {
82        Self {
83            max_ratio: 1000.0,
84            max_decompressed_size: 500 * 1024 * 1024, // 500 MiB
85            max_compressed_size: 500 * 1024 * 1024,   // 500 MiB
86            max_compression_depth: 5,
87            check_interval_bytes: 128 * 1024, // Check every 128 KiB
88        }
89    }
90}
91
92/// Protected reader that monitors decompression ratios and sizes
93#[derive(Debug)]
94pub struct CompressionBombProtector<R: Read> {
95    inner: R,
96    config: CompressionBombConfig,
97    compressed_size: usize,
98    decompressed_size: usize,
99    bytes_since_check: usize,
100    compression_depth: usize,
101}
102
103impl<R: Read> CompressionBombProtector<R> {
104    /// Create new protector with given reader and configuration
105    pub fn new(inner: R, config: CompressionBombConfig, compressed_size: usize) -> Self {
106        Self {
107            inner,
108            config,
109            compressed_size,
110            decompressed_size: 0,
111            bytes_since_check: 0,
112            compression_depth: 0,
113        }
114    }
115
116    /// Create new protector with nested compression tracking
117    pub fn with_depth(
118        inner: R,
119        config: CompressionBombConfig,
120        compressed_size: usize,
121        depth: usize,
122    ) -> Result<Self> {
123        if depth > config.max_compression_depth {
124            return Err(Error::SecurityError(
125                CompressionBombError::DepthExceeded {
126                    depth,
127                    max_depth: config.max_compression_depth,
128                }
129                .to_string(),
130            ));
131        }
132
133        Ok(Self {
134            inner,
135            config,
136            compressed_size,
137            decompressed_size: 0,
138            bytes_since_check: 0,
139            compression_depth: depth,
140        })
141    }
142
143    /// Check current compression ratio and size limits
144    fn check_limits(&self) -> Result<()> {
145        // Check decompressed size limit
146        if self.decompressed_size > self.config.max_decompressed_size {
147            return Err(Error::SecurityError(
148                CompressionBombError::SizeExceeded {
149                    size: self.decompressed_size,
150                    max_size: self.config.max_decompressed_size,
151                }
152                .to_string(),
153            ));
154        }
155
156        // Check compression ratio (avoid division by zero)
157        if self.compressed_size > 0 && self.decompressed_size > 0 {
158            let ratio = self.decompressed_size as f64 / self.compressed_size as f64;
159            if ratio > self.config.max_ratio {
160                return Err(Error::SecurityError(
161                    CompressionBombError::RatioExceeded {
162                        ratio,
163                        max_ratio: self.config.max_ratio,
164                    }
165                    .to_string(),
166                ));
167            }
168        }
169
170        Ok(())
171    }
172
173    /// Get current compression statistics
174    pub fn stats(&self) -> CompressionStats {
175        let ratio = if self.compressed_size > 0 {
176            self.decompressed_size as f64 / self.compressed_size as f64
177        } else {
178            0.0
179        };
180
181        CompressionStats {
182            compressed_size: self.compressed_size,
183            decompressed_size: self.decompressed_size,
184            ratio,
185            compression_depth: self.compression_depth,
186        }
187    }
188
189    /// Get inner reader
190    pub fn into_inner(self) -> R {
191        self.inner
192    }
193}
194
195impl<R: Read> Read for CompressionBombProtector<R> {
196    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
197        let bytes_read = self.inner.read(buf)?;
198
199        self.decompressed_size += bytes_read;
200        self.bytes_since_check += bytes_read;
201
202        // Check limits periodically
203        if self.bytes_since_check >= self.config.check_interval_bytes {
204            if let Err(e) = self.check_limits() {
205                return Err(std::io::Error::new(
206                    std::io::ErrorKind::InvalidData,
207                    e.to_string(),
208                ));
209            }
210            self.bytes_since_check = 0;
211        }
212
213        Ok(bytes_read)
214    }
215}
216
217/// Compression statistics for monitoring
218#[derive(Debug, Clone)]
219pub struct CompressionStats {
220    pub compressed_size: usize,
221    pub decompressed_size: usize,
222    pub ratio: f64,
223    pub compression_depth: usize,
224}
225
226/// High-level compression bomb detector
227pub struct CompressionBombDetector {
228    config: CompressionBombConfig,
229}
230
231impl Default for CompressionBombDetector {
232    fn default() -> Self {
233        Self::new(CompressionBombConfig::default())
234    }
235}
236
237impl CompressionBombDetector {
238    /// Create new detector with configuration
239    pub fn new(config: CompressionBombConfig) -> Self {
240        Self { config }
241    }
242
243    /// Validate compressed input size before decompression.
244    ///
245    /// Rejects inputs whose compressed size exceeds `max_compressed_size`. This guards against
246    /// oversized inputs before any decoding begins. It does NOT compare against
247    /// `max_decompressed_size` — decompressed output is monitored by [`CompressionBombProtector`]
248    /// during streaming.
249    pub fn validate_pre_decompression(&self, compressed_size: usize) -> Result<()> {
250        if compressed_size > self.config.max_compressed_size {
251            return Err(Error::SecurityError(format!(
252                "Compressed data size {} exceeds maximum allowed compressed size {}",
253                compressed_size, self.config.max_compressed_size
254            )));
255        }
256        Ok(())
257    }
258
259    /// Create protected reader for safe decompression
260    pub fn protect_reader<R: Read>(
261        &self,
262        reader: R,
263        compressed_size: usize,
264    ) -> CompressionBombProtector<R> {
265        CompressionBombProtector::new(reader, self.config.clone(), compressed_size)
266    }
267
268    /// Create protected reader with compression depth tracking
269    pub fn protect_nested_reader<R: Read>(
270        &self,
271        reader: R,
272        compressed_size: usize,
273        depth: usize,
274    ) -> Result<CompressionBombProtector<R>> {
275        CompressionBombProtector::with_depth(reader, self.config.clone(), compressed_size, depth)
276    }
277
278    /// Validate decompression result after completion
279    pub fn validate_result(&self, compressed_size: usize, decompressed_size: usize) -> Result<()> {
280        if decompressed_size > self.config.max_decompressed_size {
281            return Err(Error::SecurityError(
282                CompressionBombError::SizeExceeded {
283                    size: decompressed_size,
284                    max_size: self.config.max_decompressed_size,
285                }
286                .to_string(),
287            ));
288        }
289
290        if compressed_size > 0 {
291            let ratio = decompressed_size as f64 / compressed_size as f64;
292            if ratio > self.config.max_ratio {
293                return Err(Error::SecurityError(
294                    CompressionBombError::RatioExceeded {
295                        ratio,
296                        max_ratio: self.config.max_ratio,
297                    }
298                    .to_string(),
299                ));
300            }
301        }
302
303        Ok(())
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use std::io::Cursor;
311
312    #[test]
313    fn test_compression_bomb_config() {
314        let config = CompressionBombConfig::default();
315        assert!(config.max_ratio > 0.0);
316        assert!(config.max_decompressed_size > 0);
317
318        let high_sec = CompressionBombConfig::high_security();
319        assert!(high_sec.max_ratio < config.max_ratio);
320
321        let low_mem = CompressionBombConfig::low_memory();
322        assert!(low_mem.max_decompressed_size < config.max_decompressed_size);
323
324        let high_throughput = CompressionBombConfig::high_throughput();
325        assert!(high_throughput.max_decompressed_size > config.max_decompressed_size);
326    }
327
328    #[test]
329    fn test_compression_bomb_detector() {
330        let detector = CompressionBombDetector::default();
331
332        // Should pass validation for reasonable sizes
333        assert!(detector.validate_pre_decompression(1024).is_ok());
334        assert!(detector.validate_result(1024, 10 * 1024).is_ok());
335    }
336
337    #[test]
338    fn test_size_limit_exceeded() {
339        let config = CompressionBombConfig {
340            max_decompressed_size: 1024,
341            ..Default::default()
342        };
343        let detector = CompressionBombDetector::new(config);
344
345        // Should fail for size exceeding limit
346        let result = detector.validate_result(100, 2048);
347        assert!(result.is_err());
348        let error_msg = result.unwrap_err().to_string();
349        assert!(error_msg.contains("Size exceeded") || error_msg.contains("Security error"));
350    }
351
352    #[test]
353    fn test_ratio_limit_exceeded() {
354        let config = CompressionBombConfig {
355            max_ratio: 10.0,
356            ..Default::default()
357        };
358        let detector = CompressionBombDetector::new(config);
359
360        // Should fail for ratio exceeding limit (100 -> 2000 = 20x ratio)
361        let result = detector.validate_result(100, 2000);
362        assert!(result.is_err());
363        assert!(
364            result
365                .unwrap_err()
366                .to_string()
367                .contains("Compression ratio exceeded")
368        );
369    }
370
371    #[test]
372    fn test_protected_reader() {
373        let data = b"Hello, world! This is test data for compression testing.";
374        let cursor = Cursor::new(data.as_slice());
375
376        let config = CompressionBombConfig::default();
377        let mut protector = CompressionBombProtector::new(cursor, config, data.len());
378
379        let mut buffer = Vec::new();
380        let bytes_read = protector.read_to_end(&mut buffer).unwrap();
381
382        assert_eq!(bytes_read, data.len());
383        assert_eq!(buffer.as_slice(), data);
384
385        let stats = protector.stats();
386        assert_eq!(stats.compressed_size, data.len());
387        assert_eq!(stats.decompressed_size, data.len());
388        assert!((stats.ratio - 1.0).abs() < 0.01); // Should be ~1.0 for identical data
389    }
390
391    #[test]
392    fn test_protected_reader_size_limit() {
393        let data = vec![0u8; 2048]; // 2KB of data
394        let cursor = Cursor::new(data);
395
396        let config = CompressionBombConfig {
397            max_decompressed_size: 1024, // 1KB limit
398            check_interval_bytes: 512,   // Check every 512 bytes
399            ..Default::default()
400        };
401
402        let mut protector = CompressionBombProtector::new(cursor, config, 100); // Simulating high compression
403
404        let mut buffer = vec![0u8; 2048];
405        let result = protector.read(&mut buffer);
406
407        // Should either succeed initially or fail on second read
408        if result.is_ok() {
409            // Try reading more to trigger the limit
410            let result2 = protector.read(&mut buffer[512..]);
411            assert!(result2.is_err());
412        } else {
413            // Failed immediately
414            assert!(result.is_err());
415        }
416    }
417
418    #[test]
419    fn test_compression_depth_limit() {
420        let data = b"test data";
421        let cursor = Cursor::new(data.as_slice());
422
423        let config = CompressionBombConfig {
424            max_compression_depth: 2,
425            ..Default::default()
426        };
427
428        // Depth 2 should succeed
429        let protector = CompressionBombProtector::with_depth(cursor, config.clone(), data.len(), 2);
430        assert!(protector.is_ok());
431
432        // Depth 3 should fail
433        let cursor2 = Cursor::new(data.as_slice());
434        let result = CompressionBombProtector::with_depth(cursor2, config, data.len(), 3);
435        assert!(result.is_err());
436    }
437
438    #[test]
439    fn test_zero_compressed_size_handling() {
440        let detector = CompressionBombDetector::default();
441
442        // Zero compressed size should not cause division by zero
443        assert!(detector.validate_result(0, 1024).is_ok());
444    }
445
446    #[test]
447    fn test_stats_calculation() {
448        let data = b"test";
449        let cursor = Cursor::new(data.as_slice());
450
451        let protector = CompressionBombProtector::new(cursor, CompressionBombConfig::default(), 2);
452        let stats = protector.stats();
453
454        assert_eq!(stats.compressed_size, 2);
455        assert_eq!(stats.decompressed_size, 0); // No reads yet
456        assert_eq!(stats.ratio, 0.0);
457        assert_eq!(stats.compression_depth, 0);
458    }
459
460    #[test]
461    fn test_stats_with_zero_compressed_size() {
462        let data = b"test";
463        let cursor = Cursor::new(data.as_slice());
464
465        // Create protector with zero compressed size
466        let protector = CompressionBombProtector::new(cursor, CompressionBombConfig::default(), 0);
467        let stats = protector.stats();
468
469        assert_eq!(stats.compressed_size, 0);
470        assert_eq!(stats.ratio, 0.0); // Should handle division by zero
471    }
472
473    #[test]
474    fn test_into_inner() {
475        let data = b"test data";
476        let cursor = Cursor::new(data.as_slice());
477        let original_position = cursor.position();
478
479        let protector =
480            CompressionBombProtector::new(cursor, CompressionBombConfig::default(), data.len());
481
482        // Extract inner reader
483        let inner = protector.into_inner();
484        assert_eq!(inner.position(), original_position);
485    }
486
487    #[test]
488    fn test_protect_nested_reader_success() {
489        let detector = CompressionBombDetector::new(CompressionBombConfig {
490            max_compression_depth: 3,
491            ..Default::default()
492        });
493
494        let data = b"nested compression test";
495        let cursor = Cursor::new(data.as_slice());
496
497        // Create nested reader at depth 1 (within limit)
498        let result = detector.protect_nested_reader(cursor, data.len(), 1);
499        assert!(result.is_ok());
500
501        let protector = result.unwrap();
502        let stats = protector.stats();
503        assert_eq!(stats.compression_depth, 1);
504    }
505
506    #[test]
507    fn test_protect_nested_reader_depth_exceeded() {
508        let detector = CompressionBombDetector::new(CompressionBombConfig {
509            max_compression_depth: 2,
510            ..Default::default()
511        });
512
513        let data = b"nested compression test";
514        let cursor = Cursor::new(data.as_slice());
515
516        // Try to create nested reader at depth 3 (exceeds limit)
517        let result = detector.protect_nested_reader(cursor, data.len(), 3);
518        assert!(result.is_err());
519
520        let error_msg = result.unwrap_err().to_string();
521        assert!(
522            error_msg.contains("Compression depth exceeded")
523                || error_msg.contains("Security error")
524        );
525    }
526
527    #[test]
528    fn test_validate_pre_decompression_size_exceeded() {
529        let config = CompressionBombConfig {
530            max_compressed_size: 1024,
531            ..Default::default()
532        };
533        let detector = CompressionBombDetector::new(config);
534
535        // Compressed input larger than max_compressed_size must be rejected before decoding.
536        let result = detector.validate_pre_decompression(2048);
537        assert!(result.is_err());
538
539        let error_msg = result.unwrap_err().to_string();
540        assert!(error_msg.contains("exceeds maximum allowed"));
541    }
542
543    #[test]
544    fn test_validate_pre_decompression_success() {
545        let detector = CompressionBombDetector::default();
546
547        // Reasonable size should pass
548        let result = detector.validate_pre_decompression(1024);
549        assert!(result.is_ok());
550    }
551
552    #[test]
553    fn test_protected_reader_stats_after_read() {
554        let data = b"Hello, world!";
555        let cursor = Cursor::new(data.as_slice());
556
557        let compressed_size = 5; // Simulating 5 bytes compressed to 13 bytes
558        let mut protector = CompressionBombProtector::new(
559            cursor,
560            CompressionBombConfig::default(),
561            compressed_size,
562        );
563
564        let mut buffer = Vec::new();
565        protector.read_to_end(&mut buffer).unwrap();
566
567        let stats = protector.stats();
568        assert_eq!(stats.compressed_size, compressed_size);
569        assert_eq!(stats.decompressed_size, data.len());
570
571        let expected_ratio = data.len() as f64 / compressed_size as f64;
572        assert!((stats.ratio - expected_ratio).abs() < 0.01);
573    }
574
575    #[test]
576    fn test_compression_bomb_error_display() {
577        let ratio_err = CompressionBombError::RatioExceeded {
578            ratio: 150.5,
579            max_ratio: 100.0,
580        };
581        assert!(ratio_err.to_string().contains("150.5"));
582        assert!(ratio_err.to_string().contains("100.0"));
583
584        let size_err = CompressionBombError::SizeExceeded {
585            size: 2048,
586            max_size: 1024,
587        };
588        assert!(size_err.to_string().contains("2048"));
589        assert!(size_err.to_string().contains("1024"));
590
591        let depth_err = CompressionBombError::DepthExceeded {
592            depth: 5,
593            max_depth: 3,
594        };
595        assert!(depth_err.to_string().contains("5"));
596        assert!(depth_err.to_string().contains("3"));
597    }
598
599    #[test]
600    fn test_detector_default() {
601        let detector1 = CompressionBombDetector::default();
602        let detector2 = CompressionBombDetector::new(CompressionBombConfig::default());
603
604        // Both should have same configuration values
605        assert_eq!(detector1.config.max_ratio, detector2.config.max_ratio);
606        assert_eq!(
607            detector1.config.max_decompressed_size,
608            detector2.config.max_decompressed_size
609        );
610    }
611
612    #[test]
613    fn test_slow_drip_decompression_bomb() {
614        // Simulate a slow-drip attack: many small expansions that sum to a large total
615        let config = CompressionBombConfig {
616            max_decompressed_size: 10_000,
617            check_interval_bytes: 1000, // Check every 1KB
618            ..Default::default()
619        };
620
621        // Create 15KB of data (exceeds 10KB limit)
622        let data = vec![0u8; 15_000];
623        let cursor = Cursor::new(data);
624
625        let mut protector = CompressionBombProtector::new(cursor, config, 100);
626
627        let mut buffer = [0u8; 1024];
628        let mut total_read = 0;
629        let mut detected = false;
630
631        // Read in small chunks until bomb detected
632        loop {
633            match protector.read(&mut buffer) {
634                Ok(0) => break, // EOF
635                Ok(n) => {
636                    total_read += n;
637                }
638                Err(e) => {
639                    // Should detect bomb before all data is read
640                    // Error message can be either "Size exceeded" or generic security error
641                    let err_str = e.to_string();
642                    assert!(
643                        err_str.contains("Size exceeded") || err_str.contains("Security"),
644                        "Expected size limit error, got: {}",
645                        err_str
646                    );
647                    detected = true;
648                    break;
649                }
650            }
651        }
652
653        assert!(detected, "Slow-drip bomb should be detected");
654        assert!(total_read < 15_000, "Should not read all data");
655    }
656
657    #[test]
658    fn test_integer_overflow_protection_in_ratio() {
659        let detector = CompressionBombDetector::default();
660
661        // Try extreme values that could cause overflow
662        let result = detector.validate_result(1, usize::MAX);
663        assert!(result.is_err());
664    }
665
666    #[test]
667    fn test_integer_overflow_protection_in_size() {
668        let config = CompressionBombConfig {
669            max_decompressed_size: usize::MAX - 1,
670            ..Default::default()
671        };
672        let detector = CompressionBombDetector::new(config);
673
674        // Should reject at MAX
675        let result = detector.validate_result(100, usize::MAX);
676        assert!(result.is_err());
677    }
678
679    #[test]
680    fn test_boundary_max_decompressed_size() {
681        let max_size = 10_000;
682        let config = CompressionBombConfig {
683            max_decompressed_size: max_size,
684            ..Default::default()
685        };
686        let detector = CompressionBombDetector::new(config);
687
688        // Exactly at limit should pass
689        assert!(detector.validate_result(100, max_size).is_ok());
690
691        // One byte over should fail
692        assert!(detector.validate_result(100, max_size + 1).is_err());
693    }
694
695    #[test]
696    fn test_boundary_max_ratio() {
697        let max_ratio = 50.0;
698        let config = CompressionBombConfig {
699            max_ratio,
700            ..Default::default()
701        };
702        let detector = CompressionBombDetector::new(config);
703
704        let compressed = 100;
705        let at_limit = (compressed as f64 * max_ratio) as usize;
706
707        // At limit should pass
708        assert!(detector.validate_result(compressed, at_limit).is_ok());
709
710        // Just over limit should fail
711        assert!(
712            detector
713                .validate_result(compressed, at_limit + 100)
714                .is_err()
715        );
716    }
717
718    #[test]
719    fn test_boundary_max_compression_depth() {
720        let max_depth = 5;
721        let config = CompressionBombConfig {
722            max_compression_depth: max_depth,
723            ..Default::default()
724        };
725
726        let data = b"test";
727        let cursor = Cursor::new(data.as_slice());
728
729        // At limit should succeed
730        let result =
731            CompressionBombProtector::with_depth(cursor, config.clone(), data.len(), max_depth);
732        assert!(result.is_ok());
733
734        // Over limit should fail
735        let cursor2 = Cursor::new(data.as_slice());
736        let result2 =
737            CompressionBombProtector::with_depth(cursor2, config, data.len(), max_depth + 1);
738        assert!(result2.is_err());
739    }
740
741    #[test]
742    fn test_nested_compression_attack_simulation() {
743        // Simulate nested compression: each layer expands the data
744        let detector = CompressionBombDetector::new(CompressionBombConfig {
745            max_compression_depth: 2,
746            max_decompressed_size: 10_000,
747            ..Default::default()
748        });
749
750        // Layer 1: 100 bytes compressed
751        let layer1_data = vec![0u8; 1000]; // Expands to 1KB
752        let cursor1 = Cursor::new(layer1_data.clone());
753
754        let protector1 = detector.protect_nested_reader(cursor1, 100, 1);
755        assert!(protector1.is_ok());
756
757        // Layer 2: Within limit
758        let cursor2 = Cursor::new(layer1_data.clone());
759        let protector2 = detector.protect_nested_reader(cursor2, 100, 2);
760        assert!(protector2.is_ok());
761
762        // Layer 3: Exceeds depth limit
763        let cursor3 = Cursor::new(layer1_data);
764        let protector3 = detector.protect_nested_reader(cursor3, 100, 3);
765        assert!(protector3.is_err());
766    }
767
768    #[test]
769    fn test_check_limits_called_at_intervals() {
770        let check_interval = 100;
771        let config = CompressionBombConfig {
772            max_decompressed_size: 500,
773            check_interval_bytes: check_interval,
774            max_ratio: 10.0,
775            ..Default::default()
776        };
777
778        // Create data that will exceed limits after multiple reads
779        let data = vec![0u8; 600];
780        let cursor = Cursor::new(data);
781
782        let mut protector = CompressionBombProtector::new(cursor, config, 10); // High compression ratio
783
784        let mut buffer = [0u8; 50]; // Read in small chunks
785        let mut total_read = 0;
786        let mut error_occurred = false;
787
788        loop {
789            match protector.read(&mut buffer) {
790                Ok(0) => break,
791                Ok(n) => {
792                    total_read += n;
793                    // Check should trigger every check_interval bytes
794                    if total_read > 500 {
795                        // Should have failed by now
796                        break;
797                    }
798                }
799                Err(_) => {
800                    error_occurred = true;
801                    break;
802                }
803            }
804        }
805
806        assert!(error_occurred, "Should detect bomb during periodic checks");
807    }
808
809    #[test]
810    fn test_ratio_calculation_with_large_numbers() {
811        let detector = CompressionBombDetector::new(CompressionBombConfig {
812            max_ratio: 100.0,
813            ..Default::default()
814        });
815
816        // Large numbers that are still within ratio
817        let compressed = 1_000_000;
818        let decompressed = 50_000_000; // 50x ratio
819
820        assert!(detector.validate_result(compressed, decompressed).is_ok());
821
822        // Exceeds ratio (150x)
823        let decompressed_bad = 150_000_000;
824        assert!(
825            detector
826                .validate_result(compressed, decompressed_bad)
827                .is_err()
828        );
829    }
830
831    #[test]
832    fn test_protected_reader_multiple_small_reads() {
833        // Test that protection works across many small read operations
834        let data = vec![1u8; 5000];
835        let cursor = Cursor::new(data);
836
837        let config = CompressionBombConfig {
838            max_decompressed_size: 10_000,
839            check_interval_bytes: 1000,
840            ..Default::default()
841        };
842
843        let mut protector = CompressionBombProtector::new(cursor, config, 5000);
844
845        // Read in very small increments
846        let mut buffer = [0u8; 10];
847        let mut total = 0;
848
849        while let Ok(n) = protector.read(&mut buffer) {
850            if n == 0 {
851                break;
852            }
853            total += n;
854        }
855
856        assert_eq!(total, 5000);
857        let stats = protector.stats();
858        assert_eq!(stats.decompressed_size, 5000);
859    }
860
861    #[test]
862    fn test_error_on_exact_check_interval_boundary() {
863        let check_interval = 1000;
864        let config = CompressionBombConfig {
865            max_decompressed_size: 1500,
866            check_interval_bytes: check_interval,
867            ..Default::default()
868        };
869
870        // Data that exceeds limit at exactly the check interval
871        let data = vec![0u8; 2000];
872        let cursor = Cursor::new(data);
873
874        let mut protector = CompressionBombProtector::new(cursor, config, 100);
875
876        let mut buffer = [0u8; 1000]; // Read exactly check_interval bytes
877        let mut detected = false;
878
879        loop {
880            match protector.read(&mut buffer) {
881                Ok(0) => break,
882                Ok(_) => {}
883                Err(_) => {
884                    detected = true;
885                    break;
886                }
887            }
888        }
889
890        assert!(detected);
891    }
892
893    #[test]
894    fn test_config_serialization_roundtrip() {
895        let config = CompressionBombConfig {
896            max_ratio: 123.45,
897            max_decompressed_size: 999_888,
898            max_compressed_size: 512_000,
899            max_compression_depth: 7,
900            check_interval_bytes: 16_384,
901        };
902
903        // Serialize to JSON
904        let json = serde_json::to_string(&config).unwrap();
905
906        // Deserialize back
907        let deserialized: CompressionBombConfig = serde_json::from_str(&json).unwrap();
908
909        assert_eq!(config.max_ratio, deserialized.max_ratio);
910        assert_eq!(
911            config.max_decompressed_size,
912            deserialized.max_decompressed_size
913        );
914        assert_eq!(
915            config.max_compression_depth,
916            deserialized.max_compression_depth
917        );
918        assert_eq!(
919            config.check_interval_bytes,
920            deserialized.check_interval_bytes
921        );
922    }
923
924    #[test]
925    fn test_all_preset_configs() {
926        // Ensure all preset configurations are valid and ordered correctly
927        let default_cfg = CompressionBombConfig::default();
928        let high_sec = CompressionBombConfig::high_security();
929        let low_mem = CompressionBombConfig::low_memory();
930        let high_throughput = CompressionBombConfig::high_throughput();
931
932        // High security should be strictest
933        assert!(high_sec.max_ratio < default_cfg.max_ratio);
934        assert!(high_sec.max_decompressed_size < default_cfg.max_decompressed_size);
935
936        // Low memory should limit size
937        assert!(low_mem.max_decompressed_size < default_cfg.max_decompressed_size);
938
939        // High throughput should be most permissive
940        assert!(high_throughput.max_ratio > default_cfg.max_ratio);
941        assert!(high_throughput.max_decompressed_size > default_cfg.max_decompressed_size);
942    }
943
944    #[test]
945    fn test_protect_reader_basic_usage() {
946        let detector = CompressionBombDetector::default();
947        let data = b"test data for protect_reader";
948        let cursor = Cursor::new(data.as_slice());
949
950        let mut protector = detector.protect_reader(cursor, data.len());
951
952        let mut buffer = Vec::new();
953        let bytes_read = protector.read_to_end(&mut buffer).unwrap();
954
955        assert_eq!(bytes_read, data.len());
956        assert_eq!(buffer.as_slice(), data);
957
958        let stats = protector.stats();
959        assert_eq!(stats.compressed_size, data.len());
960        assert_eq!(stats.decompressed_size, data.len());
961    }
962
963    #[test]
964    fn test_protect_reader_with_size_limit() {
965        let config = CompressionBombConfig {
966            max_decompressed_size: 500,
967            check_interval_bytes: 100,
968            ..Default::default()
969        };
970        let detector = CompressionBombDetector::new(config);
971
972        let data = vec![0u8; 1000];
973        let cursor = Cursor::new(data);
974
975        let mut protector = detector.protect_reader(cursor, 50);
976
977        let mut buffer = [0u8; 200];
978        let mut error_occurred = false;
979
980        loop {
981            match protector.read(&mut buffer) {
982                Ok(0) => break,
983                Ok(_) => {}
984                Err(_) => {
985                    error_occurred = true;
986                    break;
987                }
988            }
989        }
990
991        assert!(error_occurred, "protect_reader should detect size limit");
992    }
993
994    struct FailingReader {
995        fail_after: usize,
996        bytes_read: usize,
997    }
998
999    impl FailingReader {
1000        fn new(fail_after: usize) -> Self {
1001            Self {
1002                fail_after,
1003                bytes_read: 0,
1004            }
1005        }
1006    }
1007
1008    impl Read for FailingReader {
1009        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1010            if self.bytes_read >= self.fail_after {
1011                return Err(std::io::Error::new(
1012                    std::io::ErrorKind::BrokenPipe,
1013                    "simulated read failure",
1014                ));
1015            }
1016            let to_read = std::cmp::min(buf.len(), self.fail_after - self.bytes_read);
1017            for b in buf.iter_mut().take(to_read) {
1018                *b = 0;
1019            }
1020            self.bytes_read += to_read;
1021            Ok(to_read)
1022        }
1023    }
1024
1025    #[test]
1026    fn test_inner_reader_error_propagation() {
1027        let failing_reader = FailingReader::new(50);
1028        let config = CompressionBombConfig::default();
1029        let mut protector = CompressionBombProtector::new(failing_reader, config, 100);
1030
1031        let mut buffer = [0u8; 100];
1032
1033        let result1 = protector.read(&mut buffer);
1034        assert!(result1.is_ok());
1035        assert_eq!(result1.unwrap(), 50);
1036
1037        let result2 = protector.read(&mut buffer);
1038        assert!(result2.is_err());
1039        let err = result2.unwrap_err();
1040        assert_eq!(err.kind(), std::io::ErrorKind::BrokenPipe);
1041        assert!(err.to_string().contains("simulated read failure"));
1042    }
1043
1044    #[test]
1045    fn test_check_limits_with_zero_compressed_size_and_data_read() {
1046        let config = CompressionBombConfig {
1047            max_decompressed_size: 1000,
1048            check_interval_bytes: 50,
1049            ..Default::default()
1050        };
1051
1052        let data = vec![0u8; 100];
1053        let cursor = Cursor::new(data);
1054
1055        let mut protector = CompressionBombProtector::new(cursor, config, 0);
1056
1057        let mut buffer = [0u8; 60];
1058
1059        let result = protector.read(&mut buffer);
1060        assert!(result.is_ok());
1061        assert_eq!(result.unwrap(), 60);
1062
1063        let stats = protector.stats();
1064        assert_eq!(stats.compressed_size, 0);
1065        assert_eq!(stats.decompressed_size, 60);
1066        assert_eq!(stats.ratio, 0.0);
1067    }
1068
1069    #[test]
1070    fn test_check_limits_ratio_ok_branch() {
1071        let config = CompressionBombConfig {
1072            max_ratio: 100.0,
1073            max_decompressed_size: 10_000,
1074            check_interval_bytes: 50,
1075            ..Default::default()
1076        };
1077
1078        let data = vec![0u8; 100];
1079        let cursor = Cursor::new(data);
1080
1081        let mut protector = CompressionBombProtector::new(cursor, config, 50);
1082
1083        let mut buffer = [0u8; 60];
1084
1085        let result = protector.read(&mut buffer);
1086        assert!(result.is_ok());
1087        assert_eq!(result.unwrap(), 60);
1088
1089        let stats = protector.stats();
1090        assert_eq!(stats.decompressed_size, 60);
1091        assert!((stats.ratio - 1.2).abs() < 0.01);
1092    }
1093}