From 514e77bd8f6bf43fd871dcad1325b771572729cd Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Thu, 22 Oct 2026 05:24:13 +0000 Subject: [PATCH] gh-161102: Fix idna incremental decoder after leading empty labels Count the separating dot for every label after the first, so dots after leading empty labels are not decoded again by the next call. --- Lib/encodings/idna.py | 5 +++-- Lib/test/test_codecs.py | 18 ++++++++++++++++++ ...10-22-05-20-02.gh-issue-161102.gqDOIM7X.rst | 4 ++++ 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-22-05-20-02.gh-issue-161102.gqDOIM7X.rst diff --git a/Lib/encodings/idna.py b/Lib/encodings/idna.py index 98d7d891594c648..d5c06c7c73f027b 100644 --- a/Lib/encodings/idna.py +++ b/Lib/encodings/idna.py @@ -364,6 +364,9 @@ def _buffer_decode(self, input, errors, final): result = [] size = 0 for label in labels: + if result: + # Join with U+002E + size += 1 try: u_label = ToUnicode(label) except (UnicodeEncodeError, UnicodeDecodeError) as exc: @@ -376,8 +379,6 @@ def _buffer_decode(self, input, errors, final): ) else: result.append(u_label) - if size: - size += 1 size += len(label) result = ".".join(result) + trailing_dot diff --git a/Lib/test/test_codecs.py b/Lib/test/test_codecs.py index 37ac57063c35390..9b4fdc93ead6b82 100644 --- a/Lib/test/test_codecs.py +++ b/Lib/test/test_codecs.py @@ -1667,6 +1667,8 @@ class IDNACodecTest(unittest.TestCase): (b"xn--pythn-&mua.org", UnicodeDecodeError("idna", b"xn--pythn-&mua.org", 10, 11, "")), (b"xn--pythn-m&ua.org", UnicodeDecodeError("idna", b"xn--pythn-m&ua.org", 11, 12, "")), (b"xn--pythn-mua&.org", UnicodeDecodeError("idna", b"xn--pythn-mua&.org", 13, 14, "")), + (b"bugs.xn--pythn-&mua.org", UnicodeDecodeError("idna", b"bugs.xn--pythn-&mua.org", 15, 16, "")), + (b".xn--pythn-&mua.org", UnicodeDecodeError("idna", b".xn--pythn-&mua.org", 11, 12, "")), ] invalid_encode_testcases = [ (f"foo.{'\xff'*60}", UnicodeEncodeError("idna", f"foo.{'\xff'*60}", 4, 64, "")), @@ -1786,6 +1788,22 @@ def test_incremental_decode(self): self.assertEqual(decoder.decode(b"rg."), "org.") self.assertEqual(decoder.decode(b"", True), "") + def test_incremental_decode_leading_empty_labels(self): + # The dot after an empty label at the start of the input has to be + # counted as consumed, or it is decoded again by the next call. + for data in b".python.org", b"..python.org.", b"..", b"." * 100: + with self.subTest(data=data): + decoder = codecs.getincrementaldecoder("idna")() + result = decoder.decode(data) + self.assertNotIn(b".", decoder.getstate()[0]) + result += decoder.decode(b"", True) + self.assertEqual(result, data.decode("idna")) + self.assertEqual(decoder.getstate(), (b"", 0)) + self.assertEqual(decoder.decode(b"", True), "") + + with io.TextIOWrapper(io.BytesIO(b"." * 100), encoding="idna") as f: + self.assertEqual(f.readline(), "." * 100) + def test_incremental_decode_invalid(self): iterdecode_testcases = [ (b"\xFFpython.org", UnicodeDecodeError("idna", b"\xFF", 0, 1, "")), diff --git a/Misc/NEWS.d/next/Library/2026-10-22-05-20-02.gh-issue-161102.gqDOIM7X.rst b/Misc/NEWS.d/next/Library/2026-10-22-05-20-02.gh-issue-161102.gqDOIM7X.rst new file mode 100644 index 000000000000000..e3089a0560bcbf7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-22-05-20-02.gh-issue-161102.gqDOIM7X.rst @@ -0,0 +1,4 @@ +Fix the incremental decoder of the :mod:`idna ` codec +decoding some dots twice when its input starts with empty labels, for example +``b".example.com"``. Reading a run of dots through :class:`io.TextIOWrapper` +no longer produces quadratically more text than the input holds.