Skip to content

Commit 4b355b4

Browse files
committed
gh-154859: Keep the iconv shift state across incremental decode calls
The iconv codecs opened a fresh conversion for every call, so decoding a stateful encoding in chunks lost the shift state and silently produced wrong text: incremental decoding of ISO-2022-CN dropped the escape sequences and returned the raw bytes as ASCII. _codecs.iconv_state() now opens a conversion that the incremental decoder and the stream reader keep and pass back to iconv_decode(), so one conversion spans the whole stream. reset() starts a new one.
1 parent 9ccd5bb commit 4b355b4

6 files changed

Lines changed: 206 additions & 18 deletions

File tree

Include/internal/pycore_unicodeobject.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,19 @@ extern int _PyUnicodeWriter_FormatV(
185185
/* --- iconv Codec -------------------------------------------------------- */
186186

187187
#ifdef HAVE_ICONV
188+
#include <iconv.h>
189+
190+
/* Open a conversion for decoding ENCODING, to reuse across calls. Returns
191+
(iconv_t)-1 with an exception set on failure. */
192+
extern iconv_t _PyUnicode_IconvOpenDecoder(const char *encoding);
193+
188194
extern PyObject* _PyUnicode_DecodeIconv(
189195
const char *encoding, /* iconv encoding name */
190196
const char *string, /* encoded string */
191197
Py_ssize_t length, /* size of string */
192198
const char *errors, /* error handling */
193-
Py_ssize_t *consumed); /* bytes consumed, or NULL for non-stateful */
199+
Py_ssize_t *consumed, /* bytes consumed, or NULL for non-stateful */
200+
iconv_t *cdp); /* conversion to reuse, or NULL to open one */
194201

195202
extern PyObject* _PyUnicode_EncodeIconv(
196203
const char *encoding, /* iconv encoding name */

Lib/encodings/_iconv_codecs.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import codecs
22

33
def create_iconv_codec(name, encoding):
4-
from _codecs import iconv_encode, iconv_decode
4+
from _codecs import iconv_encode, iconv_decode, iconv_state
55

66
def encode(input, errors='strict'):
77
return iconv_encode(encoding, input, errors)
@@ -14,16 +14,32 @@ def encode(self, input, final=False):
1414
return iconv_encode(encoding, input, self.errors)[0]
1515

1616
class IncrementalDecoder(codecs.BufferedIncrementalDecoder):
17+
def __init__(self, errors='strict'):
18+
super().__init__(errors)
19+
self._state = iconv_state(encoding)
20+
1721
def _buffer_decode(self, input, errors, final):
18-
return iconv_decode(encoding, input, errors, final)
22+
return iconv_decode(encoding, input, errors, final, self._state)
23+
24+
def reset(self):
25+
super().reset()
26+
self._state = iconv_state(encoding)
1927

2028
class StreamWriter(codecs.StreamWriter):
2129
def encode(self, input, errors='strict'):
2230
return iconv_encode(encoding, input, errors)
2331

2432
class StreamReader(codecs.StreamReader):
33+
def __init__(self, stream, errors='strict'):
34+
super().__init__(stream, errors)
35+
self._state = iconv_state(encoding)
36+
2537
def decode(self, input, errors, final=False):
26-
return iconv_decode(encoding, input, errors, final)
38+
return iconv_decode(encoding, input, errors, final, self._state)
39+
40+
def reset(self):
41+
super().reset()
42+
self._state = iconv_state(encoding)
2743

2844
return codecs.CodecInfo(
2945
name=name,

Lib/test/test_codecs.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3676,6 +3676,10 @@ def iconv_encoding_available(name):
36763676
('ISO-8859-1', 'Grüße'),
36773677
]
36783678
_ICONV_MULTIBYTE = ['EUC-JP', 'SHIFT_JIS', 'GBK', 'GB18030', 'BIG5']
3679+
# Stateful encodings: the shift state set by an escape sequence has to survive
3680+
# from one incremental call to the next. CPython has no built-in codec for
3681+
# these, so the plain name reaches the iconv codec.
3682+
_ICONV_STATEFUL = ['ISO-2022-CN']
36793683
# Encodings iconv may provide but for which CPython has no built-in codec
36803684
# (cp1047 is EBCDIC, i.e. not ASCII-compatible).
36813685
_ICONV_ONLY = ['cp1047', 'cp1133', 'GEORGIAN-PS', 'ARMSCII-8']
@@ -3804,6 +3808,26 @@ def test_stream(self):
38043808
reader = codecs.getreader('iconv:' + enc)(io.BytesIO(raw))
38053809
self.assertEqual(reader.read(), text)
38063810

3811+
def test_incremental_decode_shift_state(self):
3812+
enc = self.require(*_ICONV_STATEFUL)
3813+
text = 'ABC\u4e2d\u6587DEF'
3814+
data = codecs.encode(text, 'iconv:' + enc)
3815+
self.assertEqual(codecs.decode(data, 'iconv:' + enc), text)
3816+
dec = codecs.getincrementaldecoder('iconv:' + enc)()
3817+
out = ''.join(dec.decode(data[i:i+1]) for i in range(len(data)))
3818+
out += dec.decode(b'', True)
3819+
self.assertEqual(out, text)
3820+
# reset() starts a new conversion, so decoding can begin again.
3821+
dec.reset()
3822+
self.assertEqual(dec.decode(data, True), text)
3823+
3824+
def test_stream_shift_state(self):
3825+
enc = self.require(*_ICONV_STATEFUL)
3826+
text = 'ABC\u4e2d\u6587DEF'
3827+
raw = codecs.encode(text, 'iconv:' + enc)
3828+
reader = codecs.getreader('iconv:' + enc)(io.BytesIO(raw))
3829+
self.assertEqual(''.join(iter(lambda: reader.read(1), '')), text)
3830+
38073831
def test_encode_kinds(self):
38083832
# The string's own buffer is fed to iconv per storage kind; check each
38093833
# of the 1-, 2- and 4-byte kinds against the built-in codec.

Modules/_codecsmodule.c

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -644,24 +644,89 @@ _codecs_code_page_decode_impl(PyObject *module, int codepage,
644644

645645
#ifdef HAVE_ICONV
646646

647+
#ifdef HAVE_ICONV
648+
#define ICONV_STATE_CAPSULE "_codecs.iconv_state"
649+
650+
static void
651+
iconv_state_destructor(PyObject *capsule)
652+
{
653+
iconv_t *cdp = PyCapsule_GetPointer(capsule, ICONV_STATE_CAPSULE);
654+
if (cdp == NULL) {
655+
PyErr_Clear();
656+
return;
657+
}
658+
iconv_close(*cdp);
659+
PyMem_Free(cdp);
660+
}
661+
#endif
662+
663+
/*[clinic input]
664+
_codecs.iconv_state
665+
666+
encoding: str
667+
/
668+
669+
Open an iconv conversion for decoding, to reuse across calls.
670+
671+
The result is an opaque object. Reusing one conversion keeps the
672+
shift state of a stateful encoding, such as ISO-2022-CN, across calls.
673+
[clinic start generated code]*/
674+
675+
static PyObject *
676+
_codecs_iconv_state_impl(PyObject *module, const char *encoding)
677+
/*[clinic end generated code: output=4100a9b65a64d65c input=6ffbfa5bb1d2d208]*/
678+
{
679+
#ifdef HAVE_ICONV
680+
iconv_t *cdp = PyMem_Malloc(sizeof(iconv_t));
681+
if (cdp == NULL) {
682+
return PyErr_NoMemory();
683+
}
684+
*cdp = _PyUnicode_IconvOpenDecoder(encoding);
685+
if (*cdp == (iconv_t)-1) {
686+
PyMem_Free(cdp);
687+
return NULL;
688+
}
689+
PyObject *capsule = PyCapsule_New(cdp, ICONV_STATE_CAPSULE,
690+
iconv_state_destructor);
691+
if (capsule == NULL) {
692+
iconv_close(*cdp);
693+
PyMem_Free(cdp);
694+
return NULL;
695+
}
696+
return capsule;
697+
#else
698+
PyErr_SetString(PyExc_LookupError, "iconv is not available");
699+
return NULL;
700+
#endif
701+
}
702+
647703
/*[clinic input]
648704
_codecs.iconv_decode
649705
encoding: str
650706
data: Py_buffer
651707
errors: str(accept={str, NoneType}) = None
652708
final: bool = False
709+
state: object = None
653710
/
654711
[clinic start generated code]*/
655712

656713
static PyObject *
657714
_codecs_iconv_decode_impl(PyObject *module, const char *encoding,
658-
Py_buffer *data, const char *errors, int final)
659-
/*[clinic end generated code: output=6c6145a9decc2ba8 input=d15a04d7d3a3e0cd]*/
715+
Py_buffer *data, const char *errors, int final,
716+
PyObject *state)
717+
/*[clinic end generated code: output=ed99087a9b21d007 input=b23f4298c963f9c5]*/
660718
{
719+
iconv_t *cdp = NULL;
720+
if (state != Py_None) {
721+
cdp = PyCapsule_GetPointer(state, ICONV_STATE_CAPSULE);
722+
if (cdp == NULL) {
723+
return NULL;
724+
}
725+
}
661726
Py_ssize_t consumed = data->len;
662727
PyObject *decoded = _PyUnicode_DecodeIconv(encoding, data->buf, data->len,
663728
errors,
664-
final ? NULL : &consumed);
729+
final ? NULL : &consumed, cdp);
665730
return codec_tuple(decoded, consumed);
666731
}
667732

@@ -1155,6 +1220,7 @@ static PyMethodDef _codecs_functions[] = {
11551220
_CODECS_CODE_PAGE_DECODE_METHODDEF
11561221
_CODECS_ICONV_ENCODE_METHODDEF
11571222
_CODECS_ICONV_DECODE_METHODDEF
1223+
_CODECS_ICONV_STATE_METHODDEF
11581224
_CODECS_REGISTER_ERROR_METHODDEF
11591225
_CODECS__UNREGISTER_ERROR_METHODDEF
11601226
_CODECS_LOOKUP_ERROR_METHODDEF

Modules/clinic/_codecsmodule.c.h

Lines changed: 60 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Objects/unicodeobject.c

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8239,26 +8239,42 @@ iconv_open_or_set_error(const char *tocode, const char *fromcode,
82398239
return cd;
82408240
}
82418241

8242+
iconv_t
8243+
_PyUnicode_IconvOpenDecoder(const char *encoding)
8244+
{
8245+
return iconv_open_or_set_error(ICONV_PIVOT, encoding, encoding);
8246+
}
8247+
82428248
/*
82438249
* Decode bytes with iconv() into a str.
82448250
*
82458251
* The input is converted to native-endian UTF-32 one chunk at a time and
82468252
* appended to a _PyUnicodeWriter. If *consumed* is non-NULL the decode is
82478253
* stateful: a trailing incomplete sequence stops and sets *consumed*.
8254+
*
8255+
* If *cdp* is non-NULL the conversion it points to is used and left open, so
8256+
* that a shift state survives from one call to the next.
82488257
*/
82498258
PyObject *
82508259
_PyUnicode_DecodeIconv(const char *encoding,
82518260
const char *s, Py_ssize_t size,
8252-
const char *errors, Py_ssize_t *consumed)
8261+
const char *errors, Py_ssize_t *consumed,
8262+
iconv_t *cdp)
82538263
{
82548264
if (size < 0) {
82558265
PyErr_BadInternalCall();
82568266
return NULL;
82578267
}
82588268

8259-
iconv_t cd = iconv_open_or_set_error(ICONV_PIVOT, encoding, encoding);
8260-
if (cd == (iconv_t)-1) {
8261-
return NULL;
8269+
iconv_t cd;
8270+
if (cdp != NULL) {
8271+
cd = *cdp;
8272+
}
8273+
else {
8274+
cd = _PyUnicode_IconvOpenDecoder(encoding);
8275+
if (cd == (iconv_t)-1) {
8276+
return NULL;
8277+
}
82628278
}
82638279

82648280
/* Scratch buffer for one iconv() output chunk, as UTF-32 code points. */
@@ -8337,13 +8353,17 @@ _PyUnicode_DecodeIconv(const char *encoding,
83378353
if (consumed != NULL) {
83388354
*consumed = in - starts;
83398355
}
8340-
iconv_close(cd);
8356+
if (cdp == NULL) {
8357+
iconv_close(cd);
8358+
}
83418359
Py_XDECREF(errorHandler);
83428360
Py_XDECREF(exc);
83438361
return _PyUnicodeWriter_Finish(&writer);
83448362

83458363
error:
8346-
iconv_close(cd);
8364+
if (cdp == NULL) {
8365+
iconv_close(cd);
8366+
}
83478367
_PyUnicodeWriter_Dealloc(&writer);
83488368
Py_XDECREF(errorHandler);
83498369
Py_XDECREF(exc);

0 commit comments

Comments
 (0)