halldyll_core/fetch/
compression.rs

1//! Compression - Secure decompression (gzip, brotli, deflate)
2
3use bytes::Bytes;
4use flate2::read::{DeflateDecoder, GzDecoder};
5use std::io::Read;
6
7use crate::types::error::{Error, Result};
8
9/// Compression type
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CompressionType {
12    /// No compression
13    None,
14    /// Gzip compression
15    Gzip,
16    /// Deflate compression
17    Deflate,
18    /// Brotli compression
19    Brotli,
20}
21
22impl CompressionType {
23    /// Detects compression type from Content-Encoding
24    pub fn from_header(content_encoding: Option<&str>) -> Self {
25        match content_encoding.map(|s| s.to_lowercase()).as_deref() {
26            Some("gzip") | Some("x-gzip") => CompressionType::Gzip,
27            Some("deflate") => CompressionType::Deflate,
28            Some("br") => CompressionType::Brotli,
29            _ => CompressionType::None,
30        }
31    }
32}
33
34/// Decompressor with anti zip-bomb protection
35pub struct Decompressor {
36    /// Max decompressed size
37    max_size: u64,
38    /// Max compression ratio (anti zip-bomb)
39    max_ratio: f64,
40}
41
42impl Default for Decompressor {
43    fn default() -> Self {
44        Self {
45            max_size: 100 * 1024 * 1024, // 100 MB
46            max_ratio: 100.0,
47        }
48    }
49}
50
51impl Decompressor {
52    /// New decompressor with limits
53    pub fn new(max_size: u64, max_ratio: f64) -> Self {
54        Self { max_size, max_ratio }
55    }
56
57    /// Decompresses the data
58    pub fn decompress(&self, data: &[u8], compression: CompressionType) -> Result<Bytes> {
59        match compression {
60            CompressionType::None => Ok(Bytes::copy_from_slice(data)),
61            CompressionType::Gzip => self.decompress_gzip(data),
62            CompressionType::Deflate => self.decompress_deflate(data),
63            CompressionType::Brotli => self.decompress_brotli(data),
64        }
65    }
66
67    /// Decompresses gzip
68    fn decompress_gzip(&self, data: &[u8]) -> Result<Bytes> {
69        let mut decoder = GzDecoder::new(data);
70        self.read_with_limits(&mut decoder, data.len())
71    }
72
73    /// Decompresses deflate
74    fn decompress_deflate(&self, data: &[u8]) -> Result<Bytes> {
75        let mut decoder = DeflateDecoder::new(data);
76        self.read_with_limits(&mut decoder, data.len())
77    }
78
79    /// Decompresses brotli
80    fn decompress_brotli(&self, data: &[u8]) -> Result<Bytes> {
81        let mut decoder = brotli::Decompressor::new(data, 4096);
82        self.read_with_limits(&mut decoder, data.len())
83    }
84
85    /// Reads with limit verification
86    fn read_with_limits<R: Read>(&self, reader: &mut R, compressed_size: usize) -> Result<Bytes> {
87        let mut output = Vec::new();
88        let mut buffer = [0u8; 8192];
89        let mut total_read: u64 = 0;
90
91        loop {
92            let n = reader.read(&mut buffer).map_err(|e| {
93                Error::Decompression(format!("Read error: {}", e))
94            })?;
95
96            if n == 0 {
97                break;
98            }
99
100            total_read += n as u64;
101
102            // Max size verification
103            if total_read > self.max_size {
104                return Err(Error::SizeExceeded {
105                    max: self.max_size,
106                    actual: total_read,
107                });
108            }
109
110            // Ratio verification (anti zip-bomb)
111            if compressed_size > 0 {
112                let ratio = total_read as f64 / compressed_size as f64;
113                if ratio > self.max_ratio {
114                    return Err(Error::Decompression(format!(
115                        "Compression ratio {} exceeds limit {}",
116                        ratio, self.max_ratio
117                    )));
118                }
119            }
120
121            output.extend_from_slice(&buffer[..n]);
122        }
123
124        Ok(Bytes::from(output))
125    }
126}
127
128/// Decompression result
129#[derive(Debug)]
130pub struct DecompressionResult {
131    /// Decompressed data
132    pub data: Bytes,
133    /// Compressed size
134    pub compressed_size: u64,
135    /// Decompressed size
136    pub decompressed_size: u64,
137    /// Compression ratio
138    pub ratio: f64,
139}
140
141impl DecompressionResult {
142    /// Creates a new result
143    pub fn new(data: Bytes, compressed_size: u64) -> Self {
144        let decompressed_size = data.len() as u64;
145        let ratio = if compressed_size > 0 {
146            decompressed_size as f64 / compressed_size as f64
147        } else {
148            1.0
149        };
150        Self {
151            data,
152            compressed_size,
153            decompressed_size,
154            ratio,
155        }
156    }
157}