diff --git a/Lib/plistlib.py b/Lib/plistlib.py index 93f3ef5e38af84..8f3c7404c62f2e 100644 --- a/Lib/plistlib.py +++ b/Lib/plistlib.py @@ -164,11 +164,10 @@ def _escape(text): if m is not None: raise ValueError("strings can't contain control characters; " "use bytes instead") - text = text.replace("\r\n", "\n") # convert DOS line endings - text = text.replace("\r", "\n") # convert Mac line endings text = text.replace("&", "&") # escape '&' text = text.replace("<", "<") # escape '<' text = text.replace(">", ">") # escape '>' + text = text.replace("\r", " ") # preserve CR via character reference return text class _PlistParser: diff --git a/Lib/test/test_plistlib.py b/Lib/test/test_plistlib.py index b9c261310bb567..e697d8fef73ff0 100644 --- a/Lib/test/test_plistlib.py +++ b/Lib/test/test_plistlib.py @@ -818,13 +818,26 @@ def test_controlcharacters(self): if i >= 32 or c in "\r\n\t": # \r, \n and \t are the only legal control chars in XML data = plistlib.dumps(testString, fmt=plistlib.FMT_XML) - if c != "\r": - self.assertEqual(plistlib.loads(data), testString) + self.assertEqual(plistlib.loads(data), testString) else: with self.assertRaises(ValueError): plistlib.dumps(testString, fmt=plistlib.FMT_XML) plistlib.dumps(testString, fmt=plistlib.FMT_BINARY) + def test_cr_newline_roundtrip(self): + # gh-139423: Carriage returns should survive XML plist round-trip. + test_cases = [ + "hello\rworld", # standalone CR + "hello\r\nworld", # CRLF + "a\rb\nc\r\nd", # mixed newlines + "\r", # bare CR + "\r\n", # bare CRLF + ] + for s in test_cases: + with self.subTest(s=s): + data = plistlib.dumps(s, fmt=plistlib.FMT_XML) + self.assertEqual(plistlib.loads(data), s) + def test_non_bmp_characters(self): pl = {'python': '\U0001f40d'} for fmt in ALL_FORMATS: diff --git a/Misc/NEWS.d/next/Library/2026-04-09-14-30-00.gh-issue-139423.UD0SN_qTKdI.rst b/Misc/NEWS.d/next/Library/2026-04-09-14-30-00.gh-issue-139423.UD0SN_qTKdI.rst new file mode 100644 index 00000000000000..3712383fc1b4da --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-09-14-30-00.gh-issue-139423.UD0SN_qTKdI.rst @@ -0,0 +1,4 @@ +Fixed :mod:`plistlib` to preserve carriage return characters (``\r``) during +XML plist round-trips. Previously, ``\r`` and ``\r\n`` were normalized to +``\n`` during serialization, causing data corruption. Carriage returns are now +encoded as `` `` XML character references, which the XML parser preserves.