https://github.com/python/cpython/commit/1ccb123f7cc3fef63ec651183f136d4ef16d5bb7
commit: 1ccb123f7cc3fef63ec651183f136d4ef16d5bb7
branch: 3.15
author: Gregory P. Smith <[email protected]>
committer: gpshead <[email protected]>
date: 2026-11-01T20:47:08Z
summary:

[3.15] gh-161102: Fix idna incremental decoder after leading empty labels 
(GH-161103) (#161130)

Count the separating dot for every label after the first, so dots after
leading empty labels are not decoded again by the next call.

ANT-2026-KFX1PUWZ

(cherry picked from commit 2982d9388cef180166e040fe0d4e59be9c824a87)

files:
A Misc/NEWS.d/next/Library/2026-10-22-05-20-02.gh-issue-161102.gqDOIM7X.rst
M Lib/encodings/idna.py
M Lib/test/test_codecs.py

diff --git a/Lib/encodings/idna.py b/Lib/encodings/idna.py
index d31ee07ab45b76d..948ecd583979e55 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 296dfb6007056d6..9437919c6bc476d 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 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 <encodings.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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to