From 136bddbf8019f0edb4612bb7e1f57406388f7c44 Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Sat, 15 Aug 2026 02:25:59 +0530 Subject: [PATCH] Raise OpcError instead of lxml XMLSyntaxError for a part with invalid XML Opening a .docx whose document.xml, [Content_Types].xml or a .rels part is not well-formed XML let a bare lxml.etree.XMLSyntaxError escape Document()/open. Wrap the two package-loading parse points (XmlPart.load and the OPC parse_xml) so a malformed part raises OpcError, matching how a non-package file already raises PackageNotFoundError. --- src/docx/opc/oxml.py | 8 +++++++- src/docx/opc/part.py | 12 +++++++++++- tests/opc/test_part.py | 8 ++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/docx/opc/oxml.py b/src/docx/opc/oxml.py index 7d3c489d6..47a419236 100644 --- a/src/docx/opc/oxml.py +++ b/src/docx/opc/oxml.py @@ -15,6 +15,7 @@ from docx.opc.constants import NAMESPACE as NS from docx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM +from docx.opc.exceptions import OpcError # configure XML parser element_class_lookup = etree.ElementNamespaceClassLookup() @@ -35,7 +36,12 @@ def parse_xml(text: str) -> etree._Element: """`etree.fromstring()` replacement that uses oxml parser.""" - return etree.fromstring(text, oxml_parser) + try: + return etree.fromstring(text, oxml_parser) + except etree.XMLSyntaxError as exc: + # A malformed [Content_Types].xml or .rels part would otherwise raise a + # bare lxml XMLSyntaxError while opening the package. + raise OpcError("package contains invalid XML: %s" % exc) from exc def qn(tag: str) -> str: diff --git a/src/docx/opc/part.py b/src/docx/opc/part.py index cbb4ab556..04115202d 100644 --- a/src/docx/opc/part.py +++ b/src/docx/opc/part.py @@ -6,6 +6,9 @@ from typing import TYPE_CHECKING, Callable, Type, cast +from lxml.etree import XMLSyntaxError + +from docx.opc.exceptions import OpcError from docx.opc.oxml import serialize_part_xml from docx.opc.packuri import PackURI from docx.opc.rel import Relationships @@ -228,7 +231,14 @@ def element(self): @classmethod def load(cls, partname: PackURI, content_type: str, blob: bytes, package: Package): - element = parse_xml(blob) + try: + element = parse_xml(blob) + except XMLSyntaxError as exc: + # A part with malformed XML would otherwise raise a bare lxml + # XMLSyntaxError out of Document()/open; report it as a package error. + raise OpcError( + "part '%s' contains invalid XML: %s" % (partname, exc) + ) from exc return cls(partname, content_type, element, package) @property diff --git a/tests/opc/test_part.py b/tests/opc/test_part.py index dbbcaf262..8fe05e014 100644 --- a/tests/opc/test_part.py +++ b/tests/opc/test_part.py @@ -345,6 +345,14 @@ def it_can_be_constructed_by_PartFactory( __init_.assert_called_once_with(ANY, partname_, content_type_, element_, package_) assert isinstance(part, XmlPart) + def it_raises_OpcError_on_a_part_with_invalid_xml( + self, partname_, content_type_, package_ + ): + from docx.opc.exceptions import OpcError + + with pytest.raises(OpcError): + XmlPart.load(partname_, content_type_, b"