refactor: replace single-retry unresolved loop with fixed-point itera…#127
refactor: replace single-retry unresolved loop with fixed-point itera…#127
Conversation
…tion Replace the single-pass retry in load_objects_with_decryption with a convergent fixed-point loop that iteratively resolves forward references (e.g. indirect /Length entries in stream dictionaries). Changes: - Extract try_load_object helper to deduplicate parse/decrypt/insert logic - Iterate retries until all resolved, no progress, or 16-iteration cap - Add UnresolvedObjects error variant with count, iterations, first_offset - Add tests for indirect /Length resolution and unresolvable references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Refactors PdfReader object loading to resolve forward references (notably indirect /Length in streams) via a convergent fixed-point retry loop, improving robustness when xref iteration order encounters dependencies that haven’t been loaded yet.
Changes:
- Adds
PdfReaderError::UnresolvedObjects { count, iterations, first_offset }for cases where forward references can’t be resolved after iterative retries. - Replaces the prior single retry pass with an iterative retry loop capped at 16 iterations and stopping on no-progress convergence.
- Introduces
try_load_objecthelper and adds tests covering indirect/Lengthresolution and unresolvable references.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| assert!(result.is_err(), "Should fail for unresolvable reference"); | ||
| let err_msg = match result { | ||
| Err(e) => e.to_string(), | ||
| Ok(_) => unreachable!(), | ||
| }; | ||
| assert!( | ||
| err_msg.contains("failed to resolve"), | ||
| "Expected UnresolvedObjects error, got: {err_msg}" | ||
| ); |
There was a problem hiding this comment.
test_unresolvable_reference_returns_error only asserts that the error string contains "failed to resolve", which would also pass for other error variants/messages. Consider matching on Err(PdfReaderError::UnresolvedObjects { .. }) (and optionally asserting count, iterations, and first_offset == obj4_offset) so the test reliably verifies the new error behavior.
| assert!(result.is_err(), "Should fail for unresolvable reference"); | |
| let err_msg = match result { | |
| Err(e) => e.to_string(), | |
| Ok(_) => unreachable!(), | |
| }; | |
| assert!( | |
| err_msg.contains("failed to resolve"), | |
| "Expected UnresolvedObjects error, got: {err_msg}" | |
| ); | |
| match result { | |
| Err(super::PdfReaderError::UnresolvedObjects { | |
| count, | |
| iterations, | |
| first_offset, | |
| .. | |
| }) => { | |
| assert_eq!(count, 1, "Expected one unresolved object"); | |
| assert_eq!(iterations, 0, "Expected failure after one no-progress pass"); | |
| assert_eq!( | |
| first_offset as usize, | |
| obj4_offset, | |
| "Expected unresolved object to be object 4" | |
| ); | |
| } | |
| Err(e) => panic!("Expected UnresolvedObjects error, got: {e}"), | |
| Ok(_) => panic!("Should fail for unresolvable reference"), | |
| } |
…tion
Replace the single-pass retry in load_objects_with_decryption with a convergent fixed-point loop that iteratively resolves forward references (e.g. indirect /Length entries in stream dictionaries).
Changes: