use crate::error::{Error, Result};
use bytes::Bytes;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum CompressionAlgorithm {
#[default]
None,
Zstd,
Lz4,
}
impl CompressionAlgorithm {
#[inline]
pub fn name(&self) -> &'static str {
match self {
CompressionAlgorithm::None => "none",
CompressionAlgorithm::Zstd => "zstd",
CompressionAlgorithm::Lz4 => "lz4",
}
}
#[inline]
pub fn is_compressed(&self) -> bool {
!matches!(self, CompressionAlgorithm::None)
}
pub fn all() -> &'static [CompressionAlgorithm] {
&[
CompressionAlgorithm::None,
CompressionAlgorithm::Zstd,
CompressionAlgorithm::Lz4,
]
}
}
impl std::fmt::Display for CompressionAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
pub fn compress(data: &Bytes, algorithm: CompressionAlgorithm, level: u8) -> Result<Bytes> {
if level > 9 {
return Err(Error::InvalidInput(format!(
"Invalid compression level {}, must be 0-9",
level
)));
}
match algorithm {
CompressionAlgorithm::None => Ok(data.clone()),
CompressionAlgorithm::Zstd => compress_zstd(data, level),
CompressionAlgorithm::Lz4 => compress_lz4(data, level),
}
}
pub fn decompress(data: &Bytes, algorithm: CompressionAlgorithm) -> Result<Bytes> {
match algorithm {
CompressionAlgorithm::None => Ok(data.clone()),
CompressionAlgorithm::Zstd => decompress_zstd(data),
CompressionAlgorithm::Lz4 => decompress_lz4(data),
}
}
fn compress_zstd(data: &Bytes, level: u8) -> Result<Bytes> {
let zstd_level = if level == 0 {
1
} else {
1 + (level as i32 * 21 / 9)
};
let compressed = oxiarc_zstd::compress_with_level(data, zstd_level)
.map_err(|e| Error::Internal(format!("Zstd compression failed: {}", e)))?;
Ok(Bytes::from(compressed))
}
fn decompress_zstd(data: &Bytes) -> Result<Bytes> {
let decompressed = oxiarc_zstd::decompress(data)
.map_err(|e| Error::Internal(format!("Zstd decompression failed: {}", e)))?;
Ok(Bytes::from(decompressed))
}
fn compress_lz4(data: &Bytes, _level: u8) -> Result<Bytes> {
let payload = oxiarc_lz4::compress(data)
.map_err(|e| Error::Internal(format!("LZ4 compression failed: {}", e)))?;
let orig_len = data.len() as u32;
let mut out = Vec::with_capacity(4 + payload.len());
out.extend_from_slice(&orig_len.to_le_bytes());
out.extend_from_slice(&payload);
Ok(Bytes::from(out))
}
fn decompress_lz4(data: &Bytes) -> Result<Bytes> {
if data.len() < 4 {
return Err(Error::Internal(
"LZ4 block too short (missing original size header)".to_string(),
));
}
let orig_size = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let max_output = orig_size.saturating_mul(2).max(orig_size + 64);
let decompressed = oxiarc_lz4::decompress(&data[4..], max_output)
.map_err(|e| Error::Internal(format!("LZ4 decompression failed: {}", e)))?;
Ok(Bytes::from(decompressed))
}
pub fn compression_ratio(data: &Bytes, algorithm: CompressionAlgorithm, level: u8) -> Result<f64> {
if data.is_empty() {
return Ok(0.0);
}
let compressed = compress(data, algorithm, level)?;
Ok(compressed.len() as f64 / data.len() as f64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compression_none() {
let data = Bytes::from_static(b"Hello, World!");
let compressed = compress(&data, CompressionAlgorithm::None, 5).unwrap();
assert_eq!(data, compressed);
let decompressed = decompress(&compressed, CompressionAlgorithm::None).unwrap();
assert_eq!(data, decompressed);
}
#[test]
fn test_compression_zstd() {
let data = Bytes::from("Hello, World! ".repeat(100));
let compressed = compress(&data, CompressionAlgorithm::Zstd, 5).unwrap();
assert!(compressed.len() < data.len());
let decompressed = decompress(&compressed, CompressionAlgorithm::Zstd).unwrap();
assert_eq!(data, decompressed);
}
#[test]
fn test_compression_lz4() {
let data = Bytes::from("Hello, World! ".repeat(100));
let compressed = compress(&data, CompressionAlgorithm::Lz4, 5).unwrap();
assert!(compressed.len() < data.len());
let decompressed = decompress(&compressed, CompressionAlgorithm::Lz4).unwrap();
assert_eq!(data, decompressed);
}
#[test]
fn test_compression_levels() {
let data = Bytes::from_static(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
let compressed_0 = compress(&data, CompressionAlgorithm::Zstd, 0).unwrap();
let compressed_9 = compress(&data, CompressionAlgorithm::Zstd, 9).unwrap();
let decompressed_0 = decompress(&compressed_0, CompressionAlgorithm::Zstd).unwrap();
let decompressed_9 = decompress(&compressed_9, CompressionAlgorithm::Zstd).unwrap();
assert_eq!(data, decompressed_0);
assert_eq!(data, decompressed_9);
assert!(compressed_9.len() <= compressed_0.len());
}
#[test]
fn test_invalid_compression_level() {
let data = Bytes::from_static(b"Hello");
let result = compress(&data, CompressionAlgorithm::Zstd, 10);
assert!(result.is_err());
}
#[test]
fn test_compression_ratio() {
let data = Bytes::from("a".repeat(1000));
let ratio = compression_ratio(&data, CompressionAlgorithm::Zstd, 5).unwrap();
assert!(ratio < 0.1); }
#[test]
fn test_compression_algorithm_name() {
assert_eq!(CompressionAlgorithm::None.name(), "none");
assert_eq!(CompressionAlgorithm::Zstd.name(), "zstd");
assert_eq!(CompressionAlgorithm::Lz4.name(), "lz4");
}
#[test]
fn test_compression_algorithm_is_compressed() {
assert!(!CompressionAlgorithm::None.is_compressed());
assert!(CompressionAlgorithm::Zstd.is_compressed());
assert!(CompressionAlgorithm::Lz4.is_compressed());
}
#[test]
fn test_compression_algorithm_all() {
let all = CompressionAlgorithm::all();
assert_eq!(all.len(), 3);
assert!(all.contains(&CompressionAlgorithm::None));
assert!(all.contains(&CompressionAlgorithm::Zstd));
assert!(all.contains(&CompressionAlgorithm::Lz4));
}
#[test]
fn test_empty_data() {
let data = Bytes::new();
let compressed = compress(&data, CompressionAlgorithm::Zstd, 5).unwrap();
let decompressed = decompress(&compressed, CompressionAlgorithm::Zstd).unwrap();
assert_eq!(data, decompressed);
}
#[test]
fn test_large_data() {
let data = Bytes::from(vec![42u8; 1_000_000]); let compressed = compress(&data, CompressionAlgorithm::Zstd, 5).unwrap();
assert!(compressed.len() < data.len() / 100);
let decompressed = decompress(&compressed, CompressionAlgorithm::Zstd).unwrap();
assert_eq!(data, decompressed);
}
#[test]
fn test_algorithm_display() {
assert_eq!(CompressionAlgorithm::None.to_string(), "none");
assert_eq!(CompressionAlgorithm::Zstd.to_string(), "zstd");
assert_eq!(CompressionAlgorithm::Lz4.to_string(), "lz4");
}
#[test]
fn test_all_algorithms_roundtrip() {
let data = Bytes::from_static(b"The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.");
for algorithm in CompressionAlgorithm::all() {
for level in 0..=9 {
let compressed = compress(&data, *algorithm, level).unwrap();
let decompressed = decompress(&compressed, *algorithm).unwrap();
assert_eq!(
data, decompressed,
"Failed for {:?} at level {}",
algorithm, level
);
}
}
}
}