Repository: py-pdf/pypdf (Python)

Issue:
[PyPDF2-2.10.6] ValueError: invalid literal for int() with base 16
Merging one of the pdfs is raising a ValueError. This happens since the release of PyPDF2-2..10.6. 

The pdfs that are merged are owasp dependency check reports. Only one of them causes the exception. Note: this also happens when I only merge this specific PDF and not the other, working, PDFs.

Note: this is my first issue raised for PyPDF2. I've tried to include all the information required but am not familiar with the process. If anything's missing or invalid, let me know. 

**Environment**
Python 3.9

**Code:**
```python
from PyPDF2 import PdfFileMerger
if __name__ == '__main__':
    pdf_file = 'path-to-file-that-exists.pdf'
    merger.append(pdf_file)
```

**Code in PyPDF2 where it breaks:**
```python
@staticmethod
    def unnumber(sin: str) -> str:
        i = sin.find("#")
        while i >= 0:
            sin = sin[:i] + chr(int(sin[i + 1 : i + 3], 16)) + sin[i + 3 :]
            i = sin.find("#")
        return sin
```
**Value of sin at moment of exception:**
`'/ñjÔª\x0cÎ\x87´³°#J#86#fe#2a#b2jYJ#94'`

**Exception raised:**

```
PyPDF2.errors.PdfReadError: ValueError("invalid literal for int() with base 16: 'J#'")
```

**Stacktrace:**
```
Traceback (most recent call last):
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\generic\_data_structures.py", line 266, in read_from_stream
    key = read_object(stream, pdf)
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\generic\_data_structures.py", line 824, in read_object
    return NameObject.read_from_stream(stream, pdf)
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\generic\_base.py", line 444, in read_from_stream
    ret = NameObject.unnumber(ret)
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\generic\_base.py", line 427, in unnumber
    sin = sin[:i] + chr(int(sin[i + 1 : i + 3], 16)) + sin[i + 3 :]
ValueError: invalid literal for int() with base 16: 'J#'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\main.py", line 37, in <module>
    merge_files(args.depcheck, 'depcheck')
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\main.py", line 27, in merge_files
    merger.append(pdf_file)
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\_utils.py", line 389, in wrapper
    return func(*args, **kwargs)
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\_merger.py", line 283, in append
    self.merge(len(self.pages), fileobj, outline_item, pages, import_outline)
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\_utils.py", line 389, in wrapper
    return func(*args, **kwargs)
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\_merger.py", line 184, in merge
    outline = reader.outline
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\_reader.py", line 696, in outline
    return self._get_outline()
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\_reader.py", line 725, in _get_outline
    self._namedDests = self._get_named_destinations()
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\_reader.py", line 643, in _get_named_destinations
    tree = cast(TreeObject, catalog[CA.DESTS])
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\generic\_data_structures.py", line 149, in __getitem__
    return dict.__getitem__(self, key).get_object()
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\generic\_base.py", line 163, in get_object
    obj = self.pdf.get_object(self)
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\_reader.py", line 1179, in get_object
    retval = read_object(self.stream, self)  # type: ignore
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\generic\_data_structures.py", line 831, in read_object
    return DictionaryObject.read_from_stream(stream, pdf, forced_encoding)
  File "C:\Users\MG78YH\PycharmProjects\pythonProject\venv\lib\site-packages\PyPDF2\generic\_data_structures.py", line 272, in read_from_stream
    raise PdfReadError(exc.__repr__())
PyPDF2.errors.PdfReadError: ValueError("invalid literal for int() with base 16: 'J#'")
```

**Pdf:**
Unfortunately I'm not allowed to share the file (yet). I've tried to include as much information as possible to reproduce this. Please let me know if it's enough, otherwise I can check again with my manager if I can share some redacted version of the pdf that causes the exception to be raised.


Discussion:
@sietzeberends 
thanks for this very complete report with a very good first analysis. 
in order to complete it 
a) can you confirm that the PDF can be opened with Acrobat Reader
b) the outlines are correctly displayed
c) the sequence at first sight '/ñjÔª\x0cÎ\x87´³°#J#86#fe#2a#b2jYJ#94' seems to be the value of a key in a dictionary. Can you check if you can locate it in file with a hexa/binary viewer, and share the data around the object. This may be sufficient to have a better understanding
@pubpub-zz Thanks. I did some more digging and found some stuff...

a) yes
b) yes. Also, all links (bookmarks) in the document work and can be clicked
c) I couldn't find it but noticed that before the string that's passed to unnumber() looks different within unnumber() then it looks from the caller, which is the read_from_stream() method. **This string I could find in 2 places**. 

String in unnumber(): 
```'/ñjÔª\x0cÎ\x87´³°#J#86#fe#2a#b2jYJ#94'```

String in read_from_stream():
```'/#f1j#d4#aa#0c#ce#87#b4#b3#b0#23J#86#fe#2a#b2jYJ#94'```

**Occurence 1** :

``` endobj
77 0 obj
[0 /XYZ 31.5000000  
664.250000  0]
endobj
78 0 obj
[0 /XYZ 31.5000000  
300.500000  0]
endobj
79 0 obj
[0 /XYZ 31.5000000  
575.750000  0]
endobj
80 0 obj
[0 /XYZ 42  
175.250000  0]
endobj
81 0 obj
[0 /XYZ 28.5000000  
813.500000  0]
endobj
82 0 obj
[0 /XYZ 28.5000000  
813.500000  0]
endobj
83 0 obj
[0 /XYZ 42  
149  0]
endobj
84 0 obj
[0 /XYZ 28.5000000  
813.500000  0]
endobj
85 0 obj
[0 /XYZ 42  
87.5000000  0]
endobj
86 0 obj
[0 /XYZ 28.5000000  
813.500000  0]
endobj
87 0 obj
[0 /XYZ 28.5000000  
813.500000  0]
endobj
88 0 obj
[0 /XYZ 28.5000000  
813.500000  0]
endobj
89 0 obj
<<
/Type /Annot
/Subtype /Link
/Rect [35.2500000  505.250000  67.5000000  525.500000 ]
/Border [0 0 0]
/Dest /MX#da#09#02#5c#a6#0dN#fcb#9d#0c#ae0#90m#0aM#7e
>>
endobj
90 0 obj
<<
/Type /Annot
/Subtype /Link
/Rect [35.2500000  484.250000  70.5000000  497.750000 ]
/Border [0 0 0]
/Dest /#f1j#d4#aa#0c#ce#87#b4#b3#b0#23J#86#fe#2a#b2jYJ#94
>>
endobj
91 0 obj
<<
/Type /Annot
/Subtype /Link
/Rect [35.2500000  457.250000  60  477.500000 ]
/Border [0 0 0]
/Dest /#bePC#d6c#12.6#ec#d6#d5#ca#da#15q#7d#b8#adL#bf
>>
endobj
92 0 obj
<<
/Type /Annot
/Subtype /Link
/Rect [35.2500000  429.500000  75  449.750000 ]
/Border [0 0 0]
/Dest /#9eS#bc#bf#3f#8fdOI#ecX#f0#ebL4b#ec#91K#14
>>
endobj
93 0 obj
<<
/Type /Annot
/Subtype /Link
/Rect [35.2500000  408.500000  66  422 ]
/Border [0 0 0]
/Dest /#8b#d7#20#03#ae#c0#9fa#c7#1c#89#7eK#27#3ek#da#1d#b7#fc
>>
endobj
94 0 obj
<<
/Type /Annot
/Subtype /Link
/Rect [35.2500000  388.250000  66.7500000  401.750000 ]
/Border [0 0 0]
/Dest /#1e!#c4#b7#dfq5#e1#d06#3d#ee#edzT#e5K#81#a3#0c
>>
endobj
95 0 obj
<<
/Type /Annot
/Subtype /Link
<<
/Type /Action
/S /URI
/URI (https://www.npmjs.com/advisories)
>>
>>
endobj
1148 0 obj
<<
/Type /Annot
/Subtype /Link
/Rect [151.500000  146  174.750000  152.750000 ]
/Border [0 0 0]
/A <<
/Type /Action
/S /URI
/URI (https://retirejs.github.io/retire.js/)
>>
>>
endobj
1149 0 obj
<<
/Type /Annot
/Subtype /Link
/Rect [161.250000  138.500000  217.500000  145.250000 ]
/Border [0 0 0]
/A <<
/Type /Action
/S /URI
/URI (https://ossindex.sonatype.org)
>>
>>
endobj
1150 0 obj
<<
/#8b#d7#20#03#ae#c0#9fa#c7#1c#89#7eK#27#3ek#da#1d#b7#fc 88 0 R
/__WKANCHOR_1a 544 0 R
/__WKANCHOR_1c 545 0 R
/__WKANCHOR_1e 546 0 R
/#f1j#d4#aa#0c#ce#87#b4#b3#b0#23J#86#fe#2a#b2jYJ#94 87 0 R
/__WKANCHOR_1g 547 0 R
/__WKANCHOR_1i 548 0 R
/__WKANCHOR_1k 615 0 R
/__WKANCHOR_1m 616 0 R
/__WKANCHOR_1o 617 0 R
/__WKANCHOR_10 449 0 R
/__WKANCHOR_12 450 0 R
/__WKANCHOR_14 451 0 R
/__WKANCHOR_a 76 0 R
/__WKANCHOR_1q 618 0 R
/__WKANCHOR_16 452 0 R
/#1e!#c4#b7#dfq5#e1#d06#3d#ee#edzT#e5K#81#a3#0c 73 0 R
/__WKANCHOR_c 78 0 R
/__WKANCHOR_1s 784 0 R
/MX#da#09#02#5c#a6#0dN#fcb#9d#0c#ae0#90m#0aM#7e 86 0 R
/#9eS#bc#bf#3f#8fdOI#ecX#f0#ebL4b#ec#91K#14 82 0 R
/__WKANCHOR_18 448 0 R
/
```

**Occurence 2:** 

```<<
/Type /Action
/S /URI
/URI (https://www.npmjs.com/advisories)
>>
>>
endobj
1148 0 obj
<<
/Type /Annot
/Subtype /Link
/Rect [151.500000  146  174.750000  152.750000 ]
/Border [0 0 0]
/A <<
/Type /Action
/S /URI
/URI (https://retirejs.github.io/retire.js/)
>>
>>
endobj
1149 0 obj
<<
/Type /Annot
/Subtype /Link
/Rect [161.250000  138.500000  217.500000  145.250000 ]
/Border [0 0 0]
/A <<
/Type /Action
/S /URI
/URI (https://ossindex.sonatype.org)
>>
>>
endobj
1150 0 obj
<<
/#8b#d7#20#03#ae#c0#9fa#c7#1c#89#7eK#27#3ek#da#1d#b7#fc 88 0 R
/__WKANCHOR_1a 544 0 R
/__WKANCHOR_1c 545 0 R
/__WKANCHOR_1e 546 0 R
/#f1j#d4#aa#0c#ce#87#b4#b3#b0#23J#86#fe#2a#b2jYJ#94 87 0 R
/__WKANCHOR_1g 547 0 R
/__WKANCHOR_1i 548 0 R
/__WKANCHOR_1k 615 0 R
/__WKANCHOR_1m 616 0 R
/__WKANCHOR_1o 617 0 R
/__WKANCHOR_10 449 0 R
/__WKANCHOR_12 450 0 R
/__WKANCHOR_14 451 0 R
/__WKANCHOR_a 76 0 R
/__WKANCHOR_1q 618 0 R
/__WKANCHOR_16 452 0 R
/#1e!#c4#b7#dfq5#e1#d06#3d#ee#edzT#e5K#81#a3#0c 73 0 R
/__WKANCHOR_c 78 0 R
/__WKANCHOR_1s 784 0 R
/MX#da#09#02#5c#a6#0dN#fcb#9d#0c#ae0#90m#0aM#7e 86 0 R
/#9eS#bc#bf#3f#8fdOI#ecX#f0#ebL4b#ec#91K#14 82 0 R
/__WKANCHOR_18 448 0 R
/```
Got it!!!
The problem is because of the #23 converted int # which is trying to be converted again:
can you replace unnumber() in _base.py with this code:
```
    @staticmethod
    def unnumber(sin: str) -> str:
        i=0
        i = sin.find("#",0)
        while i >= 0:
            sin = sin[:i] + chr(int(sin[i + 1 : i + 3], 16)) + sin[i + 3 :]
            i = sin.find("#",i+1)
        return sin
```
and confirm the error is fixed
@pubpub-zz thanks for the fix, I can confirm it works 👍. I'm assuming your commit & PR will take care of the rest, correct? Or is there something else I can do?

Thanks again 👍 
Just prepare I will give the go.
@sietzeberends Thank you for the excellent issue description :pray: We value that a lot as it helps us to be more efficient in ironing the problems out.

As a "Thank you", I would like to add you as a contributor: https://pypdf2.readthedocs.io/en/latest/meta/CONTRIBUTORS.html - would that be fine for you?
I'll make a new release to PyPI today which will include the fix :-)
@MartinThoma sure, cool :)! I have to say I'm a java dev that uses python mainly in Azure pipelines but I will try to contribute whenever I think I can.  



Patch:
diff --git a/PyPDF2/generic/_base.py b/PyPDF2/generic/_base.py
index b968ef2bba..0da89bc8c5 100644
--- a/PyPDF2/generic/_base.py
+++ b/PyPDF2/generic/_base.py
@@ -422,10 +422,10 @@ def writeToStream(
 
     @staticmethod
     def unnumber(sin: str) -> str:
-        i = sin.find("#")
+        i = sin.find("#", 0)
         while i >= 0:
             sin = sin[:i] + chr(int(sin[i + 1 : i + 3], 16)) + sin[i + 3 :]
-            i = sin.find("#")
+            i = sin.find("#", i + 1)
         return sin
 
     @staticmethod


Test patch:
diff --git a/tests/test_generic.py b/tests/test_generic.py
index d0798c5f6c..deff2ddab6 100644
--- a/tests/test_generic.py
+++ b/tests/test_generic.py
@@ -186,6 +186,12 @@ def test_NameObject():
         == "/paired()parentheses"
     )
     assert NameObject.read_from_stream(BytesIO(b"/A#42"), None) == "/AB"
+    assert (
+        NameObject.read_from_stream(
+            BytesIO(b"/#f1j#d4#aa#0c#ce#87#b4#b3#b0#23J#86#fe#2a#b2jYJ#94"), None
+        )
+        == "/ñjÔª\x0cÎ\x87´³°#J\x86þ*²jYJ\x94"
+    )
 
 
 def test_destination_fit_r():