Skip to content

Commit d67f2a9

Browse files
[3.14] gh-159739: Preserve CJK codec state across chunk boundaries and callbacks (GH-160400) (#161070)
The HZ encoder can consume a tilde after writing only its first escape byte. A split ISO-2022 escape can exceed the incremental decoder’s pending buffer, and a reset can leave the previous character set selected. Reserve space for both HZ bytes before consuming the tilde, allow the full pending escape, and restore the initial ISO-2022 state on reset. Keep the native codec table and the active encoder error policy alive so replacing their Python owners cannot leave a codec using freed memory. (cherry picked from commit a96fbdc) Co-authored-by: Jelle Zijlstra <jelle@openai.com>
1 parent 0c85dc8 commit d67f2a9

6 files changed

Lines changed: 58 additions & 15 deletions

File tree

‎Lib/test/test_multibytecodec.py‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ def test_nullcoding(self):
3939
self.assertEqual(str(b'', enc), '')
4040
self.assertEqual(''.encode(enc), b'')
4141

42+
def test_hz_tilde_at_buffer_boundary(self):
43+
text = 'a' + '中a' * 6 + '~'
44+
self.assertEqual(text.encode('hz').decode('hz'), text)
45+
4246
def test_str_decode(self):
4347
for enc in ALL_CJKENCODINGS:
4448
self.assertEqual('abcd'.encode(enc), b'abcd')
@@ -91,6 +95,12 @@ def test_decode_unicode(self):
9195

9296
class Test_IncrementalEncoder(unittest.TestCase):
9397

98+
def test_iso2022_kr_reset(self):
99+
encoder = codecs.getincrementalencoder('iso2022_kr')()
100+
encoder.encode('가')
101+
encoder.reset()
102+
self.assertEqual(encoder.encode('가', final=True), '가'.encode('iso2022_kr'))
103+
94104
def test_stateless(self):
95105
# cp949 encoder isn't stateful at all.
96106
encoder = codecs.getincrementalencoder('cp949')()
@@ -242,6 +252,19 @@ def test_subinterp(self):
242252

243253
class Test_IncrementalDecoder(unittest.TestCase):
244254

255+
def test_iso2022_split_escape(self):
256+
raw = b'\x1b$0000000B'
257+
decoder = codecs.getincrementaldecoder('iso2022_jp')('replace')
258+
decoded = decoder.decode(raw[:-1]) + decoder.decode(raw[-1:], final=True)
259+
self.assertEqual(decoded, raw.decode('iso2022_jp', 'replace'))
260+
261+
def test_iso2022_reset(self):
262+
decoder = codecs.getincrementaldecoder('iso2022_jp')()
263+
decoder.decode(b'\x1b%')
264+
decoder.reset()
265+
with self.assertRaises(UnicodeDecodeError):
266+
decoder.decode(b'\xff', final=True)
267+
245268
def test_dbcs(self):
246269
# cp949 decoder is simple with only 1 or 2 bytes sequences.
247270
decoder = codecs.getincrementaldecoder('cp949')()
@@ -325,7 +348,7 @@ def test_setstate_validates_input(self):
325348
self.assertRaises(TypeError, decoder.setstate, 123)
326349
self.assertRaises(TypeError, decoder.setstate, ("invalid", 0))
327350
self.assertRaises(TypeError, decoder.setstate, (b"1234", "invalid"))
328-
self.assertRaises(UnicodeDecodeError, decoder.setstate, (b"123456789", 0))
351+
self.assertRaises(UnicodeDecodeError, decoder.setstate, (b"0" * 17, 0))
329352

330353
def test_setstate_invalid_designation(self):
331354
# gh-153603: an unknown charset designation in the state must not crash
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix HZ encoding of a tilde at an output buffer boundary and ISO-2022 decoding
2+
of split escape sequences. Reset ISO-2022 incremental codecs to their initial
3+
state. Keep multibyte codec tables and custom encoder error policies alive
4+
when Python code replaces them during use.

‎Modules/cjkcodecs/_codecs_cn.c‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -361,11 +361,13 @@ ENCODER(hz)
361361
NEXT_OUT(2);
362362
state->c[CN_STATE_OFFSET] = 0;
363363
}
364-
WRITEBYTE1((unsigned char)c);
365-
NEXT(1, 1);
366364
if (c == '~') {
367-
WRITEBYTE1('~');
368-
NEXT_OUT(1);
365+
WRITEBYTE2('~', '~');
366+
NEXT(1, 2);
367+
}
368+
else {
369+
WRITEBYTE1((unsigned char)c);
370+
NEXT(1, 1);
369371
}
370372
continue;
371373
}

‎Modules/cjkcodecs/_codecs_iso2022.c‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,8 @@ ENCODER_RESET(iso2022)
174174
if (STATE_G0 != CHARSET_ASCII) {
175175
WRITEBYTE3(ESC, '(', 'B');
176176
NEXT_OUT(3);
177-
STATE_SETG0(CHARSET_ASCII);
178177
}
179-
return 0;
178+
return iso2022_encode_init(state, codec);
180179
}
181180

182181
ENCODER(iso2022)
@@ -311,9 +310,7 @@ DECODER_INIT(iso2022)
311310

312311
DECODER_RESET(iso2022)
313312
{
314-
STATE_SETG0(CHARSET_ASCII);
315-
STATE_CLEARFLAG(F_SHIFTED);
316-
return 0;
313+
return iso2022_decode_init(state, codec);
317314
}
318315

319316
static Py_ssize_t

‎Modules/cjkcodecs/multibytecodec.c‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,11 @@ multibytecodec_encode(const MultibyteCodec *codec,
521521
if (datalen == 0 && !(flags & MBENC_RESET))
522522
return PyBytes_FromStringAndSize(NULL, 0);
523523

524+
/* A callback may change a stateful encoder's errors attribute. */
525+
if (ERROR_ISCUSTOM(errors)) {
526+
Py_INCREF(errors);
527+
}
528+
524529
buf.excobj = NULL;
525530
buf.outobj = NULL;
526531
buf.inobj = text; /* borrowed reference */
@@ -581,10 +586,12 @@ multibytecodec_encode(const MultibyteCodec *codec,
581586
if (inpos_t)
582587
*inpos_t = buf.inpos;
583588
Py_XDECREF(buf.excobj);
589+
ERROR_DECREF(errors);
584590
return buf.outobj;
585591

586592
errorexit:
587593
Py_XDECREF(buf.excobj);
594+
ERROR_DECREF(errors);
588595
Py_XDECREF(buf.outobj);
589596
return NULL;
590597
}
@@ -1120,7 +1127,7 @@ mbiencoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
11201127
self->codec->encinit(&self->state, self->codec) != 0)
11211128
goto errorexit;
11221129

1123-
Py_DECREF(codec);
1130+
self->codec_owner = codec;
11241131
return (PyObject *)self;
11251132

11261133
errorexit:
@@ -1141,6 +1148,7 @@ mbiencoder_traverse(PyObject *op, visitproc visit, void *arg)
11411148
MultibyteIncrementalEncoderObject *self = _MultibyteIncrementalEncoderObject_CAST(op);
11421149
if (ERROR_ISCUSTOM(self->errors))
11431150
Py_VISIT(self->errors);
1151+
Py_VISIT(self->codec_owner);
11441152
return 0;
11451153
}
11461154

@@ -1152,6 +1160,7 @@ mbiencoder_dealloc(PyObject *op)
11521160
PyObject_GC_UnTrack(self);
11531161
ERROR_DECREF(self->errors);
11541162
Py_CLEAR(self->pending);
1163+
Py_XDECREF(self->codec_owner);
11551164
tp->tp_free(self);
11561165
Py_DECREF(tp);
11571166
}
@@ -1403,7 +1412,7 @@ mbidecoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
14031412
self->codec->decinit(&self->state, self->codec) != 0)
14041413
goto errorexit;
14051414

1406-
Py_DECREF(codec);
1415+
self->codec_owner = codec;
14071416
return (PyObject *)self;
14081417

14091418
errorexit:
@@ -1424,6 +1433,7 @@ mbidecoder_traverse(PyObject *op, visitproc visit, void *arg)
14241433
MultibyteIncrementalDecoderObject *self = _MultibyteIncrementalDecoderObject_CAST(op);
14251434
if (ERROR_ISCUSTOM(self->errors))
14261435
Py_VISIT(self->errors);
1436+
Py_VISIT(self->codec_owner);
14271437
return 0;
14281438
}
14291439

@@ -1434,6 +1444,7 @@ mbidecoder_dealloc(PyObject *op)
14341444
PyTypeObject *tp = Py_TYPE(self);
14351445
PyObject_GC_UnTrack(self);
14361446
ERROR_DECREF(self->errors);
1447+
Py_XDECREF(self->codec_owner);
14371448
tp->tp_free(self);
14381449
Py_DECREF(tp);
14391450
}
@@ -1720,7 +1731,7 @@ mbstreamreader_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
17201731
self->codec->decinit(&self->state, self->codec) != 0)
17211732
goto errorexit;
17221733

1723-
Py_DECREF(codec);
1734+
self->codec_owner = codec;
17241735
return (PyObject *)self;
17251736

17261737
errorexit:
@@ -1742,6 +1753,7 @@ mbstreamreader_traverse(PyObject *op, visitproc visit, void *arg)
17421753
if (ERROR_ISCUSTOM(self->errors))
17431754
Py_VISIT(self->errors);
17441755
Py_VISIT(self->stream);
1756+
Py_VISIT(self->codec_owner);
17451757
return 0;
17461758
}
17471759

@@ -1753,6 +1765,7 @@ mbstreamreader_dealloc(PyObject *op)
17531765
PyObject_GC_UnTrack(self);
17541766
ERROR_DECREF(self->errors);
17551767
Py_XDECREF(self->stream);
1768+
Py_XDECREF(self->codec_owner);
17561769
tp->tp_free(self);
17571770
Py_DECREF(tp);
17581771
}
@@ -1944,7 +1957,7 @@ mbstreamwriter_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
19441957
self->codec->encinit(&self->state, self->codec) != 0)
19451958
goto errorexit;
19461959

1947-
Py_DECREF(codec);
1960+
self->codec_owner = codec;
19481961
return (PyObject *)self;
19491962

19501963
errorexit:
@@ -1966,6 +1979,7 @@ mbstreamwriter_traverse(PyObject *op, visitproc visit, void *arg)
19661979
if (ERROR_ISCUSTOM(self->errors))
19671980
Py_VISIT(self->errors);
19681981
Py_VISIT(self->stream);
1982+
Py_VISIT(self->codec_owner);
19691983
return 0;
19701984
}
19711985

@@ -1977,6 +1991,7 @@ mbstreamwriter_dealloc(PyObject *op)
19771991
PyObject_GC_UnTrack(self);
19781992
ERROR_DECREF(self->errors);
19791993
Py_XDECREF(self->stream);
1994+
Py_XDECREF(self->codec_owner);
19801995
tp->tp_free(self);
19811996
Py_DECREF(tp);
19821997
}

‎Modules/cjkcodecs/multibytecodec.h‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ typedef struct {
7777
#define _MultibyteStatefulCodec_HEAD \
7878
PyObject_HEAD \
7979
const MultibyteCodec *codec; \
80+
PyObject *codec_owner; \
8081
MultibyteCodec_State state; \
8182
PyObject *errors;
8283
typedef struct {
@@ -91,7 +92,8 @@ typedef struct {
9192
_MultibyteStatefulEncoder_HEAD
9293
} MultibyteStatefulEncoderContext;
9394

94-
#define MAXDECPENDING 8
95+
/* ISO-2022 may retain an incomplete escape sequence up to 15 bytes. */
96+
#define MAXDECPENDING 16
9597
#define _MultibyteStatefulDecoder_HEAD \
9698
_MultibyteStatefulCodec_HEAD \
9799
unsigned char pending[MAXDECPENDING]; \

0 commit comments

Comments
 (0)