Skip to main content

compress_lib/
limits.rs

1//! Decoder limits: the defence against decompression bombs.
2
3/// Caps a decoder enforces while it decodes.
4///
5/// Every check runs per output chunk, before the chunk is written, so a
6/// stream that would pass a cap stops at the cap; a decompression bomb costs
7/// at most `max_output` bytes of output and the time to produce them.
8///
9/// | Limit | Default | Cost |
10/// |---|---|---|
11/// | `max_output` | 64 MiB | one compare per output chunk |
12/// | `max_ratio` | off | one multiply and compare per output chunk |
13/// | `ratio_floor` | 1 MiB | (the ratio is not checked below it) |
14/// | `max_header` (gzip) | 64 KiB | one counter per header byte |
15/// | `max_members` (gzip) | 1024 | one counter per member |
16///
17/// The ratio cap is off by default: DEFLATE's largest possible ratio is
18/// about 1032 to 1, legitimate data (zero-filled images, repeated logs)
19/// reaches hundreds to one, and the absolute cap is what bounds memory. Turn
20/// it on when you know your data, for example at 50 for JSON APIs.
21///
22/// # Examples
23///
24/// ```
25/// use compress_lib::Limits;
26///
27/// let limits = Limits::new().with_max_output(1 << 20).with_max_ratio(50);
28/// assert_eq!(limits.max_output(), 1 << 20);
29/// assert_eq!(limits.max_ratio(), 50);
30/// ```
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
32#[non_exhaustive]
33pub struct Limits {
34    max_output: u64,
35    max_ratio: u32,
36    ratio_floor: u64,
37    max_header: usize,
38    max_members: u32,
39}
40
41impl Limits {
42    /// The default absolute output cap: 64 MiB.
43    pub const DEFAULT_MAX_OUTPUT: u64 = 64 << 20;
44    /// The default output size below which the ratio is not checked: 1 MiB.
45    pub const DEFAULT_RATIO_FLOOR: u64 = 1 << 20;
46    /// The default cap on gzip optional header fields together: 64 KiB.
47    pub const DEFAULT_MAX_HEADER: usize = 64 << 10;
48    /// The default cap on gzip members in one stream: 1024.
49    pub const DEFAULT_MAX_MEMBERS: u32 = 1024;
50
51    /// The safe defaults (see the table on [`Limits`]).
52    ///
53    /// # Examples
54    ///
55    /// ```
56    /// use compress_lib::Limits;
57    ///
58    /// assert_eq!(Limits::new().max_output(), 64 << 20);
59    /// assert_eq!(Limits::new(), Limits::default());
60    /// ```
61    #[must_use]
62    pub const fn new() -> Self {
63        Self {
64            max_output: Self::DEFAULT_MAX_OUTPUT,
65            max_ratio: 0,
66            ratio_floor: Self::DEFAULT_RATIO_FLOOR,
67            max_header: Self::DEFAULT_MAX_HEADER,
68            max_members: Self::DEFAULT_MAX_MEMBERS,
69        }
70    }
71
72    /// No output cap and no ratio cap, for trusted data such as your own
73    /// files. Named so that its use stands out in review.
74    ///
75    /// # Examples
76    ///
77    /// ```
78    /// use compress_lib::Limits;
79    ///
80    /// assert_eq!(Limits::unlimited().max_output(), u64::MAX);
81    /// ```
82    #[must_use]
83    pub const fn unlimited() -> Self {
84        Self {
85            max_output: u64::MAX,
86            max_ratio: 0,
87            ratio_floor: Self::DEFAULT_RATIO_FLOOR,
88            max_header: Self::DEFAULT_MAX_HEADER,
89            max_members: u32::MAX,
90        }
91    }
92
93    /// Sets the total output a decoder may produce.
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// let limits = compress_lib::Limits::new().with_max_output(16 << 20);
99    /// assert_eq!(limits.max_output(), 16 << 20);
100    /// ```
101    #[must_use]
102    pub const fn with_max_output(mut self, bytes: u64) -> Self {
103        self.max_output = bytes;
104        self
105    }
106
107    /// Sets the largest allowed ratio of output to input consumed, checked
108    /// once the output passes the ratio floor. `0` turns the check off.
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// let limits = compress_lib::Limits::new().with_max_ratio(100);
114    /// assert_eq!(limits.max_ratio(), 100);
115    /// ```
116    #[must_use]
117    pub const fn with_max_ratio(mut self, ratio: u32) -> Self {
118        self.max_ratio = ratio;
119        self
120    }
121
122    /// Sets the output size below which the ratio is not checked (small
123    /// inputs legitimately have high ratios).
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// let limits = compress_lib::Limits::new().with_ratio_floor(64 << 10);
129    /// assert_eq!(limits.ratio_floor(), 64 << 10);
130    /// ```
131    #[must_use]
132    pub const fn with_ratio_floor(mut self, bytes: u64) -> Self {
133        self.ratio_floor = bytes;
134        self
135    }
136
137    /// Sets the cap on the gzip header's optional fields (FEXTRA, FNAME,
138    /// FCOMMENT) together.
139    ///
140    /// # Examples
141    ///
142    /// ```
143    /// let limits = compress_lib::Limits::new().with_max_header(1024);
144    /// assert_eq!(limits.max_header(), 1024);
145    /// ```
146    #[must_use]
147    pub const fn with_max_header(mut self, bytes: usize) -> Self {
148        self.max_header = bytes;
149        self
150    }
151
152    /// Sets the cap on members in one gzip stream.
153    ///
154    /// # Examples
155    ///
156    /// ```
157    /// let limits = compress_lib::Limits::new().with_max_members(1);
158    /// assert_eq!(limits.max_members(), 1);
159    /// ```
160    #[must_use]
161    pub const fn with_max_members(mut self, members: u32) -> Self {
162        self.max_members = members;
163        self
164    }
165
166    /// The total output cap.
167    ///
168    /// # Examples
169    ///
170    /// ```
171    /// assert_eq!(compress_lib::Limits::new().max_output(), 64 << 20);
172    /// ```
173    #[must_use]
174    pub const fn max_output(&self) -> u64 {
175        self.max_output
176    }
177
178    /// The ratio cap (0 = off).
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// assert_eq!(compress_lib::Limits::new().max_ratio(), 0);
184    /// ```
185    #[must_use]
186    pub const fn max_ratio(&self) -> u32 {
187        self.max_ratio
188    }
189
190    /// The output size below which the ratio is not checked.
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// assert_eq!(compress_lib::Limits::new().ratio_floor(), 1 << 20);
196    /// ```
197    #[must_use]
198    pub const fn ratio_floor(&self) -> u64 {
199        self.ratio_floor
200    }
201
202    /// The gzip header field cap.
203    ///
204    /// # Examples
205    ///
206    /// ```
207    /// assert_eq!(compress_lib::Limits::new().max_header(), 64 << 10);
208    /// ```
209    #[must_use]
210    pub const fn max_header(&self) -> usize {
211        self.max_header
212    }
213
214    /// The gzip member cap.
215    ///
216    /// # Examples
217    ///
218    /// ```
219    /// assert_eq!(compress_lib::Limits::new().max_members(), 1024);
220    /// ```
221    #[must_use]
222    pub const fn max_members(&self) -> u32 {
223        self.max_members
224    }
225
226    /// `true` when `output` bytes from `input` bytes break the ratio cap.
227    #[cfg(test)]
228    #[must_use]
229    pub(crate) const fn ratio_exceeded(&self, output: u64, input: u64) -> bool {
230        self.max_ratio != 0
231            && output > self.ratio_floor
232            && output > input.saturating_mul(self.max_ratio as u64)
233    }
234}
235
236impl Default for Limits {
237    fn default() -> Self {
238        Self::new()
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_limits_defaults_are_fail_closed() {
248        let l = Limits::new();
249        assert_eq!(l.max_output(), 64 << 20);
250        assert_eq!(l.max_ratio(), 0);
251        assert_eq!(l.ratio_floor(), 1 << 20);
252        assert_eq!(l.max_header(), 64 << 10);
253        assert_eq!(l.max_members(), 1024);
254        assert_eq!(Limits::default(), l);
255    }
256
257    #[test]
258    fn test_limits_ratio_off_never_exceeded() {
259        assert!(!Limits::new().ratio_exceeded(u64::MAX, 1));
260    }
261
262    #[test]
263    fn test_limits_ratio_below_floor_not_checked() {
264        let l = Limits::new().with_max_ratio(2).with_ratio_floor(100);
265        assert!(!l.ratio_exceeded(100, 1));
266        assert!(l.ratio_exceeded(101, 1));
267        assert!(!l.ratio_exceeded(101, 51));
268        assert!(l.ratio_exceeded(103, 51));
269    }
270
271    #[test]
272    fn test_limits_ratio_saturates_on_huge_input() {
273        let l = Limits::new().with_max_ratio(u32::MAX).with_ratio_floor(0);
274        assert!(!l.ratio_exceeded(u64::MAX, u64::MAX));
275    }
276
277    #[test]
278    fn test_limits_unlimited_has_no_output_cap() {
279        let l = Limits::unlimited();
280        assert_eq!(l.max_output(), u64::MAX);
281        assert_eq!(l.max_members(), u32::MAX);
282        assert_eq!(l.with_max_members(3).max_members(), 3);
283    }
284}