use super::traits::Compressor;
use crate::common::error::{FlareError, Result};
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum CompressionAlgorithm {
None,
Gzip,
Zstd,
Custom(String),
}
impl CompressionAlgorithm {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"none" | "" => Some(CompressionAlgorithm::None),
"gzip" => Some(CompressionAlgorithm::Gzip),
"zstd" => Some(CompressionAlgorithm::Zstd),
custom => Some(CompressionAlgorithm::Custom(custom.to_string())),
}
}
pub fn as_str(&self) -> String {
match self {
CompressionAlgorithm::None => "none".to_string(),
CompressionAlgorithm::Gzip => "gzip".to_string(),
CompressionAlgorithm::Zstd => "zstd".to_string(),
CompressionAlgorithm::Custom(name) => name.clone(),
}
}
pub fn is_custom(&self) -> bool {
matches!(self, CompressionAlgorithm::Custom(_))
}
pub fn custom_name(&self) -> Option<&str> {
match self {
CompressionAlgorithm::Custom(name) => Some(name),
_ => None,
}
}
}
pub struct NoCompressor;
impl Compressor for NoCompressor {
fn compress(&self, data: &[u8]) -> Result<Vec<u8>> {
Ok(data.to_vec())
}
fn decompress(&self, data: &[u8]) -> Result<Vec<u8>> {
Ok(data.to_vec())
}
fn algorithm(&self) -> CompressionAlgorithm {
CompressionAlgorithm::None
}
fn name(&self) -> &'static str {
"none"
}
fn can_detect(&self, _data: &[u8]) -> bool {
false
}
}
#[cfg(feature = "compression-gzip")]
const MAX_GZIP_DECOMPRESSED_LEN: u64 = 16 * 1024 * 1024;
pub struct GzipCompressor;
impl Compressor for GzipCompressor {
fn compress(&self, data: &[u8]) -> Result<Vec<u8>> {
#[cfg(feature = "compression-gzip")]
{
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(data).map_err(|e| {
FlareError::encoding_error(format!("Gzip compression failed: {}", e))
})?;
encoder.finish().map_err(|e| {
FlareError::encoding_error(format!("Gzip compression finish failed: {}", e))
})
}
#[cfg(not(feature = "compression-gzip"))]
{
let _ = data;
Err(FlareError::operation_not_supported(
"gzip compression feature is disabled",
))
}
}
fn decompress(&self, data: &[u8]) -> Result<Vec<u8>> {
#[cfg(feature = "compression-gzip")]
{
use flate2::read::GzDecoder;
use std::io::Read;
let mut decoder = GzDecoder::new(data).take(MAX_GZIP_DECOMPRESSED_LEN + 1);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed).map_err(|e| {
FlareError::encoding_error(format!("Gzip decompression failed: {}", e))
})?;
if decompressed.len() as u64 > MAX_GZIP_DECOMPRESSED_LEN {
return Err(FlareError::encoding_error(format!(
"Gzip decompressed payload exceeds limit ({MAX_GZIP_DECOMPRESSED_LEN} bytes)"
)));
}
Ok(decompressed)
}
#[cfg(not(feature = "compression-gzip"))]
{
let _ = data;
Err(FlareError::operation_not_supported(
"gzip compression feature is disabled",
))
}
}
fn algorithm(&self) -> CompressionAlgorithm {
CompressionAlgorithm::Gzip
}
fn name(&self) -> &'static str {
"gzip"
}
fn can_detect(&self, data: &[u8]) -> bool {
data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b
}
}
#[cfg(all(test, feature = "compression-gzip"))]
mod gzip_decompress_limit_tests {
use super::GzipCompressor;
use crate::common::compression::traits::Compressor;
#[test]
fn roundtrip_under_limit_is_unchanged() {
let c = GzipCompressor;
let data = vec![7u8; 1024 * 1024]; let compressed = c.compress(&data).unwrap();
let restored = c.decompress(&compressed).unwrap();
assert_eq!(restored, data);
}
#[test]
fn decompress_rejects_zip_bomb_over_limit() {
let c = GzipCompressor;
let data = vec![0u8; 17 * 1024 * 1024];
let compressed = c.compress(&data).unwrap();
assert!(
compressed.len() < 1024 * 1024,
"bomb payload should compress small"
);
assert!(
c.decompress(&compressed).is_err(),
"decompressed payload exceeding the limit must be rejected"
);
}
}