use crate::{
Error, Result,
security::{CompressionBombDetector, CompressionStats},
};
#[cfg(feature = "compression")]
use std::io::Write;
use std::io::{Cursor, Read};
use tracing::{debug, info, warn};
#[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
use zstd;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ByteCodec {
#[default]
None,
Deflate,
Gzip,
Brotli,
#[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
ZstdDict(std::sync::Arc<crate::compression::zstd::ZstdDictionary>),
}
#[derive(Debug, Clone, Copy, Default)]
pub enum CompressionQuality {
Fast,
#[default]
Balanced,
Best,
}
impl CompressionQuality {
#[cfg(feature = "compression")]
fn flate2_level(self) -> flate2::Compression {
match self {
Self::Fast => flate2::Compression::fast(),
Self::Balanced => flate2::Compression::default(),
Self::Best => flate2::Compression::best(),
}
}
#[cfg(feature = "compression")]
fn brotli_quality(self) -> i32 {
match self {
Self::Fast => 1,
Self::Balanced => 5,
Self::Best => 11,
}
}
}
#[derive(Debug, Clone)]
pub struct SecureCompressedData {
pub data: Vec<u8>,
pub original_size: usize,
pub compression_ratio: f64,
pub codec: ByteCodec,
}
pub struct SecureCompressor {
detector: CompressionBombDetector,
codec: ByteCodec,
#[cfg_attr(not(feature = "compression"), allow(dead_code))]
quality: CompressionQuality,
}
impl SecureCompressor {
pub fn new(detector: CompressionBombDetector, codec: ByteCodec) -> Self {
Self {
detector,
codec,
quality: CompressionQuality::default(),
}
}
pub fn with_default_security(codec: ByteCodec) -> Self {
Self::new(CompressionBombDetector::default(), codec)
}
pub fn with_quality(
detector: CompressionBombDetector,
codec: ByteCodec,
quality: CompressionQuality,
) -> Self {
Self {
detector,
codec,
quality,
}
}
pub fn compress(&self, data: &[u8]) -> Result<SecureCompressedData> {
self.detector.validate_pre_decompression(data.len())?;
let compressed_bytes = self.encode(data)?;
let compression_ratio = data.len() as f64 / compressed_bytes.len().max(1) as f64;
info!("Compression completed: {:.2}x ratio", compression_ratio);
Ok(SecureCompressedData {
original_size: data.len(),
compression_ratio,
codec: self.codec.clone(),
data: compressed_bytes,
})
}
pub fn decompress_protected(&self, compressed: &SecureCompressedData) -> Result<Vec<u8>> {
self.detector
.validate_pre_decompression(compressed.data.len())?;
self.decode_with_protection(&compressed.data, compressed.codec.clone(), None)
}
pub fn decompress_nested(
&self,
compressed: &SecureCompressedData,
depth: usize,
) -> Result<Vec<u8>> {
self.detector
.validate_pre_decompression(compressed.data.len())?;
self.decode_with_protection(&compressed.data, compressed.codec.clone(), Some(depth))
}
fn encode(&self, data: &[u8]) -> Result<Vec<u8>> {
match &self.codec {
ByteCodec::None => {
debug!("No compression applied");
Ok(data.to_vec())
}
#[cfg(feature = "compression")]
ByteCodec::Deflate => {
use flate2::write::DeflateEncoder;
let mut enc = DeflateEncoder::new(Vec::new(), self.quality.flate2_level());
enc.write_all(data)
.map_err(|e| Error::CompressionError(format!("deflate encode: {e}")))?;
enc.finish()
.map_err(|e| Error::CompressionError(format!("deflate finish: {e}")))
}
#[cfg(feature = "compression")]
ByteCodec::Gzip => {
use flate2::write::GzEncoder;
let mut enc = GzEncoder::new(Vec::new(), self.quality.flate2_level());
enc.write_all(data)
.map_err(|e| Error::CompressionError(format!("gzip encode: {e}")))?;
enc.finish()
.map_err(|e| Error::CompressionError(format!("gzip finish: {e}")))
}
#[cfg(feature = "compression")]
ByteCodec::Brotli => {
let params = brotli::enc::BrotliEncoderParams {
quality: self.quality.brotli_quality(),
..Default::default()
};
let mut out = Vec::new();
brotli::BrotliCompress(&mut Cursor::new(data), &mut out, ¶ms)
.map_err(|e| Error::CompressionError(format!("brotli encode: {e}")))?;
Ok(out)
}
#[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
ByteCodec::ZstdDict(dict) => {
crate::compression::zstd::ZstdDictCompressor::compress(data, dict.as_ref())
}
#[cfg(not(feature = "compression"))]
ByteCodec::Deflate | ByteCodec::Gzip | ByteCodec::Brotli => Err(
Error::CompressionError("feature `compression` is not enabled".into()),
),
}
}
fn decode_with_protection(
&self,
data: &[u8],
codec: ByteCodec,
depth: Option<usize>,
) -> Result<Vec<u8>> {
macro_rules! run {
($decoder:expr) => {{
let compressed_size = data.len();
let mut out = Vec::new();
let result = if let Some(d) = depth {
let mut protector =
self.detector
.protect_nested_reader($decoder, compressed_size, d)?;
let r = protector.read_to_end(&mut out);
let stats = protector.stats();
self.log_decompression_stats(&stats);
if stats.compression_depth > 0 {
warn!(
"Nested decompression detected at depth {}",
stats.compression_depth
);
}
r
} else {
let mut protector = self.detector.protect_reader($decoder, compressed_size);
let r = protector.read_to_end(&mut out);
let stats = protector.stats();
self.log_decompression_stats(&stats);
r
};
match result {
Ok(_) => {
self.detector.validate_result(compressed_size, out.len())?;
Ok(out)
}
Err(e) => {
warn!("Decompression failed: {}", e);
Err(Error::SecurityError(format!(
"Protected decompression failed: {}",
e
)))
}
}
}};
}
match codec {
ByteCodec::None => run!(Cursor::new(data)),
#[cfg(feature = "compression")]
ByteCodec::Deflate => run!(flate2::read::DeflateDecoder::new(Cursor::new(data))),
#[cfg(feature = "compression")]
ByteCodec::Gzip => run!(flate2::read::GzDecoder::new(Cursor::new(data))),
#[cfg(feature = "compression")]
ByteCodec::Brotli => run!(brotli::Decompressor::new(Cursor::new(data), 4096)),
#[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
ByteCodec::ZstdDict(dict) => {
let decoder = zstd::stream::read::Decoder::with_dictionary(
Cursor::new(data),
dict.as_bytes(),
)
.map_err(|e| Error::CompressionError(format!("zstd decoder init: {e}")))?;
run!(decoder)
}
#[cfg(not(feature = "compression"))]
ByteCodec::Deflate | ByteCodec::Gzip | ByteCodec::Brotli => Err(
Error::CompressionError("feature `compression` is not enabled".into()),
),
}
}
fn log_decompression_stats(&self, stats: &CompressionStats) {
info!(
"Decompression stats: {}B -> {}B (ratio: {:.2}x, depth: {})",
stats.compressed_size, stats.decompressed_size, stats.ratio, stats.compression_depth
);
}
}
pub struct SecureDecompressionContext {
detector: CompressionBombDetector,
current_depth: usize,
max_concurrent_streams: usize,
active_streams: usize,
}
impl SecureDecompressionContext {
pub fn new(detector: CompressionBombDetector, max_concurrent_streams: usize) -> Self {
Self {
detector,
current_depth: 0,
max_concurrent_streams,
active_streams: 0,
}
}
pub fn start_stream(
&mut self,
compressed_size: usize,
) -> Result<crate::security::CompressionBombProtector<Cursor<Vec<u8>>>> {
if self.active_streams >= self.max_concurrent_streams {
return Err(Error::SecurityError(format!(
"Too many concurrent decompression streams: {}/{}",
self.active_streams, self.max_concurrent_streams
)));
}
let cursor = Cursor::new(Vec::new());
let protector =
self.detector
.protect_nested_reader(cursor, compressed_size, self.current_depth)?;
self.active_streams += 1;
info!(
"Started secure decompression stream (active: {})",
self.active_streams
);
Ok(protector)
}
pub fn finish_stream(&mut self) {
if self.active_streams > 0 {
self.active_streams -= 1;
info!(
"Finished secure decompression stream (active: {})",
self.active_streams
);
}
}
pub fn stats(&self) -> DecompressionContextStats {
DecompressionContextStats {
current_depth: self.current_depth,
active_streams: self.active_streams,
max_concurrent_streams: self.max_concurrent_streams,
}
}
}
#[derive(Debug, Clone)]
pub struct DecompressionContextStats {
pub current_depth: usize,
pub active_streams: usize,
pub max_concurrent_streams: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::security::CompressionBombConfig;
#[test]
fn test_secure_compressor_creation() {
let detector = CompressionBombDetector::default();
let compressor = SecureCompressor::new(detector, ByteCodec::None);
assert!(!std::ptr::addr_of!(compressor).cast::<u8>().is_null());
}
#[test]
fn test_secure_compression_none() {
let compressor = SecureCompressor::with_default_security(ByteCodec::None);
let data = b"Hello, world! This is test data for compression.";
let result = compressor.compress(data);
assert!(result.is_ok());
let compressed = result.unwrap();
assert_eq!(compressed.original_size, data.len());
assert_eq!(compressed.codec, ByteCodec::None);
}
#[test]
fn test_none_roundtrip() {
let compressor = SecureCompressor::with_default_security(ByteCodec::None);
let data = b"round-trip test";
let compressed = compressor.compress(data).unwrap();
let decompressed = compressor.decompress_protected(&compressed).unwrap();
assert_eq!(decompressed, data);
}
#[test]
fn test_compression_size_limit() {
let config = CompressionBombConfig {
max_compressed_size: 100, ..Default::default()
};
let detector = CompressionBombDetector::new(config);
let compressor = SecureCompressor::new(detector, ByteCodec::None);
let large_data = vec![0u8; 1000]; let result = compressor.compress(&large_data);
assert!(result.is_err());
}
#[test]
fn test_different_codecs_none() {
let compressor = SecureCompressor::with_default_security(ByteCodec::None);
let data = b"test data";
let result = compressor.compress(data);
assert!(result.is_ok());
let compressed = result.unwrap();
assert_eq!(compressed.compression_ratio, 1.0);
assert_eq!(compressed.codec, ByteCodec::None);
}
#[cfg(feature = "compression")]
mod compression_tests {
use super::*;
fn repetitive_json() -> Vec<u8> {
let item = br#"{"id":1,"name":"test","value":42,"active":true}"#;
item.repeat(100)
}
#[test]
fn test_deflate_roundtrip() {
let compressor = SecureCompressor::with_default_security(ByteCodec::Deflate);
let data = repetitive_json();
let compressed = compressor.compress(&data).unwrap();
assert_eq!(compressed.codec, ByteCodec::Deflate);
assert!(
compressed.data.len() < data.len(),
"deflate must reduce size"
);
let decompressed = compressor.decompress_protected(&compressed).unwrap();
assert_eq!(decompressed, data);
}
#[test]
fn test_gzip_roundtrip() {
let compressor = SecureCompressor::with_default_security(ByteCodec::Gzip);
let data = repetitive_json();
let compressed = compressor.compress(&data).unwrap();
assert_eq!(compressed.codec, ByteCodec::Gzip);
assert!(compressed.data.len() < data.len(), "gzip must reduce size");
let decompressed = compressor.decompress_protected(&compressed).unwrap();
assert_eq!(decompressed, data);
}
#[test]
fn test_brotli_roundtrip() {
let compressor = SecureCompressor::with_default_security(ByteCodec::Brotli);
let data = repetitive_json();
let compressed = compressor.compress(&data).unwrap();
assert_eq!(compressed.codec, ByteCodec::Brotli);
assert!(
compressed.data.len() < data.len(),
"brotli must reduce size"
);
let decompressed = compressor.decompress_protected(&compressed).unwrap();
assert_eq!(decompressed, data);
}
#[test]
fn test_all_qualities_deflate() {
let data = repetitive_json();
for quality in [
CompressionQuality::Fast,
CompressionQuality::Balanced,
CompressionQuality::Best,
] {
let c = SecureCompressor::with_quality(
CompressionBombDetector::default(),
ByteCodec::Deflate,
quality,
);
let compressed = c.compress(&data).unwrap();
let decompressed = c.decompress_protected(&compressed).unwrap();
assert_eq!(decompressed, data);
}
}
#[test]
fn test_all_qualities_brotli() {
let data = repetitive_json();
let c = SecureCompressor::with_quality(
CompressionBombDetector::default(),
ByteCodec::Brotli,
CompressionQuality::Fast,
);
let compressed = c.compress(&data).unwrap();
let decompressed = c.decompress_protected(&compressed).unwrap();
assert_eq!(decompressed, data);
}
#[test]
fn test_codec_mismatch_returns_error() {
let c = SecureCompressor::with_default_security(ByteCodec::Brotli);
let data = b"codec mismatch test data";
let mut compressed = c.compress(data).unwrap();
compressed.codec = ByteCodec::Gzip;
let result = c.decompress_protected(&compressed);
assert!(
result.is_err(),
"wrong codec must produce an error, not garbage"
);
}
#[test]
fn test_bomb_detection_on_real_codec() {
let config = CompressionBombConfig {
max_decompressed_size: 200, max_compressed_size: 10_000, max_ratio: 300.0,
check_interval_bytes: 64,
..Default::default()
};
let detector = CompressionBombDetector::new(config);
let compressor =
SecureCompressor::new(CompressionBombDetector::default(), ByteCodec::Gzip);
let data = repetitive_json();
let compressed = compressor.compress(&data).unwrap();
let strict_compressor = SecureCompressor::new(detector, ByteCodec::Gzip);
let result = strict_compressor.decompress_protected(&compressed);
assert!(
result.is_err(),
"bomb detector must stop oversized decompression"
);
}
}
#[test]
fn test_secure_decompression_context() {
let detector = CompressionBombDetector::default();
let mut context = SecureDecompressionContext::new(detector, 2);
assert!(context.start_stream(1024).is_ok());
assert!(context.start_stream(1024).is_ok());
assert!(context.start_stream(1024).is_err());
context.finish_stream();
assert!(context.start_stream(1024).is_ok());
}
#[test]
fn test_context_stats() {
let detector = CompressionBombDetector::default();
let context = SecureDecompressionContext::new(detector, 5);
let stats = context.stats();
assert_eq!(stats.current_depth, 0);
assert_eq!(stats.active_streams, 0);
assert_eq!(stats.max_concurrent_streams, 5);
}
#[test]
fn test_context_finish_stream_underflow_safe() {
let detector = CompressionBombDetector::default();
let mut context = SecureDecompressionContext::new(detector, 5);
context.finish_stream();
let stats = context.stats();
assert_eq!(stats.active_streams, 0);
}
#[test]
fn test_byte_codec_default_is_none() {
assert_eq!(ByteCodec::default(), ByteCodec::None);
}
#[test]
fn test_byte_codec_clone() {
let codec = ByteCodec::None;
let cloned = codec.clone();
assert_eq!(codec, cloned);
}
#[test]
fn test_compression_quality_default_is_balanced() {
let c = SecureCompressor::with_default_security(ByteCodec::None);
let data = b"quality default test";
let compressed = c.compress(data).unwrap();
let decompressed = c.decompress_protected(&compressed).unwrap();
assert_eq!(decompressed.as_slice(), data);
}
#[test]
fn test_secure_compressed_data_clone() {
let c = SecureCompressor::with_default_security(ByteCodec::None);
let compressed = c.compress(b"clone test").unwrap();
let cloned = compressed.clone();
assert_eq!(compressed.data, cloned.data);
assert_eq!(compressed.original_size, cloned.original_size);
assert_eq!(compressed.codec, cloned.codec);
}
#[test]
fn test_none_roundtrip_empty_payload() {
let c = SecureCompressor::with_default_security(ByteCodec::None);
let compressed = c.compress(b"").unwrap();
let decompressed = c.decompress_protected(&compressed).unwrap();
assert_eq!(decompressed, b"");
}
#[test]
fn test_decompress_nested_none() {
let c = SecureCompressor::with_default_security(ByteCodec::None);
let data = b"nested roundtrip";
let compressed = c.compress(data).unwrap();
let decompressed = c.decompress_nested(&compressed, 0).unwrap();
assert_eq!(decompressed.as_slice(), data);
}
#[cfg(all(feature = "compression", not(target_arch = "wasm32")))]
mod zstd_dict_tests {
use super::*;
use crate::compression::zstd::{MAX_DICT_SIZE, N_TRAIN, ZstdDictCompressor};
use crate::security::CompressionBombConfig;
use std::sync::Arc;
fn repetitive_json() -> Vec<u8> {
let item = br#"{"id":1,"name":"test","value":42,"active":true}"#;
item.repeat(100)
}
fn trained_dict() -> crate::compression::zstd::ZstdDictionary {
let samples: Vec<Vec<u8>> = (0..N_TRAIN)
.map(|i| {
format!(
r#"{{"id":{i},"name":"item-{i}","value":{},"active":true}}"#,
i * 10
)
.into_bytes()
})
.collect();
ZstdDictCompressor::train(&samples, MAX_DICT_SIZE).unwrap()
}
#[test]
fn test_zstd_dict_roundtrip_via_secure_compressor() {
let dict = Arc::new(trained_dict());
let compressor =
SecureCompressor::with_default_security(ByteCodec::ZstdDict(dict.clone()));
let data = repetitive_json();
let compressed = compressor.compress(&data).unwrap();
assert!(matches!(compressed.codec, ByteCodec::ZstdDict(_)));
assert!(
compressed.data.len() < data.len(),
"zstd dict must reduce size on repetitive data"
);
let decompressed = compressor.decompress_protected(&compressed).unwrap();
assert_eq!(decompressed, data);
}
#[test]
fn test_zstd_dict_bomb_detection() {
let dict = Arc::new(trained_dict());
let producer =
SecureCompressor::with_default_security(ByteCodec::ZstdDict(dict.clone()));
let data = repetitive_json();
let compressed = producer.compress(&data).unwrap();
let config = CompressionBombConfig {
max_decompressed_size: 200,
max_compressed_size: 10_000,
max_ratio: 300.0,
check_interval_bytes: 64,
..Default::default()
};
let strict = SecureCompressor::new(
crate::security::CompressionBombDetector::new(config),
ByteCodec::ZstdDict(dict),
);
let result = strict.decompress_protected(&compressed);
assert!(
result.is_err(),
"bomb detector must block oversized zstd dict output"
);
}
#[test]
fn test_zstd_dict_codec_mismatch_errors() {
let dict = Arc::new(trained_dict());
let c = SecureCompressor::with_default_security(ByteCodec::ZstdDict(dict));
let data = b"codec mismatch test data";
let mut compressed = c.compress(data).unwrap();
compressed.codec = ByteCodec::Gzip;
assert!(
c.decompress_protected(&compressed).is_err(),
"wrong codec must produce an error"
);
}
#[test]
fn test_zstd_dict_empty_payload_roundtrip() {
let dict = Arc::new(trained_dict());
let c = SecureCompressor::with_default_security(ByteCodec::ZstdDict(dict));
let compressed = c.compress(b"").unwrap();
let decompressed = c.decompress_protected(&compressed).unwrap();
assert_eq!(decompressed, b"");
}
#[test]
fn test_zstd_dict_wrong_dictionary_errors() {
let samples_a: Vec<Vec<u8>> = (0..N_TRAIN)
.map(|i| format!(r#"{{"corpus":"alpha","id":{i},"score":{}}}"#, i * 7).into_bytes())
.collect();
let samples_b: Vec<Vec<u8>> = (0..N_TRAIN)
.map(|i| format!(r#"{{"corpus":"beta","seq":{i},"label":"x-{i}"}}"#).into_bytes())
.collect();
let dict_a = ZstdDictCompressor::train(&samples_a, MAX_DICT_SIZE).unwrap();
let dict_b = ZstdDictCompressor::train(&samples_b, MAX_DICT_SIZE).unwrap();
let data = b"some representative payload data";
let compressed =
ZstdDictCompressor::compress(data, &dict_a).expect("compress with dict_a");
let result = ZstdDictCompressor::decompress(&compressed, &dict_b, data.len() * 4);
assert!(
result.is_err(),
"wrong dictionary must produce a libzstd error"
);
}
}
#[cfg(feature = "compression")]
mod extended_compression_tests {
use super::*;
fn incompressible_payload() -> Vec<u8> {
let mut state: u64 = 0x_dead_beef_cafe_babe;
(0..512)
.map(|_| {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
(state >> 33) as u8
})
.collect()
}
#[test]
fn test_deflate_roundtrip_incompressible() {
let c = SecureCompressor::with_default_security(ByteCodec::Deflate);
let data = incompressible_payload();
let compressed = c.compress(&data).unwrap();
assert_eq!(compressed.codec, ByteCodec::Deflate);
let decompressed = c.decompress_protected(&compressed).unwrap();
assert_eq!(decompressed, data);
}
#[test]
fn test_gzip_roundtrip_incompressible() {
let c = SecureCompressor::with_default_security(ByteCodec::Gzip);
let data = incompressible_payload();
let compressed = c.compress(&data).unwrap();
assert_eq!(compressed.codec, ByteCodec::Gzip);
let decompressed = c.decompress_protected(&compressed).unwrap();
assert_eq!(decompressed, data);
}
#[test]
fn test_brotli_roundtrip_incompressible() {
let c = SecureCompressor::with_default_security(ByteCodec::Brotli);
let data = incompressible_payload();
let compressed = c.compress(&data).unwrap();
assert_eq!(compressed.codec, ByteCodec::Brotli);
let decompressed = c.decompress_protected(&compressed).unwrap();
assert_eq!(decompressed, data);
}
#[test]
fn test_gzip_all_qualities() {
let item = br#"{"id":1,"name":"test","value":42}"#;
let data: Vec<u8> = item.repeat(50);
for quality in [
CompressionQuality::Fast,
CompressionQuality::Balanced,
CompressionQuality::Best,
] {
let c = SecureCompressor::with_quality(
CompressionBombDetector::default(),
ByteCodec::Gzip,
quality,
);
let compressed = c.compress(&data).unwrap();
let decompressed = c.decompress_protected(&compressed).unwrap();
assert_eq!(
decompressed, data,
"gzip quality {quality:?} roundtrip failed"
);
}
}
#[test]
fn test_brotli_balanced_quality() {
let item = br#"{"key":"value","n":99}"#;
let data: Vec<u8> = item.repeat(80);
let c = SecureCompressor::with_quality(
CompressionBombDetector::default(),
ByteCodec::Brotli,
CompressionQuality::Balanced,
);
let compressed = c.compress(&data).unwrap();
let decompressed = c.decompress_protected(&compressed).unwrap();
assert_eq!(decompressed, data);
}
#[test]
fn test_decompress_nested_with_depth() {
let c = SecureCompressor::with_default_security(ByteCodec::Deflate);
let item = br#"{"x":1}"#;
let data: Vec<u8> = item.repeat(100);
let compressed = c.compress(&data).unwrap();
let decompressed = c.decompress_nested(&compressed, 1).unwrap();
assert_eq!(decompressed, data);
}
#[test]
fn test_decompress_nested_depth_exceeded_returns_error() {
use crate::security::CompressionBombConfig;
let config = CompressionBombConfig {
max_compression_depth: 2,
..Default::default()
};
let c = SecureCompressor::new(CompressionBombDetector::new(config), ByteCodec::Deflate);
let item = br#"{"x":1}"#;
let data: Vec<u8> = item.repeat(100);
let compressed = c.compress(&data).unwrap();
let result = c.decompress_nested(&compressed, 3);
assert!(result.is_err(), "depth beyond limit must return an error");
}
#[test]
fn test_bomb_detection_deflate() {
use crate::security::CompressionBombConfig;
let config = CompressionBombConfig {
max_decompressed_size: 200,
max_compressed_size: 10_000,
max_ratio: 300.0,
check_interval_bytes: 64,
..Default::default()
};
let item = br#"{"id":1,"name":"test","value":42,"active":true}"#;
let data: Vec<u8> = item.repeat(100);
let producer = SecureCompressor::with_default_security(ByteCodec::Deflate);
let compressed = producer.compress(&data).unwrap();
let strict =
SecureCompressor::new(CompressionBombDetector::new(config), ByteCodec::Deflate);
let result = strict.decompress_protected(&compressed);
assert!(
result.is_err(),
"bomb detector must block oversized deflate output"
);
}
#[test]
fn test_bomb_detection_brotli() {
use crate::security::CompressionBombConfig;
let config = CompressionBombConfig {
max_decompressed_size: 200,
max_compressed_size: 10_000,
max_ratio: 300.0,
check_interval_bytes: 64,
..Default::default()
};
let item = br#"{"id":1,"name":"test","value":42,"active":true}"#;
let data: Vec<u8> = item.repeat(100);
let producer = SecureCompressor::with_default_security(ByteCodec::Brotli);
let compressed = producer.compress(&data).unwrap();
let strict =
SecureCompressor::new(CompressionBombDetector::new(config), ByteCodec::Brotli);
let result = strict.decompress_protected(&compressed);
assert!(
result.is_err(),
"bomb detector must block oversized brotli output"
);
}
#[test]
fn test_codec_mismatch_deflate_as_gzip() {
let c = SecureCompressor::with_default_security(ByteCodec::Deflate);
let data = b"deflate mismatch test payload";
let mut compressed = c.compress(data).unwrap();
compressed.codec = ByteCodec::Gzip;
let result = c.decompress_protected(&compressed);
assert!(result.is_err(), "Deflate data decoded as Gzip must fail");
}
#[test]
fn test_empty_payload_all_codecs() {
for codec in [ByteCodec::Deflate, ByteCodec::Gzip, ByteCodec::Brotli] {
let label = format!("{codec:?}");
let c = SecureCompressor::with_default_security(codec);
let compressed = c.compress(b"").unwrap();
let decompressed = c.decompress_protected(&compressed).unwrap();
assert_eq!(decompressed, b"", "empty roundtrip failed for {label}");
}
}
}
}