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}