Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Doc/c-api/unicode.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1945,7 +1945,7 @@ object.
On success, return ``0``.
On error, set an exception, leave the writer unchanged, and return ``-1``.

.. c:function:: int PyUnicodeWriter_DecodeUTF8Stateful(PyUnicodeWriter *writer, const char *string, Py_ssize_t length, const char *errors, Py_ssize_t *consumed)
.. c:function:: int PyUnicodeWriter_DecodeUTF8Stateful(PyUnicodeWriter *writer, const char *str, Py_ssize_t size, const char *errors, Py_ssize_t *consumed)

Decode the string *str* from UTF-8 with *errors* error handler and write the
output into *writer*.
Expand Down
7 changes: 5 additions & 2 deletions Include/cpython/unicodeobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -516,8 +516,8 @@ PyAPI_FUNC(int) PyUnicodeWriter_Format(
...);
PyAPI_FUNC(int) PyUnicodeWriter_DecodeUTF8Stateful(
PyUnicodeWriter *writer,
const char *string, /* UTF-8 encoded string */
Py_ssize_t length, /* size of string */
const char *str, /* UTF-8 encoded string */
Py_ssize_t size, /* size of string */
const char *errors, /* error handling */
Py_ssize_t *consumed); /* bytes consumed */

Expand All @@ -538,6 +538,9 @@ typedef struct {
/* minimum character (default: 127, ASCII) */
Py_UCS4 min_char;

// If non-zero, _PyUnicodeWriter_Finish() needs to check maxchar.
int recheck_maxchar;

/* If non-zero, overallocate the buffer (default: 0). */
unsigned char overallocate;

Expand Down
67 changes: 62 additions & 5 deletions Lib/test/test_capi/test_unicode.py
Original file line number Diff line number Diff line change
Expand Up @@ -1976,7 +1976,11 @@ def test_invalid_utf8(self):
writer.write_utf8(b"invalid=\xFF", -1)

def test_recover_utf8_error(self):
# test recovering from PyUnicodeWriter_WriteUTF8() error
# Recover from PyUnicodeWriter_WriteUTF8() errors. A temporary write
# changes the buffer kind to UCS-2 before raising UnicodeDecodeError.
# Then, PyUnicodeWriter_Finish() has to change the buffer kind back to
# ASCII.

writer = self.create_writer(0)
writer.write_utf8(b"value=", -1)

Expand All @@ -1986,12 +1990,13 @@ def test_recover_utf8_error(self):
with self.assertRaises(UnicodeDecodeError):
s = "truncated\u20AC".encode()
writer.write_utf8(s, len(s) - 1)
with self.assertRaises(UnicodeDecodeError):
# Change buffer kind to UCS-2 then raise UnicodeDecodeError
s = "\u20AC\u20AC".encode()
writer.write_utf8(s, len(s) - 1)

# retry write with a valid string
writer.write_utf8(b"valid", -1)

self.assertEqual(writer.finish(),
"value=valid")
self.assertEqual(writer.finish(), "value=valid")

def test_decode_utf8(self):
# test PyUnicodeWriter_DecodeUTF8Stateful()
Expand Down Expand Up @@ -2262,6 +2267,35 @@ def __repr__(self):
self.assertEqual(writer.get_buffer(), expected)
self.assertIs(writer.finish(), unique_string)

def test_readonly_optim_large_int(self):
# Read-only optimization in _PyLong_FormatWriter() for large integer:
# use _pylong.int_to_decimal_string() result as a read-only string.
# See pylong_int_to_decimal_string().

self.addCleanup(sys.set_int_max_str_digits,
sys.get_int_max_str_digits())
sys.set_int_max_str_digits(0)

# _PyLong_FormatWriter() calls _pylong.int_to_decimal_string() for
# integer with Py_SIZE() > 1000.
large_int = 1 << (sys.int_info.bits_per_digit * 1020)
large_int_str = str(large_int)
expected = (len(large_int_str), 127, True)

for size in (0, 123):
with self.subTest(size=size):
# Test PyUnicodeWriter_WriteStr()
writer = self.create_writer(size)
writer.write_str(large_int)
self.assertEqual(writer.get_buffer(), expected)
self.assertEqual(writer.finish(), large_int_str)

# Test PyUnicodeWriter_WriteRepr()
writer = self.create_writer(size)
writer.write_repr(large_int)
self.assertEqual(writer.get_buffer(), expected)
self.assertEqual(writer.finish(), large_int_str)


# Test PyUnicodeWriter_Format()
@unittest.skipIf(ctypes is None, 'need ctypes')
Expand Down Expand Up @@ -2299,6 +2333,29 @@ def test_recover_error(self):

self.assertEqual(writer.finish(), 'Hello World.')

def test_recheck_maxchar(self):
# PyUnicodeWriter_Format() changes buffer kind to UCS-2 before raising
# an exception. Then, PyUnicodeWriter_Finish() has to change the buffer
# kind back to ASCII.
from ctypes import py_object

class StrError:
def __str__(self):
raise RuntimeError("bug")

writer = self.create_writer(0)
# Allocate ASCII buffer
writer.write_str('ascii')

obj = StrError()
ucs2_utf8 = '\u20ac'.encode()
with self.assertRaises(RuntimeError):
# Change buffer kind to UCS-2, but then raise RuntimeError
self.writer_format(writer, b"%s%S", ucs2_utf8, py_object(obj))

writer.write_str('.')
self.assertEqual(writer.finish(), 'ascii.')

def test_readonly_optim(self):
# Read-only optimization: if the first and only write is a Python str
# object and no buffer was allocated yet, return the object unchanged
Expand Down
6 changes: 3 additions & 3 deletions Mac/BuildScript/build-installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,9 @@ def library_recipes():

result.extend([
dict(
name="OpenSSL 3.5.8",
url="https://github.com/openssl/openssl/releases/download/openssl-3.5.8/openssl-3.5.8.tar.gz",
checksum="a8f84a39918ec6415ce765d9b429d313ba97b8143169c172e734b9514464f5b2",
name="OpenSSL 3.5.9",
url="https://github.com/openssl/openssl/releases/download/openssl-3.5.9/openssl-3.5.9.tar.gz",
checksum="603f5602e2eef00d77fbd429d34dcd5822bb301757a1bc9cdb24c670f1eb859a",
buildrecipe=build_universal_openssl,
configure=None,
install=None,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update Android and iOS build scripts to use OpenSSL 3.5.9.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
If :c:func:`PyUnicodeWriter_WriteUTF8` or :c:func:`PyUnicodeWriter_Format`
fails, :c:func:`PyUnicodeWriter_Finish` now checks if the string uses the most
efficient storage. Previously, :c:func:`PyUnicodeWriter_Finish` could return a
string with an inefficient storage. Patch by Victor Stinner.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update macOS installer to use OpenSSL 3.5.9.
4 changes: 0 additions & 4 deletions Objects/longobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -2045,10 +2045,6 @@ pylong_int_to_decimal_string(PyObject *aa,
goto error;
}
if (writer) {
Py_ssize_t size = PyUnicode_GET_LENGTH(s);
if (_PyUnicodeWriter_Prepare(writer, size, '9') == -1) {
goto error;
}
if (_PyUnicodeWriter_WriteStr(writer, s) < 0) {
goto error;
}
Expand Down
76 changes: 43 additions & 33 deletions Objects/unicode_writer.c
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ PyUnicodeWriter_Create(Py_ssize_t length)
}


void PyUnicodeWriter_Discard(PyUnicodeWriter *writer)
void
PyUnicodeWriter_Discard(PyUnicodeWriter *writer)
{
if (writer == NULL) {
return;
Expand Down Expand Up @@ -511,8 +512,7 @@ PyUnicodeWriter_WriteASCII(PyUnicodeWriter *writer,
assert(writer != NULL);
_Py_AssertHoldsTstate();

_PyUnicodeWriter *priv_writer = (_PyUnicodeWriter*)writer;
return _PyUnicodeWriter_WriteASCIIString(priv_writer, str, size);
return _PyUnicodeWriter_WriteASCIIString((_PyUnicodeWriter*)writer, str, size);
}


Expand All @@ -525,40 +525,25 @@ PyUnicodeWriter_WriteUTF8(PyUnicodeWriter *writer,
size = strlen(str);
}

_PyUnicodeWriter *_writer = (_PyUnicodeWriter*)writer;
Py_ssize_t old_pos = _writer->pos;
int res = _PyUnicode_DecodeUTF8Writer(_writer, str, size,
_Py_ERROR_STRICT, NULL, NULL);
if (res < 0) {
_writer->pos = old_pos;
}
return res;
return _PyUnicode_DecodeUTF8Writer((_PyUnicodeWriter*)writer, str, size,
_Py_ERROR_STRICT, NULL, NULL);
}


int
PyUnicodeWriter_DecodeUTF8Stateful(PyUnicodeWriter *writer,
const char *string,
Py_ssize_t length,
const char *str,
Py_ssize_t size,
const char *errors,
Py_ssize_t *consumed)
{
if (length < 0) {
length = strlen(string);
}

_PyUnicodeWriter *_writer = (_PyUnicodeWriter*)writer;
Py_ssize_t old_pos = _writer->pos;
int res = _PyUnicode_DecodeUTF8Writer(_writer, string, length,
_Py_ERROR_UNKNOWN, errors,
consumed);
if (res < 0) {
_writer->pos = old_pos;
if (consumed) {
*consumed = 0;
}
if (size < 0) {
size = strlen(str);
}
return res;

return _PyUnicode_DecodeUTF8Writer((_PyUnicodeWriter*)writer, str, size,
_Py_ERROR_UNKNOWN, errors,
consumed);
}


Expand Down Expand Up @@ -600,30 +585,55 @@ _PyUnicodeWriter_Finish(_PyUnicodeWriter *writer)

Py_ssize_t final_size = writer->pos;
if (final_size == 0) {
// Get the empty string singleton
PyObject *empty = _PyUnicode_GetEmpty();
Py_XDECREF(str); // writer->buffer can be NULL if the position is 0
return empty;
}

Py_ssize_t length = PyUnicode_GET_LENGTH(str);
if (writer->readonly) {
assert(final_size == PyUnicode_GET_LENGTH(str));
goto done;
}
assert(final_size <= PyUnicode_GET_LENGTH(str));

if (final_size == 1 && PyUnicode_KIND(str) == PyUnicode_1BYTE_KIND) {
assert(length >= 1);
// Get the single character singleton
assert(PyUnicode_GET_LENGTH(str) >= 1);
const Py_UCS1 *data = PyUnicode_1BYTE_DATA(str);
Py_UCS1 ch = data[0];
PyObject *latin1_char = _Py_LATIN1_CHR(ch);
Py_DECREF(str);
return latin1_char;
str = _Py_LATIN1_CHR(ch);
goto done;
}

if (writer->recheck_maxchar) {
Py_UCS4 maxchar = _PyUnicode_FindMaxChar(str, 0, final_size);
if (maxchar != writer->maxchar) {
// Adjust the string kind
PyObject *str2 = PyUnicode_New(final_size, maxchar);
if (str2 == NULL) {
Py_DECREF(str);
return NULL;
}
_PyUnicode_FastCopyCharacters(str2, 0, str, 0, final_size);
Py_SETREF(str, str2);
goto done;
}
}

if (!writer->readonly && length != final_size) {
if (PyUnicode_GET_LENGTH(str) != final_size) {
// Truncate the string
PyObject *str2 = _PyUnicode_ResizeCompact(str, final_size);
if (str2 == NULL) {
Py_DECREF(str);
return NULL;
}
str = str2;
goto done;
}

done:
assert(_PyUnicode_CheckConsistency(str, 1));
return str;
}
Expand Down
47 changes: 35 additions & 12 deletions Objects/unicodeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -3166,16 +3166,21 @@ PyUnicodeWriter_Format(PyUnicodeWriter *writer, const char *format, ...)
}

int
_PyUnicodeWriter_FormatV(PyUnicodeWriter *writer, const char *format,
_PyUnicodeWriter_FormatV(PyUnicodeWriter *pub_writer, const char *format,
va_list vargs)
{
_PyUnicodeWriter *_writer = (_PyUnicodeWriter*)writer;
Py_ssize_t old_pos = _writer->pos;
_PyUnicodeWriter *writer = (_PyUnicodeWriter*)pub_writer;
Py_ssize_t old_pos = writer->pos;
Py_UCS4 old_maxchar = writer->maxchar;

int res = unicode_from_format(_writer, format, vargs);
int res = unicode_from_format(writer, format, vargs);

if (res < 0) {
_writer->pos = old_pos;
writer->pos = old_pos;
if (writer->maxchar > old_maxchar) {
// _PyUnicodeWriter_Finish() will check maxchar
writer->recheck_maxchar = 1;
}
}
return res;
}
Expand Down Expand Up @@ -5364,7 +5369,7 @@ unicode_decode_utf8(const char *s, Py_ssize_t size,
}


// Used by PyUnicodeWriter_WriteUTF8() implementation
// Used by PyUnicodeWriter_WriteUTF8() and PyUnicodeWriter_DecodeUTF8Stateful()
int
_PyUnicode_DecodeUTF8Writer(_PyUnicodeWriter *writer,
const char *s, Py_ssize_t size,
Expand All @@ -5378,18 +5383,20 @@ _PyUnicode_DecodeUTF8Writer(_PyUnicodeWriter *writer,
return 0;
}

Py_ssize_t old_pos = writer->pos;
Py_UCS4 old_maxchar = writer->maxchar;

// fast path: try ASCII string.
if (_PyUnicodeWriter_Prepare(writer, size, 127) < 0) {
return -1;
goto error;
}
assert(_PyUnicodeWriter_CanWrite(writer));

const char *starts = s;
const char *end = s + size;
Py_ssize_t decoded = 0;
Py_UCS1 *dest = (Py_UCS1*)writer->data + writer->pos * writer->kind;
if (writer->kind == PyUnicode_1BYTE_KIND) {
decoded = ascii_decode(s, end, dest);
Py_UCS1 *dest = (Py_UCS1*)writer->data + writer->pos * writer->kind;
Py_ssize_t decoded = ascii_decode(s, end, dest);
writer->pos += decoded;

if (decoded == size) {
Expand All @@ -5401,8 +5408,24 @@ _PyUnicode_DecodeUTF8Writer(_PyUnicodeWriter *writer,
s += decoded;
}

return unicode_decode_utf8_impl(writer, starts, s, end,
error_handler, errors, consumed);
int res = unicode_decode_utf8_impl(writer, starts, s, end,
error_handler, errors, consumed);
if (res < 0) {
goto error;
}
return 0;

error:
// Restore the writer to its previous state
writer->pos = old_pos;
if (writer->maxchar > old_maxchar) {
// _PyUnicodeWriter_Finish() will check maxchar
writer->recheck_maxchar = 1;
}
if (consumed) {
*consumed = 0;
}
return -1;
}


Expand Down
2 changes: 1 addition & 1 deletion Platforms/Android/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def unpack_deps(host, prefix_dir, cache_dir):
for name_ver in [
"bzip2-1.0.8-3",
"libffi-3.4.4-3",
"openssl-3.5.8-0",
"openssl-3.5.9-0",
"sqlite-3.53.4-0",
"xz-5.4.6-1",
"zstd-1.5.7-2"
Expand Down
Loading
Loading