pdf_engine/limits.rs
1//! Resource limits for PDF processing.
2//!
3//! These limits protect against adversarial inputs that could cause OOM, stack overflow,
4//! CPU exhaustion, or zip bombs. All limits are enforced with clean error returns — no panics.
5//!
6//! # Default limits
7//!
8//! The defaults cover 99.9% of real-world PDFs while providing strong safety guarantees:
9//!
10//! | Resource | Default |
11//! |---|---|
12//! | PDF file size | 500 MB |
13//! | Single decompressed stream | 256 MB |
14//! | Total memory per document | 1 GB |
15//! | Object reference depth | 100 levels |
16//! | Content stream operators | 10,000,000 |
17//! | Image pixel count | 256 megapixels (16384×16384) |
18//! | XFA template nesting depth | 50 levels |
19//! | FormCalc recursion depth | 200 levels |
20
21// Copyright (c) 2026 Innovation Trigger B.V.
22//
23// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
24// the PDFluent Commercial Licence. See the LICENSE file in this repository --
25// that file travels with the copy you received, which a URL does not.
26
27/// Resource limits for a single PDF processing operation.
28///
29/// Construct via [`ProcessingLimits::default()`] for standard limits,
30/// or use the builder methods to customize for your use case.
31///
32/// # Examples
33///
34/// ```rust
35/// use pdf_engine::limits::ProcessingLimits;
36///
37/// // Default limits (recommended for server-side processing):
38/// let limits = ProcessingLimits::default();
39///
40/// // Stricter limits for WASM/browser context:
41/// let wasm_limits = ProcessingLimits::wasm();
42///
43/// // Custom limits:
44/// let custom = ProcessingLimits::default()
45/// .max_file_bytes(100 * 1024 * 1024) // 100 MB
46/// .max_stream_bytes(64 * 1024 * 1024); // 64 MB per stream
47/// ```
48#[derive(Debug, Clone)]
49pub struct ProcessingLimits {
50 /// Maximum PDF file size in bytes. Default: 500 MB.
51 pub max_file_bytes: u64,
52 /// Maximum decompressed size of any single stream. Default: 256 MB.
53 /// Prevents zip bombs via FlateDecode or LZWDecode.
54 pub max_stream_bytes: u64,
55 /// Maximum total memory allocated per document. Default: 1 GB.
56 pub max_total_memory_bytes: u64,
57 /// Maximum object reference depth (prevents stack overflow on recursive refs). Default: 100.
58 pub max_object_depth: u32,
59 /// Maximum content stream operators per page. Default: 10,000,000.
60 pub max_operator_count: u64,
61 /// Maximum pixel count per image (width × height). Default: 268,435,456 (16384²).
62 pub max_image_pixels: u64,
63 /// Maximum XFA template XML nesting depth. Default: 50.
64 pub max_xfa_nesting_depth: u32,
65 /// Maximum FormCalc recursion depth. Default: 200.
66 pub max_formcalc_depth: u32,
67}
68
69impl Default for ProcessingLimits {
70 fn default() -> Self {
71 Self {
72 max_file_bytes: 500 * 1024 * 1024, // 500 MB
73 max_stream_bytes: 256 * 1024 * 1024, // 256 MB
74 max_total_memory_bytes: 1024 * 1024 * 1024, // 1 GB
75 max_object_depth: 100,
76 max_operator_count: 10_000_000,
77 max_image_pixels: 16384 * 16384, // 268 MP
78 max_xfa_nesting_depth: 50,
79 max_formcalc_depth: 200,
80 }
81 }
82}
83
84impl ProcessingLimits {
85 /// Create a new set of limits with default values.
86 pub fn new() -> Self {
87 Self::default()
88 }
89
90 /// Strict limits for WASM/browser contexts with limited memory.
91 ///
92 /// - Max file: 50 MB
93 /// - Max stream: 32 MB
94 /// - Max total memory: 128 MB
95 /// - Image pixels: 64 MP (8192×8192)
96 pub fn wasm() -> Self {
97 Self {
98 max_file_bytes: 50 * 1024 * 1024, // 50 MB
99 max_stream_bytes: 32 * 1024 * 1024, // 32 MB
100 max_total_memory_bytes: 128 * 1024 * 1024, // 128 MB
101 max_object_depth: 50,
102 max_operator_count: 1_000_000,
103 max_image_pixels: 8192 * 8192, // 64 MP
104 max_xfa_nesting_depth: 30,
105 max_formcalc_depth: 100,
106 }
107 }
108
109 /// Unlimited: no resource limits (use only in trusted environments).
110 pub fn unlimited() -> Self {
111 Self {
112 max_file_bytes: u64::MAX,
113 max_stream_bytes: u64::MAX,
114 max_total_memory_bytes: u64::MAX,
115 max_object_depth: u32::MAX,
116 max_operator_count: u64::MAX,
117 max_image_pixels: u64::MAX,
118 max_xfa_nesting_depth: u32::MAX,
119 max_formcalc_depth: u32::MAX,
120 }
121 }
122
123 /// Set maximum PDF file size.
124 pub fn max_file_bytes(mut self, bytes: u64) -> Self {
125 self.max_file_bytes = bytes;
126 self
127 }
128
129 /// Set maximum decompressed stream size.
130 pub fn max_stream_bytes(mut self, bytes: u64) -> Self {
131 self.max_stream_bytes = bytes;
132 self
133 }
134
135 /// Set maximum total memory per document.
136 pub fn max_total_memory_bytes(mut self, bytes: u64) -> Self {
137 self.max_total_memory_bytes = bytes;
138 self
139 }
140
141 /// Set maximum object reference depth.
142 pub fn max_object_depth(mut self, depth: u32) -> Self {
143 self.max_object_depth = depth;
144 self
145 }
146
147 /// Set maximum content stream operator count.
148 pub fn max_operator_count(mut self, count: u64) -> Self {
149 self.max_operator_count = count;
150 self
151 }
152
153 /// Set maximum image pixel count (width × height).
154 pub fn max_image_pixels(mut self, pixels: u64) -> Self {
155 self.max_image_pixels = pixels;
156 self
157 }
158
159 /// Set maximum XFA template nesting depth.
160 pub fn max_xfa_nesting_depth(mut self, depth: u32) -> Self {
161 self.max_xfa_nesting_depth = depth;
162 self
163 }
164
165 /// Set maximum FormCalc recursion depth.
166 pub fn max_formcalc_depth(mut self, depth: u32) -> Self {
167 self.max_formcalc_depth = depth;
168 self
169 }
170
171 /// Check if a file size is within limits. Returns `Err` with a descriptive message if exceeded.
172 pub fn check_file_size(&self, bytes: u64) -> Result<(), LimitError> {
173 if bytes > self.max_file_bytes {
174 Err(LimitError::FileTooLarge {
175 actual_bytes: bytes,
176 limit_bytes: self.max_file_bytes,
177 })
178 } else {
179 Ok(())
180 }
181 }
182
183 /// Check if a decompressed stream size is within limits.
184 pub fn check_stream_size(&self, bytes: u64) -> Result<(), LimitError> {
185 if bytes > self.max_stream_bytes {
186 Err(LimitError::StreamTooLarge {
187 actual_bytes: bytes,
188 limit_bytes: self.max_stream_bytes,
189 })
190 } else {
191 Ok(())
192 }
193 }
194
195 /// Check if image dimensions are within limits.
196 pub fn check_image_pixels(&self, width: u64, height: u64) -> Result<(), LimitError> {
197 let pixels = width.saturating_mul(height);
198 if pixels > self.max_image_pixels {
199 Err(LimitError::ImageTooLarge {
200 width,
201 height,
202 pixels,
203 limit_pixels: self.max_image_pixels,
204 })
205 } else {
206 Ok(())
207 }
208 }
209
210 /// Check if an object depth is within limits.
211 pub fn check_object_depth(&self, depth: u32) -> Result<(), LimitError> {
212 if depth > self.max_object_depth {
213 Err(LimitError::ObjectDepthExceeded {
214 depth,
215 limit: self.max_object_depth,
216 })
217 } else {
218 Ok(())
219 }
220 }
221}
222
223/// Error returned when a configured resource limit is exceeded.
224///
225/// A `LimitError` is a hard stop on the current operation — the caller
226/// should either raise the relevant cap on the [`ProcessingLimits`] used
227/// to construct the engine, or refuse the input. All variants carry both
228/// the observed value and the limit so callers can produce useful error
229/// messages without re-running detection.
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub enum LimitError {
232 /// PDF file size exceeded [`ProcessingLimits::max_file_bytes`].
233 /// Defends against trivially-DoS-ing the parser with an oversized
234 /// input.
235 FileTooLarge {
236 /// Observed file size in bytes.
237 actual_bytes: u64,
238 /// Configured maximum file size in bytes.
239 limit_bytes: u64,
240 },
241 /// A decompressed stream exceeded
242 /// [`ProcessingLimits::max_stream_bytes`]. Defends against
243 /// "decompression bomb" inputs (Flate, LZW, ASCII85) that expand
244 /// far beyond their compressed footprint.
245 StreamTooLarge {
246 /// Observed decompressed stream size in bytes.
247 actual_bytes: u64,
248 /// Configured maximum stream size in bytes.
249 limit_bytes: u64,
250 },
251 /// An image XObject exceeded [`ProcessingLimits::max_image_pixels`].
252 /// `pixels = width * height` regardless of color depth. Guards the
253 /// memory used by rasterization and color-space conversion.
254 ImageTooLarge {
255 /// Observed image width in pixels.
256 width: u64,
257 /// Observed image height in pixels.
258 height: u64,
259 /// Observed total pixel count (`width * height`).
260 pixels: u64,
261 /// Configured maximum pixel count.
262 limit_pixels: u64,
263 },
264 /// Indirect-object reference chain exceeded
265 /// [`ProcessingLimits::max_object_depth`]. Defends against cyclic or
266 /// pathologically nested object graphs that would otherwise blow
267 /// the parser stack.
268 ObjectDepthExceeded {
269 /// Observed reference-chain depth.
270 depth: u32,
271 /// Configured maximum reference-chain depth.
272 limit: u32,
273 },
274 /// Content-stream operator count exceeded
275 /// [`ProcessingLimits::max_operator_count`]. Caps per-page
276 /// rendering work so a billion no-op operators can't pin a CPU.
277 TooManyOperators {
278 /// Observed operator count.
279 count: u64,
280 /// Configured maximum operator count.
281 limit: u64,
282 },
283 /// XFA template subform nesting exceeded
284 /// [`ProcessingLimits::max_xfa_nesting_depth`]. Defends the XFA
285 /// layout engine against pathologically nested templates.
286 XfaNestingTooDeep {
287 /// Observed XFA subform nesting depth.
288 depth: u32,
289 /// Configured maximum nesting depth.
290 limit: u32,
291 },
292 /// FormCalc expression recursion exceeded
293 /// [`ProcessingLimits::max_formcalc_depth`]. Stops infinite or
294 /// deeply mutually-recursive expressions from blowing the FormCalc
295 /// interpreter stack.
296 FormCalcRecursionTooDeep {
297 /// Observed recursion depth.
298 depth: u32,
299 /// Configured maximum recursion depth.
300 limit: u32,
301 },
302}
303
304impl std::fmt::Display for LimitError {
305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306 match self {
307 Self::FileTooLarge {
308 actual_bytes,
309 limit_bytes,
310 } => write!(
311 f,
312 "PDF file too large: {} MB (limit: {} MB)",
313 actual_bytes / 1024 / 1024,
314 limit_bytes / 1024 / 1024
315 ),
316 Self::StreamTooLarge {
317 actual_bytes,
318 limit_bytes,
319 } => write!(
320 f,
321 "Decompressed stream too large: {} MB (limit: {} MB)",
322 actual_bytes / 1024 / 1024,
323 limit_bytes / 1024 / 1024
324 ),
325 Self::ImageTooLarge {
326 width,
327 height,
328 pixels,
329 limit_pixels,
330 } => write!(
331 f,
332 "Image too large: {}×{} ({} MP, limit: {} MP)",
333 width,
334 height,
335 pixels / 1_000_000,
336 limit_pixels / 1_000_000
337 ),
338 Self::ObjectDepthExceeded { depth, limit } => write!(
339 f,
340 "Object reference depth exceeded: {} (limit: {})",
341 depth, limit
342 ),
343 Self::TooManyOperators { count, limit } => write!(
344 f,
345 "Content stream has too many operators: {} (limit: {})",
346 count, limit
347 ),
348 Self::XfaNestingTooDeep { depth, limit } => write!(
349 f,
350 "XFA template nesting too deep: {} (limit: {})",
351 depth, limit
352 ),
353 Self::FormCalcRecursionTooDeep { depth, limit } => write!(
354 f,
355 "FormCalc recursion too deep: {} (limit: {})",
356 depth, limit
357 ),
358 }
359 }
360}
361
362impl std::error::Error for LimitError {}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn test_default_limits() {
370 let l = ProcessingLimits::default();
371 assert_eq!(l.max_file_bytes, 500 * 1024 * 1024);
372 assert_eq!(l.max_stream_bytes, 256 * 1024 * 1024);
373 assert_eq!(l.max_image_pixels, 16384 * 16384);
374 }
375
376 #[test]
377 fn test_wasm_limits_stricter_than_default() {
378 let wasm = ProcessingLimits::wasm();
379 let default = ProcessingLimits::default();
380 assert!(wasm.max_file_bytes < default.max_file_bytes);
381 assert!(wasm.max_stream_bytes < default.max_stream_bytes);
382 assert!(wasm.max_image_pixels < default.max_image_pixels);
383 }
384
385 #[test]
386 fn test_file_size_check() {
387 let l = ProcessingLimits::default();
388 assert!(l.check_file_size(100 * 1024 * 1024).is_ok()); // 100 MB: ok
389 assert!(l.check_file_size(500 * 1024 * 1024).is_ok()); // 500 MB: ok (at limit)
390 assert!(l.check_file_size(501 * 1024 * 1024).is_err()); // 501 MB: exceeded
391 }
392
393 #[test]
394 fn test_image_pixel_check() {
395 let l = ProcessingLimits::default();
396 assert!(l.check_image_pixels(1920, 1080).is_ok());
397 assert!(l.check_image_pixels(16384, 16384).is_ok()); // at limit
398 assert!(l.check_image_pixels(16385, 16384).is_err()); // just over
399 }
400
401 #[test]
402 fn test_stream_size_check() {
403 let l = ProcessingLimits::default();
404 assert!(l.check_stream_size(100 * 1024 * 1024).is_ok());
405 assert!(l.check_stream_size(256 * 1024 * 1024).is_ok()); // at limit
406 assert!(l.check_stream_size(257 * 1024 * 1024).is_err()); // exceeded
407 }
408
409 #[test]
410 fn test_builder_pattern() {
411 let l = ProcessingLimits::default()
412 .max_file_bytes(10 * 1024 * 1024)
413 .max_stream_bytes(5 * 1024 * 1024);
414 assert_eq!(l.max_file_bytes, 10 * 1024 * 1024);
415 assert_eq!(l.max_stream_bytes, 5 * 1024 * 1024);
416 }
417
418 #[test]
419 fn test_limit_error_display() {
420 let err = LimitError::FileTooLarge {
421 actual_bytes: 600 * 1024 * 1024,
422 limit_bytes: 500 * 1024 * 1024,
423 };
424 let msg = err.to_string();
425 assert!(msg.contains("600 MB"));
426 assert!(msg.contains("500 MB"));
427 }
428}