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        assert_eq!(child_context.start_time, parent_context.start_time);
152    }
153
154    // Integration tests with actual PDF structures
155
156    #[test]
157    fn test_deeply_nested_arrays() {
158        // Create a deeply nested array structure
159        let mut nested_array = PdfObject::Integer(42);
160
161        // Create 50 levels of nesting (well within stack limits)
162        for _ in 0..50 {
163            let array = PdfArray(vec![nested_array]);
164            nested_array = PdfObject::Array(array);
165        }
166
167        // This should parse without stack issues
168        // (In a real implementation, the parser would use StackSafeContext)
169        match nested_array {
170            PdfObject::Array(_) => {
171                // Successfully created deeply nested structure
172                assert!(true);
173            }
174            _ => panic!("Expected array"),
175        }
176    }
177
178    #[test]
179    fn test_deeply_nested_dictionaries() {
180        // Create a deeply nested dictionary structure
181        let mut nested_dict = PdfObject::Integer(42);
182
183        // Create 50 levels of nesting
184        for i in 0..50 {
185            let mut dict = PdfDictionary::new();
186            dict.insert(format!("level_{i}"), nested_dict);
187            nested_dict = PdfObject::Dictionary(dict);
188        }
189
190        // This should parse without stack issues
191        match nested_dict {
192            PdfObject::Dictionary(_) => {
193                // Successfully created deeply nested structure
194                assert!(true);
195            }
196            _ => panic!("Expected dictionary"),
197        }
198    }
199
200    #[test]
201    fn test_malicious_reference_chain() {
202        // Simulate detection of a malicious reference chain
203        let mut context = StackSafeContext::new();
204
205        // Create a chain of references
206        let refs = [(1, 0), (2, 0), (3, 0), (4, 0), (5, 0)];
207
208        // Push all references to stack (simulating navigation chain)
209        for &(obj, gen) in &refs {
210            assert!(context.push_ref(obj, gen).is_ok());
211        }
212
213        // Attempt to revisit the first one (simulating a cycle)
214        assert!(context.push_ref(1, 0).is_err());
215    }
216
217    #[test]
218    fn test_stack_safe_context_defaults() {
219        let context = StackSafeContext::new();
220
221        assert_eq!(context.depth, 0);
222        assert_eq!(context.max_depth, 1000);
223        assert!(context.active_stack.is_empty());
224        assert!(context.completed_refs.is_empty());
225        assert_eq!(context.timeout, Duration::from_secs(30));
226    }
227
228    #[test]
229    fn test_stack_safe_context_custom_limits() {
230        let context = StackSafeContext::with_limits(500, 10);
231
232        assert_eq!(context.depth, 0);
233        assert_eq!(context.max_depth, 500);
234        assert!(context.active_stack.is_empty());
235        assert!(context.completed_refs.is_empty());
236        assert_eq!(context.timeout, Duration::from_secs(10));
237    }
238
239    #[test]
240    fn test_multiple_reference_generations() {
241        let mut context = StackSafeContext::new();
242
243        // Different generations of the same object should be allowed in stack
244        assert!(context.push_ref(1, 0).is_ok());
245        assert!(context.push_ref(1, 1).is_ok());
246        assert!(context.push_ref(1, 2).is_ok());
247
248        // But same generation should fail (circular reference)
249        assert!(context.push_ref(1, 0).is_err());
250        assert!(context.push_ref(1, 1).is_err());
251        assert!(context.push_ref(1, 2).is_err());
252    }
253
254    #[test]
255    fn test_context_exit_idempotent() {
256        let mut context = StackSafeContext::new();
257
258        // Exit without enter should be safe
259        context.exit();
260        assert_eq!(context.depth, 0);
261
262        // Enter and exit
263        context.enter().unwrap();
264        assert_eq!(context.depth, 1);
265        context.exit();
266        assert_eq!(context.depth, 0);
267
268        // Multiple exits should be safe
269        context.exit();
270        context.exit();
271        assert_eq!(context.depth, 0);
272    }
273
274    #[test]
275    fn test_performance_with_many_references() {
276        let mut context = StackSafeContext::new();
277
278        // Process many references sequentially (testing completed_refs performance)
279        for obj_num in 0..1000 {
280            assert!(context.push_ref(obj_num, 0).is_ok());
281            context.pop_ref(); // Mark as completed
282        }
283
284        // Should still be fast to check completed references
285        // Since 500 was completed, it should be OK to push again
286        assert!(context.push_ref(500, 0).is_ok());
287        context.pop_ref();
288        assert!(context.push_ref(1001, 0).is_ok()); // New one
289    }
290
291    #[test]
292    fn test_error_messages_informatve() {
293        let mut context = StackSafeContext::with_limits(2, 1); // Very small limits
294
295        // Test depth limit error
296        context.enter().unwrap();
297        context.enter().unwrap();
298        let depth_error = context.enter().err().unwrap();
299        if let ParseError::SyntaxError { message, .. } = depth_error {
300            assert!(message.contains("Maximum recursion depth exceeded"));
301            assert!(message.contains("3"));
302            assert!(message.contains("2"));
303        } else {
304            panic!("Expected SyntaxError for depth limit");
305        }
306
307        // Test circular reference error
308        context.push_ref(10, 5).unwrap();
309        let cycle_error = context.push_ref(10, 5).err().unwrap();
310        if let ParseError::SyntaxError { message, .. } = cycle_error {
311            assert!(message.contains("Circular reference detected"));
312            assert!(message.contains("10 5 R"));
313        } else {
314            panic!("Expected SyntaxError for circular reference");
315        }
316
317        // Test timeout error (after waiting)
318        std::thread::sleep(Duration::from_millis(1100));
319        let timeout_error = context.check_timeout().err().unwrap();
320        if let ParseError::SyntaxError { message, .. } = timeout_error {
321            assert!(message.contains("Parsing timeout exceeded"));
322            assert!(message.contains("1s"));
323        } else {
324            panic!("Expected SyntaxError for timeout");
325        }
326    }
327}