Skip to content

Commit 7e6b0c9

Browse files
[3.14] gh-160522: Restore CJK encoder state after a failed incremental encode (GH-160523) (#161001)
gh-160522: Restore CJK encoder state after a failed incremental encode (GH-160523) A failed IncrementalEncoder.encode() or StreamWriter.write() of a stateful CJK codec kept the shift state reached before the error although its output was discarded. (cherry picked from commit 5b25582) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
1 parent 466b431 commit 7e6b0c9

3 files changed

Lines changed: 51 additions & 0 deletions

File tree

‎Lib/test/test_multibytecodec.py‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,48 @@ def test_incrementalencoder_final(self):
438438
self.assertEqual(output, self.expected_reset)
439439
self.assertEqual(encoder.encode('', final=True), b'')
440440

441+
def test_incrementalencoder_error(self):
442+
# The output of a failed encode() is lost, so the shift state
443+
# must not be changed.
444+
encoder = codecs.getincrementalencoder(self.encoding)()
445+
state = encoder.getstate()
446+
for final in False, True:
447+
self.assertRaises(UnicodeEncodeError, encoder.encode,
448+
self.text + '\U0001f600', final)
449+
self.assertEqual(encoder.getstate(), state)
450+
self.assertEqual(encoder.encode(self.text), self.expected)
451+
state = encoder.getstate()
452+
for final in False, True:
453+
self.assertRaises(UnicodeEncodeError, encoder.encode,
454+
'a\U0001f600', final)
455+
self.assertEqual(encoder.getstate(), state)
456+
self.assertEqual(encoder.encode('a'), self.reset + b'a')
457+
458+
def test_streamwriter_error(self):
459+
stream = io.BytesIO()
460+
writer = codecs.getwriter(self.encoding)(stream)
461+
self.assertRaises(UnicodeEncodeError, writer.write,
462+
self.text + '\U0001f600')
463+
self.assertEqual(stream.getvalue(), b'')
464+
writer.write(self.text)
465+
self.assertEqual(stream.getvalue(), self.expected)
466+
467+
def test_textiowrapper_error(self):
468+
raw = io.BytesIO()
469+
with io.TextIOWrapper(raw, encoding=self.encoding) as f:
470+
self.assertRaises(UnicodeEncodeError, f.write,
471+
self.text + '\U0001f600')
472+
f.write(self.text)
473+
f.flush()
474+
self.assertEqual(raw.getvalue(), self.expected)
475+
476+
class TestISO2022KRStateful(TestStateful):
477+
text = '\uac00\uac00'
478+
encoding = 'iso-2022-kr'
479+
expected = b'\x1b$)C\x0e0!0!'
480+
reset = b'\x0f'
481+
expected_reset = expected + reset
482+
441483
class TestHZStateful(TestStateful):
442484
text = '\u804a\u804a'
443485
encoding = 'hz'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix the incremental encoders and stream writers of the stateful CJK codecs
2+
(``iso2022_*`` and ``hz``) keeping a changed shift state after an
3+
:meth:`!encode` or :meth:`!write` call failed. The next call could omit the
4+
escape sequence, writing multibyte characters in ASCII mode.

‎Modules/cjkcodecs/multibytecodec.c‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,7 @@ encoder_encode_stateful(MultibyteStatefulEncoderContext *ctx,
801801
PyObject *inbuf = NULL;
802802
Py_ssize_t inpos, datalen;
803803
PyObject *origpending = NULL;
804+
MultibyteCodec_State origstate;
804805

805806
if (PyUnicode_Check(unistr))
806807
ucvt = NULL;
@@ -816,6 +817,7 @@ encoder_encode_stateful(MultibyteStatefulEncoderContext *ctx,
816817
}
817818
}
818819

820+
origstate = ctx->state;
819821
if (ctx->pending) {
820822
PyObject *inbuf_tmp;
821823

@@ -873,6 +875,9 @@ encoder_encode_stateful(MultibyteStatefulEncoderContext *ctx,
873875
return r;
874876

875877
errorexit:
878+
/* recover the original codec state: the discarded output may have
879+
* included an escape sequence for a state change */
880+
ctx->state = origstate;
876881
Py_XDECREF(r);
877882
Py_XDECREF(ucvt);
878883
Py_XDECREF(origpending);

0 commit comments

Comments
 (0)