diff --git a/Lib/plistlib.py b/Lib/plistlib.py index 93f3ef5e38af84..9c0ca6dc5f151b 100644 --- a/Lib/plistlib.py +++ b/Lib/plistlib.py @@ -67,7 +67,7 @@ import os import re import struct -from xml.parsers.expat import ParserCreate +from xml.parsers.expat import ExpatError, ParserCreate PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__) @@ -185,7 +185,20 @@ def parse(self, fileobj): self.parser.EndElementHandler = self.handle_end_element self.parser.CharacterDataHandler = self.handle_data self.parser.EntityDeclHandler = self.handle_entity_decl - self.parser.ParseFile(fileobj) + try: + self.parser.ParseFile(fileobj) + except ExpatError as e: + raise InvalidFileException(str(e)) from e + except LookupError as e: + # An declaration naming an encoding that Python's + # codec registry does not know raises a plain LookupError. A + # KeyError or IndexError from caller-provided code (e.g. a custom + # dict_type or file object) is a LookupError subclass and must not + # be masked as a malformed-file error, so only translate the exact + # LookupError type and let subclasses propagate unchanged. + if type(e) is not LookupError: + raise + raise InvalidFileException(str(e)) from e return self.root def handle_entity_decl(self, entity_name, is_parameter_entity, value, base, system_id, public_id, notation_name): diff --git a/Lib/test/test_plistlib.py b/Lib/test/test_plistlib.py index b9c261310bb567..8c61f68eb0521a 100644 --- a/Lib/test/test_plistlib.py +++ b/Lib/test/test_plistlib.py @@ -916,6 +916,36 @@ def test_load_invalid_file(self): with self.assertRaises(plistlib.InvalidFileException): plistlib.loads(b"these are not plist file contents") + def test_xml_plist_not_well_formed(self): + # gh-155397: a not-well-formed XML plist must raise the documented + # InvalidFileException, not a raw xml.parsers.expat.ExpatError. + with self.assertRaises(plistlib.InvalidFileException): + plistlib.loads(b"", fmt=plistlib.FMT_XML) + + def test_xml_plist_unknown_encoding(self): + # gh-155397: an declaration naming an encoding that + # Python does not know must raise the documented InvalidFileException, + # not a raw LookupError. + data = b'' + with self.assertRaises(plistlib.InvalidFileException): + plistlib.loads(data, fmt=plistlib.FMT_XML) + + def test_xml_plist_dict_type_lookup_error_propagates(self): + # gh-155397: a KeyError/IndexError raised by caller-supplied code (here + # a custom dict_type) is a LookupError subclass but signals a bug in the + # caller, not a malformed file. It must propagate unchanged rather than + # be masked as InvalidFileException, matching the pre-fix behaviour and + # the binary parser. Only a plain codec-registry LookupError (unknown + # declared encoding) is translated. + class RaisingDict(dict): + def __setitem__(self, key, value): + raise KeyError("boom from caller code") + + data = (b'' + b'ab') + with self.assertRaises(KeyError): + plistlib.loads(data, fmt=plistlib.FMT_XML, dict_type=RaisingDict) + def test_modified_uid_negative(self): neg_uid = UID(1) neg_uid.data = -1 # dodge the negative check in the constructor diff --git a/Misc/NEWS.d/next/Library/2026-08-21-17-45-42.gh-issue-155397.wet1xZ.rst b/Misc/NEWS.d/next/Library/2026-08-21-17-45-42.gh-issue-155397.wet1xZ.rst new file mode 100644 index 00000000000000..03ec877a0773ac --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-21-17-45-42.gh-issue-155397.wet1xZ.rst @@ -0,0 +1 @@ +:func:`plistlib.load` and :func:`plistlib.loads` now raise :exc:`plistlib.InvalidFileException` for XML plists that are not well-formed or that declare an unknown encoding, instead of leaking the underlying :exc:`~xml.parsers.expat.ExpatError` or :exc:`LookupError`.