use crate::{Error, Result};
use std::io::Read;
use thiserror::Error;
#[derive(Error, Debug, Clone)]
pub enum CompressionBombError {
#[error("Compression ratio exceeded: {ratio:.2}x > {max_ratio:.2}x")]
RatioExceeded { ratio: f64, max_ratio: f64 },
#[error("Decompressed size exceeded: {size} bytes > {max_size} bytes")]
SizeExceeded { size: usize, max_size: usize },
#[error("Compression depth exceeded: {depth} > {max_depth}")]
DepthExceeded { depth: usize, max_depth: usize },
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CompressionBombConfig {
pub max_ratio: f64,
pub max_decompressed_size: usize,
pub max_compressed_size: usize,
pub max_compression_depth: usize,
pub check_interval_bytes: usize,
}
impl Default for CompressionBombConfig {
fn default() -> Self {
Self {
max_ratio: 300.0,
max_decompressed_size: 100 * 1024 * 1024, max_compressed_size: 100 * 1024 * 1024, max_compression_depth: 3,
check_interval_bytes: 64 * 1024, }
}
}
impl CompressionBombConfig {
pub fn high_security() -> Self {
Self {
max_ratio: 20.0,
max_decompressed_size: 10 * 1024 * 1024, max_compressed_size: 10 * 1024 * 1024, max_compression_depth: 2,
check_interval_bytes: 32 * 1024, }
}
pub fn low_memory() -> Self {
Self {
max_ratio: 50.0,
max_decompressed_size: 5 * 1024 * 1024, max_compressed_size: 5 * 1024 * 1024, max_compression_depth: 2,
check_interval_bytes: 16 * 1024, }
}
pub fn high_throughput() -> Self {
Self {
max_ratio: 1000.0,
max_decompressed_size: 500 * 1024 * 1024, max_compressed_size: 500 * 1024 * 1024, max_compression_depth: 5,
check_interval_bytes: 128 * 1024, }
}
}
#[derive(Debug)]
pub struct CompressionBombProtector<R: Read> {
inner: R,
config: CompressionBombConfig,
compressed_size: usize,
decompressed_size: usize,
bytes_since_check: usize,
compression_depth: usize,
}
impl<R: Read> CompressionBombProtector<R> {
pub fn new(inner: R, config: CompressionBombConfig, compressed_size: usize) -> Self {
Self {
inner,
config,
compressed_size,
decompressed_size: 0,
bytes_since_check: 0,
compression_depth: 0,
}
}
pub fn with_depth(
inner: R,
config: CompressionBombConfig,
compressed_size: usize,
depth: usize,
) -> Result<Self> {
if depth > config.max_compression_depth {
return Err(Error::SecurityError(
CompressionBombError::DepthExceeded {
depth,
max_depth: config.max_compression_depth,
}
.to_string(),
));
}
Ok(Self {
inner,
config,
compressed_size,
decompressed_size: 0,
bytes_since_check: 0,
compression_depth: depth,
})
}
fn check_limits(&self) -> Result<()> {
if self.decompressed_size > self.config.max_decompressed_size {
return Err(Error::SecurityError(
CompressionBombError::SizeExceeded {
size: self.decompressed_size,
max_size: self.config.max_decompressed_size,
}
.to_string(),
));
}
if self.compressed_size > 0 && self.decompressed_size > 0 {
let ratio = self.decompressed_size as f64 / self.compressed_size as f64;
if ratio > self.config.max_ratio {
return Err(Error::SecurityError(
CompressionBombError::RatioExceeded {
ratio,
max_ratio: self.config.max_ratio,
}
.to_string(),
));
}
}
Ok(())
}
pub fn stats(&self) -> CompressionStats {
let ratio = if self.compressed_size > 0 {
self.decompressed_size as f64 / self.compressed_size as f64
} else {
0.0
};
CompressionStats {
compressed_size: self.compressed_size,
decompressed_size: self.decompressed_size,
ratio,
compression_depth: self.compression_depth,
}
}
pub fn into_inner(self) -> R {
self.inner
}
}
impl<R: Read> Read for CompressionBombProtector<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let bytes_read = self.inner.read(buf)?;
self.decompressed_size += bytes_read;
self.bytes_since_check += bytes_read;
if self.bytes_since_check >= self.config.check_interval_bytes {
if let Err(e) = self.check_limits() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
e.to_string(),
));
}
self.bytes_since_check = 0;
}
Ok(bytes_read)
}
}
#[derive(Debug, Clone)]
pub struct CompressionStats {
pub compressed_size: usize,
pub decompressed_size: usize,
pub ratio: f64,
pub compression_depth: usize,
}
pub struct CompressionBombDetector {
config: CompressionBombConfig,
}
impl Default for CompressionBombDetector {
fn default() -> Self {
Self::new(CompressionBombConfig::default())
}
}
impl CompressionBombDetector {
pub fn new(config: CompressionBombConfig) -> Self {
Self { config }
}
pub fn validate_pre_decompression(&self, compressed_size: usize) -> Result<()> {
if compressed_size > self.config.max_compressed_size {
return Err(Error::SecurityError(format!(
"Compressed data size {} exceeds maximum allowed compressed size {}",
compressed_size, self.config.max_compressed_size
)));
}
Ok(())
}
pub fn protect_reader<R: Read>(
&self,
reader: R,
compressed_size: usize,
) -> CompressionBombProtector<R> {
CompressionBombProtector::new(reader, self.config.clone(), compressed_size)
}
pub fn protect_nested_reader<R: Read>(
&self,
reader: R,
compressed_size: usize,
depth: usize,
) -> Result<CompressionBombProtector<R>> {
CompressionBombProtector::with_depth(reader, self.config.clone(), compressed_size, depth)
}
pub fn validate_result(&self, compressed_size: usize, decompressed_size: usize) -> Result<()> {
if decompressed_size > self.config.max_decompressed_size {
return Err(Error::SecurityError(
CompressionBombError::SizeExceeded {
size: decompressed_size,
max_size: self.config.max_decompressed_size,
}
.to_string(),
));
}
if compressed_size > 0 {
let ratio = decompressed_size as f64 / compressed_size as f64;
if ratio > self.config.max_ratio {
return Err(Error::SecurityError(
CompressionBombError::RatioExceeded {
ratio,
max_ratio: self.config.max_ratio,
}
.to_string(),
));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_compression_bomb_config() {
let config = CompressionBombConfig::default();
assert!(config.max_ratio > 0.0);
assert!(config.max_decompressed_size > 0);
let high_sec = CompressionBombConfig::high_security();
assert!(high_sec.max_ratio < config.max_ratio);
let low_mem = CompressionBombConfig::low_memory();
assert!(low_mem.max_decompressed_size < config.max_decompressed_size);
let high_throughput = CompressionBombConfig::high_throughput();
assert!(high_throughput.max_decompressed_size > config.max_decompressed_size);
}
#[test]
fn test_compression_bomb_detector() {
let detector = CompressionBombDetector::default();
assert!(detector.validate_pre_decompression(1024).is_ok());
assert!(detector.validate_result(1024, 10 * 1024).is_ok());
}
#[test]
fn test_size_limit_exceeded() {
let config = CompressionBombConfig {
max_decompressed_size: 1024,
..Default::default()
};
let detector = CompressionBombDetector::new(config);
let result = detector.validate_result(100, 2048);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Size exceeded") || error_msg.contains("Security error"));
}
#[test]
fn test_ratio_limit_exceeded() {
let config = CompressionBombConfig {
max_ratio: 10.0,
..Default::default()
};
let detector = CompressionBombDetector::new(config);
let result = detector.validate_result(100, 2000);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Compression ratio exceeded")
);
}
#[test]
fn test_protected_reader() {
let data = b"Hello, world! This is test data for compression testing.";
let cursor = Cursor::new(data.as_slice());
let config = CompressionBombConfig::default();
let mut protector = CompressionBombProtector::new(cursor, config, data.len());
let mut buffer = Vec::new();
let bytes_read = protector.read_to_end(&mut buffer).unwrap();
assert_eq!(bytes_read, data.len());
assert_eq!(buffer.as_slice(), data);
let stats = protector.stats();
assert_eq!(stats.compressed_size, data.len());
assert_eq!(stats.decompressed_size, data.len());
assert!((stats.ratio - 1.0).abs() < 0.01); }
#[test]
fn test_protected_reader_size_limit() {
let data = vec![0u8; 2048]; let cursor = Cursor::new(data);
let config = CompressionBombConfig {
max_decompressed_size: 1024, check_interval_bytes: 512, ..Default::default()
};
let mut protector = CompressionBombProtector::new(cursor, config, 100);
let mut buffer = vec![0u8; 2048];
let result = protector.read(&mut buffer);
if result.is_ok() {
let result2 = protector.read(&mut buffer[512..]);
assert!(result2.is_err());
} else {
assert!(result.is_err());
}
}
#[test]
fn test_compression_depth_limit() {
let data = b"test data";
let cursor = Cursor::new(data.as_slice());
let config = CompressionBombConfig {
max_compression_depth: 2,
..Default::default()
};
let protector = CompressionBombProtector::with_depth(cursor, config.clone(), data.len(), 2);
assert!(protector.is_ok());
let cursor2 = Cursor::new(data.as_slice());
let result = CompressionBombProtector::with_depth(cursor2, config, data.len(), 3);
assert!(result.is_err());
}
#[test]
fn test_zero_compressed_size_handling() {
let detector = CompressionBombDetector::default();
assert!(detector.validate_result(0, 1024).is_ok());
}
#[test]
fn test_stats_calculation() {
let data = b"test";
let cursor = Cursor::new(data.as_slice());
let protector = CompressionBombProtector::new(cursor, CompressionBombConfig::default(), 2);
let stats = protector.stats();
assert_eq!(stats.compressed_size, 2);
assert_eq!(stats.decompressed_size, 0); assert_eq!(stats.ratio, 0.0);
assert_eq!(stats.compression_depth, 0);
}
#[test]
fn test_stats_with_zero_compressed_size() {
let data = b"test";
let cursor = Cursor::new(data.as_slice());
let protector = CompressionBombProtector::new(cursor, CompressionBombConfig::default(), 0);
let stats = protector.stats();
assert_eq!(stats.compressed_size, 0);
assert_eq!(stats.ratio, 0.0); }
#[test]
fn test_into_inner() {
let data = b"test data";
let cursor = Cursor::new(data.as_slice());
let original_position = cursor.position();
let protector =
CompressionBombProtector::new(cursor, CompressionBombConfig::default(), data.len());
let inner = protector.into_inner();
assert_eq!(inner.position(), original_position);
}
#[test]
fn test_protect_nested_reader_success() {
let detector = CompressionBombDetector::new(CompressionBombConfig {
max_compression_depth: 3,
..Default::default()
});
let data = b"nested compression test";
let cursor = Cursor::new(data.as_slice());
let result = detector.protect_nested_reader(cursor, data.len(), 1);
assert!(result.is_ok());
let protector = result.unwrap();
let stats = protector.stats();
assert_eq!(stats.compression_depth, 1);
}
#[test]
fn test_protect_nested_reader_depth_exceeded() {
let detector = CompressionBombDetector::new(CompressionBombConfig {
max_compression_depth: 2,
..Default::default()
});
let data = b"nested compression test";
let cursor = Cursor::new(data.as_slice());
let result = detector.protect_nested_reader(cursor, data.len(), 3);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("Compression depth exceeded")
|| error_msg.contains("Security error")
);
}
#[test]
fn test_validate_pre_decompression_size_exceeded() {
let config = CompressionBombConfig {
max_compressed_size: 1024,
..Default::default()
};
let detector = CompressionBombDetector::new(config);
let result = detector.validate_pre_decompression(2048);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("exceeds maximum allowed"));
}
#[test]
fn test_validate_pre_decompression_success() {
let detector = CompressionBombDetector::default();
let result = detector.validate_pre_decompression(1024);
assert!(result.is_ok());
}
#[test]
fn test_protected_reader_stats_after_read() {
let data = b"Hello, world!";
let cursor = Cursor::new(data.as_slice());
let compressed_size = 5; let mut protector = CompressionBombProtector::new(
cursor,
CompressionBombConfig::default(),
compressed_size,
);
let mut buffer = Vec::new();
protector.read_to_end(&mut buffer).unwrap();
let stats = protector.stats();
assert_eq!(stats.compressed_size, compressed_size);
assert_eq!(stats.decompressed_size, data.len());
let expected_ratio = data.len() as f64 / compressed_size as f64;
assert!((stats.ratio - expected_ratio).abs() < 0.01);
}
#[test]
fn test_compression_bomb_error_display() {
let ratio_err = CompressionBombError::RatioExceeded {
ratio: 150.5,
max_ratio: 100.0,
};
assert!(ratio_err.to_string().contains("150.5"));
assert!(ratio_err.to_string().contains("100.0"));
let size_err = CompressionBombError::SizeExceeded {
size: 2048,
max_size: 1024,
};
assert!(size_err.to_string().contains("2048"));
assert!(size_err.to_string().contains("1024"));
let depth_err = CompressionBombError::DepthExceeded {
depth: 5,
max_depth: 3,
};
assert!(depth_err.to_string().contains("5"));
assert!(depth_err.to_string().contains("3"));
}
#[test]
fn test_detector_default() {
let detector1 = CompressionBombDetector::default();
let detector2 = CompressionBombDetector::new(CompressionBombConfig::default());
assert_eq!(detector1.config.max_ratio, detector2.config.max_ratio);
assert_eq!(
detector1.config.max_decompressed_size,
detector2.config.max_decompressed_size
);
}
#[test]
fn test_slow_drip_decompression_bomb() {
let config = CompressionBombConfig {
max_decompressed_size: 10_000,
check_interval_bytes: 1000, ..Default::default()
};
let data = vec![0u8; 15_000];
let cursor = Cursor::new(data);
let mut protector = CompressionBombProtector::new(cursor, config, 100);
let mut buffer = [0u8; 1024];
let mut total_read = 0;
let mut detected = false;
loop {
match protector.read(&mut buffer) {
Ok(0) => break, Ok(n) => {
total_read += n;
}
Err(e) => {
let err_str = e.to_string();
assert!(
err_str.contains("Size exceeded") || err_str.contains("Security"),
"Expected size limit error, got: {}",
err_str
);
detected = true;
break;
}
}
}
assert!(detected, "Slow-drip bomb should be detected");
assert!(total_read < 15_000, "Should not read all data");
}
#[test]
fn test_integer_overflow_protection_in_ratio() {
let detector = CompressionBombDetector::default();
let result = detector.validate_result(1, usize::MAX);
assert!(result.is_err());
}
#[test]
fn test_integer_overflow_protection_in_size() {
let config = CompressionBombConfig {
max_decompressed_size: usize::MAX - 1,
..Default::default()
};
let detector = CompressionBombDetector::new(config);
let result = detector.validate_result(100, usize::MAX);
assert!(result.is_err());
}
#[test]
fn test_boundary_max_decompressed_size() {
let max_size = 10_000;
let config = CompressionBombConfig {
max_decompressed_size: max_size,
..Default::default()
};
let detector = CompressionBombDetector::new(config);
assert!(detector.validate_result(100, max_size).is_ok());
assert!(detector.validate_result(100, max_size + 1).is_err());
}
#[test]
fn test_boundary_max_ratio() {
let max_ratio = 50.0;
let config = CompressionBombConfig {
max_ratio,
..Default::default()
};
let detector = CompressionBombDetector::new(config);
let compressed = 100;
let at_limit = (compressed as f64 * max_ratio) as usize;
assert!(detector.validate_result(compressed, at_limit).is_ok());
assert!(
detector
.validate_result(compressed, at_limit + 100)
.is_err()
);
}
#[test]
fn test_boundary_max_compression_depth() {
let max_depth = 5;
let config = CompressionBombConfig {
max_compression_depth: max_depth,
..Default::default()
};
let data = b"test";
let cursor = Cursor::new(data.as_slice());
let result =
CompressionBombProtector::with_depth(cursor, config.clone(), data.len(), max_depth);
assert!(result.is_ok());
let cursor2 = Cursor::new(data.as_slice());
let result2 =
CompressionBombProtector::with_depth(cursor2, config, data.len(), max_depth + 1);
assert!(result2.is_err());
}
#[test]
fn test_nested_compression_attack_simulation() {
let detector = CompressionBombDetector::new(CompressionBombConfig {
max_compression_depth: 2,
max_decompressed_size: 10_000,
..Default::default()
});
let layer1_data = vec![0u8; 1000]; let cursor1 = Cursor::new(layer1_data.clone());
let protector1 = detector.protect_nested_reader(cursor1, 100, 1);
assert!(protector1.is_ok());
let cursor2 = Cursor::new(layer1_data.clone());
let protector2 = detector.protect_nested_reader(cursor2, 100, 2);
assert!(protector2.is_ok());
let cursor3 = Cursor::new(layer1_data);
let protector3 = detector.protect_nested_reader(cursor3, 100, 3);
assert!(protector3.is_err());
}
#[test]
fn test_check_limits_called_at_intervals() {
let check_interval = 100;
let config = CompressionBombConfig {
max_decompressed_size: 500,
check_interval_bytes: check_interval,
max_ratio: 10.0,
..Default::default()
};
let data = vec![0u8; 600];
let cursor = Cursor::new(data);
let mut protector = CompressionBombProtector::new(cursor, config, 10);
let mut buffer = [0u8; 50]; let mut total_read = 0;
let mut error_occurred = false;
loop {
match protector.read(&mut buffer) {
Ok(0) => break,
Ok(n) => {
total_read += n;
if total_read > 500 {
break;
}
}
Err(_) => {
error_occurred = true;
break;
}
}
}
assert!(error_occurred, "Should detect bomb during periodic checks");
}
#[test]
fn test_ratio_calculation_with_large_numbers() {
let detector = CompressionBombDetector::new(CompressionBombConfig {
max_ratio: 100.0,
..Default::default()
});
let compressed = 1_000_000;
let decompressed = 50_000_000;
assert!(detector.validate_result(compressed, decompressed).is_ok());
let decompressed_bad = 150_000_000;
assert!(
detector
.validate_result(compressed, decompressed_bad)
.is_err()
);
}
#[test]
fn test_protected_reader_multiple_small_reads() {
let data = vec![1u8; 5000];
let cursor = Cursor::new(data);
let config = CompressionBombConfig {
max_decompressed_size: 10_000,
check_interval_bytes: 1000,
..Default::default()
};
let mut protector = CompressionBombProtector::new(cursor, config, 5000);
let mut buffer = [0u8; 10];
let mut total = 0;
while let Ok(n) = protector.read(&mut buffer) {
if n == 0 {
break;
}
total += n;
}
assert_eq!(total, 5000);
let stats = protector.stats();
assert_eq!(stats.decompressed_size, 5000);
}
#[test]
fn test_error_on_exact_check_interval_boundary() {
let check_interval = 1000;
let config = CompressionBombConfig {
max_decompressed_size: 1500,
check_interval_bytes: check_interval,
..Default::default()
};
let data = vec![0u8; 2000];
let cursor = Cursor::new(data);
let mut protector = CompressionBombProtector::new(cursor, config, 100);
let mut buffer = [0u8; 1000]; let mut detected = false;
loop {
match protector.read(&mut buffer) {
Ok(0) => break,
Ok(_) => {}
Err(_) => {
detected = true;
break;
}
}
}
assert!(detected);
}
#[test]
fn test_config_serialization_roundtrip() {
let config = CompressionBombConfig {
max_ratio: 123.45,
max_decompressed_size: 999_888,
max_compressed_size: 512_000,
max_compression_depth: 7,
check_interval_bytes: 16_384,
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: CompressionBombConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config.max_ratio, deserialized.max_ratio);
assert_eq!(
config.max_decompressed_size,
deserialized.max_decompressed_size
);
assert_eq!(
config.max_compression_depth,
deserialized.max_compression_depth
);
assert_eq!(
config.check_interval_bytes,
deserialized.check_interval_bytes
);
}
#[test]
fn test_all_preset_configs() {
let default_cfg = CompressionBombConfig::default();
let high_sec = CompressionBombConfig::high_security();
let low_mem = CompressionBombConfig::low_memory();
let high_throughput = CompressionBombConfig::high_throughput();
assert!(high_sec.max_ratio < default_cfg.max_ratio);
assert!(high_sec.max_decompressed_size < default_cfg.max_decompressed_size);
assert!(low_mem.max_decompressed_size < default_cfg.max_decompressed_size);
assert!(high_throughput.max_ratio > default_cfg.max_ratio);
assert!(high_throughput.max_decompressed_size > default_cfg.max_decompressed_size);
}
#[test]
fn test_protect_reader_basic_usage() {
let detector = CompressionBombDetector::default();
let data = b"test data for protect_reader";
let cursor = Cursor::new(data.as_slice());
let mut protector = detector.protect_reader(cursor, data.len());
let mut buffer = Vec::new();
let bytes_read = protector.read_to_end(&mut buffer).unwrap();
assert_eq!(bytes_read, data.len());
assert_eq!(buffer.as_slice(), data);
let stats = protector.stats();
assert_eq!(stats.compressed_size, data.len());
assert_eq!(stats.decompressed_size, data.len());
}
#[test]
fn test_protect_reader_with_size_limit() {
let config = CompressionBombConfig {
max_decompressed_size: 500,
check_interval_bytes: 100,
..Default::default()
};
let detector = CompressionBombDetector::new(config);
let data = vec![0u8; 1000];
let cursor = Cursor::new(data);
let mut protector = detector.protect_reader(cursor, 50);
let mut buffer = [0u8; 200];
let mut error_occurred = false;
loop {
match protector.read(&mut buffer) {
Ok(0) => break,
Ok(_) => {}
Err(_) => {
error_occurred = true;
break;
}
}
}
assert!(error_occurred, "protect_reader should detect size limit");
}
struct FailingReader {
fail_after: usize,
bytes_read: usize,
}
impl FailingReader {
fn new(fail_after: usize) -> Self {
Self {
fail_after,
bytes_read: 0,
}
}
}
impl Read for FailingReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.bytes_read >= self.fail_after {
return Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"simulated read failure",
));
}
let to_read = std::cmp::min(buf.len(), self.fail_after - self.bytes_read);
for b in buf.iter_mut().take(to_read) {
*b = 0;
}
self.bytes_read += to_read;
Ok(to_read)
}
}
#[test]
fn test_inner_reader_error_propagation() {
let failing_reader = FailingReader::new(50);
let config = CompressionBombConfig::default();
let mut protector = CompressionBombProtector::new(failing_reader, config, 100);
let mut buffer = [0u8; 100];
let result1 = protector.read(&mut buffer);
assert!(result1.is_ok());
assert_eq!(result1.unwrap(), 50);
let result2 = protector.read(&mut buffer);
assert!(result2.is_err());
let err = result2.unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::BrokenPipe);
assert!(err.to_string().contains("simulated read failure"));
}
#[test]
fn test_check_limits_with_zero_compressed_size_and_data_read() {
let config = CompressionBombConfig {
max_decompressed_size: 1000,
check_interval_bytes: 50,
..Default::default()
};
let data = vec![0u8; 100];
let cursor = Cursor::new(data);
let mut protector = CompressionBombProtector::new(cursor, config, 0);
let mut buffer = [0u8; 60];
let result = protector.read(&mut buffer);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 60);
let stats = protector.stats();
assert_eq!(stats.compressed_size, 0);
assert_eq!(stats.decompressed_size, 60);
assert_eq!(stats.ratio, 0.0);
}
#[test]
fn test_check_limits_ratio_ok_branch() {
let config = CompressionBombConfig {
max_ratio: 100.0,
max_decompressed_size: 10_000,
check_interval_bytes: 50,
..Default::default()
};
let data = vec![0u8; 100];
let cursor = Cursor::new(data);
let mut protector = CompressionBombProtector::new(cursor, config, 50);
let mut buffer = [0u8; 60];
let result = protector.read(&mut buffer);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 60);
let stats = protector.stats();
assert_eq!(stats.decompressed_size, 60);
assert!((stats.ratio - 1.2).abs() < 0.01);
}
}