Skip to main content

oxidize_pdf/parser/
stack_safe_tests.rs

1//! Comprehensive tests for stack-safe parsing implementations
2//!
3//! This module tests the stack-safe parsing implementations to ensure they
4//! can handle deeply nested PDF structures without stack overflow.
5
6#[cfg(test)]
7mod tests {
8    use super::super::objects::{PdfArray, PdfDictionary, PdfObject};
9    use super::super::stack_safe::{RecursionGuard, ReferenceStackGuard, StackSafeContext};
10    use super::super::ParseError;
11    use std::time::Duration;
12
13    #[test]
14    fn test_deep_recursion_limit() {
15        let mut context = StackSafeContext::with_limits(10, 60);
16
17        // Should handle up to the limit
18        for i in 0..10 {
19            let result = context.enter();
20            assert!(result.is_ok(), "Failed at depth {i}");
21        }
22
23        // Should fail at limit+1
24        let result = context.enter();
25        assert!(result.is_err(), "Should have failed at depth 11");
26
27        // Clean up
28        for _ in 0..10 {
29            context.exit();
30        }
31    }
32
33    #[test]
34    fn test_timeout_protection() {
35        let context = StackSafeContext::with_limits(1000, 0); // 0 second timeout
36
37        // Should timeout immediately
38        std::thread::sleep(Duration::from_millis(10));
39        let result = context.check_timeout();
40        assert!(result.is_err(), "Should have timed out");
41    }
42
43    #[test]
44    fn test_circular_reference_detection() {
45        let mut context = StackSafeContext::new();
46
47        // First reference should work
48        assert!(context.push_ref(1, 0).is_ok());
49
50        // Circular reference should be detected
51        assert!(context.push_ref(1, 0).is_err());
52
53        // Pop and try different generation
54        context.pop_ref();
55        assert!(context.push_ref(1, 1).is_ok());
56
57        // Pop and revisit original should work
58        context.pop_ref();
59        assert!(context.push_ref(1, 0).is_ok());
60    }
61
62    #[test]
63    fn test_recursion_guard_raii() {
64        let mut context = StackSafeContext::new();
65        assert_eq!(context.depth, 0);
66
67        {
68            let _guard = RecursionGuard::new(&mut context).unwrap();
69            // Depth incremented
70        } // Guard dropped, depth decremented
71
72        assert_eq!(context.depth, 0);
73    }
74
75    #[test]
76    fn test_reference_stack_guard_raii() {
77        let mut context = StackSafeContext::new();
78
79        {
80            let _guard = ReferenceStackGuard::new(&mut context, 5, 0).unwrap();
81            // Reference is pushed to stack
82        } // Guard dropped, reference popped
83
84        // Should be able to visit again
85        assert!(context.push_ref(5, 0).is_ok());
86    }
87
88    #[test]
89    fn test_nested_guards() {
90        let mut context = StackSafeContext::with_limits(5, 60);
91
92        // Test sequential guards (not nested due to borrow checker)
93        {
94            let _guard1 = RecursionGuard::new(&mut context).unwrap();
95            // Can't access context.depth while guard is active
96        }
97        assert_eq!(context.depth, 0); // Guard dropped, depth reset
98
99        {
100            let _guard2 = RecursionGuard::new(&mut context).unwrap();
101            // Can't access context.depth while guard is active
102        }
103        assert_eq!(context.depth, 0); // Guard dropped, depth reset
104
105        // Test that we can create multiple guards sequentially
106        for _ in 0..3 {
107            let result = RecursionGuard::new(&mut context);
108            assert!(result.is_ok());
109            // Guard is dropped at end of iteration
110        }
111
112        assert_eq!(context.depth, 0);
113    }
114
115    #[test]
116    fn test_guard_failure_cleanup() {
117        let mut context = StackSafeContext::with_limits(2, 60);
118
119        // Manually fill up to limit to test the limit behavior
120        context.enter().unwrap(); // depth = 1
121        context.enter().unwrap(); // depth = 2
122
123        // Next enter should fail
124        let result = context.enter();
125        assert!(result.is_err());
126
127        // Context should still be in valid state
128        assert_eq!(context.depth, 2);
129
130        // Clean up
131        context.exit(); // depth = 1
132        context.exit(); // depth = 0
133        assert_eq!(context.depth, 0);
134    }
135
136    #[test]
137    fn test_child_context() {
138        let mut parent_context = StackSafeContext::with_limits(100, 60);
139        parent_context.depth = 10;
140        parent_context.push_ref(1, 0).unwrap();
141        parent_context.pop_ref(); // Mark as completed
142
143        let child_context = parent_context.child();
144
145        // Child should inherit state
146        assert_eq!(child_context.depth, 10);
147        assert_eq!(child_context.max_depth, 100);
148        assert!(child_context.completed_refs.contains(&(1, 0)));
149
150        // Child inherits the start time (by design for timeout consistency)
151        #[cfg(not(target_arch = "wasm32"))]
152        assert_eq!(child_context.start_time, parent_context.start_time);
153    }
154
155    // Integration tests with actual PDF structures
156
157    #[test]
158    fn test_deeply_nested_arrays() {
159        // Create a deeply nested array structure
160        let mut nested_array = PdfObject::Integer(42);
161
162        // Create 50 levels of nesting (well within stack limits)
163        for _ in 0..50 {
164            let array = PdfArray(vec![nested_array]);
165            nested_array = PdfObject::Array(array);
166        }
167
168        // This should parse without stack issues
169        // (In a real implementation, the parser would use StackSafeContext)
170        match nested_array {
171            PdfObject::Array(_) => {
172                // Successfully created deeply nested structure
173                assert!(true);
174            }
175            _ => panic!("Expected array"),
176        }
177    }
178
179    #[test]
180    fn test_deeply_nested_dictionaries() {
181        // Create a deeply nested dictionary structure
182        let mut nested_dict = PdfObject::Integer(42);
183
184        // Create 50 levels of nesting
185        for i in 0..50 {
186            let mut dict = PdfDictionary::new();
187            dict.insert(format!("level_{i}"), nested_dict);
188            nested_dict = PdfObject::Dictionary(dict);
189        }
190
191        // This should parse without stack issues
192        match nested_dict {
193            PdfObject::Dictionary(_) => {
194                // Successfully created deeply nested structure
195                assert!(true);
196            }
197            _ => panic!("Expected dictionary"),
198        }
199    }
200
201    #[test]
202    fn test_malicious_reference_chain() {
203        // Simulate detection of a malicious reference chain
204        let mut context = StackSafeContext::new();
205
206        // Create a chain of references
207        let refs = [(1, 0), (2, 0), (3, 0), (4, 0), (5, 0)];
208
209        // Push all references to stack (simulating navigation chain)
210        for &(obj, gen) in &refs {
211            assert!(context.push_ref(obj, gen).is_ok());
212        }
213
214        // Attempt to revisit the first one (simulating a cycle)
215        assert!(context.push_ref(1, 0).is_err());
216    }
217
218    #[test]
219    fn test_stack_safe_context_defaults() {
220        let context = StackSafeContext::new();
221
222        assert_eq!(context.depth, 0);
223        assert_eq!(context.max_depth, 1000);
224        assert!(context.active_stack.is_empty());
225        assert!(context.completed_refs.is_empty());
226        #[cfg(not(target_arch = "wasm32"))]
227        assert_eq!(context.timeout, Duration::from_secs(120)); // Updated to match PARSING_TIMEOUT_SECS
228    }
229
230    #[test]
231    fn test_stack_safe_context_custom_limits() {
232        let context = StackSafeContext::with_limits(500, 10);
233
234        assert_eq!(context.depth, 0);
235        assert_eq!(context.max_depth, 500);
236        assert!(context.active_stack.is_empty());
237        assert!(context.completed_refs.is_empty());
238        #[cfg(not(target_arch = "wasm32"))]
239        assert_eq!(context.timeout, Duration::from_secs(10));
240    }
241
242    #[test]
243    fn test_multiple_reference_generations() {
244        let mut context = StackSafeContext::new();
245
246        // Different generations of the same object should be allowed in stack
247        assert!(context.push_ref(1, 0).is_ok());
248        assert!(context.push_ref(1, 1).is_ok());
249        assert!(context.push_ref(1, 2).is_ok());
250
251        // But same generation should fail (circular reference)
252        assert!(context.push_ref(1, 0).is_err());
253        assert!(context.push_ref(1, 1).is_err());
254        assert!(context.push_ref(1, 2).is_err());
255    }
256
257    #[test]
258    fn test_context_exit_idempotent() {
259        let mut context = StackSafeContext::new();
260
261        // Exit without enter should be safe
262        context.exit();
263        assert_eq!(context.depth, 0);
264
265        // Enter and exit
266        context.enter().unwrap();
267        assert_eq!(context.depth, 1);
268        context.exit();
269        assert_eq!(context.depth, 0);
270
271        // Multiple exits should be safe
272        context.exit();
273        context.exit();
274        assert_eq!(context.depth, 0);
275    }
276
277    #[test]
278    fn test_performance_with_many_references() {
279        let mut context = StackSafeContext::new();
280
281        // Process many references sequentially (testing completed_refs performance)
282        for obj_num in 0..1000 {
283            assert!(context.push_ref(obj_num, 0).is_ok());
284            context.pop_ref(); // Mark as completed
285        }
286
287        // Should still be fast to check completed references
288        // Since 500 was completed, it should be OK to push again
289        assert!(context.push_ref(500, 0).is_ok());
290        context.pop_ref();
291        assert!(context.push_ref(1001, 0).is_ok()); // New one
292    }
293
294    #[test]
295    fn test_error_messages_informatve() {
296        let mut context = StackSafeContext::with_limits(2, 1); // Very small limits
297
298        // Test depth limit error
299        context.enter().unwrap();
300        context.enter().unwrap();
301        let depth_error = context.enter().err().unwrap();
302        if let ParseError::SyntaxError { message, .. } = depth_error {
303            assert!(message.contains("Maximum recursion depth exceeded"));
304            assert!(message.contains("3"));
305            assert!(message.contains("2"));
306        } else {
307            panic!("Expected SyntaxError for depth limit");
308        }
309
310        // Test circular reference error
311        context.push_ref(10, 5).unwrap();
312        let cycle_error = context.push_ref(10, 5).err().unwrap();
313        if let ParseError::SyntaxError { message, .. } = cycle_error {
314            assert!(message.contains("Circular reference detected"));
315            assert!(message.contains("10 5 R"));
316        } else {
317            panic!("Expected SyntaxError for circular reference");
318        }
319
320        // Test timeout error (after waiting)
321        std::thread::sleep(Duration::from_millis(1100));
322        let timeout_error = context.check_timeout().err().unwrap();
323        if let ParseError::SyntaxError { message, .. } = timeout_error {
324            assert!(message.contains("Parsing timeout exceeded"));
325            assert!(message.contains("1s"));
326        } else {
327            panic!("Expected SyntaxError for timeout");
328        }
329    }
330}