Repository: J-F-Liu/lopdf (language: rust)

Issue:
Timeout (100 second) when trying to read and save file 
Code
```
fn check_file(file_path: &str) {
    match Document::load(file_path) {
        Ok(mut document) => {
            let pages = document.get_pages();

            let mut doc_clone = document.clone();
            doc_clone.decompress();

            for (i, _) in pages.iter().enumerate() {
                let page_number = (i + 1) as u32;
                let _text = document.extract_text(&[page_number]);
            }

            document.save_to(&mut Cursor::new(Vec::new())).unwrap();
        }
        Err(err) => {
            eprintln!("Error reading PDF contents: {}", err)
        }
    }
}
```
File - [compressed.zip](https://github.com/user-attachments/files/16324469/compressed.zip)



Discussion / hints:
On my machine, this code produces a stack overflow, but the cause of it is probably the same as for you.

The reason for the stack overflow is the following object:
```
3 0 obj
<< /Contents 4 0 R
   /Type /Page
   /Resources << /XObject << /Im0 5 0 R >> >>
   /Parent 3 0 R
   /MediaBox [0 0 180 240]
>>
```
Where the parent of the page should be `2 0 R` and not itself.

This causes an infinite recursion in `collect_resources()` https://github.com/J-F-Liu/lopdf/blob/79f5b41c2c465e29b7b5c55e6632e1714f602f1a/src/document.rs#L424-L435

A simple way to fix this, would be to keep track of the call depth of the function (e.g., via an additional parameter) and return an error if a certain call depth is exceeded.
A possibly more expensive, but more robust, solution would be to keep track of all IDs encountered in the previous calls to `collect_resources` and return an error if an ID is encountered twice.

Fix patch:
diff --git a/src/document.rs b/src/document.rs
index a7782b2..aea5abd 100644
--- a/src/document.rs
+++ b/src/document.rs
@@ -7,7 +7,7 @@ use crate::{Error, Result, Stream};
 use encoding_rs::UTF_16BE;
 use log::info;
 use std::cmp::max;
-use std::collections::{BTreeMap, HashMap};
+use std::collections::{BTreeMap, HashMap, HashSet};
 use std::io::Write;
 use std::str;
 
@@ -421,24 +421,29 @@ impl Document {
 
     /// Get resources used by a page.
     pub fn get_page_resources(&self, page_id: ObjectId) -> (Option<&Dictionary>, Vec<ObjectId>) {
-        fn collect_resources(page_node: &Dictionary, resource_ids: &mut Vec<ObjectId>, doc: &Document) {
-            if let Ok(resources_id) = page_node.get(b"Resources").and_then(Object::as_reference) {
-                resource_ids.push(resources_id);
+        fn collect_resources(
+            page_node: &Dictionary, resource_ids: &mut Vec<ObjectId>, doc: &Document,
+            already_seen: &mut HashSet<ObjectId>,
+        ) -> Result<()> {
+            if let Ok(resource_id) = page_node.get(b"Resources").and_then(Object::as_reference) {
+                resource_ids.push(resource_id);
             }
-            if let Ok(page_tree) = page_node
-                .get(b"Parent")
-                .and_then(Object::as_reference)
-                .and_then(|id| doc.get_dictionary(id))
-            {
-                collect_resources(page_tree, resource_ids, doc);
+            if let Ok(parent_id) = page_node.get(b"Parent").and_then(Object::as_reference) {
+                if already_seen.contains(&parent_id) {
+                    return Err(Error::ReferenceCycle);
+                }
+                already_seen.insert(parent_id);
+                let parent_dict = doc.get_dictionary(parent_id)?;
+                collect_resources(parent_dict, resource_ids, doc, already_seen)?;
             }
+            Ok(())
         }
 
         let mut resource_dict = None;
         let mut resource_ids = Vec::new();
         if let Ok(page) = self.get_dictionary(page_id) {
             resource_dict = page.get(b"Resources").and_then(Object::as_dict).ok();
-            collect_resources(page, &mut resource_ids, self);
+            collect_resources(page, &mut resource_ids, self, &mut HashSet::new()).unwrap();
         }
         (resource_dict, resource_ids)
     }
diff --git a/src/error.rs b/src/error.rs
index 04acb98..55f2dac 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -31,6 +31,8 @@ pub enum Error {
     PageNumberNotFound(u32),
     /// Invalid object while parsing at offset.
     Parse { offset: usize },
+    /// Dereferencing object failed due to a reference cycle.
+    ReferenceCycle,
     /// Dereferencing object reached the limit.
     /// This might indicate a reference loop.
     ReferenceLimit,
@@ -67,7 +69,8 @@ impl fmt::Display for Error {
             Error::Offset(o) => write!(f, "Invalid file offset: {}", o),
             Error::PageNumberNotFound(p) => write!(f, "Page number {} could not be found", p),
             Error::Parse { offset, .. } => write!(f, "Invalid object at byte {}", offset),
-            Error::ReferenceLimit => write!(f, "Could not dereference an object; possible reference loop"),
+            Error::ReferenceCycle => write!(f, "Could not dereference an object; reference cycle detected"),
+            Error::ReferenceLimit => write!(f, "Could not dereference an object; possible reference cycle"),
             Error::StringDecode => write!(f, "Could not decode string"),
             Error::Syntax(msg) => write!(f, "Syntax error: {}", msg),
             Error::Trailer => write!(f, "Invalid file trailer"),


Test patch: