use oxiarc_deflate::{gzip_compress, gzip_decompress};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
const GZIP_MARKER: &[u8] = b"CELERS_GZIP:";
#[cfg(feature = "zstd-compression")]
const ZSTD_MARKER: &[u8] = b"CELERS_ZSTD:";
const DEFAULT_COMPRESSION_THRESHOLD: usize = 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CompressionAlgorithm {
#[default]
Gzip,
#[cfg(feature = "zstd-compression")]
Zstd,
}
#[derive(Debug, Clone)]
pub struct CompressionConfig {
pub enabled: bool,
pub threshold: usize,
pub level: u32,
pub algorithm: CompressionAlgorithm,
}
impl Default for CompressionConfig {
fn default() -> Self {
Self {
enabled: true,
threshold: DEFAULT_COMPRESSION_THRESHOLD,
level: 6, algorithm: CompressionAlgorithm::default(),
}
}
}
impl CompressionConfig {
pub fn new() -> Self {
Self::default()
}
pub fn disabled() -> Self {
Self {
enabled: false,
threshold: DEFAULT_COMPRESSION_THRESHOLD,
level: 6,
algorithm: CompressionAlgorithm::default(),
}
}
pub fn with_threshold(mut self, threshold: usize) -> Self {
self.threshold = threshold;
self
}
pub fn with_level(mut self, level: u32) -> Self {
self.level = match self.algorithm {
CompressionAlgorithm::Gzip => level.min(9),
#[cfg(feature = "zstd-compression")]
CompressionAlgorithm::Zstd => level.min(22),
};
self
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn with_algorithm(mut self, algorithm: CompressionAlgorithm) -> Self {
self.algorithm = algorithm;
self
}
#[cfg(feature = "zstd-compression")]
pub fn zstd() -> Self {
Self {
enabled: true,
threshold: DEFAULT_COMPRESSION_THRESHOLD,
level: 3, algorithm: CompressionAlgorithm::Zstd,
}
}
}
pub fn maybe_compress(data: &[u8], config: &CompressionConfig) -> Result<Vec<u8>, std::io::Error> {
if !config.enabled || data.len() < config.threshold {
return Ok(data.to_vec());
}
match config.algorithm {
CompressionAlgorithm::Gzip => compress_gzip(data, config.level),
#[cfg(feature = "zstd-compression")]
CompressionAlgorithm::Zstd => compress_zstd(data, config.level),
}
}
fn compress_gzip(data: &[u8], level: u32) -> Result<Vec<u8>, std::io::Error> {
let compressed = gzip_compress(data, level.min(9) as u8)
.map_err(|e| std::io::Error::other(e.to_string()))?;
if compressed.len() < data.len() {
let mut result = Vec::with_capacity(GZIP_MARKER.len() + compressed.len());
result.extend_from_slice(GZIP_MARKER);
result.extend_from_slice(&compressed);
Ok(result)
} else {
Ok(data.to_vec())
}
}
#[cfg(feature = "zstd-compression")]
fn compress_zstd(data: &[u8], level: u32) -> Result<Vec<u8>, std::io::Error> {
let compressed = oxiarc_zstd::compress_with_level(data, level.min(22) as i32)
.map_err(|e| std::io::Error::other(e.to_string()))?;
if compressed.len() < data.len() {
let mut result = Vec::with_capacity(ZSTD_MARKER.len() + compressed.len());
result.extend_from_slice(ZSTD_MARKER);
result.extend_from_slice(&compressed);
Ok(result)
} else {
Ok(data.to_vec())
}
}
pub fn maybe_decompress(data: &[u8]) -> Result<Vec<u8>, std::io::Error> {
if data.len() >= GZIP_MARKER.len() && &data[..GZIP_MARKER.len()] == GZIP_MARKER {
let compressed = &data[GZIP_MARKER.len()..];
return gzip_decompress(compressed).map_err(|e| std::io::Error::other(e.to_string()));
}
#[cfg(feature = "zstd-compression")]
if data.len() >= ZSTD_MARKER.len() && &data[..ZSTD_MARKER.len()] == ZSTD_MARKER {
let compressed = &data[ZSTD_MARKER.len()..];
return oxiarc_zstd::decompress(compressed)
.map_err(|e| std::io::Error::other(e.to_string()));
}
Ok(data.to_vec())
}
pub fn compression_ratio(original_size: usize, compressed_size: usize) -> f64 {
if original_size == 0 {
return 1.0;
}
compressed_size as f64 / original_size as f64
}
#[derive(Debug)]
pub struct CompressionStats {
operations: AtomicU64,
total_original_bytes: AtomicUsize,
total_compressed_bytes: AtomicUsize,
compressions_applied: AtomicU64,
}
impl Default for CompressionStats {
fn default() -> Self {
Self::new()
}
}
impl CompressionStats {
pub fn new() -> Self {
Self {
operations: AtomicU64::new(0),
total_original_bytes: AtomicUsize::new(0),
total_compressed_bytes: AtomicUsize::new(0),
compressions_applied: AtomicU64::new(0),
}
}
pub fn record(&self, original_size: usize, compressed_size: usize) {
self.operations.fetch_add(1, Ordering::Relaxed);
self.total_original_bytes
.fetch_add(original_size, Ordering::Relaxed);
self.total_compressed_bytes
.fetch_add(compressed_size, Ordering::Relaxed);
if compressed_size < original_size {
self.compressions_applied.fetch_add(1, Ordering::Relaxed);
}
}
pub fn operation_count(&self) -> u64 {
self.operations.load(Ordering::Relaxed)
}
pub fn total_original_bytes(&self) -> usize {
self.total_original_bytes.load(Ordering::Relaxed)
}
pub fn total_compressed_bytes(&self) -> usize {
self.total_compressed_bytes.load(Ordering::Relaxed)
}
pub fn overall_ratio(&self) -> f64 {
let original = self.total_original_bytes.load(Ordering::Relaxed);
let compressed = self.total_compressed_bytes.load(Ordering::Relaxed);
compression_ratio(original, compressed)
}
pub fn compressions_applied(&self) -> u64 {
self.compressions_applied.load(Ordering::Relaxed)
}
pub fn bytes_saved(&self) -> usize {
let original = self.total_original_bytes.load(Ordering::Relaxed);
let compressed = self.total_compressed_bytes.load(Ordering::Relaxed);
original.saturating_sub(compressed)
}
pub fn reset(&self) {
self.operations.store(0, Ordering::Relaxed);
self.total_original_bytes.store(0, Ordering::Relaxed);
self.total_compressed_bytes.store(0, Ordering::Relaxed);
self.compressions_applied.store(0, Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compression_config_defaults() {
let config = CompressionConfig::default();
assert!(config.enabled);
assert_eq!(config.threshold, 1024);
assert_eq!(config.level, 6);
assert_eq!(config.algorithm, CompressionAlgorithm::Gzip);
}
#[test]
fn test_compression_config_builder() {
let config = CompressionConfig::new()
.with_threshold(2048)
.with_level(9)
.with_enabled(true);
assert!(config.enabled);
assert_eq!(config.threshold, 2048);
assert_eq!(config.level, 9);
}
#[test]
fn test_compression_disabled() {
let config = CompressionConfig::disabled();
assert!(!config.enabled);
let data = b"test data that should not be compressed";
let result = maybe_compress(data, &config).expect("compress failed");
assert_eq!(result, data);
}
#[test]
fn test_compression_below_threshold() {
let config = CompressionConfig::new().with_threshold(1000);
let data = b"small"; let result = maybe_compress(data, &config).expect("compress failed");
assert_eq!(result, data);
}
#[test]
fn test_gzip_compression_and_decompression() {
let config = CompressionConfig::new().with_threshold(10);
let original = b"This is a test string that should compress well because it has repetition. \
This is a test string that should compress well because it has repetition. \
This is a test string that should compress well because it has repetition.";
let compressed = maybe_compress(original, &config).expect("compress failed");
assert!(compressed.starts_with(GZIP_MARKER));
assert!(compressed.len() < original.len());
let decompressed = maybe_decompress(&compressed).expect("decompress failed");
assert_eq!(decompressed, original);
}
#[cfg(feature = "zstd-compression")]
#[test]
fn test_zstd_compression_and_decompression() {
let config = CompressionConfig::zstd().with_threshold(10);
let original = b"This is a test string that should compress well because it has repetition. \
This is a test string that should compress well because it has repetition. \
This is a test string that should compress well because it has repetition.";
let compressed = maybe_compress(original, &config).expect("compress failed");
assert!(compressed.starts_with(ZSTD_MARKER));
assert!(compressed.len() < original.len());
let decompressed = maybe_decompress(&compressed).expect("decompress failed");
assert_eq!(decompressed, original);
}
#[cfg(feature = "zstd-compression")]
#[test]
fn test_auto_detect_gzip_vs_zstd() {
let original = b"This is a test string that should compress well because it has repetition. \
This is a test string that should compress well because it has repetition. \
This is a test string that should compress well because it has repetition.";
let gzip_config = CompressionConfig::new().with_threshold(10);
let gzip_compressed = maybe_compress(original, &gzip_config).expect("gzip compress failed");
assert!(gzip_compressed.starts_with(GZIP_MARKER));
let zstd_config = CompressionConfig::zstd().with_threshold(10);
let zstd_compressed = maybe_compress(original, &zstd_config).expect("zstd compress failed");
assert!(zstd_compressed.starts_with(ZSTD_MARKER));
let gzip_decompressed = maybe_decompress(&gzip_compressed).expect("gzip decompress failed");
let zstd_decompressed = maybe_decompress(&zstd_compressed).expect("zstd decompress failed");
assert_eq!(gzip_decompressed, original);
assert_eq!(zstd_decompressed, original);
}
#[test]
fn test_decompress_uncompressed_data() {
let data = b"uncompressed data";
let result = maybe_decompress(data).expect("decompress failed");
assert_eq!(result, data);
}
#[test]
fn test_compression_ratio_calculation() {
assert_eq!(compression_ratio(1000, 500), 0.5);
assert_eq!(compression_ratio(1000, 1000), 1.0);
assert_eq!(compression_ratio(0, 0), 1.0);
}
#[test]
fn test_compression_level_capping() {
let config = CompressionConfig::new().with_level(999);
assert_eq!(config.level, 9);
}
#[cfg(feature = "zstd-compression")]
#[test]
fn test_zstd_compression_level_capping() {
let config = CompressionConfig::zstd().with_level(999);
assert_eq!(config.level, 22);
}
#[test]
fn test_compression_no_benefit() {
let config = CompressionConfig::new().with_threshold(0);
let data = b"abc123xyz";
let result = maybe_compress(data, &config).expect("compress failed");
if !result.starts_with(GZIP_MARKER) {
assert_eq!(result, data);
}
}
#[test]
fn test_compression_stats() {
let stats = CompressionStats::new();
assert_eq!(stats.operation_count(), 0);
assert_eq!(stats.total_original_bytes(), 0);
assert_eq!(stats.total_compressed_bytes(), 0);
assert_eq!(stats.compressions_applied(), 0);
stats.record(1000, 500); stats.record(100, 100); stats.record(2000, 800);
assert_eq!(stats.operation_count(), 3);
assert_eq!(stats.total_original_bytes(), 3100);
assert_eq!(stats.total_compressed_bytes(), 1400);
assert_eq!(stats.compressions_applied(), 2);
assert_eq!(stats.bytes_saved(), 1700);
let ratio = stats.overall_ratio();
assert!((ratio - 1400.0 / 3100.0).abs() < 0.001);
stats.reset();
assert_eq!(stats.operation_count(), 0);
assert_eq!(stats.total_original_bytes(), 0);
}
#[test]
fn test_compression_algorithm_default() {
assert_eq!(CompressionAlgorithm::default(), CompressionAlgorithm::Gzip);
}
}