https://github.com/python/cpython/commit/1ebc13bd729d3e2085a5e5b83f7a2b2603da2569
commit: 1ebc13bd729d3e2085a5e5b83f7a2b2603da2569
branch: 3.15
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-10-16T16:04:10Z
summary:

[3.15] gh-157242: Add more PyBytesWriter tests (#159546)

* gh-157242: Add more PyBytesWriter tests (#157352)

* Fix PyBytesWriter_Grow() check on the size argument.
* Fix undefined behavior in _PyBytes_ResizeKeepOnError(): raise OverflowError
  on integer overflow.
* Add write(), grow() and get_data() methods to the _testcapi wrapper of
  PyBytesWriter.
* Remove the string argument from _testcapi.PyBytesWriter() and
  resize() method.
* Add tests on PyBytesWriter_Resize() invalid sizes.

(cherry picked from commit 63d4398be879e346ef68f899115f86cdb9fa555c)

* gh-157242: Allow negative grow in PyBytesWriter_Grow() (#157375)

PyBytesWriter_Resize() doesn't need to call byteswriter_resize() if
the buffer is already large enough.

(cherry picked from commit bab97c599f7c66b6b7b718a1c03a4bf4520b504c)

* gh-157242: Add byteswriter_check_consistency() (#157723)

Add byteswriter_check_consistency() to check that the bytes/bytearray
object can be mutated. It is called by all PyBytesWriter functions.

* Add test_thread to PyBytesWriter tests.
* _PyBytes_IsMutable() now checks _PyObject_IsUniquelyReferenced().
* Clarify thread safety in PyBytesWriter documentation.

(cherry picked from commit e2ff498cc03886b73fca0429ee745b04646ad8e0)

* Run prek --all-files

files:
M Doc/c-api/bytes.rst
M Lib/test/test_capi/test_bytes.py
M Modules/_testcapi/bytes.c
M Objects/bytesobject.c

diff --git a/Doc/c-api/bytes.rst b/Doc/c-api/bytes.rst
index 516e8b0bbfa856..3b995de6eb34f5 100644
--- a/Doc/c-api/bytes.rst
+++ b/Doc/c-api/bytes.rst
@@ -299,8 +299,8 @@ object.
 
    A bytes writer instance.
 
-   The API is **not thread safe**: a writer should only be used by a single
-   thread at the same time.
+   The API is **not thread safe**. To share a writer with multiple threads, a
+   critical section or a lock is needed.
 
    The instance must be destroyed by :c:func:`PyBytesWriter_Finish` on
    success, or :c:func:`PyBytesWriter_Discard` on error.
@@ -436,7 +436,7 @@ Low-level API
    On success, return ``0``.
    On error, set an exception and return ``-1``.
 
-   *size* can be negative to shrink the writer.
+   *grow* can be negative to shrink the writer.
 
 .. c:function:: void* PyBytesWriter_GrowAndUpdatePointer(PyBytesWriter 
*writer, Py_ssize_t size, void *buf)
 
diff --git a/Lib/test/test_capi/test_bytes.py b/Lib/test/test_capi/test_bytes.py
index 9ee200191de930..05c7f90eec7cbc 100644
--- a/Lib/test/test_capi/test_bytes.py
+++ b/Lib/test/test_capi/test_bytes.py
@@ -1,7 +1,9 @@
 import sys
+import threading
 import unittest
 from test import support
 from test.support import import_helper
+from test.support import threading_helper
 
 _testlimitedcapi = import_helper.import_module('_testlimitedcapi')
 _testcapi = import_helper.import_module('_testcapi')
@@ -318,6 +320,10 @@ def test_join(self):
 
 class BaseWriterTest:
     RESULT_TYPE = NotImplementedError
+    SMALL_BUFFER = 11  # bytes
+    assert SMALL_BUFFER < _testcapi.PyBytesWriter_small_buffer
+    LARGE_BUFFER = _testcapi.PyBytesWriter_small_buffer + 17  # bytes
+    NEW_BYTE = b'\xff'
 
     def create_writer(self, alloc=0, string=b''):
         raise NotImplementedError
@@ -334,23 +340,60 @@ def test_create(self):
         self.assertEqual(writer.get_size(), 0)
         self.bytes_equal(writer.finish(), b'')
 
-        writer = self.create_writer(3, b'abc')
+        writer = self.create_writer(3)
+        writer.write(0, b'abc')
         self.assertEqual(writer.get_size(), 3)
         self.bytes_equal(writer.finish(), b'abc')
 
+    @unittest.skipUnless(support.Py_DEBUG, 'need Py_DEBUG')
+    def test_get_data(self):
+        # Test PyBytesWriter_GetData()
+        writer = self.create_writer(6)
+        NEW_BYTE = self.NEW_BYTE
+        self.assertEqual(writer.get_data(), NEW_BYTE * 6)
+        writer.write(0, b'abc')
+        self.assertEqual(writer.get_data(), b'abc' + NEW_BYTE * 3)
+        writer.write(3, b'123')
+        self.assertEqual(writer.get_data(), b'abc123')
+
+        # Switch from small buffer to large buffer
+        small, large = self.SMALL_BUFFER, self.LARGE_BUFFER
+        writer = self.create_writer(small)
+        self.assertEqual(writer.get_data(), NEW_BYTE * small)
+        writer.write(0, b's' * small)
+        self.assertEqual(writer.get_data(), b's' * small)
+        writer.resize(large)
+        self.assertEqual(writer.get_data(), b's' * small + NEW_BYTE * (large - 
small))
+        writer.write(small, b'L' * (large - small))
+        self.assertEqual(writer.get_data(), b's' * small + b'L' * (large - 
small))
+
+        # Resize large buffer
+        small, large = self.SMALL_BUFFER, self.LARGE_BUFFER
+        writer = self.create_writer(large)
+        self.assertEqual(writer.get_data(), NEW_BYTE * large)
+        writer.write(0, b'L' * large)
+        self.assertEqual(writer.get_data(), b'L' * large)
+        writer.resize(large + 10)
+        self.assertEqual(writer.get_data(), b'L' * large + NEW_BYTE * 10)
+        writer.write(large, b'#' * 10)
+        self.assertEqual(writer.get_data(), b'L' * large + b'#' * 10)
+
     def test_finish_with_size(self):
         # Test PyBytesWriter_FinishWithSize()
-        writer = self.create_writer(10, b'abcdef')
+        writer = self.create_writer(10)
+        writer.write(0, b'abc123')
         self.assertEqual(writer.get_size(), 10)
         self.bytes_equal(writer.finish_with_size(3), b'abc')
 
         # Error if the size is negative
-        writer = self.create_writer(3, b'abc')
+        writer = self.create_writer(3, )
+        writer.write(0, b'abc')
         with self.assertRaises(ValueError):
             writer.finish_with_size(-3)
 
         # Error if the requested size is larger than the allocated size
-        writer = self.create_writer(3, b'abc')
+        writer = self.create_writer(3)
+        writer.write(0, b'abc')
         with self.assertRaises(ValueError):
             writer.finish_with_size(4)
 
@@ -368,44 +411,118 @@ def test_write_bytes(self):
     def test_resize(self):
         # Test PyBytesWriter_Resize()
         writer = self.create_writer()
-        writer.resize(len(b'number=123456'), b'number=123456')
-        writer.resize(len(b'number=123456'), b'')
-        self.assertEqual(writer.get_size(), len(b'number=123456'))
-        self.bytes_equal(writer.finish(), b'number=123456')
+        writer.resize(len(b'hello'))
+        writer.write(0, b'hello')
+        self.assertEqual(writer.finish(), b'hello')
 
         writer = self.create_writer()
-        writer.resize(0, b'')
-        writer.resize(len(b'number=123456'), b'number=123456')
-        self.bytes_equal(writer.finish(), b'number=123456')
-
+        writer.resize(0)  # noop
+        writer.resize(len(b'number'))
+        writer.write(0, b'number')
+        writer.resize(len(b'number='))
+        writer.write(len(b'number'), b'=')
+        writer.resize(len(b'number=123'), )
+        writer.write(len(b'number='), b'123')
+        writer.resize(len(b'number=123'))  # noop
+        self.assertEqual(writer.finish(), b'number=123')
+
+        # Switch from small buffer to large buffer
         writer = self.create_writer()
-        writer.resize(len(b'number='), b'number=')
-        writer.resize(len(b'number=123456'), b'123456')
-        self.bytes_equal(writer.finish(), b'number=123456')
-
+        small, large = self.SMALL_BUFFER, self.LARGE_BUFFER
+        writer.resize(small)
+        writer.write(0, b's' * small)
+        writer.resize(large)
+        writer.write(small, b'L' * (large - small))
+        self.assertEqual(writer.finish(),
+                         b's' * small + b'L' * (large - small))
+
+        # Make sure that it's possible to write after a resize to zero
+        # when a bytes/bytearray object is allocated.
         writer = self.create_writer()
-        writer.resize(len(b'number='), b'number=')
-        writer.resize(len(b'number='), b'')
-        writer.resize(len(b'number=123456'), b'123456')
-        self.bytes_equal(writer.finish(), b'number=123456')
+        writer.resize(self.LARGE_BUFFER)
+        writer.resize(0)
+        writer.write_bytes(b'abc', 3)
+        self.assertEqual(writer.finish(), b'abc')
+
+        # invalid size
+        for size in (self.SMALL_BUFFER, self.LARGE_BUFFER):
+            with self.subTest(size=size):
+                writer = self.create_writer()
+                writer.write_bytes(b'x' * size, -1)
+                with self.assertRaisesRegex(ValueError, 'size must be >= 0'):
+                    writer.resize(-1)
+                with self.assertRaises((MemoryError, OverflowError)):
+                    writer.resize(_testcapi.PY_SSIZE_T_MAX)
+                self.assertEqual(writer.finish(), b'x' * size)
+
+    def test_grow(self):
+        # Test PyBytesWriter_Grow()
+        writer = self.create_writer(0)
+        writer.grow(len(b'number=123'))
+        writer.write(0, b'number=123')
+        self.assertEqual(writer.finish(), b'number=123')
 
         writer = self.create_writer()
-        writer.resize(len(b'number'), b'number')
-        writer.resize(len(b'number='), b'=')
-        writer.resize(len(b'number=123'), b'123')
-        writer.resize(len(b'number=123456'), b'456')
-        self.bytes_equal(writer.finish(), b'number=123456')
+        writer.grow(0)  # noop
+        writer.grow(len(b'number'))
+        writer.write(0, b'number')
+        writer.grow(len(b'='))
+        writer.write(len(b'number'), b'=')
+        writer.grow(len(b'123'), )
+        writer.write(len(b'number='), b'123')
+        writer.grow(0)  # noop
+        self.assertEqual(writer.finish(), b'number=123')
+
+        for size in (self.SMALL_BUFFER, self.LARGE_BUFFER):
+            with self.subTest(size=size):
+                # Truncate the last byte
+                data = b'x' * size
+                writer = self.create_writer(size)
+                writer.write(0, data)
+                self.assertEqual(writer.get_data(), data)
+                writer.grow(-1)
+                self.assertEqual(writer.get_data(), data[:-1])
+                self.assertEqual(writer.finish(),  data[:-1])
+
+                # Make the buffer empty
+                writer = self.create_writer(size)
+                writer.write(0, data)
+                writer.grow(-size)
+                self.assertEqual(writer.get_data(), b'')
+                self.assertEqual(writer.finish(),  b'')
+
+        # Switch from small buffer to large buffer
+        writer = self.create_writer()
+        small, large = self.SMALL_BUFFER, self.LARGE_BUFFER
+        writer.grow(small)
+        writer.write(0, b's' * small)
+        writer.grow(large - small)
+        writer.write(small, b'L' * (large - small))
+        self.assertEqual(writer.finish(),
+                         b's' * small + b'L' * (large - small))
+
+        # invalid size
+        for size in (self.SMALL_BUFFER, self.LARGE_BUFFER):
+            with self.subTest(size=size):
+                writer = self.create_writer()
+                writer.write_bytes(b'x' * size, -1)
+                with self.assertRaisesRegex(ValueError, 'invalid size'):
+                    writer.grow(-size - 1)
+                with self.assertRaises(MemoryError):
+                    writer.grow(_testcapi.PY_SSIZE_T_MAX)
+                self.assertEqual(writer.finish(), b'x' * size)
 
     @support.nomemtest
     def test_resize_error(self):
-        small_buffer = _testcapi.PyBytesWriter_small_buffer
-        init = b'x' * (small_buffer * 2)
-        writer = self.create_writer(len(init), init)
+        # Test PyBytesWriter_Resize() error
+        init = b'x' * self.LARGE_BUFFER
+        writer = self.create_writer(len(init))
+        writer.write(0, init)
         size = len(init) + 100
         try:
             with self.assertRaises(MemoryError):
                 _testcapi.set_nomemory(0)
-                writer.resize(size, b'')
+                writer.resize(size)
         finally:
             _testcapi.remove_mem_hooks()
         suffix = b'still working'
@@ -443,36 +560,71 @@ def test_format_s_memory_error(self):
             finally:
                 _testcapi.remove_mem_hooks()
         writer.write_bytes(b'after', -1)
-        self.bytes_equal(writer.finish(), b'after')
+
+    @threading_helper.requires_working_threading()
+    def test_thread(self):
+        # PyBytesWriter can be used by multiple threads: it's up to the caller
+        # to implement a lock to prevent concurrent accesses.
+        writer = self.create_writer(0)
+        size = None
+        data = None
+
+        def thread_func(writer, LARGE_BUFFER):
+            nonlocal size, data
+
+            # create a bytes object for the buffer
+            writer.write_bytes(b'x' * LARGE_BUFFER, LARGE_BUFFER)
+
+            # so we can check a write with a bytes object
+            writer.write_bytes(b'yz', 2)
+            writer.format_i(b'i=%i', 5)
+            writer.resize(10)
+            data = writer.get_data()
+            size = writer.get_size()
+
+        thread = threading.Thread(target=thread_func,
+                                  args=(writer, self.LARGE_BUFFER))
+        thread.start()
+        threading_helper.join_thread(thread)
+
+        self.assertEqual(size, 10)
+        self.assertEqual(data, b'x' * 10)
+        self.assertEqual(writer.finish(), b'x' * 10)
 
 
 class BytesWriterTest(BaseWriterTest, unittest.TestCase):
     RESULT_TYPE = bytes
 
-    def create_writer(self, alloc=0, string=b''):
+    def create_writer(self, size=0):
         # Test PyBytesWriter_Create()
-        return _testcapi.PyBytesWriter(alloc, string, 0)
+        return _testcapi.PyBytesWriter(size, 0)
 
     # Only PyBytesWriter_Create() returns singletons
     def test_singletons(self):
         empty = b''
         singletons = {ch: bytes((ch,)) for ch in range(256)}
-        small_buffer = _testcapi.PyBytesWriter_small_buffer
 
         writer = self.create_writer()
         self.assertIs(writer.finish(), empty)
 
-        # Test writer larger than small_buffer
+        # Large buffer
         writer = self.create_writer()
-        unused_text = b'x' * (small_buffer * 2)
+        unused_text = b'x' * self.LARGE_BUFFER
         writer.write_bytes(unused_text, len(unused_text))
         self.assertIs(writer.finish_with_size(0), empty)
 
+        # Large buffer with resize
+        writer = self.create_writer()
+        unused_text = b'x' * self.LARGE_BUFFER
+        writer.write_bytes(unused_text, len(unused_text))
+        writer.resize(0)
+        self.assertIs(writer.finish(), empty)
+
         for ch in range(256):
-            text = bytes((ch,))
+            byte = bytes((ch,))
 
             writer = self.create_writer()
-            writer.write_bytes(text, 1)
+            writer.write_bytes(byte, len(byte))
             self.assertIs(writer.finish(), singletons[ch])
 
     def test_example_abc(self):
@@ -488,9 +640,9 @@ def test_example_highlevel(self):
 class ByteArrayWriterTest(BaseWriterTest, unittest.TestCase):
     RESULT_TYPE = bytearray
 
-    def create_writer(self, alloc=0, string=b''):
+    def create_writer(self, size=0):
         # Test private _PyBytesWriter_CreateByteArray()
-        return _testcapi.PyBytesWriter(alloc, string, 1)
+        return _testcapi.PyBytesWriter(size, 1)
 
 
 if __name__ == "__main__":
diff --git a/Modules/_testcapi/bytes.c b/Modules/_testcapi/bytes.c
index 984a857a83db11..0c3b323aca7b8e 100644
--- a/Modules/_testcapi/bytes.c
+++ b/Modules/_testcapi/bytes.c
@@ -87,34 +87,25 @@ writer_init(PyObject *self_raw, PyObject *args, PyObject 
*kwargs)
         return -1;
     }
 
-    Py_ssize_t alloc;
-    char *str;
-    Py_ssize_t str_size;
-    int use_bytearray;
-    if (!PyArg_ParseTuple(args, "ny#i",
-                          &alloc, &str, &str_size, &use_bytearray)) {
+    Py_ssize_t size;
+    int use_bytearray = 0;
+    if (!PyArg_ParseTuple(args, "n|i", &size, &use_bytearray)) {
         return -1;
     }
 
     WriterObject *self = (WriterObject *)self_raw;
-    if (self->writer) {
-        PyBytesWriter_Discard(self->writer);
-    }
+    PyBytesWriter_Discard(self->writer);
+
     if (use_bytearray) {
-        self->writer = _PyBytesWriter_CreateByteArray(alloc);
+        self->writer = _PyBytesWriter_CreateByteArray(size);
     }
     else {
-        self->writer = PyBytesWriter_Create(alloc);
+        self->writer = PyBytesWriter_Create(size);
     }
     if (self->writer == NULL) {
         return -1;
     }
 
-    if (str_size) {
-        char *buf = PyBytesWriter_GetData(self->writer);
-        memcpy(buf, str, str_size);
-    }
-
     return 0;
 }
 
@@ -143,6 +134,32 @@ writer_check(WriterObject *self)
 }
 
 
+static PyObject*
+writer_write(PyObject *self_raw, PyObject *args)
+{
+    WriterObject *self = (WriterObject *)self_raw;
+    if (writer_check(self) < 0) {
+        return NULL;
+    }
+
+    Py_ssize_t pos, size;
+    char *str;
+    if (!PyArg_ParseTuple(args, "ny#", &pos, &str, &size)) {
+        return NULL;
+    }
+
+    if (pos < 0 || (pos + size) > PyBytesWriter_GetSize(self->writer)) {
+        PyErr_SetString(PyExc_ValueError, "invalid position or size");
+        return NULL;
+    }
+
+    char *data = PyBytesWriter_GetData(self->writer);
+    memcpy(data + pos, str, size);
+
+    Py_RETURN_NONE;
+}
+
+
 static PyObject*
 writer_write_bytes(PyObject *self_raw, PyObject *args)
 {
@@ -185,6 +202,7 @@ writer_format_i(PyObject *self_raw, PyObject *args)
 }
 
 
+// PyBytesWriter_Resize
 static PyObject*
 writer_format_s(PyObject *self_raw, PyObject *args)
 {
@@ -215,27 +233,52 @@ writer_resize(PyObject *self_raw, PyObject *args)
     }
 
     Py_ssize_t size;
-    char *str;
-    Py_ssize_t str_size;
-    if (!PyArg_ParseTuple(args,
-                          "ny#",
-                          &size, &str, &str_size)) {
+    if (!PyArg_ParseTuple(args, "n", &size)) {
         return NULL;
     }
-    assert(size >= str_size);
 
-    Py_ssize_t pos = PyBytesWriter_GetSize(self->writer);
     if (PyBytesWriter_Resize(self->writer, size) < 0) {
         return NULL;
     }
+    Py_RETURN_NONE;
+}
+
+
+// Test PyBytesWriter_Grow()
+static PyObject*
+writer_grow(PyObject *self_raw, PyObject *args)
+{
+    WriterObject *self = (WriterObject *)self_raw;
+    if (writer_check(self) < 0) {
+        return NULL;
+    }
 
-    char *buf = PyBytesWriter_GetData(self->writer);
-    memcpy(buf + pos, str, str_size);
+    Py_ssize_t size;
+    if (!PyArg_ParseTuple(args, "n", &size)) {
+        return NULL;
+    }
 
+    if (PyBytesWriter_Grow(self->writer, size) < 0) {
+        return NULL;
+    }
     Py_RETURN_NONE;
 }
 
 
+static PyObject*
+writer_get_data(PyObject *self_raw, PyObject *Py_UNUSED(args))
+{
+    WriterObject *self = (WriterObject *)self_raw;
+    if (writer_check(self) < 0) {
+        return NULL;
+    }
+
+    const char *data = PyBytesWriter_GetData(self->writer);
+    Py_ssize_t size = PyBytesWriter_GetSize(self->writer);
+    return PyBytes_FromStringAndSize(data, size);
+}
+
+
 static PyObject*
 writer_get_size(PyObject *self_raw, PyObject *Py_UNUSED(args))
 {
@@ -244,8 +287,8 @@ writer_get_size(PyObject *self_raw, PyObject 
*Py_UNUSED(args))
         return NULL;
     }
 
-    Py_ssize_t alloc = PyBytesWriter_GetSize(self->writer);
-    return PyLong_FromSsize_t(alloc);
+    Py_ssize_t size = PyBytesWriter_GetSize(self->writer);
+    return PyLong_FromSsize_t(size);
 }
 
 
@@ -283,10 +326,13 @@ writer_finish_with_size(PyObject *self_raw, PyObject 
*args)
 
 
 static PyMethodDef writer_methods[] = {
+    {"write", _PyCFunction_CAST(writer_write), METH_VARARGS},
     {"write_bytes", _PyCFunction_CAST(writer_write_bytes), METH_VARARGS},
     {"format_i", _PyCFunction_CAST(writer_format_i), METH_VARARGS},
     {"format_s", _PyCFunction_CAST(writer_format_s), METH_VARARGS},
     {"resize", _PyCFunction_CAST(writer_resize), METH_VARARGS},
+    {"grow", _PyCFunction_CAST(writer_grow), METH_VARARGS},
+    {"get_data", _PyCFunction_CAST(writer_get_data), METH_NOARGS},
     {"get_size", _PyCFunction_CAST(writer_get_size), METH_NOARGS},
     {"finish", _PyCFunction_CAST(writer_finish), METH_NOARGS},
     {"finish_with_size", _PyCFunction_CAST(writer_finish_with_size), 
METH_VARARGS},
@@ -335,8 +381,9 @@ byteswriter_resize(PyObject *Py_UNUSED(module), PyObject 
*Py_UNUSED(args))
     char *buf = PyBytesWriter_GetData(writer);
 
     // Write some bytes
-    memcpy(buf, "Hello ", strlen("Hello "));
-    buf += strlen("Hello ");
+    const char *hello = "Hello ";
+    memcpy(buf, hello, strlen(hello));
+    buf += strlen(hello);
 
     // Allocate 10 more bytes
     buf = PyBytesWriter_GrowAndUpdatePointer(writer, 10, buf);
@@ -346,8 +393,9 @@ byteswriter_resize(PyObject *Py_UNUSED(module), PyObject 
*Py_UNUSED(args))
     }
 
     // Write more bytes
-    memcpy(buf, "World", strlen("World"));
-    buf += strlen("World");
+    const char *world = "World";
+    memcpy(buf, world, strlen(world));
+    buf += strlen(world);
 
     // Truncate to the exact size and create a bytes object
     return PyBytesWriter_FinishWithPointer(writer, buf);
diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c
index 233f9dc3d63bbc..559f59af184b5c 100644
--- a/Objects/bytesobject.c
+++ b/Objects/bytesobject.c
@@ -37,7 +37,7 @@ static Py_ssize_t 
_PyBytesWriter_ResizeToAllocated(PyBytesWriter *writer);
 
 #define CHARACTERS _Py_SINGLETON(bytes_characters)
 #define CHARACTER(ch) \
-     ((PyBytesObject *)&(CHARACTERS[ch]));
+     ((PyBytesObject *)&(CHARACTERS[ch]))
 #define EMPTY (&_Py_SINGLETON(bytes_empty))
 
 
@@ -3324,6 +3324,33 @@ PyBytes_ConcatAndDel(PyObject **pv, PyObject *w)
 }
 
 
+#ifndef NDEBUG
+// Make sure that a bytes object can still be mutated.
+//
+// Usage: assert(_PyBytes_IsMutable(obj)).
+int
+_PyBytes_IsMutable(PyObject *self)
+{
+    assert(PyBytes_Check(self));
+    // Do not use _PyObject_IsUniquelyReferenced(): this function is called
+    // by bytearray and PyBytesWriter which can be used by multiple threads.
+    assert(Py_REFCNT(self) == 1);
+    assert(!_Py_IsImmortal(self));
+
+    // Check that the object is not a singleton
+    Py_ssize_t size = PyBytes_GET_SIZE(self);
+    if (size == 0) {
+        assert(self != bytes_get_empty());
+    }
+    else if (size == 1) {
+        unsigned char ch = PyBytes_AS_STRING(self)[0];
+        assert(self != (PyObject*)CHARACTER(ch));
+    }
+    return 1;
+}
+#endif
+
+
 /* The following function breaks the notion that bytes are immutable:
    it changes the size of a bytes object.  You can think of it
    as creating a new bytes object and destroying the old one, only
@@ -3386,6 +3413,12 @@ _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t 
newsize)
     }
     assert(v != bytes_get_empty());
 
+    if ((size_t)newsize > (size_t)PY_SSIZE_T_MAX - PyBytesObject_SIZE) {
+        PyErr_SetString(PyExc_OverflowError,
+                        "byte string is too large");
+        return -1;
+    }
+
 #ifdef Py_TRACE_REFS
     _Py_ForgetReference(v);
 #endif
@@ -3610,6 +3643,8 @@ _PyBytes_Repeat(char* dest, Py_ssize_t len_dest,
 
 // --- PyBytesWriter API -----------------------------------------------------
 
+#define PyBytesWrite_NEW_BYTE 0xff
+
 static inline char*
 byteswriter_data(PyBytesWriter *writer)
 {
@@ -3632,6 +3667,33 @@ byteswriter_allocated(PyBytesWriter *writer)
 }
 
 
+#ifndef NDEBUG
+static int
+byteswriter_check_consistency(PyBytesWriter *writer)
+{
+    PyObject *obj = writer->obj;
+    if (obj != NULL) {
+        if (writer->use_bytearray) {
+            assert(PyByteArray_CheckExact(obj));
+            // Do not use _PyObject_IsUniquelyReferenced(): the caller can have
+            // its own lock to prevent a writer from being used by two threads
+            // at the same time.
+            assert(Py_REFCNT(obj) == 1);
+            PyByteArrayObject *bytearray = (PyByteArrayObject*)obj;
+            obj = bytearray->ob_bytes_object;
+            assert(obj != NULL);
+        }
+
+        // Code adapted from _PyBytes_IsMutable()
+        assert(PyBytes_CheckExact(obj));
+        assert(_PyBytes_IsMutable(obj));
+        assert((size_t)PyBytes_GET_SIZE(obj) > sizeof(writer->small_buffer));
+    }
+    return 1;
+}
+#endif
+
+
 #ifdef MS_WINDOWS
    /* On Windows, overallocate by 50% is the best factor */
 #  define OVERALLOCATE_FACTOR 2
@@ -3647,6 +3709,7 @@ byteswriter_resize(PyBytesWriter *writer, Py_ssize_t 
size, int resize)
 
     Py_ssize_t old_allocated = byteswriter_allocated(writer);
     if (size <= old_allocated) {
+        // Do not shrink the buffer before PyBytesWriter_FinishWithSize()
         return 0;
     }
 
@@ -3659,12 +3722,15 @@ byteswriter_resize(PyBytesWriter *writer, Py_ssize_t 
size, int resize)
     if (writer->obj != NULL) {
         if (writer->use_bytearray) {
             if (PyByteArray_Resize(writer->obj, size)) {
+                assert(byteswriter_check_consistency(writer));
                 return -1;
             }
         }
         else {
+            // Can raise MemoryError or OverflowError
             if (_PyBytes_ResizeKeepOnError(&writer->obj, size)) {
                 assert(writer->obj != NULL);
+                assert(byteswriter_check_consistency(writer));
                 return -1;
             }
         }
@@ -3698,7 +3764,7 @@ byteswriter_resize(PyBytesWriter *writer, Py_ssize_t 
size, int resize)
 #ifdef Py_DEBUG
     Py_ssize_t allocated = byteswriter_allocated(writer);
     if (resize && allocated > old_allocated) {
-        memset(byteswriter_data(writer) + old_allocated, 0xff,
+        memset(byteswriter_data(writer) + old_allocated, PyBytesWrite_NEW_BYTE,
                allocated - old_allocated);
     }
 #endif
@@ -3736,8 +3802,10 @@ byteswriter_create(Py_ssize_t size, int use_bytearray)
         writer->size = size;
     }
 #ifdef Py_DEBUG
-    memset(byteswriter_data(writer), 0xff, byteswriter_allocated(writer));
+    memset(byteswriter_data(writer), PyBytesWrite_NEW_BYTE,
+           byteswriter_allocated(writer));
 #endif
+    assert(byteswriter_check_consistency(writer));
     return writer;
 }
 
@@ -3761,6 +3829,8 @@ PyBytesWriter_Discard(PyBytesWriter *writer)
         return;
     }
 
+    assert(byteswriter_check_consistency(writer));
+
     Py_XDECREF(writer->obj);
     _Py_FREELIST_FREE(bytes_writers, writer, PyMem_Free);
 }
@@ -3769,6 +3839,8 @@ PyBytesWriter_Discard(PyBytesWriter *writer)
 PyObject*
 PyBytesWriter_FinishWithSize(PyBytesWriter *writer, Py_ssize_t size)
 {
+    assert(byteswriter_check_consistency(writer));
+
     // Check for negative size here to raise ValueError in all cases, rather
     // than having a different exception depending on the code path. For
     // example, _PyBytes_Resize() raises SystemError on negative size.
@@ -3810,6 +3882,13 @@ PyBytesWriter_FinishWithSize(PyBytesWriter *writer, 
Py_ssize_t size)
     else {
         result = PyBytes_FromStringAndSize(writer->small_buffer, size);
     }
+
+#ifdef Py_DEBUG
+    // Reset the writer, so byteswriter_check_consistency() doesn't fail
+    // in PyBytesWriter_Discard().
+    writer->size = 0;
+#endif
+
     PyBytesWriter_Discard(writer);
     return result;
 
@@ -3836,6 +3915,8 @@ PyBytesWriter_FinishWithPointer(PyBytesWriter *writer, 
void *buf)
 void*
 PyBytesWriter_GetData(PyBytesWriter *writer)
 {
+    assert(byteswriter_check_consistency(writer));
+
     return byteswriter_data(writer);
 }
 
@@ -3843,6 +3924,8 @@ PyBytesWriter_GetData(PyBytesWriter *writer)
 Py_ssize_t
 PyBytesWriter_GetSize(PyBytesWriter *writer)
 {
+    assert(byteswriter_check_consistency(writer));
+
     return _PyBytesWriter_GetSize(writer);
 }
 
@@ -3850,14 +3933,22 @@ PyBytesWriter_GetSize(PyBytesWriter *writer)
 int
 PyBytesWriter_Resize(PyBytesWriter *writer, Py_ssize_t size)
 {
+    assert(byteswriter_check_consistency(writer));
+
     if (size < 0) {
         PyErr_SetString(PyExc_ValueError, "size must be >= 0");
         return -1;
     }
-    if (byteswriter_resize(writer, size, 1) < 0) {
-        return -1;
+    if (writer->size < size) {
+        if (byteswriter_resize(writer, size, 1) < 0) {
+            return -1;
+        }
+    }
+    else {
+        // The buffer is already large enough. Never shrink the buffer.
     }
     writer->size = size;
+    assert(byteswriter_check_consistency(writer));
     return 0;
 }
 
@@ -3875,22 +3966,34 @@ _PyBytesWriter_ResizeAndUpdatePointer(PyBytesWriter 
*writer, Py_ssize_t size,
 
 
 int
-PyBytesWriter_Grow(PyBytesWriter *writer, Py_ssize_t size)
+PyBytesWriter_Grow(PyBytesWriter *writer, Py_ssize_t grow)
 {
-    if (size < 0 && writer->size + size < 0) {
-        PyErr_SetString(PyExc_ValueError, "invalid size");
-        return -1;
+    assert(byteswriter_check_consistency(writer));
+
+    if (grow == 0) {
+        // Nothing to do
+        return 0;
     }
-    if (size > PY_SSIZE_T_MAX - writer->size) {
-        PyErr_NoMemory();
-        return -1;
+
+    if (grow >= 0) {
+        if (grow > PY_SSIZE_T_MAX - writer->size) {
+            PyErr_NoMemory();
+            return -1;
+        }
     }
-    size = writer->size + size;
+    else {
+        if (writer->size + grow < 0) {
+            PyErr_SetString(PyExc_ValueError, "invalid size");
+            return -1;
+        }
+    }
+    Py_ssize_t size = writer->size + grow;
 
     if (byteswriter_resize(writer, size, 1) < 0) {
         return -1;
     }
     writer->size = size;
+    assert(byteswriter_check_consistency(writer));
     return 0;
 }
 
@@ -3926,6 +4029,8 @@ PyBytesWriter_WriteBytes(PyBytesWriter *writer,
     }
     char *buf = byteswriter_data(writer);
     memcpy(buf + pos, bytes, size);
+
+    assert(byteswriter_check_consistency(writer));
     return 0;
 }
 
@@ -3963,5 +4068,7 @@ _PyBytesWriter_ResizeToAllocated(PyBytesWriter *writer)
 {
     Py_ssize_t allocated = byteswriter_allocated(writer);
     writer->size = allocated;
+
+    assert(byteswriter_check_consistency(writer));
     return allocated;
 }

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to