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

Issue:
Panic in setup_outline_page_ids()
Calling get_toc() on the following pdf causes a panic in `setup_outline_page_ids`:
https://www.etsi.org/deliver/etsi_ts/102000_102099/102034/01.02.01_60/ts_102034v010201p.pdf

<pre>
The application panicked (crashed).                                                                                                                                                           Message:  called `Result::unwrap()` on an `Err` value: Type                                                                                                                                   
Location: /home/daniel/.cargo/registry/src/index.crates.io-6f17d22bba15001f/lopdf-0.32.0/src/toc.rs:71                                                                                                                                                                                                                                                                                      
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━                                                                                                                                            ⋮ 9 frames hidden ⋮                               
  10: core::result::Result<T,E>::unwrap::h1dd6925ad870affb                                                                                                                                    
      at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/result.rs:1073
  11: lopdf::toc::setup_outline_page_ids::h8d4e621112de3625                                                                                                                                   
      at /home/daniel/.cargo/registry/src/index.crates.io-6f17d22bba15001f/lopdf-0.32.0/src/toc.rs:71
  12: lopdf::toc::<impl lopdf::document::Document>::get_toc::h2f5fa8a1257fe404                                                                                                                
      at /home/daniel/.caThe application panicked (crashed).                                                                                                                                                           Message:  called `Result::unwrap()` on an `Err` value: Type                                                                                                                                   
Location: /home/daniel/.cargo/registry/src/index.crates.io-6f17d22bba15001f/lopdf-0.32.0/src/toc.rs:71                                                                                                                                                                                                                                                                                      
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━                                                                                                                                            ⋮ 9 frames hidden ⋮                               
rgo/registry/src/index.crates.io-6f17d22bba15001f/lopdf-0.32.0/src/toc.rs:99
  13: getsi::pdf::lopdf::get_toc::h528e8dc44864dd4d                                                                                                                                           
      at /home/daniel/scm/osmo/rust/getsi/src/pdf/lopdf.rs:7       
</pre>

Opening the file in evince shows that some chapters are missing pages (4 and 4.1) in the index view.
It looks like the second unwrap in the line fails and `destination.page().unwrap().as_reference()` returns an error


Discussion / hints:
Reposting the PDF in case the link goes dead:
[ts_102034v010201p.pdf](https://github.com/user-attachments/files/16481784/ts_102034v010201p.pdf)

The panic happens because the PDF is malformed, cf. [PDF 1.7, table 151 Destination syntax](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#page=374).

Two of the contained `/Dest` entries in the document outline contain `null` instead of an indirect reference.
This causes a "type error" when trying to call `as_reference()` on the `null` value.

In a sense, the panic is thus expected behavior. In the future, lopdf should probably handle similar errors more gracefully by returning in error instead of panicking, discussion on this probably should probably be held in #285.
+1

I'm having the same problem.

Some user provided PDFs don't contain a proper TOC structure (or in my case, the references to destinations are broken).

The PDF in question is somehow broken in more than one way - and the problem should be fixed by using a proper PDF file - but still I'd like to be able to handle the error properly in my code and unwind my code properly and also display a proper error message.

Right now it will just panic randomly when calling `pdf_doc.get_toc()`, which is not very handy.

However, valid files work like a breeze, which is awesome.

The offending code in toc.rs:

```rust
fn setup_outline_page_ids<'a>(
    outlines: &'a Vec<Outline>, result: &mut OutlinePageIds, level: usize,
) -> &'a Vec<Outline> {
    for outline in outlines.iter() {
        match outline {
            Outline::Destination(destination) => {

// The following unwraps() should probably return an Err instead.

                result.insert(
                    destination.title().unwrap().as_str().unwrap().to_vec(),
                    (destination.page().unwrap().as_reference().unwrap(), result.len(), level),
                );
            }
            Outline::SubOutlines(sub_outlines) => {
                setup_outline_page_ids(sub_outlines, result, level + 1);
            }
        }
    }
    outlines
}
```

Fix patch:
diff --git a/src/destinations.rs b/src/destinations.rs
index 8b57824..eb1d889 100644
--- a/src/destinations.rs
+++ b/src/destinations.rs
@@ -1,33 +1,31 @@
-use std::collections::BTreeMap;
-
 use super::{Dictionary, Document, Object, Result};
 use indexmap::IndexMap;
 #[derive(Debug, Clone)]
-pub struct Destination {
-    map: BTreeMap<Vec<u8>, Object>,
-}
+pub struct Destination(Dictionary);
 
 impl Destination {
     pub fn new(title: Object, page: Object, typ: Object) -> Self {
-        let mut map = BTreeMap::new();
-        map.insert(b"Title".to_vec(), title);
-        map.insert(b"Page".to_vec(), page);
-        map.insert(b"Type".to_vec(), typ);
-        Destination { map }
+        let mut dict = Dictionary::new();
+        dict.set(b"Title", title);
+        dict.set(b"Page", page);
+        dict.set(b"Type", typ);
+        Destination(dict)
     }
 
-    pub fn set(&mut self, key: Vec<u8>, value: Object) {
-        self.map.insert(key, value);
+    pub fn set<K, V>(&mut self, key: K, value: V)
+    where
+        K: Into<Vec<u8>>,
+        V: Into<Object>,
+    {
+        self.0.set(key, value);
     }
 
-    #[allow(dead_code)]
-    pub fn title(&self) -> Option<&Object> {
-        self.map.get(b"Title".as_slice())
+    pub fn title(&self) -> Result<&Object> {
+        self.0.get(b"Title")
     }
 
-    #[allow(dead_code)]
-    pub fn page(&self) -> Option<&Object> {
-        self.map.get(b"Page".as_slice())
+    pub fn page(&self) -> Result<&Object> {
+        self.0.get(b"Page")
     }
 }
 
diff --git a/src/outlines.rs b/src/outlines.rs
index b968ed1..fa81fa2 100644
--- a/src/outlines.rs
+++ b/src/outlines.rs
@@ -103,7 +103,7 @@ impl Document {
             )),
             Object::String(ref key, ref _fmt) => {
                 if let Some(destination) = named_destinations.get_mut(key) {
-                    destination.set(b"Title".to_vec(), title.to_owned());
+                    destination.set(b"Title", title.to_owned());
                     Outline::Destination(destination.clone())
                 } else {
                     return Ok(None);
diff --git a/src/toc.rs b/src/toc.rs
index 6a36186..2d15ef4 100644
--- a/src/toc.rs
+++ b/src/toc.rs
@@ -62,21 +62,21 @@ type OutlinePageIds = IndexMap<Vec<u8>, ((u32, u16), usize, usize)>;
 
 fn setup_outline_page_ids<'a>(
     outlines: &'a Vec<Outline>, result: &mut OutlinePageIds, level: usize,
-) -> &'a Vec<Outline> {
+) -> Result<&'a Vec<Outline>> {
     for outline in outlines.iter() {
         match outline {
             Outline::Destination(destination) => {
                 result.insert(
-                    destination.title().unwrap().as_str().unwrap().to_vec(),
-                    (destination.page().unwrap().as_reference().unwrap(), result.len(), level),
+                    destination.title()?.as_str()?.to_vec(),
+                    (destination.page()?.as_reference()?, result.len(), level),
                 );
             }
             Outline::SubOutlines(sub_outlines) => {
-                setup_outline_page_ids(sub_outlines, result, level + 1);
+                setup_outline_page_ids(sub_outlines, result, level + 1)?;
             }
         }
     }
-    outlines
+    Ok(outlines)
 }
 
 impl Document {
@@ -96,7 +96,7 @@ impl Document {
         let mut named_destinations = IndexMap::new();
         if let Some(outlines) = self.get_outlines(None, None, &mut named_destinations)? {
             let mut outline_page_ids = IndexMap::new();
-            setup_outline_page_ids(&outlines, &mut outline_page_ids, 1);
+            setup_outline_page_ids(&outlines, &mut outline_page_ids, 1)?;
             let page_id_to_page_numbers = self.setup_page_id_to_num();
             for (title, (page_id, _page_idx, level)) in outline_page_ids {
                 if let Some(page_num) = page_id_to_page_numbers.get(&page_id) {


Test patch: