1use crate::{Error, Result, MAGIC_COMPRESSED};
4use alloc::vec::Vec;
5use serde::{Deserialize, Serialize};
6
7pub const MAX_DECOMPRESSED_SIZE: usize = 512 * 1024 * 1024;
9
10pub const MAX_COMPRESSION_RATIO: usize = 256;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[repr(u8)]
16pub enum CompressionAlgorithm {
17 None = 0x00,
19 Gzip = 0x01,
21 Brotli = 0x02,
23}
24
25impl CompressionAlgorithm {
26 pub fn from_byte(byte: u8) -> Result<Self> {
28 match byte {
29 0x00 => Ok(CompressionAlgorithm::None),
30 0x01 => Ok(CompressionAlgorithm::Gzip),
31 0x02 => Ok(CompressionAlgorithm::Brotli),
32 _ => Err(Error::UnsupportedAlgorithm(byte)),
33 }
34 }
35
36 pub fn name(&self) -> &'static str {
38 match self {
39 CompressionAlgorithm::None => "none",
40 CompressionAlgorithm::Gzip => "gzip",
41 CompressionAlgorithm::Brotli => "brotli",
42 }
43 }
44
45 pub fn from_name(name: &str) -> Result<Self> {
47 match name.to_lowercase().as_str() {
48 "none" => Ok(CompressionAlgorithm::None),
49 "gzip" => Ok(CompressionAlgorithm::Gzip),
50 "brotli" => Ok(CompressionAlgorithm::Brotli),
51 _ => Err(Error::UnsupportedAlgorithm(0)),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct CompressionResult {
59 pub compressed: Vec<u8>,
61 pub algorithm: CompressionAlgorithm,
63 pub original_size: usize,
65 pub compressed_size: usize,
67 pub compression_ratio: f64,
69}
70
71#[derive(Debug, Clone)]
73pub struct CompressionOptions {
74 pub algorithm: CompressionAlgorithm,
76 pub min_size_threshold: usize,
78 pub level: u32,
80}
81
82impl Default for CompressionOptions {
83 fn default() -> Self {
84 Self {
85 algorithm: CompressionAlgorithm::Brotli,
86 min_size_threshold: 100,
87 level: 6,
88 }
89 }
90}
91
92pub fn compress(data: &[u8], options: Option<CompressionOptions>) -> Result<CompressionResult> {
94 let opts = options.unwrap_or_default();
95 validate_compression_level(opts.algorithm, opts.level)?;
96 let original_size = data.len();
97
98 if original_size < opts.min_size_threshold {
100 return Ok(CompressionResult {
101 compressed: data.to_vec(),
102 algorithm: CompressionAlgorithm::None,
103 original_size,
104 compressed_size: original_size,
105 compression_ratio: 1.0,
106 });
107 }
108
109 if opts.algorithm == CompressionAlgorithm::None {
111 return Ok(CompressionResult {
112 compressed: data.to_vec(),
113 algorithm: CompressionAlgorithm::None,
114 original_size,
115 compressed_size: original_size,
116 compression_ratio: 1.0,
117 });
118 }
119
120 let (compressed, algorithm) = match opts.algorithm {
121 CompressionAlgorithm::Brotli => compress_brotli(data, opts.level)?,
122 CompressionAlgorithm::Gzip => compress_gzip(data, opts.level)?,
123 CompressionAlgorithm::None => (data.to_vec(), CompressionAlgorithm::None),
124 };
125
126 let compressed_size = compressed.len();
127 let compression_ratio = compressed_size as f64 / original_size as f64;
128
129 let within_expansion_policy = compressed_size > 0
132 && original_size <= compressed_size.saturating_mul(MAX_COMPRESSION_RATIO);
133 if compression_ratio < 0.9 && within_expansion_policy {
134 Ok(CompressionResult {
135 compressed,
136 algorithm,
137 original_size,
138 compressed_size,
139 compression_ratio,
140 })
141 } else {
142 Ok(CompressionResult {
143 compressed: data.to_vec(),
144 algorithm: CompressionAlgorithm::None,
145 original_size,
146 compressed_size: original_size,
147 compression_ratio: 1.0,
148 })
149 }
150}
151
152fn validate_compression_level(algorithm: CompressionAlgorithm, level: u32) -> Result<()> {
153 let valid = match algorithm {
154 CompressionAlgorithm::None => level == 0 || level == CompressionOptions::default().level,
155 CompressionAlgorithm::Gzip => level <= 9,
156 CompressionAlgorithm::Brotli => level <= 11,
157 };
158 if valid {
159 Ok(())
160 } else {
161 Err(Error::InvalidConfiguration(format!(
162 "invalid compression level {level} for {}",
163 algorithm.name()
164 )))
165 }
166}
167
168pub fn decompress(data: &[u8], algorithm: CompressionAlgorithm) -> Result<Vec<u8>> {
170 decompress_with_limits(
171 data,
172 algorithm,
173 MAX_DECOMPRESSED_SIZE,
174 MAX_COMPRESSION_RATIO,
175 )
176}
177
178pub fn decompress_with_limits(
180 data: &[u8],
181 algorithm: CompressionAlgorithm,
182 max_output_size: usize,
183 max_ratio: usize,
184) -> Result<Vec<u8>> {
185 if max_ratio == 0 {
186 return Err(Error::InvalidConfiguration(
187 "decompression ratio limit must be greater than zero".to_string(),
188 ));
189 }
190
191 let ratio_limit = data.len().saturating_mul(max_ratio);
192 let effective_limit = max_output_size.min(ratio_limit);
193 match algorithm {
194 CompressionAlgorithm::None => {
195 if data.len() > max_output_size {
196 return Err(Error::PayloadTooLarge {
197 size: data.len(),
198 limit: max_output_size,
199 });
200 }
201 Ok(data.to_vec())
202 }
203 CompressionAlgorithm::Gzip => decompress_gzip(data, effective_limit),
204 CompressionAlgorithm::Brotli => decompress_brotli(data, effective_limit),
205 }
206}
207
208pub fn decompress_exact(
211 data: &[u8],
212 algorithm: CompressionAlgorithm,
213 expected_size: usize,
214) -> Result<Vec<u8>> {
215 if expected_size > MAX_DECOMPRESSED_SIZE {
216 return Err(Error::PayloadTooLarge {
217 size: expected_size,
218 limit: MAX_DECOMPRESSED_SIZE,
219 });
220 }
221 let output = decompress_with_limits(data, algorithm, expected_size, MAX_COMPRESSION_RATIO)?;
222 if output.len() != expected_size {
223 return Err(Error::SizeMismatch {
224 expected: expected_size,
225 actual: output.len(),
226 });
227 }
228 Ok(output)
229}
230
231fn read_decompressed_with_limit<R: std::io::Read>(
232 reader: R,
233 max_output_size: usize,
234) -> Result<Vec<u8>> {
235 use std::io::Read;
236
237 let read_limit = max_output_size.saturating_add(1) as u64;
238 let mut limited = reader.take(read_limit);
239 let mut output = Vec::with_capacity(max_output_size.min(64 * 1024));
240 limited
241 .read_to_end(&mut output)
242 .map_err(|e| Error::DecompressionFailed(e.to_string()))?;
243 if output.len() > max_output_size {
244 return Err(Error::PayloadTooLarge {
245 size: output.len(),
246 limit: max_output_size,
247 });
248 }
249 Ok(output)
250}
251
252fn compress_brotli(data: &[u8], level: u32) -> Result<(Vec<u8>, CompressionAlgorithm)> {
254 use brotli::enc::BrotliEncoderParams;
255
256 let mut output = Vec::new();
257 let mut params = BrotliEncoderParams::default();
258 params.quality = level as i32;
259
260 brotli::BrotliCompress(&mut std::io::Cursor::new(data), &mut output, ¶ms)
261 .map_err(|e| Error::CompressionFailed(e.to_string()))?;
262
263 Ok((output, CompressionAlgorithm::Brotli))
264}
265
266fn decompress_brotli(data: &[u8], max_output_size: usize) -> Result<Vec<u8>> {
268 let decoder = brotli::Decompressor::new(std::io::Cursor::new(data), 4096);
269 read_decompressed_with_limit(decoder, max_output_size)
270}
271
272fn compress_gzip(data: &[u8], level: u32) -> Result<(Vec<u8>, CompressionAlgorithm)> {
274 use flate2::write::GzEncoder;
275 use flate2::Compression;
276 use std::io::Write;
277
278 let mut encoder = GzEncoder::new(Vec::new(), Compression::new(level));
279 encoder
280 .write_all(data)
281 .map_err(|e| Error::CompressionFailed(e.to_string()))?;
282
283 let output = encoder
284 .finish()
285 .map_err(|e| Error::CompressionFailed(e.to_string()))?;
286
287 Ok((output, CompressionAlgorithm::Gzip))
288}
289
290fn decompress_gzip(data: &[u8], max_output_size: usize) -> Result<Vec<u8>> {
292 use flate2::read::GzDecoder;
293
294 read_decompressed_with_limit(GzDecoder::new(data), max_output_size)
295}
296
297pub fn serialize_with_header(result: &CompressionResult) -> Result<Vec<u8>> {
299 let original_size =
300 u32::try_from(result.original_size).map_err(|_| Error::PayloadTooLarge {
301 size: result.original_size,
302 limit: u32::MAX as usize,
303 })?;
304 let mut output = Vec::with_capacity(7 + result.compressed.len());
305
306 output.extend_from_slice(MAGIC_COMPRESSED);
308 output.push(result.algorithm as u8);
310 output.extend_from_slice(&original_size.to_be_bytes());
312 output.extend_from_slice(&result.compressed);
314
315 Ok(output)
316}
317
318pub fn deserialize_with_header(data: &[u8]) -> Result<(Vec<u8>, CompressionAlgorithm, usize)> {
320 if data.len() < 7 {
321 return Err(Error::TruncatedPayload {
322 expected: 7,
323 actual: data.len(),
324 });
325 }
326
327 if &data[0..2] != MAGIC_COMPRESSED {
329 return Err(Error::InvalidFormat);
330 }
331
332 let algorithm = CompressionAlgorithm::from_byte(data[2])?;
334
335 let original_size = u32::from_be_bytes([data[3], data[4], data[5], data[6]]) as usize;
337
338 let compressed = data[7..].to_vec();
340
341 Ok((compressed, algorithm, original_size))
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn test_gzip_roundtrip() {
350 let data = b"Hello, World! This is a test message that should be compressed.";
351
352 let result = compress(
353 data,
354 Some(CompressionOptions {
355 algorithm: CompressionAlgorithm::Gzip,
356 min_size_threshold: 10,
357 level: 6,
358 }),
359 )
360 .unwrap();
361
362 let decompressed = decompress(&result.compressed, result.algorithm).unwrap();
363 assert_eq!(data, &decompressed[..]);
364 }
365
366 #[test]
367 fn test_brotli_roundtrip() {
368 let data = b"Hello, World! This is a test message that should be compressed with Brotli.";
369
370 let result = compress(
371 data,
372 Some(CompressionOptions {
373 algorithm: CompressionAlgorithm::Brotli,
374 min_size_threshold: 10,
375 level: 6,
376 }),
377 )
378 .unwrap();
379
380 let decompressed = decompress(&result.compressed, result.algorithm).unwrap();
381 assert_eq!(data, &decompressed[..]);
382 }
383
384 #[test]
385 fn test_skip_small_data() {
386 let data = b"tiny";
387
388 let result = compress(
389 data,
390 Some(CompressionOptions {
391 algorithm: CompressionAlgorithm::Brotli,
392 min_size_threshold: 100, level: 6,
394 }),
395 )
396 .unwrap();
397
398 assert_eq!(result.algorithm, CompressionAlgorithm::None);
399 assert_eq!(result.compressed, data);
400 }
401
402 #[test]
403 fn test_header_serialization() {
404 let data = b"Test data for header serialization test with enough content.";
405
406 let result = compress(
407 data,
408 Some(CompressionOptions {
409 algorithm: CompressionAlgorithm::Gzip,
410 min_size_threshold: 10,
411 level: 6,
412 }),
413 )
414 .unwrap();
415
416 let serialized = serialize_with_header(&result).unwrap();
417 let (compressed, algorithm, original_size) = deserialize_with_header(&serialized).unwrap();
418
419 assert_eq!(algorithm, result.algorithm);
420 assert_eq!(original_size, result.original_size);
421 assert_eq!(compressed, result.compressed);
422 }
423
424 #[test]
425 fn test_decompression_rejects_bomb_before_full_expansion() {
426 let plaintext = vec![0u8; 2 * 1024 * 1024];
427 let (compressed, _) = compress_gzip(&plaintext, 6).unwrap();
428 assert!(compressed.len().saturating_mul(MAX_COMPRESSION_RATIO) < plaintext.len());
429 assert!(matches!(
430 decompress(&compressed, CompressionAlgorithm::Gzip),
431 Err(Error::PayloadTooLarge { .. })
432 ));
433 }
434
435 #[test]
436 fn test_decompression_exact_enforces_expected_size() {
437 let plaintext = b"bounded decompression".repeat(64);
438 let (compressed, _) = compress_gzip(&plaintext, 6).unwrap();
439 assert_eq!(
440 decompress_exact(&compressed, CompressionAlgorithm::Gzip, plaintext.len()).unwrap(),
441 plaintext
442 );
443 assert!(matches!(
444 decompress_exact(&compressed, CompressionAlgorithm::Gzip, plaintext.len() - 1),
445 Err(Error::PayloadTooLarge { .. }) | Err(Error::SizeMismatch { .. })
446 ));
447 }
448
449 #[test]
450 fn test_none_decompression_obeys_output_bound() {
451 assert!(matches!(
452 decompress_with_limits(&[0u8; 9], CompressionAlgorithm::None, 8, 256),
453 Err(Error::PayloadTooLarge { size: 9, limit: 8 })
454 ));
455 }
456
457 #[test]
458 fn test_compression_rejects_invalid_levels_before_processing() {
459 assert!(matches!(
460 compress(
461 b"small",
462 Some(CompressionOptions {
463 algorithm: CompressionAlgorithm::Gzip,
464 min_size_threshold: usize::MAX,
465 level: 10,
466 })
467 ),
468 Err(Error::InvalidConfiguration(_))
469 ));
470 assert!(matches!(
471 compress(
472 b"small",
473 Some(CompressionOptions {
474 algorithm: CompressionAlgorithm::Brotli,
475 min_size_threshold: usize::MAX,
476 level: 12,
477 })
478 ),
479 Err(Error::InvalidConfiguration(_))
480 ));
481 }
482}