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/// Resource limits for a single PDF processing operation.
22///
23/// Construct via [`ProcessingLimits::default()`] for standard limits,
24/// or use the builder methods to customize for your use case.
25///
26/// # Examples
27///
28/// ```rust
29/// use pdf_engine::limits::ProcessingLimits;
30///
31/// // Default limits (recommended for server-side processing):
32/// let limits = ProcessingLimits::default();
33///
34/// // Stricter limits for WASM/browser context:
35/// let wasm_limits = ProcessingLimits::wasm();
36///
37/// // Custom limits:
38/// let custom = ProcessingLimits::default()
39///     .max_file_bytes(100 * 1024 * 1024)   // 100 MB
40///     .max_stream_bytes(64 * 1024 * 1024);  // 64 MB per stream
41/// ```
42#[derive(Debug, Clone)]
43pub struct ProcessingLimits {
44    /// Maximum PDF file size in bytes. Default: 500 MB.
45    pub max_file_bytes: u64,
46    /// Maximum decompressed size of any single stream. Default: 256 MB.
47    /// Prevents zip bombs via FlateDecode or LZWDecode.
48    pub max_stream_bytes: u64,
49    /// Maximum total memory allocated per document. Default: 1 GB.
50    pub max_total_memory_bytes: u64,
51    /// Maximum object reference depth (prevents stack overflow on recursive refs). Default: 100.
52    pub max_object_depth: u32,
53    /// Maximum content stream operators per page. Default: 10,000,000.
54    pub max_operator_count: u64,
55    /// Maximum pixel count per image (width × height). Default: 268,435,456 (16384²).
56    pub max_image_pixels: u64,
57    /// Maximum XFA template XML nesting depth. Default: 50.
58    pub max_xfa_nesting_depth: u32,
59    /// Maximum FormCalc recursion depth. Default: 200.
60    pub max_formcalc_depth: u32,
61}
62
63impl Default for ProcessingLimits {
64    fn default() -> Self {
65        Self {
66            max_file_bytes: 500 * 1024 * 1024,          // 500 MB
67            max_stream_bytes: 256 * 1024 * 1024,        // 256 MB
68            max_total_memory_bytes: 1024 * 1024 * 1024, // 1 GB
69            max_object_depth: 100,
70            max_operator_count: 10_000_000,
71            max_image_pixels: 16384 * 16384, // 268 MP
72            max_xfa_nesting_depth: 50,
73            max_formcalc_depth: 200,
74        }
75    }
76}
77
78impl ProcessingLimits {
79    /// Create a new set of limits with default values.
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    /// Strict limits for WASM/browser contexts with limited memory.
85    ///
86    /// - Max file: 50 MB
87    /// - Max stream: 32 MB
88    /// - Max total memory: 128 MB
89    /// - Image pixels: 64 MP (8192×8192)
90    pub fn wasm() -> Self {
91        Self {
92            max_file_bytes: 50 * 1024 * 1024,          // 50 MB
93            max_stream_bytes: 32 * 1024 * 1024,        // 32 MB
94            max_total_memory_bytes: 128 * 1024 * 1024, // 128 MB
95            max_object_depth: 50,
96            max_operator_count: 1_000_000,
97            max_image_pixels: 8192 * 8192, // 64 MP
98            max_xfa_nesting_depth: 30,
99            max_formcalc_depth: 100,
100        }
101    }
102
103    /// Unlimited: no resource limits (use only in trusted environments).
104    pub fn unlimited() -> Self {
105        Self {
106            max_file_bytes: u64::MAX,
107            max_stream_bytes: u64::MAX,
108            max_total_memory_bytes: u64::MAX,
109            max_object_depth: u32::MAX,
110            max_operator_count: u64::MAX,
111            max_image_pixels: u64::MAX,
112            max_xfa_nesting_depth: u32::MAX,
113            max_formcalc_depth: u32::MAX,
114        }
115    }
116
117    /// Set maximum PDF file size.
118    pub fn max_file_bytes(mut self, bytes: u64) -> Self {
119        self.max_file_bytes = bytes;
120        self
121    }
122
123    /// Set maximum decompressed stream size.
124    pub fn max_stream_bytes(mut self, bytes: u64) -> Self {
125        self.max_stream_bytes = bytes;
126        self
127    }
128
129    /// Set maximum total memory per document.
130    pub fn max_total_memory_bytes(mut self, bytes: u64) -> Self {
131        self.max_total_memory_bytes = bytes;
132        self
133    }
134
135    /// Set maximum object reference depth.
136    pub fn max_object_depth(mut self, depth: u32) -> Self {
137        self.max_object_depth = depth;
138        self
139    }
140
141    /// Set maximum content stream operator count.
142    pub fn max_operator_count(mut self, count: u64) -> Self {
143        self.max_operator_count = count;
144        self
145    }
146
147    /// Set maximum image pixel count (width × height).
148    pub fn max_image_pixels(mut self, pixels: u64) -> Self {
149        self.max_image_pixels = pixels;
150        self
151    }
152
153    /// Set maximum XFA template nesting depth.
154    pub fn max_xfa_nesting_depth(mut self, depth: u32) -> Self {
155        self.max_xfa_nesting_depth = depth;
156        self
157    }
158
159    /// Set maximum FormCalc recursion depth.
160    pub fn max_formcalc_depth(mut self, depth: u32) -> Self {
161        self.max_formcalc_depth = depth;
162        self
163    }
164
165    /// Check if a file size is within limits. Returns `Err` with a descriptive message if exceeded.
166    pub fn check_file_size(&self, bytes: u64) -> Result<(), LimitError> {
167        if bytes > self.max_file_bytes {
168            Err(LimitError::FileTooLarge {
169                actual_bytes: bytes,
170                limit_bytes: self.max_file_bytes,
171            })
172        } else {
173            Ok(())
174        }
175    }
176
177    /// Check if a decompressed stream size is within limits.
178    pub fn check_stream_size(&self, bytes: u64) -> Result<(), LimitError> {
179        if bytes > self.max_stream_bytes {
180            Err(LimitError::StreamTooLarge {
181                actual_bytes: bytes,
182                limit_bytes: self.max_stream_bytes,
183            })
184        } else {
185            Ok(())
186        }
187    }
188
189    /// Check if image dimensions are within limits.
190    pub fn check_image_pixels(&self, width: u64, height: u64) -> Result<(), LimitError> {
191        let pixels = width.saturating_mul(height);
192        if pixels > self.max_image_pixels {
193            Err(LimitError::ImageTooLarge {
194                width,
195                height,
196                pixels,
197                limit_pixels: self.max_image_pixels,
198            })
199        } else {
200            Ok(())
201        }
202    }
203
204    /// Check if an object depth is within limits.
205    pub fn check_object_depth(&self, depth: u32) -> Result<(), LimitError> {
206        if depth > self.max_object_depth {
207            Err(LimitError::ObjectDepthExceeded {
208                depth,
209                limit: self.max_object_depth,
210            })
211        } else {
212            Ok(())
213        }
214    }
215}
216
217/// Error returned when a resource limit is exceeded.
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub enum LimitError {
220    /// PDF file exceeds the maximum allowed size.
221    FileTooLarge { actual_bytes: u64, limit_bytes: u64 },
222    /// A decompressed stream exceeds the maximum allowed size.
223    StreamTooLarge { actual_bytes: u64, limit_bytes: u64 },
224    /// An image exceeds the maximum allowed pixel count.
225    ImageTooLarge {
226        width: u64,
227        height: u64,
228        pixels: u64,
229        limit_pixels: u64,
230    },
231    /// An object reference chain exceeds the maximum allowed depth.
232    ObjectDepthExceeded { depth: u32, limit: u32 },
233    /// A content stream has too many operators.
234    TooManyOperators { count: u64, limit: u64 },
235    /// XFA template nesting is too deep.
236    XfaNestingTooDeep { depth: u32, limit: u32 },
237    /// FormCalc recursion is too deep.
238    FormCalcRecursionTooDeep { depth: u32, limit: u32 },
239}
240
241impl std::fmt::Display for LimitError {
242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        match self {
244            Self::FileTooLarge {
245                actual_bytes,
246                limit_bytes,
247            } => write!(
248                f,
249                "PDF file too large: {} MB (limit: {} MB)",
250                actual_bytes / 1024 / 1024,
251                limit_bytes / 1024 / 1024
252            ),
253            Self::StreamTooLarge {
254                actual_bytes,
255                limit_bytes,
256            } => write!(
257                f,
258                "Decompressed stream too large: {} MB (limit: {} MB)",
259                actual_bytes / 1024 / 1024,
260                limit_bytes / 1024 / 1024
261            ),
262            Self::ImageTooLarge {
263                width,
264                height,
265                pixels,
266                limit_pixels,
267            } => write!(
268                f,
269                "Image too large: {}×{} ({} MP, limit: {} MP)",
270                width,
271                height,
272                pixels / 1_000_000,
273                limit_pixels / 1_000_000
274            ),
275            Self::ObjectDepthExceeded { depth, limit } => write!(
276                f,
277                "Object reference depth exceeded: {} (limit: {})",
278                depth, limit
279            ),
280            Self::TooManyOperators { count, limit } => write!(
281                f,
282                "Content stream has too many operators: {} (limit: {})",
283                count, limit
284            ),
285            Self::XfaNestingTooDeep { depth, limit } => write!(
286                f,
287                "XFA template nesting too deep: {} (limit: {})",
288                depth, limit
289            ),
290            Self::FormCalcRecursionTooDeep { depth, limit } => write!(
291                f,
292                "FormCalc recursion too deep: {} (limit: {})",
293                depth, limit
294            ),
295        }
296    }
297}
298
299impl std::error::Error for LimitError {}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn test_default_limits() {
307        let l = ProcessingLimits::default();
308        assert_eq!(l.max_file_bytes, 500 * 1024 * 1024);
309        assert_eq!(l.max_stream_bytes, 256 * 1024 * 1024);
310        assert_eq!(l.max_image_pixels, 16384 * 16384);
311    }
312
313    #[test]
314    fn test_wasm_limits_stricter_than_default() {
315        let wasm = ProcessingLimits::wasm();
316        let default = ProcessingLimits::default();
317        assert!(wasm.max_file_bytes < default.max_file_bytes);
318        assert!(wasm.max_stream_bytes < default.max_stream_bytes);
319        assert!(wasm.max_image_pixels < default.max_image_pixels);
320    }
321
322    #[test]
323    fn test_file_size_check() {
324        let l = ProcessingLimits::default();
325        assert!(l.check_file_size(100 * 1024 * 1024).is_ok()); // 100 MB: ok
326        assert!(l.check_file_size(500 * 1024 * 1024).is_ok()); // 500 MB: ok (at limit)
327        assert!(l.check_file_size(501 * 1024 * 1024).is_err()); // 501 MB: exceeded
328    }
329
330    #[test]
331    fn test_image_pixel_check() {
332        let l = ProcessingLimits::default();
333        assert!(l.check_image_pixels(1920, 1080).is_ok());
334        assert!(l.check_image_pixels(16384, 16384).is_ok()); // at limit
335        assert!(l.check_image_pixels(16385, 16384).is_err()); // just over
336    }
337
338    #[test]
339    fn test_stream_size_check() {
340        let l = ProcessingLimits::default();
341        assert!(l.check_stream_size(100 * 1024 * 1024).is_ok());
342        assert!(l.check_stream_size(256 * 1024 * 1024).is_ok()); // at limit
343        assert!(l.check_stream_size(257 * 1024 * 1024).is_err()); // exceeded
344    }
345
346    #[test]
347    fn test_builder_pattern() {
348        let l = ProcessingLimits::default()
349            .max_file_bytes(10 * 1024 * 1024)
350            .max_stream_bytes(5 * 1024 * 1024);
351        assert_eq!(l.max_file_bytes, 10 * 1024 * 1024);
352        assert_eq!(l.max_stream_bytes, 5 * 1024 * 1024);
353    }
354
355    #[test]
356    fn test_limit_error_display() {
357        let err = LimitError::FileTooLarge {
358            actual_bytes: 600 * 1024 * 1024,
359            limit_bytes: 500 * 1024 * 1024,
360        };
361        let msg = err.to_string();
362        assert!(msg.contains("600 MB"));
363        assert!(msg.contains("500 MB"));
364    }
365}