diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index ac663dde1c6..d5475b07f5c 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -164,6 +164,42 @@ def test_linearize_page_tree() -> None: assert pdf.linearize_page_tree() == page_ids +def test_linearize_page_tree_duplicate_reference() -> None: + b = BytesIO() + with PdfParser(f=b, mode="wb") as pdf: + pdf.start_writing() + pdf.write_header() + + pages_ids = [pdf.next_object_id(0) for _ in range(2)] + pdf.write_obj( + pages_ids[0], Type=PdfName(b"Pages"), Count=1, Kids=[pages_ids[1]] + ) + pdf.write_obj( + pages_ids[1], Type=PdfName(b"Pages"), Count=1, Kids=[pages_ids[0]] + ) + + pdf.write_catalog() + pdf.write_xref_and_trailer() + + with PdfParser(f=b) as pdf: + with pytest.raises(PdfFormatError, match="cyclic or duplicate reference"): + pdf.linearize_page_tree( + PdfDict({b"Type": b"Pages", b"Kids": [pages_ids[0]]}) + ) + + with PdfParser(f=b, mode="wb") as pdf: + pdf.start_writing() + pdf.write_header() + + pdf.write_catalog() + pdf.write_xref_and_trailer() + + with PdfParser(f=b) as pdf: + pdf.page_tree_root[b"Kids"] = [pdf.pages_ref] + with pytest.raises(PdfFormatError, match="cyclic or duplicate reference"): + pdf.linearize_page_tree() + + def test_duplicate_xref_entry() -> None: pdf = PdfParser("Tests/images/duplicate_xref_entry.pdf") assert pdf.xref_table.existing_entries[6][0] == 1197 diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 39ae1acba3e..5f75cf4d3ee 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -617,7 +617,7 @@ def read_pdf_info(self) -> None: self.pages_ref = self.root[b"Pages"] assert self.pages_ref is not None self.page_tree_root = self.read_indirect(self.pages_ref) - self.pages = self.linearize_page_tree(self.page_tree_root) + self.pages = self.linearize_page_tree() # save the original list of page references # in case the user modifies, adds or deletes some pages # and we need to rewrite the pages and their list @@ -1083,17 +1083,29 @@ def read_indirect(self, ref: IndirectReference, max_nesting: int = -1) -> Any: return value def linearize_page_tree( - self, node: PdfDict | None = None + self, node: PdfDict | None = None, processed_ids: set[int] | None = None ) -> list[IndirectReference]: - page_node = node if node is not None else self.page_tree_root + if processed_ids is None: + processed_ids = set() + if node is not None: + page_node = node + else: + page_node = self.page_tree_root + if self.pages_ref is not None: + processed_ids.add(self.pages_ref.object_id) check_format_condition( page_node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages" ) pages = [] for kid in page_node[b"Kids"]: + check_format_condition( + kid.object_id not in processed_ids, + f"page tree contains a cyclic or duplicate reference to {kid}", + ) + processed_ids.add(kid.object_id) kid_object = self.read_indirect(kid) if kid_object[b"Type"] == b"Page": pages.append(kid) else: - pages.extend(self.linearize_page_tree(node=kid_object)) + pages.extend(self.linearize_page_tree(kid_object, processed_ids)) return pages