Skip to content

Commit c56c2f8

Browse files
gh-155016: Fix encoding to Windows code pages which require dwFlags=0
WideCharToMultiByte() only converts to some code pages (ISO-2022, HZ-GB2312, GB18030, ISCII and UTF-7) with dwFlags set to 0 and lpUsedDefaultChar set to NULL, and rejects any other call with ERROR_INVALID_PARAMETER. Encode to them with these arguments, and detect replaced characters by decoding the result back, since lpUsedDefaultChar is no longer available for this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a646c99 commit c56c2f8

3 files changed

Lines changed: 162 additions & 12 deletions

File tree

Lib/test/test_codecs.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3562,6 +3562,49 @@ def test_cp_utf7(self):
35623562
(b'[\xff]', 'strict', '[\xff]'),
35633563
))
35643564

3565+
def test_cp50220(self):
3566+
# ISO-2022-JP, a code page which only supports conversion with flags=0
3567+
self.check_encode(50220, (
3568+
('abc', 'strict', b'abc'),
3569+
('\u3042', 'strict', b'\x1b$B$"\x1b(B'),
3570+
('\u3042abc', 'strict', b'\x1b$B$"\x1b(Babc'),
3571+
# test error handlers
3572+
('[\u20ac]', 'strict', None),
3573+
('[\u20ac]', 'ignore', b'[]'),
3574+
('[\u20ac]', 'replace', b'[?]'),
3575+
('[\u20ac]', 'backslashreplace', b'[\\u20ac]'),
3576+
('[\u20ac]', 'xmlcharrefreplace', b'[&#8364;]'),
3577+
('\u3042[\u20ac]\u3042', 'ignore',
3578+
b'\x1b$B$"\x1b(B[]\x1b$B$"\x1b(B'),
3579+
('[\udc80]', 'strict', None),
3580+
('[\udc80]', 'ignore', b'[]'),
3581+
))
3582+
self.check_decode(50220, (
3583+
(b'abc', 'strict', 'abc'),
3584+
(b'\x1b$B$"\x1b(B', 'strict', '\u3042'),
3585+
(b'\x1b$B$"\x1b(Babc', 'strict', '\u3042abc'),
3586+
))
3587+
3588+
def test_cp54936(self):
3589+
# GB18030, a code page which only supports conversion with flags=0
3590+
self.check_encode(54936, (
3591+
('abc', 'strict', b'abc'),
3592+
('\u4e2d', 'strict', b'\xd6\xd0'),
3593+
('\u20ac', 'strict', b'\xa2\xe3'),
3594+
('\U00020000', 'strict', b'\x952\x826'),
3595+
# test error handlers
3596+
('[\udc80]', 'strict', None),
3597+
('[\udc80]', 'ignore', b'[]'),
3598+
('[\udc80]', 'replace', b'[?]'),
3599+
('[\udc80]', 'backslashreplace', b'[\\udc80]'),
3600+
('[\udc80]', 'surrogateescape', b'[\x80]'),
3601+
))
3602+
self.check_decode(54936, (
3603+
(b'abc', 'strict', 'abc'),
3604+
(b'\xd6\xd0', 'strict', '\u4e2d'),
3605+
(b'\x952\x826', 'strict', '\U00020000'),
3606+
))
3607+
35653608
def test_multibyte_encoding(self):
35663609
self.check_decode(932, (
35673610
(b'\x84\xe9\x80', 'ignore', '\u9a3e'),
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix encoding to Windows code pages which only support conversion with
2+
``dwFlags`` set to 0: ISO-2022 (50220, 50221, 50222, 50225, 50227 and 50229),
3+
HZ-GB2312 (52936), GB18030 (54936) and ISCII (57002--57011). Previously
4+
encoding to them always failed with :exc:`OSError`.

Objects/unicodeobject.c

Lines changed: 115 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7829,14 +7829,34 @@ PyUnicode_DecodeMBCS(const char *s,
78297829
return PyUnicode_DecodeMBCSStateful(s, size, errors, NULL);
78307830
}
78317831

7832+
/* WideCharToMultiByte() only supports some code pages with flags=0 and
7833+
lpUsedDefaultChar=NULL, see its documentation. Unencodable characters
7834+
are silently replaced then, so they are detected by decoding the result
7835+
back (see encode_code_page_lossy()). */
7836+
static int
7837+
encode_code_page_zero_flags(UINT code_page)
7838+
{
7839+
switch (code_page) {
7840+
case 42: /* Symbol */
7841+
case 50220: case 50221: case 50222: /* ISO-2022-JP */
7842+
case 50225: /* ISO-2022-KR */
7843+
case 50227: case 50229: /* ISO-2022-CN */
7844+
case 52936: /* HZ-GB2312 */
7845+
case 54936: /* GB18030 */
7846+
case CP_UTF7:
7847+
return 1;
7848+
default:
7849+
return 57002 <= code_page && code_page <= 57011; /* ISCII */
7850+
}
7851+
}
7852+
78327853
static DWORD
78337854
encode_code_page_flags(UINT code_page, const char *errors)
78347855
{
78357856
if (code_page == CP_UTF8) {
78367857
return WC_ERR_INVALID_CHARS;
78377858
}
7838-
else if (code_page == CP_UTF7) {
7839-
/* CP_UTF7 only supports flags=0 */
7859+
else if (encode_code_page_zero_flags(code_page)) {
78407860
return 0;
78417861
}
78427862
else {
@@ -7847,6 +7867,56 @@ encode_code_page_flags(UINT code_page, const char *errors)
78477867
}
78487868
}
78497869

7870+
/* Code pages encoded without WC_NO_BEST_FIT_CHARS and lpUsedDefaultChar
7871+
need the result to be decoded back to detect replaced characters.
7872+
UTF-7 encodes every character, so it is exempted. */
7873+
static int
7874+
encode_code_page_check_lossy(UINT code_page)
7875+
{
7876+
return code_page != CP_UTF7 && encode_code_page_zero_flags(code_page);
7877+
}
7878+
7879+
/*
7880+
* Check whether some characters were replaced when encoding, i.e. whether
7881+
* the encoded string is not decoded back into the original string.
7882+
*
7883+
* Returns 1 if some characters were replaced, 0 if not, or raise an OSError
7884+
* and returns -1 on error.
7885+
*/
7886+
static int
7887+
encode_code_page_lossy(UINT code_page,
7888+
const wchar_t *p, int size,
7889+
const char *out, int outsize)
7890+
{
7891+
wchar_t *decoded;
7892+
int n, lossy;
7893+
7894+
n = MultiByteToWideChar(code_page, 0, out, outsize, NULL, 0);
7895+
if (n <= 0) {
7896+
PyErr_SetFromWindowsErr(0);
7897+
return -1;
7898+
}
7899+
if (n != size) {
7900+
return 1;
7901+
}
7902+
7903+
decoded = PyMem_New(wchar_t, n);
7904+
if (decoded == NULL) {
7905+
PyErr_NoMemory();
7906+
return -1;
7907+
}
7908+
n = MultiByteToWideChar(code_page, 0, out, outsize, decoded, n);
7909+
if (n <= 0) {
7910+
PyErr_SetFromWindowsErr(0);
7911+
lossy = -1;
7912+
}
7913+
else {
7914+
lossy = (memcmp(decoded, p, (size_t)size * sizeof(wchar_t)) != 0);
7915+
}
7916+
PyMem_Free(decoded);
7917+
return lossy;
7918+
}
7919+
78507920
/*
78517921
* Encode a Unicode string to a Windows code page into a byte string in strict
78527922
* mode.
@@ -7866,14 +7936,15 @@ encode_code_page_strict(UINT code_page, PyBytesWriter **writer,
78667936
Py_ssize_t size;
78677937
const DWORD flags = encode_code_page_flags(code_page, NULL);
78687938
char *out;
7939+
Py_ssize_t start;
78697940
/* Create a substring so that we can get the UTF-16 representation
78707941
of just the slice under consideration. */
78717942
PyObject *substring;
78727943
int ret = -1;
78737944

78747945
assert(len > 0);
78757946

7876-
if (code_page != CP_UTF8 && code_page != CP_UTF7)
7947+
if (code_page != CP_UTF8 && !encode_code_page_zero_flags(code_page))
78777948
pusedDefaultChar = &usedDefaultChar;
78787949
else
78797950
pusedDefaultChar = NULL;
@@ -7903,6 +7974,7 @@ encode_code_page_strict(UINT code_page, PyBytesWriter **writer,
79037974

79047975
if (*writer == NULL) {
79057976
/* Create string object */
7977+
start = 0;
79067978
*writer = PyBytesWriter_Create(outsize);
79077979
if (*writer == NULL) {
79087980
goto done;
@@ -7911,11 +7983,11 @@ encode_code_page_strict(UINT code_page, PyBytesWriter **writer,
79117983
}
79127984
else {
79137985
/* Extend string object */
7914-
Py_ssize_t n = PyBytesWriter_GetSize(*writer);
7986+
start = PyBytesWriter_GetSize(*writer);
79157987
if (PyBytesWriter_Grow(*writer, outsize) < 0) {
79167988
goto done;
79177989
}
7918-
out = (char*)PyBytesWriter_GetData(*writer) + n;
7990+
out = (char*)PyBytesWriter_GetData(*writer) + start;
79197991
}
79207992

79217993
/* Do the conversion */
@@ -7929,6 +8001,22 @@ encode_code_page_strict(UINT code_page, PyBytesWriter **writer,
79298001
ret = -2;
79308002
goto done;
79318003
}
8004+
if (encode_code_page_check_lossy(code_page)) {
8005+
int lossy = encode_code_page_lossy(code_page, p, (int)size,
8006+
out, outsize);
8007+
if (lossy < 0) {
8008+
goto done;
8009+
}
8010+
if (lossy) {
8011+
/* Drop the lossy result, it will be re-encoded with an error
8012+
handler. */
8013+
if (PyBytesWriter_Resize(*writer, start) < 0) {
8014+
goto done;
8015+
}
8016+
ret = -2;
8017+
goto done;
8018+
}
8019+
}
79328020
ret = 0;
79338021

79348022
done:
@@ -7962,8 +8050,10 @@ encode_code_page_errors(UINT code_page, PyBytesWriter **writer,
79628050
/* Ideally, we should get reason from FormatMessage. This is the Windows
79638051
2000 English version of the message. */
79648052
const char *reason = "invalid character";
7965-
/* 4=maximum length of a UTF-8 sequence */
7966-
char buffer[4];
8053+
/* 4=maximum length of a UTF-8 sequence, 16 is enough for a character
8054+
encoded together with escape sequences (e.g. in ISO-2022) */
8055+
char buffer[16];
8056+
int maxcharsize;
79678057
BOOL usedDefaultChar = FALSE, *pusedDefaultChar;
79688058
Py_ssize_t outsize;
79698059
char *out;
@@ -7993,16 +8083,21 @@ encode_code_page_errors(UINT code_page, PyBytesWriter **writer,
79938083
return -1;
79948084
}
79958085

7996-
if (code_page != CP_UTF8 && code_page != CP_UTF7)
8086+
if (code_page != CP_UTF8 && !encode_code_page_zero_flags(code_page))
79978087
pusedDefaultChar = &usedDefaultChar;
79988088
else
79998089
pusedDefaultChar = NULL;
80008090

8001-
if (Py_ARRAY_LENGTH(buffer) > PY_SSIZE_T_MAX / insize) {
8091+
/* Only code pages encoded with flags=0 can need more than 4 bytes
8092+
per character. */
8093+
maxcharsize = encode_code_page_zero_flags(code_page)
8094+
? (int)Py_ARRAY_LENGTH(buffer) : 4;
8095+
8096+
if (maxcharsize > PY_SSIZE_T_MAX / insize) {
80028097
PyErr_NoMemory();
80038098
goto error;
80048099
}
8005-
outsize = insize * Py_ARRAY_LENGTH(buffer);
8100+
outsize = insize * maxcharsize;
80068101

80078102
if (*writer == NULL) {
80088103
/* Create string object */
@@ -8039,10 +8134,18 @@ encode_code_page_errors(UINT code_page, PyBytesWriter **writer,
80398134

80408135
outsize = WideCharToMultiByte(code_page, flags,
80418136
chars, charsize,
8042-
buffer, Py_ARRAY_LENGTH(buffer),
8137+
buffer, maxcharsize,
80438138
NULL, pusedDefaultChar);
80448139
if (outsize > 0) {
8045-
if (pusedDefaultChar == NULL || !(*pusedDefaultChar))
8140+
int lossy = (pusedDefaultChar != NULL && *pusedDefaultChar);
8141+
if (!lossy && encode_code_page_check_lossy(code_page)) {
8142+
lossy = encode_code_page_lossy(code_page, chars, charsize,
8143+
buffer, (int)outsize);
8144+
if (lossy < 0) {
8145+
goto error;
8146+
}
8147+
}
8148+
if (!lossy)
80468149
{
80478150
pos++;
80488151
memcpy(out, buffer, outsize);

0 commit comments

Comments
 (0)