diff --git a/Lib/encodings/idna.py b/Lib/encodings/idna.py index d31ee07ab45b76..948ecd583979e5 100644 --- a/Lib/encodings/idna.py +++ b/Lib/encodings/idna.py @@ -348,6 +348,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: @@ -360,8 +363,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 296dfb6007056d..9437919c6bc476 100644 --- a/Lib/test/test_codecs.py +++ b/Lib/test/test_codecs.py @@ -1587,6 +1587,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, "")), @@ -1684,6 +1686,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 00000000000000..e3089a0560bcbf --- /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.