Skip to main content

rust_serv/handler/
compress.rs

1use crate::error::{Error, Result};
2use hyper::HeaderMap;
3
4/// Compression encoding type
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum CompressionType {
7    Gzip,
8    Brotli,
9    None,
10}
11
12/// Extract compression preference from Accept-Encoding header
13pub fn parse_accept_encoding(headers: &HeaderMap) -> CompressionType {
14    let accept_encoding = match headers.get("Accept-Encoding")
15        .and_then(|v| v.to_str().ok()) {
16        Some(h) => h,
17        None => return CompressionType::None,
18    };
19
20    // Check for brotli first (higher preference)
21    if accept_encoding.contains("br") {
22        return CompressionType::Brotli;
23    }
24
25    // Check for gzip
26    if accept_encoding.contains("gzip") {
27        return CompressionType::Gzip;
28    }
29
30    CompressionType::None
31}
32
33/// Check if content type should be skipped for compression
34pub fn should_skip_compression(content_type: &str) -> bool {
35    // Skip compression for already compressed formats, images, and small files
36    content_type.starts_with("image/")
37        || content_type.starts_with("video/")
38        || content_type.starts_with("audio/")
39        || content_type == "application/gzip"
40        || content_type == "application/x-gzip"
41        || content_type == "application/x-brotli"
42        || content_type == "application/zip"
43        || content_type == "application/x-rar-compressed"
44        || content_type == "application/x-7z-compressed"
45        || content_type == "application/x-tar"
46        || content_type == "application/x-tar-gz"
47}
48
49/// Compress data using gzip
50pub fn compress_gzip(data: &[u8]) -> Result<Vec<u8>> {
51    use flate2::write::GzEncoder;
52    use flate2::Compression;
53    use std::io::Write;
54
55    let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
56    encoder.write_all(data)?;
57    encoder.finish()
58        .map_err(|e| Error::Internal(format!("Gzip compression failed: {}", e)))
59}
60
61/// Compress data using brotli
62pub fn compress_brotli(data: &[u8]) -> Result<Vec<u8>> {
63    use std::io::Write;
64
65    let mut compressed = Vec::new();
66
67    // Use brotli compress with default settings
68    {
69        let quality = 11u32;
70        let lgwin = 22u32;
71        let mut encoder = brotli::CompressorWriter::new(&mut compressed, 4096, quality, lgwin);
72        encoder.write_all(data)
73            .map_err(|e| Error::Internal(format!("Brotli compression failed: {}", e)))?;
74        encoder.flush()
75            .map_err(|e| Error::Internal(format!("Brotli flush failed: {}", e)))?;
76        // encoder is dropped here, releasing the borrow on compressed
77    }
78
79    Ok(compressed)
80}
81
82/// Compress data based on the specified compression type
83pub fn compress(data: &[u8], compression_type: CompressionType) -> Result<Vec<u8>> {
84    // Don't compress very small data (overhead might be larger than benefit)
85    if data.len() < 512 {
86        return Ok(data.to_vec());
87    }
88
89    match compression_type {
90        CompressionType::Gzip => compress_gzip(data),
91        CompressionType::Brotli => compress_brotli(data),
92        CompressionType::None => Ok(data.to_vec()),
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use hyper::HeaderMap;
100
101    #[test]
102    fn test_parse_accept_encoding_gzip() {
103        let mut headers = HeaderMap::new();
104        headers.insert("Accept-Encoding", "gzip, deflate".parse().unwrap());
105        assert_eq!(parse_accept_encoding(&headers), CompressionType::Gzip);
106    }
107
108    #[test]
109    fn test_parse_accept_encoding_brotli() {
110        let mut headers = HeaderMap::new();
111        headers.insert("Accept-Encoding", "br, gzip".parse().unwrap());
112        assert_eq!(parse_accept_encoding(&headers), CompressionType::Brotli);
113    }
114
115    #[test]
116    fn test_parse_accept_encoding_none() {
117        let headers = HeaderMap::new();
118        assert_eq!(parse_accept_encoding(&headers), CompressionType::None);
119    }
120
121    #[test]
122    fn test_parse_accept_encoding_identity() {
123        let mut headers = HeaderMap::new();
124        headers.insert("Accept-Encoding", "identity".parse().unwrap());
125        assert_eq!(parse_accept_encoding(&headers), CompressionType::None);
126    }
127
128    #[test]
129    fn test_should_skip_compression_images() {
130        assert!(should_skip_compression("image/jpeg"));
131        assert!(should_skip_compression("image/png"));
132    }
133
134    #[test]
135    fn test_should_skip_compression_videos() {
136        assert!(should_skip_compression("video/mp4"));
137        assert!(should_skip_compression("video/webm"));
138    }
139
140    #[test]
141    fn test_should_skip_compression_audio() {
142        assert!(should_skip_compression("audio/mpeg"));
143        assert!(should_skip_compression("audio/ogg"));
144    }
145
146    #[test]
147    fn test_should_skip_compression_compressed() {
148        assert!(should_skip_compression("application/gzip"));
149        assert!(should_skip_compression("application/zip"));
150    }
151
152    #[test]
153    fn test_should_compress_text() {
154        assert!(!should_skip_compression("text/html"));
155        assert!(!should_skip_compression("application/json"));
156        assert!(!should_skip_compression("text/css"));
157        assert!(!should_skip_compression("text/javascript"));
158    }
159
160    #[test]
161    fn test_compress_gzip() {
162        let data = b"Hello, World! Hello, World! Hello, World!";
163        let compressed = compress_gzip(data).unwrap();
164
165        // Compressed data should be smaller
166        assert!(compressed.len() < data.len());
167    }
168
169    #[test]
170    fn test_compress_gzip_large() {
171        let data = b"Hello, World! ".repeat(100);
172        let compressed = compress_gzip(&data).unwrap();
173
174        // Compressed data should be significantly smaller for repetitive data
175        assert!(compressed.len() < data.len() / 2);
176    }
177
178    #[test]
179    fn test_compress_brotli() {
180        let data = b"Hello, World! Hello, World! Hello, World!";
181        let compressed = compress_brotli(data).unwrap();
182
183        // Compressed data should be smaller
184        assert!(compressed.len() < data.len());
185    }
186
187    #[test]
188    fn test_compress_none() {
189        let data = b"Hello, World!";
190        let compressed = compress(data, CompressionType::None).unwrap();
191
192        // Should return original data unchanged
193        assert_eq!(compressed, data.to_vec());
194    }
195
196    #[test]
197    fn test_compress_small_data() {
198        let data = b"Hi!";
199        let compressed = compress(data, CompressionType::Gzip).unwrap();
200
201        // Small data should not be compressed
202        assert_eq!(compressed, data.to_vec());
203    }
204
205    #[test]
206    fn test_compress_repetitive_data() {
207        let data = b"ABC ".repeat(1000);
208        let compressed = compress_gzip(&data).unwrap();
209
210        // Highly repetitive data compresses very well
211        assert!(compressed.len() < data.len() / 10);
212    }
213
214    #[test]
215    fn test_compress_with_none_type() {
216        let data = b"Hello, World! This is a test.";
217        let result = compress(data, CompressionType::None);
218        assert!(result.is_ok());
219        // With None type, data should be returned as-is
220        let compressed = result.unwrap();
221        assert_eq!(compressed, data.to_vec());
222    }
223}