Skip to content

Commit 894408e

Browse files
[3.15] gh-160522: Restore CJK encoder state after a failed incremental encode (GH-160523) (#161000)
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 35605cc commit 894408e

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
@@ -793,6 +793,7 @@ encoder_encode_stateful(MultibyteStatefulEncoderContext *ctx,
793793
PyObject *inbuf = NULL;
794794
Py_ssize_t inpos, datalen;
795795
PyObject *origpending = NULL;
796+
MultibyteCodec_State origstate;
796797

797798
if (PyUnicode_Check(unistr))
798799
ucvt = NULL;
@@ -808,6 +809,7 @@ encoder_encode_stateful(MultibyteStatefulEncoderContext *ctx,
808809
}
809810
}
810811

812+
origstate = ctx->state;
811813
if (ctx->pending) {
812814
PyObject *inbuf_tmp;
813815

@@ -865,6 +867,9 @@ encoder_encode_stateful(MultibyteStatefulEncoderContext *ctx,
865867
return r;
866868

867869
errorexit:
870+
/* recover the original codec state: the discarded output may have
871+
* included an escape sequence for a state change */
872+
ctx->state = origstate;
868873
Py_XDECREF(r);
869874
Py_XDECREF(ucvt);
870875
Py_XDECREF(origpending);

0 commit comments

Comments
 (0)