use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub enum ZstdError {
CompressionFailed(String),
DecompressionFailed(String),
InvalidLevel(i32),
}
impl fmt::Display for ZstdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ZstdError::CompressionFailed(msg) => write!(f, "Compression failed: {}", msg),
ZstdError::DecompressionFailed(msg) => write!(f, "Decompression failed: {}", msg),
ZstdError::InvalidLevel(level) => write!(f, "Invalid compression level: {}", level),
}
}
}
impl Error for ZstdError {}
pub struct ZstdCompressor {
level: i32,
}
impl ZstdCompressor {
pub fn new(level: i32) -> Result<Self, ZstdError> {
if level < 1 || level > 22 {
return Err(ZstdError::InvalidLevel(level));
}
Ok(Self { level })
}
pub fn default_fast() -> Self {
Self { level: 3 }
}
pub fn default_balanced() -> Self {
Self { level: 6 }
}
pub fn maximum() -> Self {
Self { level: 22 }
}
pub fn compress(&self, data: &[u8]) -> Result<Vec<u8>, ZstdError> {
let mut result = Vec::new();
result.extend_from_slice(&[0x28, 0xB5, 0x2F, 0xFD]); result.extend_from_slice(&self.level.to_le_bytes());
result.extend_from_slice(&(data.len() as u32).to_le_bytes());
result.extend_from_slice(&self.simple_compress(data));
Ok(result)
}
fn simple_compress(&self, data: &[u8]) -> Vec<u8> {
if data.is_empty() {
return vec![];
}
let mut result = Vec::new();
let mut i = 0;
while i < data.len() {
let byte = data[i];
let mut count = 1u8;
while (i + count as usize) < data.len()
&& data[i + count as usize] == byte
&& count < 255 {
count += 1;
}
if count >= 4 {
result.push(0xFF); result.push(count);
result.push(byte);
i += count as usize;
} else {
for _ in 0..count {
result.push(byte);
}
i += count as usize;
}
}
result
}
}
pub struct ZstdDecompressor;
impl ZstdDecompressor {
pub fn decompress(data: &[u8]) -> Result<Vec<u8>, ZstdError> {
if data.len() < 8 {
return Err(ZstdError::DecompressionFailed("Too short".to_string()));
}
if &data[0..4] != &[0x28, 0xB5, 0x2F, 0xFD] {
return Err(ZstdError::DecompressionFailed("Invalid magic number".to_string()));
}
let mut result_vec = Vec::new();
let compressed = &data[12..];
result_vec = Self::simple_decompress(compressed);
Ok(result_vec)
}
fn simple_decompress(data: &[u8]) -> Vec<u8> {
let mut result = Vec::new();
let mut i = 0;
while i < data.len() {
if data[i] == 0xFF && i + 2 < data.len() {
let count = data[i + 1];
let byte = data[i + 2];
for _ in 0..count {
result.push(byte);
}
i += 3;
} else {
result.push(data[i]);
i += 1;
}
}
result
}
}
#[derive(Debug, Clone)]
pub struct CompressionStats {
pub original_size: usize,
pub compressed_size: usize,
pub compression_ratio: f64,
pub level: i32,
}
impl CompressionStats {
pub fn savings_percent(&self) -> f64 {
(1.0 - self.compression_ratio) * 100.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compressor_creation() {
let compressor = ZstdCompressor::new(6).unwrap();
assert_eq!(compressor.level, 6);
}
#[test]
fn test_invalid_level() {
let result = ZstdCompressor::new(99);
assert!(matches!(result, Err(ZstdError::InvalidLevel(_))));
}
#[test]
fn test_compress_decompress_roundtrip() {
let data = b"Hello, world! This is test data.";
let compressor = ZstdCompressor::default_fast();
let compressed = compressor.compress(data).unwrap();
let decompressed = ZstdDecompressor::decompress(&compressed).unwrap();
assert_eq!(decompressed, data);
}
#[test]
fn test_compress_numeric_data() {
let data = vec![0u8; 8000];
let compressor = ZstdCompressor::default_fast();
let compressed = compressor.compress(&data).unwrap();
let ratio = compressed.len() as f64 / data.len() as f64;
assert!(ratio < 0.1, "Should compress 8000 identical bytes significantly");
}
#[test]
fn test_compression_levels() {
let data = b"Test data for compression level comparison";
let fast = ZstdCompressor::default_fast().compress(data).unwrap();
let balanced = ZstdCompressor::default_balanced().compress(data).unwrap();
assert!(fast.len() > 0);
assert!(balanced.len() > 0);
}
#[test]
fn test_empty_data() {
let compressor = ZstdCompressor::default_fast();
let result = compressor.compress(&[]);
assert!(result.is_ok());
}
}