use serde::{Deserialize, Serialize};
use std::io::{self, Read, Write};
use thiserror::Error;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum CompressionAlgorithm {
None,
Lz4,
Snappy,
Zstd,
}
impl CompressionAlgorithm {
pub fn as_str(&self) -> &'static str {
match self {
Self::None => "none",
Self::Lz4 => "lz4",
Self::Snappy => "snappy",
Self::Zstd => "zstd",
}
}
fn magic_byte(&self) -> u8 {
match self {
Self::None => 0x00,
Self::Lz4 => 0x01,
Self::Snappy => 0x02,
Self::Zstd => 0x03,
}
}
fn from_magic_byte(byte: u8) -> Result<Self, CompressionError> {
match byte {
0x00 => Ok(Self::None),
0x01 => Ok(Self::Lz4),
0x02 => Ok(Self::Snappy),
0x03 => Ok(Self::Zstd),
_ => Err(CompressionError::InvalidMagicByte(byte)),
}
}
}
impl Default for CompressionAlgorithm {
fn default() -> Self {
Self::Lz4 }
}
impl std::fmt::Display for CompressionAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompressionConfig {
pub algorithm: CompressionAlgorithm,
pub level: i32,
pub threshold: usize,
pub enable_stats: bool,
}
impl Default for CompressionConfig {
fn default() -> Self {
Self {
algorithm: CompressionAlgorithm::Lz4,
level: 4, threshold: 256, enable_stats: false,
}
}
}
impl CompressionConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_algorithm(mut self, algorithm: CompressionAlgorithm) -> Self {
self.algorithm = algorithm;
self
}
pub fn with_level(mut self, level: i32) -> Self {
self.level = level;
self
}
pub fn with_threshold(mut self, threshold: usize) -> Self {
self.threshold = threshold;
self
}
pub fn with_stats(mut self, enable: bool) -> Self {
self.enable_stats = enable;
self
}
pub fn none() -> Self {
Self {
algorithm: CompressionAlgorithm::None,
..Default::default()
}
}
pub fn fast() -> Self {
Self {
algorithm: CompressionAlgorithm::Snappy,
level: 0,
threshold: 512,
enable_stats: false,
}
}
pub fn balanced() -> Self {
Self {
algorithm: CompressionAlgorithm::Lz4,
level: 4,
threshold: 256,
enable_stats: false,
}
}
pub fn best_size() -> Self {
Self {
algorithm: CompressionAlgorithm::Zstd,
level: 15,
threshold: 128,
enable_stats: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CompressionStats {
pub compress_count: u64,
pub decompress_count: u64,
pub bytes_in: u64,
pub bytes_out: u64,
pub decompress_bytes_in: u64,
pub decompress_bytes_out: u64,
pub threshold_skipped: u64,
}
impl CompressionStats {
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 space_saved(&self) -> i64 {
self.bytes_in as i64 - self.bytes_out as i64
}
pub fn space_saved_pct(&self) -> f64 {
if self.bytes_in == 0 {
0.0
} else {
(1.0 - self.compression_ratio()) * 100.0
}
}
}
#[derive(Debug, Error)]
pub enum CompressionError {
#[error("LZ4 compression error: {0}")]
Lz4Error(String),
#[error("Snappy compression error: {0}")]
SnappyError(String),
#[error("Zstd compression error: {0}")]
ZstdError(String),
#[error("Invalid compression magic byte: {0:#x}")]
InvalidMagicByte(u8),
#[error("Data too short (must be at least 1 byte)")]
DataTooShort,
#[error("IO error: {0}")]
IoError(#[from] io::Error),
}
pub type CompressionResult<T> = Result<T, CompressionError>;
pub fn compress(data: &[u8], config: &CompressionConfig) -> CompressionResult<Vec<u8>> {
if data.len() < config.threshold {
let mut result = Vec::with_capacity(1 + data.len());
result.push(CompressionAlgorithm::None.magic_byte());
result.extend_from_slice(data);
return Ok(result);
}
match config.algorithm {
CompressionAlgorithm::None => {
let mut result = Vec::with_capacity(1 + data.len());
result.push(CompressionAlgorithm::None.magic_byte());
result.extend_from_slice(data);
Ok(result)
}
CompressionAlgorithm::Lz4 => {
let compressed = lz4::block::compress(data, Some(lz4::block::CompressionMode::HIGHCOMPRESSION(config.level)), false)
.map_err(|e| CompressionError::Lz4Error(e.to_string()))?;
let mut result = Vec::with_capacity(1 + compressed.len());
result.push(CompressionAlgorithm::Lz4.magic_byte());
result.extend_from_slice(&compressed);
Ok(result)
}
CompressionAlgorithm::Snappy => {
let mut encoder = snap::raw::Encoder::new();
let compressed = encoder
.compress_vec(data)
.map_err(|e| CompressionError::SnappyError(e.to_string()))?;
let mut result = Vec::with_capacity(1 + compressed.len());
result.push(CompressionAlgorithm::Snappy.magic_byte());
result.extend_from_slice(&compressed);
Ok(result)
}
CompressionAlgorithm::Zstd => {
let mut compressed = Vec::new();
let mut encoder = zstd::Encoder::new(&mut compressed, config.level)?;
encoder.write_all(data)?;
encoder.finish()?;
let mut result = Vec::with_capacity(1 + compressed.len());
result.push(CompressionAlgorithm::Zstd.magic_byte());
result.extend_from_slice(&compressed);
Ok(result)
}
}
}
pub fn decompress(data: &[u8], _config: &CompressionConfig) -> CompressionResult<Vec<u8>> {
if data.is_empty() {
return Err(CompressionError::DataTooShort);
}
let magic_byte = data[0];
let compressed_data = &data[1..];
let algorithm = CompressionAlgorithm::from_magic_byte(magic_byte)?;
match algorithm {
CompressionAlgorithm::None => {
Ok(compressed_data.to_vec())
}
CompressionAlgorithm::Lz4 => {
let decompressed = lz4::block::decompress(compressed_data, None)
.map_err(|e| CompressionError::Lz4Error(e.to_string()))?;
Ok(decompressed)
}
CompressionAlgorithm::Snappy => {
let mut decoder = snap::raw::Decoder::new();
let decompressed = decoder
.decompress_vec(compressed_data)
.map_err(|e| CompressionError::SnappyError(e.to_string()))?;
Ok(decompressed)
}
CompressionAlgorithm::Zstd => {
let mut decompressed = Vec::new();
let mut decoder = zstd::Decoder::new(compressed_data)?;
decoder.read_to_end(&mut decompressed)?;
Ok(decompressed)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_DATA_SMALL: &[u8] = b"Hello, World!";
const TEST_DATA_MEDIUM: &[u8] = b"This is a longer test string that should definitely be compressed because it has enough repetition. This is a longer test string that should definitely be compressed because it has enough repetition.";
fn generate_large_data(size: usize) -> Vec<u8> {
(0..size).map(|i| (i % 256) as u8).collect()
}
#[test]
fn test_algorithm_display() {
assert_eq!(CompressionAlgorithm::None.to_string(), "none");
assert_eq!(CompressionAlgorithm::Lz4.to_string(), "lz4");
assert_eq!(CompressionAlgorithm::Snappy.to_string(), "snappy");
assert_eq!(CompressionAlgorithm::Zstd.to_string(), "zstd");
}
#[test]
fn test_magic_byte_roundtrip() {
for algo in &[
CompressionAlgorithm::None,
CompressionAlgorithm::Lz4,
CompressionAlgorithm::Snappy,
CompressionAlgorithm::Zstd,
] {
let byte = algo.magic_byte();
let parsed = CompressionAlgorithm::from_magic_byte(byte).unwrap();
assert_eq!(*algo, parsed);
}
}
#[test]
fn test_invalid_magic_byte() {
let result = CompressionAlgorithm::from_magic_byte(0xFF);
assert!(result.is_err());
}
#[test]
fn test_compression_config_presets() {
let fast = CompressionConfig::fast();
assert_eq!(fast.algorithm, CompressionAlgorithm::Snappy);
let balanced = CompressionConfig::balanced();
assert_eq!(balanced.algorithm, CompressionAlgorithm::Lz4);
let best = CompressionConfig::best_size();
assert_eq!(best.algorithm, CompressionAlgorithm::Zstd);
let none = CompressionConfig::none();
assert_eq!(none.algorithm, CompressionAlgorithm::None);
}
#[test]
fn test_compression_none() {
let config = CompressionConfig::none();
let compressed = compress(TEST_DATA_MEDIUM, &config).unwrap();
let decompressed = decompress(&compressed, &config).unwrap();
assert_eq!(decompressed, TEST_DATA_MEDIUM);
assert_eq!(compressed.len(), TEST_DATA_MEDIUM.len() + 1);
}
#[test]
fn test_compression_lz4() {
let config = CompressionConfig::default().with_algorithm(CompressionAlgorithm::Lz4);
let compressed = compress(TEST_DATA_MEDIUM, &config).unwrap();
let decompressed = decompress(&compressed, &config).unwrap();
assert_eq!(decompressed, TEST_DATA_MEDIUM);
assert!(compressed.len() < TEST_DATA_MEDIUM.len());
}
#[test]
fn test_compression_snappy() {
let config = CompressionConfig::default().with_algorithm(CompressionAlgorithm::Snappy);
let compressed = compress(TEST_DATA_MEDIUM, &config).unwrap();
let decompressed = decompress(&compressed, &config).unwrap();
assert_eq!(decompressed, TEST_DATA_MEDIUM);
assert!(compressed.len() < TEST_DATA_MEDIUM.len());
}
#[test]
fn test_compression_zstd() {
let config = CompressionConfig::default().with_algorithm(CompressionAlgorithm::Zstd);
let compressed = compress(TEST_DATA_MEDIUM, &config).unwrap();
let decompressed = decompress(&compressed, &config).unwrap();
assert_eq!(decompressed, TEST_DATA_MEDIUM);
assert!(compressed.len() < TEST_DATA_MEDIUM.len());
}
#[test]
fn test_threshold_prevents_compression() {
let config = CompressionConfig::default()
.with_algorithm(CompressionAlgorithm::Lz4)
.with_threshold(1000);
let compressed = compress(TEST_DATA_SMALL, &config).unwrap();
assert_eq!(compressed[0], CompressionAlgorithm::None.magic_byte());
assert_eq!(compressed.len(), TEST_DATA_SMALL.len() + 1);
let decompressed = decompress(&compressed, &config).unwrap();
assert_eq!(decompressed, TEST_DATA_SMALL);
}
#[test]
fn test_large_data_compression() {
let large_data = generate_large_data(1024 * 1024); let config = CompressionConfig::balanced();
let compressed = compress(&large_data, &config).unwrap();
let decompressed = decompress(&compressed, &config).unwrap();
assert_eq!(decompressed, large_data);
println!("Original: {} bytes, Compressed: {} bytes, Ratio: {:.2}%",
large_data.len(),
compressed.len(),
(compressed.len() as f64 / large_data.len() as f64) * 100.0
);
}
#[test]
fn test_empty_data() {
let config = CompressionConfig::default();
let compressed = compress(&[], &config).unwrap();
assert_eq!(compressed.len(), 1);
let decompressed = decompress(&compressed, &config).unwrap();
assert_eq!(decompressed.len(), 0);
}
#[test]
fn test_decompress_empty_fails() {
let config = CompressionConfig::default();
let result = decompress(&[], &config);
assert!(result.is_err());
}
#[test]
fn test_all_algorithms_roundtrip() {
let test_data = generate_large_data(10000);
for algo in &[
CompressionAlgorithm::None,
CompressionAlgorithm::Lz4,
CompressionAlgorithm::Snappy,
CompressionAlgorithm::Zstd,
] {
let config = CompressionConfig::default()
.with_algorithm(*algo)
.with_threshold(0);
let compressed = compress(&test_data, &config).unwrap();
let decompressed = decompress(&compressed, &config).unwrap();
assert_eq!(
decompressed, test_data,
"Roundtrip failed for algorithm: {:?}",
algo
);
}
}
#[test]
fn test_compression_levels() {
let test_data = generate_large_data(10000);
for level in [1, 3, 10, 15] {
let config = CompressionConfig::default()
.with_algorithm(CompressionAlgorithm::Zstd)
.with_level(level)
.with_threshold(0);
let compressed = compress(&test_data, &config).unwrap();
let decompressed = decompress(&compressed, &config).unwrap();
assert_eq!(decompressed, test_data);
println!("Zstd level {}: {} bytes", level, compressed.len());
}
}
#[test]
fn test_stats() {
let mut stats = CompressionStats::default();
stats.compress_count = 100;
stats.bytes_in = 10000;
stats.bytes_out = 3000;
assert_eq!(stats.compression_ratio(), 0.3);
assert_eq!(stats.space_saved(), 7000);
assert_eq!(stats.space_saved_pct(), 70.0);
}
}