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

Issue:
Panic when opening pdf 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/16324470/compressed.zip)


```
thread '<unnamed>' panicked at /home/runner/.cargo/git/checkouts/lopdf-0013854ec52a6141/2cf9f98/src/object_stream.rs:43:63:
range start index 3026 out of range for slice of length 3016
stack backtrace:
   0: rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::slice::index::slice_start_index_len_fail
   3: rayon::iter::plumbing::Producer::fold_with
   4: rayon::iter::plumbing::bridge_producer_consumer::helper
   5: rayon_core::join::join_context::{{closure}}
   6: rayon::iter::plumbing::bridge_producer_consumer::helper
   7: rayon_core::join::join_context::{{closure}}
   8: rayon::iter::plumbing::bridge_producer_consumer::helper
   9: rayon_core::join::join_context::{{closure}}
  10: rayon::iter::plumbing::bridge_producer_consumer::helper
  11: rayon::iter::extend::<impl rayon::iter::ParallelExtend<(K,V)> for alloc::collections::btree::map::BTreeMap<K,V>>::par_extend
  12: rayon::iter::from_par_iter::collect_extended
  13: lopdf::object_stream::ObjectStream::new
  14: lopdf::reader::Reader::read::{{closure}}
  15: rayon::iter::plumbing::Folder::consume_iter
  16: rayon::iter::plumbing::bridge_producer_consumer::helper
  17: rayon_core::join::join_context::{{closure}}
  18: rayon_core::registry::in_worker
  19: rayon::iter::plumbing::bridge_producer_consumer::helper
  20: rayon_core::join::join_context::{{closure}}
  21: rayon_core::registry::in_worker
  22: rayon::iter::plumbing::bridge_producer_consumer::helper
  23: rayon_core::join::join_context::{{closure}}
  24: rayon_core::registry::in_worker
  25: rayon::iter::plumbing::bridge_producer_consumer::helper
  26: rayon_core::join::join_context::{{closure}}
  27: rayon_core::registry::in_worker
  28: rayon::iter::plumbing::bridge_producer_consumer::helper
  29: <rayon_core::job::StackJob<L,F,R> as rayon_core::job::Job>::execute
  30: rayon_core::registry::WorkerThread::wait_until_cold
  31: rayon_core::registry::ThreadBuilder::run
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

##### Automatic Fuzzer note, output status "Some(101)", output signal "None"
```


Discussion / hints:

Fix patch:
diff --git a/src/object_stream.rs b/src/object_stream.rs
index 7463667..2cf17af 100644
--- a/src/object_stream.rs
+++ b/src/object_stream.rs
@@ -2,10 +2,10 @@
 
 use crate::parser;
 use crate::{Error, Object, ObjectId, Result, Stream};
-use std::cmp::max;
 use std::collections::BTreeMap;
 use std::str::FromStr;
 
+use log::warn;
 #[cfg(feature = "rayon")]
 use rayon::prelude::*;
 
@@ -24,9 +24,12 @@ impl ObjectStream {
             });
         }
 
-        let first_offset = max(0, stream.dict.get(b"First").and_then(Object::as_i64)?) as usize;
-        let _count = max(0, stream.dict.get(b"N").and_then(Object::as_i64)?) as usize;
-
+        let first_offset = stream
+            .dict
+            .get(b"First")
+            .and_then(Object::as_i64)?
+            .try_into()
+            .map_err(|_| Error::Offset(0))?;
         let index_block = stream.content.get(..first_offset).ok_or(Error::Offset(first_offset))?;
 
         let numbers_str = std::str::from_utf8(index_block)?;
@@ -36,10 +39,19 @@ impl ObjectStream {
             .collect();
         let len = numbers.len() / 2 * 2; // Ensure only pairs.
 
+        let n = stream.dict.get(b"N").and_then(Object::as_i64)?;
+        if numbers.len().try_into().ok() != n.checked_mul(2) {
+            warn!("object stream: the object stream dictionary specifies a wrong number of objects")
+        }
+
         let chunks_filter_map = |chunk: &[_]| {
             let id = chunk[0]?;
             let offset = first_offset + chunk[1]? as usize;
 
+            if offset >= stream.content.len() {
+                warn!("out-of-bounds offset in object stream");
+                return None;
+            }
             let object = parser::direct_object(&stream.content[offset..])?;
 
             Some(((id, 0), object))


Test patch: