use super::{ProcessingResult, Processor};
use async_trait::async_trait;
use std::io::{self, Write};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompressionAlgorithm {
Gzip,
Brotli,
}
pub struct GzipCompressor {
level: u32,
}
impl GzipCompressor {
pub fn new() -> Self {
Self { level: 6 }
}
pub fn with_level(level: u32) -> Self {
Self {
level: level.min(9),
}
}
fn compress_gzip(&self, input: &[u8]) -> ProcessingResult<Vec<u8>> {
use flate2::Compression;
use flate2::write::GzEncoder;
let mut encoder = GzEncoder::new(Vec::new(), Compression::new(self.level));
encoder.write_all(input)?;
encoder.finish()
}
}
impl Default for GzipCompressor {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Processor for GzipCompressor {
async fn process(&self, input: &[u8], _path: &Path) -> ProcessingResult<Vec<u8>> {
self.compress_gzip(input)
}
fn can_process(&self, path: &Path) -> bool {
path.extension().is_some()
}
fn name(&self) -> &str {
"GzipCompressor"
}
}
pub struct BrotliCompressor {
quality: u32,
window_size: u32,
}
impl BrotliCompressor {
pub fn new() -> Self {
Self {
quality: 11,
window_size: 22,
}
}
pub fn with_settings(quality: u32, window_size: u32) -> Self {
Self {
quality: quality.min(11),
window_size: window_size.clamp(10, 24),
}
}
fn compress_brotli(&self, input: &[u8]) -> ProcessingResult<Vec<u8>> {
use brotli::enc::BrotliEncoderParams;
let mut output = Vec::new();
let params = BrotliEncoderParams {
quality: self.quality as i32,
lgwin: self.window_size as i32,
..Default::default()
};
brotli::BrotliCompress(&mut std::io::Cursor::new(input), &mut output, ¶ms)
.map_err(io::Error::other)?;
Ok(output)
}
}
impl Default for BrotliCompressor {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Processor for BrotliCompressor {
async fn process(&self, input: &[u8], _path: &Path) -> ProcessingResult<Vec<u8>> {
self.compress_brotli(input)
}
fn can_process(&self, path: &Path) -> bool {
path.extension().is_some()
}
fn name(&self) -> &str {
"BrotliCompressor"
}
}
#[derive(Debug, Clone)]
pub struct CompressionConfig {
pub gzip: bool,
pub gzip_level: u32,
pub brotli: bool,
pub brotli_quality: u32,
pub min_size: usize,
pub extensions: Vec<String>,
}
impl Default for CompressionConfig {
fn default() -> Self {
Self {
gzip: true,
gzip_level: 6,
brotli: true,
brotli_quality: 11,
min_size: 1024, extensions: vec![
"js".to_string(),
"css".to_string(),
"html".to_string(),
"json".to_string(),
"xml".to_string(),
"svg".to_string(),
],
}
}
}
impl CompressionConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_gzip(mut self, enable: bool) -> Self {
self.gzip = enable;
self
}
pub fn with_gzip_level(mut self, level: u32) -> Self {
self.gzip_level = level.min(9);
self
}
pub fn with_brotli(mut self, enable: bool) -> Self {
self.brotli = enable;
self
}
pub fn with_brotli_quality(mut self, quality: u32) -> Self {
self.brotli_quality = quality.min(11);
self
}
pub fn with_min_size(mut self, size: usize) -> Self {
self.min_size = size;
self
}
pub fn add_extension(mut self, ext: String) -> Self {
self.extensions.push(ext);
self
}
pub fn should_compress(&self, path: &Path, size: usize) -> bool {
if size < self.min_size {
return false;
}
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| self.extensions.iter().any(|e| e.eq_ignore_ascii_case(ext)))
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_gzip_compressor_creation() {
let compressor = GzipCompressor::new();
assert_eq!(compressor.level, 6);
}
#[test]
fn test_gzip_compressor_with_level() {
let compressor = GzipCompressor::with_level(9);
assert_eq!(compressor.level, 9);
let compressor2 = GzipCompressor::with_level(15);
assert_eq!(compressor2.level, 9); }
#[test]
fn test_gzip_can_process() {
let compressor = GzipCompressor::new();
assert!(compressor.can_process(&PathBuf::from("file.txt")));
assert!(compressor.can_process(&PathBuf::from("style.css")));
assert!(!compressor.can_process(&PathBuf::from("noext")));
}
#[tokio::test]
async fn test_gzip_compress() {
let compressor = GzipCompressor::new();
let input = b"Hello, World! This is test data that should be compressed.";
let result = compressor
.process(input, &PathBuf::from("test.txt"))
.await
.unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_brotli_compressor_creation() {
let compressor = BrotliCompressor::new();
assert_eq!(compressor.quality, 11);
assert_eq!(compressor.window_size, 22);
}
#[test]
fn test_brotli_compressor_with_settings() {
let compressor = BrotliCompressor::with_settings(9, 20);
assert_eq!(compressor.quality, 9);
assert_eq!(compressor.window_size, 20);
}
#[test]
fn test_brotli_quality_clamping() {
let compressor = BrotliCompressor::with_settings(20, 30);
assert_eq!(compressor.quality, 11);
assert_eq!(compressor.window_size, 24);
}
#[test]
fn test_brotli_can_process() {
let compressor = BrotliCompressor::new();
assert!(compressor.can_process(&PathBuf::from("file.txt")));
assert!(compressor.can_process(&PathBuf::from("style.css")));
}
#[tokio::test]
async fn test_brotli_compress() {
let compressor = BrotliCompressor::new();
let input = b"Hello, World! This is test data for brotli compression.";
let result = compressor
.process(input, &PathBuf::from("test.txt"))
.await
.unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_compression_config_default() {
let config = CompressionConfig::default();
assert!(config.gzip);
assert!(config.brotli);
assert_eq!(config.gzip_level, 6);
assert_eq!(config.brotli_quality, 11);
assert_eq!(config.min_size, 1024);
}
#[test]
fn test_compression_config_builder() {
let config = CompressionConfig::new()
.with_gzip(true)
.with_gzip_level(9)
.with_brotli(false)
.with_min_size(2048)
.add_extension("txt".to_string());
assert!(config.gzip);
assert!(!config.brotli);
assert_eq!(config.gzip_level, 9);
assert_eq!(config.min_size, 2048);
assert!(config.extensions.contains(&"txt".to_string()));
}
#[test]
fn test_should_compress() {
let config = CompressionConfig::new();
assert!(config.should_compress(&PathBuf::from("app.js"), 2000));
assert!(!config.should_compress(&PathBuf::from("app.js"), 500));
assert!(!config.should_compress(&PathBuf::from("image.png"), 2000));
assert!(config.should_compress(&PathBuf::from("style.css"), 2000));
}
#[test]
fn test_level_clamping() {
let config = CompressionConfig::new()
.with_gzip_level(20)
.with_brotli_quality(30);
assert_eq!(config.gzip_level, 9);
assert_eq!(config.brotli_quality, 11);
}
}