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_bounded(
185 data: &[u8],
186 algorithm: CompressionAlgorithm,
187 max_output_size: usize,
188) -> Result<Vec<u8>> {
189 if max_output_size > MAX_DECOMPRESSED_SIZE {
190 return Err(Error::PayloadTooLarge {
191 size: max_output_size,
192 limit: MAX_DECOMPRESSED_SIZE,
193 });
194 }
195
196 decompress_with_absolute_limit(data, algorithm, max_output_size)
197}
198
199fn decompress_with_absolute_limit(
200 data: &[u8],
201 algorithm: CompressionAlgorithm,
202 max_output_size: usize,
203) -> Result<Vec<u8>> {
204 match algorithm {
205 CompressionAlgorithm::None => {
206 if data.len() > max_output_size {
207 return Err(Error::PayloadTooLarge {
208 size: data.len(),
209 limit: max_output_size,
210 });
211 }
212 Ok(data.to_vec())
213 }
214 CompressionAlgorithm::Gzip => decompress_gzip(data, max_output_size),
215 CompressionAlgorithm::Brotli => decompress_brotli(data, max_output_size),
216 }
217}
218
219pub fn decompress_with_limits(
221 data: &[u8],
222 algorithm: CompressionAlgorithm,
223 max_output_size: usize,
224 max_ratio: usize,
225) -> Result<Vec<u8>> {
226 if max_ratio == 0 {
227 return Err(Error::InvalidConfiguration(
228 "decompression ratio limit must be greater than zero".to_string(),
229 ));
230 }
231
232 let ratio_limit = data.len().saturating_mul(max_ratio);
233 let effective_limit = max_output_size.min(ratio_limit);
234 decompress_with_absolute_limit(data, algorithm, effective_limit)
235}
236
237pub fn decompress_exact(
240 data: &[u8],
241 algorithm: CompressionAlgorithm,
242 expected_size: usize,
243) -> Result<Vec<u8>> {
244 if expected_size > MAX_DECOMPRESSED_SIZE {
245 return Err(Error::PayloadTooLarge {
246 size: expected_size,
247 limit: MAX_DECOMPRESSED_SIZE,
248 });
249 }
250 let output = decompress_with_limits(data, algorithm, expected_size, MAX_COMPRESSION_RATIO)?;
251 if output.len() != expected_size {
252 return Err(Error::SizeMismatch {
253 expected: expected_size,
254 actual: output.len(),
255 });
256 }
257 Ok(output)
258}
259
260fn read_decompressed_with_limit<R: std::io::Read>(
261 mut reader: R,
262 max_output_size: usize,
263) -> Result<Vec<u8>> {
264 let mut output = Vec::with_capacity(max_output_size.min(64 * 1024));
265 let mut buffer = vec![0u8; max_output_size.min(64 * 1024).max(1)];
266
267 loop {
268 let remaining = max_output_size.saturating_sub(output.len());
269 if remaining == 0 {
270 let mut probe = [0u8; 1];
271 let read = reader
272 .read(&mut probe)
273 .map_err(|e| Error::DecompressionFailed(e.to_string()))?;
274 if read == 0 {
275 return Ok(output);
276 }
277 return Err(Error::PayloadTooLarge {
278 size: max_output_size.saturating_add(1),
279 limit: max_output_size,
280 });
281 }
282
283 let read_capacity = remaining.min(buffer.len());
284 let read = reader
285 .read(&mut buffer[..read_capacity])
286 .map_err(|e| Error::DecompressionFailed(e.to_string()))?;
287 if read == 0 {
288 return Ok(output);
289 }
290
291 let required_capacity = output.len().saturating_add(read);
292 if required_capacity > output.capacity() {
293 let target_capacity = output
294 .capacity()
295 .saturating_mul(2)
296 .max(required_capacity)
297 .min(max_output_size);
298 output.reserve_exact(target_capacity.saturating_sub(output.len()));
299 }
300 output.extend_from_slice(&buffer[..read]);
301 }
302}
303
304fn compress_brotli(data: &[u8], level: u32) -> Result<(Vec<u8>, CompressionAlgorithm)> {
306 use brotli::enc::BrotliEncoderParams;
307
308 let mut output = Vec::new();
309 let mut params = BrotliEncoderParams::default();
310 params.quality = level as i32;
311
312 brotli::BrotliCompress(&mut std::io::Cursor::new(data), &mut output, ¶ms)
313 .map_err(|e| Error::CompressionFailed(e.to_string()))?;
314
315 Ok((output, CompressionAlgorithm::Brotli))
316}
317
318fn decompress_brotli(data: &[u8], max_output_size: usize) -> Result<Vec<u8>> {
320 let decoder = brotli::Decompressor::new(std::io::Cursor::new(data), 4096);
321 read_decompressed_with_limit(decoder, max_output_size)
322}
323
324fn compress_gzip(data: &[u8], level: u32) -> Result<(Vec<u8>, CompressionAlgorithm)> {
326 use flate2::write::GzEncoder;
327 use flate2::Compression;
328 use std::io::Write;
329
330 let mut encoder = GzEncoder::new(Vec::new(), Compression::new(level));
331 encoder
332 .write_all(data)
333 .map_err(|e| Error::CompressionFailed(e.to_string()))?;
334
335 let output = encoder
336 .finish()
337 .map_err(|e| Error::CompressionFailed(e.to_string()))?;
338
339 Ok((output, CompressionAlgorithm::Gzip))
340}
341
342fn decompress_gzip(data: &[u8], max_output_size: usize) -> Result<Vec<u8>> {
344 use flate2::read::GzDecoder;
345
346 read_decompressed_with_limit(GzDecoder::new(data), max_output_size)
347}
348
349pub fn serialize_with_header(result: &CompressionResult) -> Result<Vec<u8>> {
351 let original_size =
352 u32::try_from(result.original_size).map_err(|_| Error::PayloadTooLarge {
353 size: result.original_size,
354 limit: u32::MAX as usize,
355 })?;
356 let mut output = Vec::with_capacity(7 + result.compressed.len());
357
358 output.extend_from_slice(MAGIC_COMPRESSED);
360 output.push(result.algorithm as u8);
362 output.extend_from_slice(&original_size.to_be_bytes());
364 output.extend_from_slice(&result.compressed);
366
367 Ok(output)
368}
369
370pub fn deserialize_with_header(data: &[u8]) -> Result<(Vec<u8>, CompressionAlgorithm, usize)> {
372 if data.len() < 7 {
373 return Err(Error::TruncatedPayload {
374 expected: 7,
375 actual: data.len(),
376 });
377 }
378
379 if &data[0..2] != MAGIC_COMPRESSED {
381 return Err(Error::InvalidFormat);
382 }
383
384 let algorithm = CompressionAlgorithm::from_byte(data[2])?;
386
387 let original_size = u32::from_be_bytes([data[3], data[4], data[5], data[6]]) as usize;
389
390 let compressed = data[7..].to_vec();
392
393 Ok((compressed, algorithm, original_size))
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn test_gzip_roundtrip() {
402 let data = b"Hello, World! This is a test message that should be compressed.";
403
404 let result = compress(
405 data,
406 Some(CompressionOptions {
407 algorithm: CompressionAlgorithm::Gzip,
408 min_size_threshold: 10,
409 level: 6,
410 }),
411 )
412 .unwrap();
413
414 let decompressed = decompress(&result.compressed, result.algorithm).unwrap();
415 assert_eq!(data, &decompressed[..]);
416 }
417
418 #[test]
419 fn test_brotli_roundtrip() {
420 let data = b"Hello, World! This is a test message that should be compressed with Brotli.";
421
422 let result = compress(
423 data,
424 Some(CompressionOptions {
425 algorithm: CompressionAlgorithm::Brotli,
426 min_size_threshold: 10,
427 level: 6,
428 }),
429 )
430 .unwrap();
431
432 let decompressed = decompress(&result.compressed, result.algorithm).unwrap();
433 assert_eq!(data, &decompressed[..]);
434 }
435
436 #[test]
437 fn test_skip_small_data() {
438 let data = b"tiny";
439
440 let result = compress(
441 data,
442 Some(CompressionOptions {
443 algorithm: CompressionAlgorithm::Brotli,
444 min_size_threshold: 100, level: 6,
446 }),
447 )
448 .unwrap();
449
450 assert_eq!(result.algorithm, CompressionAlgorithm::None);
451 assert_eq!(result.compressed, data);
452 }
453
454 #[test]
455 fn test_header_serialization() {
456 let data = b"Test data for header serialization test with enough content.";
457
458 let result = compress(
459 data,
460 Some(CompressionOptions {
461 algorithm: CompressionAlgorithm::Gzip,
462 min_size_threshold: 10,
463 level: 6,
464 }),
465 )
466 .unwrap();
467
468 let serialized = serialize_with_header(&result).unwrap();
469 let (compressed, algorithm, original_size) = deserialize_with_header(&serialized).unwrap();
470
471 assert_eq!(algorithm, result.algorithm);
472 assert_eq!(original_size, result.original_size);
473 assert_eq!(compressed, result.compressed);
474 }
475
476 #[test]
477 fn test_decompression_rejects_bomb_before_full_expansion() {
478 let plaintext = vec![0u8; 2 * 1024 * 1024];
479 let (compressed, _) = compress_gzip(&plaintext, 6).unwrap();
480 assert!(compressed.len().saturating_mul(MAX_COMPRESSION_RATIO) < plaintext.len());
481 assert!(matches!(
482 decompress(&compressed, CompressionAlgorithm::Gzip),
483 Err(Error::PayloadTooLarge { .. })
484 ));
485 }
486
487 #[test]
488 fn test_decompression_exact_enforces_expected_size() {
489 let plaintext = b"bounded decompression".repeat(64);
490 let (compressed, _) = compress_gzip(&plaintext, 6).unwrap();
491 assert_eq!(
492 decompress_exact(&compressed, CompressionAlgorithm::Gzip, plaintext.len()).unwrap(),
493 plaintext
494 );
495 assert!(matches!(
496 decompress_exact(&compressed, CompressionAlgorithm::Gzip, plaintext.len() - 1),
497 Err(Error::PayloadTooLarge { .. }) | Err(Error::SizeMismatch { .. })
498 ));
499 }
500
501 #[test]
502 fn test_bounded_decompression_accepts_high_ratio_data_and_enforces_absolute_cap() {
503 let plaintext =
504 b"<section class=\"oathdoc-clause\">auditable terms</section>\n".repeat(40_000);
505 let streams = [
506 (
507 compress_brotli(&plaintext, 6).unwrap().0,
508 CompressionAlgorithm::Brotli,
509 ),
510 (
511 compress_gzip(&plaintext, 6).unwrap().0,
512 CompressionAlgorithm::Gzip,
513 ),
514 ];
515
516 for (compressed, algorithm) in streams {
517 assert!(compressed.len().saturating_mul(MAX_COMPRESSION_RATIO) < plaintext.len());
518 assert_eq!(
519 decompress_bounded(&compressed, algorithm, plaintext.len()).unwrap(),
520 plaintext
521 );
522 assert!(matches!(
523 decompress_bounded(&compressed, algorithm, plaintext.len() - 1,),
524 Err(Error::PayloadTooLarge { .. })
525 ));
526 }
527 }
528
529 #[test]
530 fn test_bounded_decompression_rejects_a_cap_above_the_global_ceiling() {
531 assert!(matches!(
532 decompress_bounded(
533 &[],
534 CompressionAlgorithm::None,
535 MAX_DECOMPRESSED_SIZE + 1,
536 ),
537 Err(Error::PayloadTooLarge {
538 size,
539 limit: MAX_DECOMPRESSED_SIZE,
540 }) if size == MAX_DECOMPRESSED_SIZE + 1
541 ));
542 }
543
544 #[test]
545 fn test_none_decompression_obeys_output_bound() {
546 assert!(matches!(
547 decompress_with_limits(&[0u8; 9], CompressionAlgorithm::None, 8, 256),
548 Err(Error::PayloadTooLarge { size: 9, limit: 8 })
549 ));
550 }
551
552 #[test]
553 fn test_compression_rejects_invalid_levels_before_processing() {
554 assert!(matches!(
555 compress(
556 b"small",
557 Some(CompressionOptions {
558 algorithm: CompressionAlgorithm::Gzip,
559 min_size_threshold: usize::MAX,
560 level: 10,
561 })
562 ),
563 Err(Error::InvalidConfiguration(_))
564 ));
565 assert!(matches!(
566 compress(
567 b"small",
568 Some(CompressionOptions {
569 algorithm: CompressionAlgorithm::Brotli,
570 min_size_threshold: usize::MAX,
571 level: 12,
572 })
573 ),
574 Err(Error::InvalidConfiguration(_))
575 ));
576 }
577}