Skip to content

Commit c4bacc6

Browse files
committed
Add output_length to _Py_EncodeLocaleEx()
Add output_length to _Py_EncodeLocaleEx(), _Py_EncodeUTF8Ex(), encode_current_locale() and encode_ascii(). So unicode_encode_locale() and wstr_to_utf8() can use the output_length, instead of having to compute strlen(). * Add encode_current_locale_impl() to simplify encode_current_locale(). * _Py_EncodeLocaleEx() now sets error_pos and reason if it fails with -1 or -3. * Add tests on Py_EncodeLocale() and Py_DecodeLocale() functions in test_codecs. * Remove reason parameter of _Py_EncodeUTF8Ex(), encode_current_locale() and encode_ascii().
1 parent 9a15440 commit c4bacc6

7 files changed

Lines changed: 332 additions & 161 deletions

File tree

‎Include/internal/pycore_fileutils.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ PyAPI_FUNC(int) _Py_DecodeLocaleEx(
3636
PyAPI_FUNC(int) _Py_EncodeLocaleEx(
3737
const wchar_t *text,
3838
char **str,
39+
size_t *output_length,
3940
size_t *error_pos,
4041
const char **reason,
4142
int current_locale,
@@ -201,8 +202,8 @@ extern int _Py_DecodeUTF8Ex(
201202
extern int _Py_EncodeUTF8Ex(
202203
const wchar_t *text,
203204
char **str,
205+
size_t *output_length,
204206
size_t *error_pos,
205-
const char **reason,
206207
int raw_malloc,
207208
_Py_error_handler errors);
208209

‎Lib/test/test_codecs.py‎

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4057,6 +4057,7 @@ def test_pickle(self):
40574057
pickle.dumps(sr, proto)
40584058

40594059

4060+
@unittest.skipIf(_testlimitedcapi is None, 'need _testlimitedcapi module')
40604061
@unittest.skipIf(_testinternalcapi is None, 'need _testinternalcapi module')
40614062
class LocaleCodecTest(unittest.TestCase):
40624063
"""
@@ -4070,7 +4071,12 @@ class LocaleCodecTest(unittest.TestCase):
40704071
BYTES_STRINGS = (b"blatin1:\xa7\xe9", b"b255:\xff")
40714072
SURROGATES = "\uDC80\uDCFF"
40724073

4073-
def encode(self, text, errors="strict"):
4074+
def encode_locale(self, text):
4075+
# Test Py_EncodeLocale(): use the "surrogateescape" error handler
4076+
return _testlimitedcapi.encode_locale(text)
4077+
4078+
def encode_locale_ex(self, text, errors="strict"):
4079+
# Test _Py_EncodeLocaleEx()
40744080
return _testinternalcapi.EncodeLocaleEx(text, 0, errors)
40754081

40764082
def check_encode_strings(self, errors):
@@ -4079,12 +4085,30 @@ def check_encode_strings(self, errors):
40794085
try:
40804086
expected = text.encode(self.ENCODING, errors)
40814087
except UnicodeEncodeError:
4088+
for error_pos in range(len(text)):
4089+
try:
4090+
text[error_pos].encode(self.ENCODING, errors)
4091+
except UnicodeEncodeError:
4092+
break
4093+
else:
4094+
self.fail("failed to compute error_pos")
4095+
4096+
if errors == "surrogateescape":
4097+
with self.assertRaises(ValueError) as cm:
4098+
self.encode_locale(text)
4099+
errmsg = str(cm.exception)
4100+
self.assertRegex(errmsg, f"Py_EncodeLocale failed: error_pos={error_pos}")
4101+
40824102
with self.assertRaises(RuntimeError) as cm:
4083-
self.encode(text, errors)
4103+
self.encode_locale_ex(text, errors)
40844104
errmsg = str(cm.exception)
4085-
self.assertRegex(errmsg, r"encode error: pos=[0-9]+, reason=")
4105+
self.assertRegex(errmsg, f"encode error: pos={error_pos}, reason=encoding error")
40864106
else:
4087-
encoded = self.encode(text, errors)
4107+
if errors in ("strict", "surrogateescape"):
4108+
encoded = self.encode_locale(text)
4109+
self.assertEqual(encoded, expected)
4110+
4111+
encoded = self.encode_locale_ex(text, errors)
40884112
self.assertEqual(encoded, expected)
40894113

40904114
def test_encode_strict(self):
@@ -4095,7 +4119,7 @@ def test_encode_surrogateescape(self):
40954119

40964120
def test_encode_surrogatepass(self):
40974121
try:
4098-
self.encode('', 'surrogatepass')
4122+
self.encode_locale_ex('', 'surrogatepass')
40994123
except ValueError as exc:
41004124
if str(exc) == 'unsupported error handler':
41014125
self.skipTest(f"{self.ENCODING!r} encoder doesn't support "
@@ -4107,12 +4131,17 @@ def test_encode_surrogatepass(self):
41074131

41084132
def test_encode_unsupported_error_handler(self):
41094133
with self.assertRaises(ValueError) as cm:
4110-
self.encode('', 'backslashreplace')
4134+
self.encode_locale_ex('', 'backslashreplace')
41114135
self.assertEqual(str(cm.exception), 'unsupported error handler')
41124136

4113-
def decode(self, encoded, errors="strict"):
4137+
def decode_locale_ex(self, encoded, errors="strict"):
4138+
# Test _Py_DecodeLocaleEx()
41144139
return _testinternalcapi.DecodeLocaleEx(encoded, 0, errors)
41154140

4141+
def decode_locale(self, encoded):
4142+
# Test DecodeLocale(): use the "surrogateescape" error handler
4143+
return _testlimitedcapi.decode_locale(encoded)
4144+
41164145
def check_decode_strings(self, errors):
41174146
is_utf8 = (self.ENCODING == "utf-8")
41184147
if is_utf8:
@@ -4139,12 +4168,20 @@ def check_decode_strings(self, errors):
41394168
try:
41404169
expected = encoded.decode(self.ENCODING, errors)
41414170
except UnicodeDecodeError:
4171+
if errors == "surrogateescape":
4172+
with self.assertRaises(ValueError):
4173+
self.decode_locale(encoded)
4174+
41424175
with self.assertRaises(RuntimeError) as cm:
4143-
self.decode(encoded, errors)
4176+
self.decode_locale_ex(encoded, errors)
41444177
errmsg = str(cm.exception)
41454178
self.assertStartsWith(errmsg, "decode error: ")
41464179
else:
4147-
decoded = self.decode(encoded, errors)
4180+
if errors == ("strict", "surrogateescape"):
4181+
decoded = self.decode_locale(encoded)
4182+
self.assertEqual(decoded, expected)
4183+
4184+
decoded = self.decode_locale_ex(encoded, errors)
41484185
self.assertEqual(decoded, expected)
41494186

41504187
def test_decode_strict(self):
@@ -4155,7 +4192,7 @@ def test_decode_surrogateescape(self):
41554192

41564193
def test_decode_surrogatepass(self):
41574194
try:
4158-
self.decode(b'', 'surrogatepass')
4195+
self.decode_locale_ex(b'', 'surrogatepass')
41594196
except ValueError as exc:
41604197
if str(exc) == 'unsupported error handler':
41614198
self.skipTest(f"{self.ENCODING!r} decoder doesn't support "
@@ -4167,7 +4204,7 @@ def test_decode_surrogatepass(self):
41674204

41684205
def test_decode_unsupported_error_handler(self):
41694206
with self.assertRaises(ValueError) as cm:
4170-
self.decode(b'', 'backslashreplace')
4207+
self.decode_locale_ex(b'', 'backslashreplace')
41714208
self.assertEqual(str(cm.exception), 'unsupported error handler')
41724209

41734210

‎Modules/_testinternalcapi.c‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,55 +1046,72 @@ get_getpath_codeobject(PyObject *self, PyObject *Py_UNUSED(args)) {
10461046
}
10471047

10481048

1049+
// Test _Py_EncodeLocaleEx()
10491050
static PyObject *
10501051
encode_locale_ex(PyObject *self, PyObject *args)
10511052
{
10521053
PyObject *unicode;
10531054
int current_locale = 0;
1054-
wchar_t *wstr;
10551055
PyObject *res = NULL;
10561056
const char *errors = NULL;
10571057

10581058
if (!PyArg_ParseTuple(args, "U|is", &unicode, &current_locale, &errors)) {
10591059
return NULL;
10601060
}
1061-
wstr = PyUnicode_AsWideCharString(unicode, NULL);
1061+
1062+
wchar_t *wstr = PyUnicode_AsWideCharString(unicode, NULL);
10621063
if (wstr == NULL) {
10631064
return NULL;
10641065
}
10651066
_Py_error_handler error_handler = _Py_GetErrorHandler(errors);
10661067

10671068
char *str = NULL;
1068-
size_t error_pos;
1069-
const char *reason = NULL;
1069+
size_t error_pos_canary = (size_t)-123;
1070+
size_t error_pos = error_pos_canary;
1071+
size_t output_length = (size_t)-123;
1072+
const char *reason_canary = "canary";
1073+
const char *reason = reason_canary;
10701074
int ret = _Py_EncodeLocaleEx(wstr,
1071-
&str, &error_pos, &reason,
1075+
&str, &output_length, &error_pos, &reason,
10721076
current_locale, error_handler);
10731077
PyMem_Free(wstr);
10741078

10751079
switch(ret) {
10761080
case 0:
1077-
res = PyBytes_FromString(str);
1081+
res = PyBytes_FromStringAndSize(str, output_length);
10781082
PyMem_RawFree(str);
10791083
break;
10801084
case -1:
1085+
assert(output_length == 0);
1086+
assert(error_pos == 0);
1087+
assert(reason == NULL);
10811088
PyErr_NoMemory();
10821089
break;
10831090
case -2:
1091+
assert(output_length == 0);
1092+
assert(error_pos != error_pos_canary);
1093+
assert(reason != reason_canary);
10841094
PyErr_Format(PyExc_RuntimeError, "encode error: pos=%zu, reason=%s",
10851095
error_pos, reason);
10861096
break;
10871097
case -3:
1098+
assert(output_length == 0);
1099+
assert(error_pos == 0);
1100+
assert(reason == NULL);
10881101
PyErr_SetString(PyExc_ValueError, "unsupported error handler");
10891102
break;
10901103
default:
1104+
assert(output_length == 0);
1105+
assert(error_pos == 0);
1106+
assert(reason == NULL);
10911107
PyErr_SetString(PyExc_ValueError, "unknown error code");
10921108
break;
10931109
}
10941110
return res;
10951111
}
10961112

10971113

1114+
// Test _Py_DecodeLocaleEx()
10981115
static PyObject *
10991116
decode_locale_ex(PyObject *self, PyObject *args)
11001117
{

‎Modules/_testlimitedcapi/codec.c‎

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
#ifdef Py_GIL_DISABLED
33
# define Py_TARGET_ABI3T 0x030f0000
44
#else
5-
// Need limited C API version 3.5 for PyCodec_NameReplaceErrors()
6-
# define Py_LIMITED_API 0x03050000
5+
// Need limited C API version 3.13 for PyMem_RawFree()
6+
# define Py_LIMITED_API 0x030d0000
77
#endif
88

99
#include "parts.h"
@@ -15,16 +15,80 @@ codec_namereplace_errors(PyObject *Py_UNUSED(module), PyObject *exc)
1515
return PyCodec_NameReplaceErrors(exc);
1616
}
1717

18+
19+
// Test Py_DecodeLocale()
20+
static PyObject *
21+
decode_locale(PyObject *Py_UNUSED(module), PyObject *arg)
22+
{
23+
const char *str;
24+
if (PyArg_Parse(arg, "y", &str) < 0) {
25+
return NULL;
26+
}
27+
28+
size_t wstr_len = (size_t)-123;
29+
wchar_t *wstr = Py_DecodeLocale(str, &wstr_len);
30+
31+
if (str == NULL) {
32+
if (wstr_len == (size_t)-1) {
33+
PyErr_NoMemory();
34+
}
35+
else if (wstr_len == (size_t)-2) {
36+
PyErr_SetString(PyExc_ValueError, "decode error");
37+
}
38+
else {
39+
PyErr_Format(PyExc_SystemError,
40+
"unknown Py_DecodeLocale() return value: %zd",
41+
(Py_ssize_t)wstr_len);
42+
}
43+
return NULL;
44+
}
45+
46+
PyObject *result = PyUnicode_FromWideChar(wstr, wstr_len);
47+
PyMem_RawFree(wstr);
48+
return result;
49+
}
50+
51+
52+
// Test Py_EncodeLocale()
53+
static PyObject *
54+
encode_locale(PyObject *Py_UNUSED(module), PyObject *arg)
55+
{
56+
PyObject *unicode;
57+
if (PyArg_Parse(arg, "U", &unicode) < 0) {
58+
return NULL;
59+
}
60+
61+
wchar_t *wstr = PyUnicode_AsWideCharString(unicode, NULL);
62+
if (wstr == NULL) {
63+
return NULL;
64+
}
65+
66+
size_t error_pos = (size_t)-123;
67+
char *str = Py_EncodeLocale(wstr, &error_pos);
68+
PyMem_Free(wstr);
69+
70+
if (str == NULL) {
71+
return PyErr_Format(PyExc_ValueError,
72+
"Py_EncodeLocale failed: error_pos=%zd",
73+
error_pos);
74+
}
75+
assert(error_pos == (size_t)-123);
76+
77+
PyObject *result = PyBytes_FromString(str);
78+
PyMem_Free(str);
79+
return result;
80+
}
81+
82+
1883
static PyMethodDef test_methods[] = {
1984
{"codec_namereplace_errors", codec_namereplace_errors, METH_O},
85+
{"decode_locale", decode_locale, METH_O},
86+
{"encode_locale", encode_locale, METH_O},
2087
{NULL},
2188
};
2289

2390
int
2491
_PyTestLimitedCAPI_Init_Codec(PyObject *module)
2592
{
26-
if (PyModule_AddFunctions(module, test_methods) < 0) {
27-
return -1;
28-
}
29-
return 0;
93+
return PyModule_AddFunctions(module, test_methods);
3094
}

0 commit comments

Comments
 (0)