Skip to main content

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}