Repository navigation
ISO-2022 encoders accept U+001B (and iso2022_kr U+000E/U+000F) although the encoding cannot represent them #160302
Description
Bug description
In ISO 2022, the bytes 0x1B (ESC), 0x0E (SO) and 0x0F (SI) are special control characters that aren't supposed to exist by themselves: ESC introduces the designation sequences that switch character sets (ESC $ B selects JIS X 0208, and so on), and iso2022_kr uses SO and SI to shift between ASCII and KS X 1001. There is no way to put the characters U+001B, U+000E or U+000F into an ISO-2022-JP or ISO-2022-KR byte stream; a 0x1B byte in the output is the start of an escape sequence to whoever reads it.
I would expect that attempting to encode these by themselves (or in some other invalid configuration) to be a failure state and raise UnicodeEncodeError (allowing error handlers do their thing), which is what they do for other characters the encoding has no bytes for.
What they actually do is write the control character through as a bare byte, because the encoder's ASCII fast path accepts every code point below 0x80, Modules/cjkcodecs/_codecs_iso2022.c lines 190-200. The output is not valid ISO 2022, and the matching decoder either rejects it or reads it as something else. Alone, the byte is an incomplete escape sequence:
>>> '\x1b'.encode('iso2022_jp')
b'\x1b'
>>> b'\x1b'.decode('iso2022_jp')
Traceback (most recent call last):
File "<python-input-2>", line 1, in <module>
b'\x1b'.decode('iso2022_jp')
~~~~~~~~^^^^^^^^^^^^^^
UnicodeDecodeError: 'iso2022_jp' codec can't decode byte 0x1b in position 0: incomplete multibyte sequenceFollowed by the right ASCII characters, it is an escape sequence, and the text that follows is decoded in the wrong character set:
>>> 'x\x1b$B$"'.encode('iso2022_jp')
b'x\x1b$B$"'
>>> _.decode('iso2022_jp')
'xあ'And in front of a real designation sequence it makes the decoder consume that sequence as text:
>>> '\x1b\xa0'.encode('iso2022_jp_2004')
b'\x1b\x1b$(Q)"\x1b(B'
>>> _.decode('iso2022_jp_2004')
'\x1b\x1b$(Q)"'Every iso2022_* codec does this for U+001B. iso2022_kr does the same with SO and SI, which its decoder then interprets as shifts:
>>> '\x0e'.encode('iso2022_kr')
b'\x0e'
>>> _.decode('iso2022_kr')
''
>>> 'a\x0f가'.encode('iso2022_kr').decode('iso2022_kr')
'a가'I do not think this can be fixed on the decoding side: the second example shows that a decoder cannot tell a stray ESC from a real one, so only refusing to encode these characters makes the round trip hold. Raising is a behavior change for text that currently encodes without complaint, but what it currently encodes to is misread by any ISO-2022 reader, not just Python's. For comparison, hz has the same problem with ~ and escapes it as ~~; the other CJK codecs pass ESC through, but for them it is an ordinary control character on both sides, so they round-trip.
Found by the Hypothesis tests in #160155, where it is pinned as RoundTripBugs.test_iso2022_escape under expectedFailure; the round-trip and error-handler tests there keep these characters out of the generated text for these codecs until it is fixed.
Activity
pythongh-107862: Reference pythongh-160302 from the ISO-2022 expected…
pythongh-107862: Reference pythongh-160302 from the ISO-2022 expected…