use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CompressionLevel {
Fast,
#[default]
Default,
Best,
Custom(u32),
}
impl CompressionLevel {
pub fn to_gzip_level(&self) -> tower_http::CompressionLevel {
match self {
CompressionLevel::Fast => tower_http::CompressionLevel::Fastest,
CompressionLevel::Default => tower_http::CompressionLevel::Default,
CompressionLevel::Best => tower_http::CompressionLevel::Best,
CompressionLevel::Custom(_) => tower_http::CompressionLevel::Default,
}
}
}
impl fmt::Display for CompressionLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CompressionLevel::Fast => write!(f, "fast"),
CompressionLevel::Default => write!(f, "default"),
CompressionLevel::Best => write!(f, "best"),
CompressionLevel::Custom(level) => write!(f, "custom({})", level),
}
}
}
#[derive(Debug, Clone)]
pub struct CompressionConfig {
pub enabled: bool,
pub level: CompressionLevel,
pub min_size_bytes: usize,
pub algorithms: Vec<String>,
}
impl Default for CompressionConfig {
fn default() -> Self {
Self {
enabled: true,
level: CompressionLevel::Default,
min_size_bytes: 1024,
algorithms: vec![
"gbp-lz4".into(),
"br".into(),
"gzip".into(),
"deflate".into(),
],
}
}
}
impl CompressionConfig {
pub fn new() -> Self {
Self::default()
}
pub fn disabled() -> Self {
Self {
enabled: false,
..Default::default()
}
}
pub fn fast() -> Self {
Self {
enabled: true,
level: CompressionLevel::Fast,
min_size_bytes: 512,
algorithms: vec!["gzip".into()], }
}
pub fn best() -> Self {
Self {
enabled: true,
level: CompressionLevel::Best,
min_size_bytes: 256,
algorithms: vec!["br".into(), "gzip".into()],
}
}
pub fn with_level(mut self, level: CompressionLevel) -> Self {
self.level = level;
self
}
pub fn with_min_size(mut self, min_size_bytes: usize) -> Self {
const MIN_SAFE_THRESHOLD: usize = 256;
if min_size_bytes < MIN_SAFE_THRESHOLD {
tracing::warn!(
requested = min_size_bytes,
enforced = MIN_SAFE_THRESHOLD,
"min_size_bytes raised to the minimum safe threshold (BREACH/CRIME mitigation)"
);
self.min_size_bytes = MIN_SAFE_THRESHOLD;
} else {
self.min_size_bytes = min_size_bytes;
}
self
}
pub fn with_algorithms(mut self, algorithms: Vec<String>) -> Self {
self.algorithms = algorithms;
self
}
pub fn brotli_enabled(&self) -> bool {
self.enabled && self.algorithms.iter().any(|a| a == "br" || a == "brotli")
}
pub fn gzip_enabled(&self) -> bool {
self.enabled && self.algorithms.iter().any(|a| a == "gzip")
}
pub fn deflate_enabled(&self) -> bool {
self.enabled && self.algorithms.iter().any(|a| a == "deflate")
}
pub fn zstd_enabled(&self) -> bool {
self.enabled && self.algorithms.iter().any(|a| a == "zstd")
}
pub fn gbp_lz4_enabled(&self) -> bool {
self.enabled && self.algorithms.iter().any(|a| a == "gbp-lz4")
}
pub fn lz4_enabled(&self) -> bool {
self.enabled && self.algorithms.iter().any(|a| a == "lz4")
}
pub fn ultra_fast() -> Self {
Self {
enabled: true,
level: CompressionLevel::Fast,
min_size_bytes: 256,
algorithms: vec!["gbp-lz4".into(), "lz4".into()],
}
}
}
pub fn create_compression_layer(
config: &CompressionConfig,
) -> tower_http::compression::CompressionLayer {
use tower_http::compression::CompressionLayer;
let mut layer = CompressionLayer::new();
layer = layer.quality(config.level.to_gzip_level());
if !config.gzip_enabled() {
layer = layer.no_gzip();
}
if !config.brotli_enabled() {
layer = layer.no_br();
}
if !config.deflate_enabled() {
layer = layer.no_deflate();
}
if !config.zstd_enabled() {
layer = layer.no_zstd();
}
layer
}
#[derive(Debug, Clone, Default)]
pub struct CompressionStats {
bytes_in: u64,
bytes_out: u64,
compressed_count: u64,
uncompressed_count: u64,
}
impl CompressionStats {
pub fn record_compressed(&mut self, original_bytes: u64, compressed_bytes: u64) {
if compressed_bytes > original_bytes {
tracing::warn!(
original = original_bytes,
compressed = compressed_bytes,
"Compressed size exceeds original — skipping stats update"
);
return;
}
self.bytes_in = self.bytes_in.saturating_add(original_bytes);
self.bytes_out = self.bytes_out.saturating_add(compressed_bytes);
self.compressed_count = self.compressed_count.saturating_add(1);
}
pub fn record_uncompressed(&mut self, bytes: u64) {
self.bytes_in = self.bytes_in.saturating_add(bytes);
self.bytes_out = self.bytes_out.saturating_add(bytes);
self.uncompressed_count = self.uncompressed_count.saturating_add(1);
}
pub fn bytes_in(&self) -> u64 {
self.bytes_in
}
pub fn bytes_out(&self) -> u64 {
self.bytes_out
}
pub fn compressed_count(&self) -> u64 {
self.compressed_count
}
pub fn uncompressed_count(&self) -> u64 {
self.uncompressed_count
}
pub fn compression_ratio(&self) -> f64 {
if self.bytes_in == 0 {
1.0
} else {
self.bytes_out as f64 / self.bytes_in as f64
}
}
pub fn savings_percentage(&self) -> f64 {
if self.bytes_in == 0 {
0.0
} else {
(1.0 - (self.bytes_out as f64 / self.bytes_in as f64)) * 100.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compression_config_default() {
let config = CompressionConfig::default();
assert!(config.enabled);
assert!(config.brotli_enabled());
assert!(config.gzip_enabled());
assert!(config.deflate_enabled());
assert!(!config.zstd_enabled());
assert_eq!(config.min_size_bytes, 1024);
}
#[test]
fn test_compression_config_disabled() {
let config = CompressionConfig::disabled();
assert!(!config.enabled);
assert!(!config.brotli_enabled());
assert!(!config.gzip_enabled());
}
#[test]
fn test_compression_config_fast() {
let config = CompressionConfig::fast();
assert!(config.enabled);
assert!(!config.brotli_enabled());
assert!(config.gzip_enabled());
assert_eq!(config.level, CompressionLevel::Fast);
}
#[test]
fn test_compression_config_best() {
let config = CompressionConfig::best();
assert!(config.enabled);
assert!(config.brotli_enabled());
assert!(config.gzip_enabled());
assert_eq!(config.level, CompressionLevel::Best);
}
#[test]
fn test_compression_config_builder() {
let config = CompressionConfig::new()
.with_level(CompressionLevel::Best)
.with_min_size(2048)
.with_algorithms(vec!["br".into()]);
assert_eq!(config.level, CompressionLevel::Best);
assert_eq!(config.min_size_bytes, 2048);
assert!(config.brotli_enabled());
assert!(!config.gzip_enabled());
}
#[test]
fn test_compression_stats() {
let mut stats = CompressionStats::default();
for _ in 0..10 {
stats.record_compressed(100, 30);
}
for _ in 0..5 {
stats.record_uncompressed(100);
}
assert!((stats.compression_ratio() - (800.0 / 1500.0)).abs() < 0.001);
assert!((stats.savings_percentage() - (1.0 - 800.0 / 1500.0) * 100.0).abs() < 0.001);
assert_eq!(stats.compressed_count(), 10);
assert_eq!(stats.uncompressed_count(), 5);
}
#[test]
fn test_compression_stats_pure_compressed() {
let mut stats = CompressionStats::default();
for _ in 0..10 {
stats.record_compressed(100, 30);
}
assert_eq!(stats.bytes_in(), 1000);
assert_eq!(stats.bytes_out(), 300);
assert!((stats.compression_ratio() - 0.3).abs() < 0.001);
assert!((stats.savings_percentage() - 70.0).abs() < 0.001);
}
#[test]
fn test_compression_level_display() {
assert_eq!(CompressionLevel::Fast.to_string(), "fast");
assert_eq!(CompressionLevel::Default.to_string(), "default");
assert_eq!(CompressionLevel::Best.to_string(), "best");
assert_eq!(CompressionLevel::Custom(5).to_string(), "custom(5)");
}
#[test]
fn test_create_compression_layer() {
let config = CompressionConfig::default();
let _layer = create_compression_layer(&config);
}
#[test]
fn test_compression_level_to_gzip() {
assert_eq!(
CompressionLevel::Fast.to_gzip_level(),
tower_http::CompressionLevel::Fastest
);
assert_eq!(
CompressionLevel::Default.to_gzip_level(),
tower_http::CompressionLevel::Default
);
assert_eq!(
CompressionLevel::Best.to_gzip_level(),
tower_http::CompressionLevel::Best
);
assert_eq!(
CompressionLevel::Custom(7).to_gzip_level(),
tower_http::CompressionLevel::Default
);
}
#[test]
fn test_compression_level_equality() {
assert_eq!(CompressionLevel::Fast, CompressionLevel::Fast);
assert_ne!(CompressionLevel::Fast, CompressionLevel::Best);
assert_eq!(CompressionLevel::Custom(5), CompressionLevel::Custom(5));
assert_ne!(CompressionLevel::Custom(5), CompressionLevel::Custom(6));
}
#[test]
fn test_compression_config_ultra_fast() {
let config = CompressionConfig::ultra_fast();
assert!(config.enabled);
assert!(config.gbp_lz4_enabled());
assert!(config.lz4_enabled());
assert_eq!(config.level, CompressionLevel::Fast);
assert_eq!(config.min_size_bytes, 256);
}
#[test]
fn test_lz4_enabled() {
let config = CompressionConfig::default();
assert!(!config.lz4_enabled());
let lz4_config = CompressionConfig::new().with_algorithms(vec!["lz4".into()]);
assert!(lz4_config.lz4_enabled());
}
#[test]
fn test_gbp_lz4_enabled() {
let config = CompressionConfig::default();
assert!(config.gbp_lz4_enabled());
let no_gbp = CompressionConfig::new().with_algorithms(vec!["gzip".into()]);
assert!(!no_gbp.gbp_lz4_enabled());
}
#[test]
fn test_zstd_enabled() {
let config = CompressionConfig::default();
assert!(!config.zstd_enabled());
let zstd_config = CompressionConfig::new().with_algorithms(vec!["zstd".into()]);
assert!(zstd_config.zstd_enabled());
}
#[test]
fn test_compression_stats_zero_input() {
let stats = CompressionStats::default();
assert_eq!(stats.compression_ratio(), 1.0);
assert_eq!(stats.savings_percentage(), 0.0);
}
#[test]
fn test_compression_stats_no_compression() {
let mut stats = CompressionStats::default();
for _ in 0..10 {
stats.record_uncompressed(100);
}
assert_eq!(stats.compression_ratio(), 1.0);
assert_eq!(stats.savings_percentage(), 0.0);
assert_eq!(stats.uncompressed_count(), 10);
}
#[test]
fn test_compression_stats_perfect_compression() {
let mut stats = CompressionStats::default();
for _ in 0..10 {
stats.record_compressed(100, 1);
}
assert!(stats.compression_ratio() < 0.02);
assert!(stats.savings_percentage() >= 99.0);
assert_eq!(stats.compressed_count(), 10);
}
#[test]
fn test_compression_config_clone() {
let config1 = CompressionConfig::best();
let config2 = config1.clone();
assert_eq!(config1.level, config2.level);
assert_eq!(config1.enabled, config2.enabled);
assert_eq!(config1.min_size_bytes, config2.min_size_bytes);
}
#[test]
fn test_compression_config_new() {
let config = CompressionConfig::new();
assert!(config.enabled);
assert_eq!(config.level, CompressionLevel::Default);
}
#[test]
fn test_compression_level_copy() {
let level1 = CompressionLevel::Fast;
let level2 = level1;
assert_eq!(level1, level2);
}
#[test]
fn test_compression_stats_default() {
let stats = CompressionStats::default();
assert_eq!(stats.bytes_in(), 0);
assert_eq!(stats.bytes_out(), 0);
assert_eq!(stats.compressed_count(), 0);
assert_eq!(stats.uncompressed_count(), 0);
}
#[test]
fn test_multiple_algorithms() {
let config = CompressionConfig::new().with_algorithms(vec![
"gbp-lz4".into(),
"lz4".into(),
"br".into(),
"gzip".into(),
"deflate".into(),
"zstd".into(),
]);
assert!(config.gbp_lz4_enabled());
assert!(config.lz4_enabled());
assert!(config.brotli_enabled());
assert!(config.gzip_enabled());
assert!(config.deflate_enabled());
assert!(config.zstd_enabled());
}
}