]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-154863: Fix iconv encoding of ISO-2022-CN-EXT returning empty bytes (GH-154899)
authorVyron Vasileiadis <hi@fedonman.com>
Fri, 31 Jul 2026 10:56:52 +0000 (13:56 +0300)
committerGitHub <noreply@github.com>
Fri, 31 Jul 2026 10:56:52 +0000 (10:56 +0000)
The flush that emits the pending shift sequence can report a nonreversible
conversion, which the loop mistook for a substituted character. It then
retried while still flushing, converted nothing and returned the empty
output buffer.

Lib/test/test_codecs.py
Objects/unicodeobject.c

index 31704955df3e14e822be2d94c255b4575ed13c64..f6f8089a065059ba89d3ffe3852422e5318af428 100644 (file)
@@ -3679,6 +3679,10 @@ _ICONV_MULTIBYTE = ['EUC-JP', 'SHIFT_JIS', 'GBK', 'GB18030', 'BIG5']
 # Encodings iconv may provide but for which CPython has no built-in codec
 # (cp1047 is EBCDIC, i.e. not ASCII-compatible).
 _ICONV_ONLY = ['cp1047', 'cp1133', 'GEORGIAN-PS', 'ARMSCII-8']
+# Encodings that leave a shift state pending, so encoding ends with a flush.
+_ICONV_SHIFT_STATE = [('ISO-2022-CN-EXT', 'ABC\u4e2dDEF'),
+                      ('ISO-2022-CN', 'ABC\u4e2dDEF'),
+                      ('ISO-2022-JP', 'ABC\u65e5DEF')]
 
 
 @unittest.skipUnless(hasattr(codecs, 'iconv_encode'),
@@ -3812,6 +3816,33 @@ class IconvTest(unittest.TestCase):
             with self.subTest(text=text):
                 self.assertEqual(text.encode('iconv:' + enc), text.encode(enc))
 
+    def test_encode_shift_state_flush(self):
+        # Encoding ends with a flush that emits the pending shift sequence.  Its
+        # return value counts nonreversible conversions, and some iconv
+        # implementations make it positive for the flush itself (glibc does for
+        # ISO-2022-CN-EXT).  That must not be read as a substituted character:
+        # doing so discarded the whole output, ASCII included.
+        #
+        # Only the ASCII around the character is checked, not a full round-trip.
+        # An iconv that cannot represent the character either rejects it or
+        # substitutes for it silently, as macOS does for ISO-2022-CN.
+        tested = False
+        for enc, text in _ICONV_SHIFT_STATE:
+            if not iconv_encoding_available(enc):
+                continue
+            with self.subTest(encoding=enc):
+                try:
+                    data = codecs.iconv_encode(enc, text)[0]
+                except UnicodeEncodeError:
+                    continue
+                tested = True
+                self.assertNotEqual(data, b'')
+                decoded = codecs.iconv_decode(enc, data, 'strict', True)[0]
+                self.assertStartsWith(decoded, 'ABC')
+                self.assertEndsWith(decoded, 'DEF')
+        if not tested:
+            self.skipTest('no shift-state iconv encoding is available')
+
     def test_encode_surrogateescape(self):
         # A lone surrogate lives in the 2-byte kind and round-trips.
         enc = self.require('ASCII')
index a4550c0f5f3363a4dbb1ceca503b4776974994e5..eec02f662e79059ff8f7deb0cba047910dfef1ae 100644 (file)
@@ -8462,6 +8462,9 @@ _PyUnicode_EncodeIconv(const char *encoding, PyObject *unicode,
         }
 
         if (ret != (size_t)-1) {
+            if (flushing) {
+                break;
+            }
             /* A positive result counts nonreversible conversions: iconv()
                substituted an unencodable character instead of failing with
                EILSEQ (musl and *BSD citrus do this).  Treat it as unencodable
@@ -8479,9 +8482,6 @@ _PyUnicode_EncodeIconv(const char *encoding, PyObject *unicode,
                 out = out_before;
                 up -= unit;
             }
-            else if (flushing) {
-                break;
-            }
             else if (careful && up < uend) {
                 continue;
             }