Skip to content

Commit 58fd80a

Browse files
ROB: Do not crash on an outline node that is not a dictionary (#4018)
The outline walk casts /First to a DictionaryObject and subscripts it, so a file where that entry holds a number, a string or an array raised 'TypeError: NumberObject object is not subscriptable' from reader.outline. The same loop already warns and stops on a cycle, so an unusable node is reported the same way and the outline read returns what it has.
1 parent 39565fe commit 58fd80a

2 files changed

Lines changed: 51 additions & 0 deletions

File tree

‎pypdf/_doc_common.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,14 @@ def _get_outline(
938938
f"Maximum outline entry limit reached: {traversal_state.entry_count} > {OUTLINE_MAX_ENTRIES}."
939939
)
940940

941+
if not isinstance(node, DictionaryObject):
942+
logger_warning(
943+
"Outline node is not a dictionary: %(node)s",
944+
source=__name__,
945+
node=node,
946+
)
947+
break
948+
941949
outline_obj = self._build_outline_item(node)
942950
if outline_obj:
943951
outline.append(outline_obj)

‎tests/test_doc_common.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,3 +1104,46 @@ def test_viewer_preferences__reads_a_well_formed_dictionary():
11041104
stream.seek(0)
11051105

11061106
assert PdfReader(stream).viewer_preferences == {"/CenterWindow": True}
1107+
1108+
1109+
@pytest.mark.parametrize(
1110+
("first", "expected"),
1111+
[
1112+
pytest.param(
1113+
NumberObject(1), "Outline node is not a dictionary: 1", id="number"
1114+
),
1115+
pytest.param(
1116+
TextStringObject("x"), "Outline node is not a dictionary: x", id="string"
1117+
),
1118+
pytest.param(ArrayObject(), "Outline node is not a dictionary: []", id="array"),
1119+
],
1120+
)
1121+
def test_outline__first_is_not_a_dictionary(caplog, first, expected):
1122+
"""A malformed /First raised a TypeError from the first subscript."""
1123+
writer = PdfWriter()
1124+
writer.add_blank_page(width=72, height=72)
1125+
writer.root_object[NameObject("/Outlines")] = DictionaryObject(
1126+
{NameObject("/First"): first}
1127+
)
1128+
stream = BytesIO()
1129+
writer.write(stream)
1130+
stream.seek(0)
1131+
1132+
assert PdfReader(stream).outline == []
1133+
assert expected in caplog.text
1134+
1135+
1136+
def test_outline__reads_a_well_formed_outline():
1137+
writer = PdfWriter()
1138+
for _ in range(2):
1139+
writer.add_blank_page(width=72, height=72)
1140+
writer.add_outline_item("Chapter 1", 0)
1141+
writer.add_outline_item("Chapter 2", 1)
1142+
stream = BytesIO()
1143+
writer.write(stream)
1144+
stream.seek(0)
1145+
1146+
assert [item.title for item in PdfReader(stream).outline] == [
1147+
"Chapter 1",
1148+
"Chapter 2",
1149+
]

0 commit comments

Comments
 (0)