Skip to content

Commit 3740f74

Browse files
vstinnerclaude
andauthored
[3.15] gh-156939: Fix two bugs in PyBytesWriter (#158404) (#158409)
* gh-156939: Fix two bugs in PyBytesWriter (#158404) Fix PyBytesWriter_Format() error handling: restore the writer to its previous state on error. Add an unit test injecting MemoryError. Fix PyBytesWriter_FinishWithSize() to return an empty bytearray when use_bytearray is set and size is 0, instead of always returning an empty bytes object. Fix test_capi.test_bytes: always check writer finish result type, to make sure that _PyBytesWriter_CreateByteArray() always create bytearray objects. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> (cherry picked from commit 24ea85a)
1 parent ab8cecf commit 3740f74

5 files changed

Lines changed: 83 additions & 26 deletions

File tree

‎Lib/test/test_capi/test_bytes.py‎

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import sys
22
import unittest
33
from test.support import import_helper
4+
from test import support
45

56
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
67
_testcapi = import_helper.import_module('_testcapi')
@@ -316,83 +317,108 @@ def test_join(self):
316317

317318

318319
class BytesWriterTest(unittest.TestCase):
319-
result_type = bytes
320+
RESULT_TYPE = bytes
320321

321322
def create_writer(self, alloc=0, string=b''):
322323
return _testcapi.PyBytesWriter(alloc, string, 0)
323324

325+
def bytes_equal(self, result, expected):
326+
# Similar to assertEqual(), but check also that the result type
327+
# is RESULT_TYPE.
328+
self.assertEqual(result, expected)
329+
self.assertEqual(type(result), self.RESULT_TYPE)
330+
324331
def test_create(self):
325332
# Test PyBytesWriter_Create()
326333
writer = self.create_writer()
327334
self.assertEqual(writer.get_size(), 0)
328-
self.assertEqual(writer.finish(), self.result_type(b''))
335+
self.bytes_equal(writer.finish(), b'')
329336

330337
writer = self.create_writer(3, b'abc')
331338
self.assertEqual(writer.get_size(), 3)
332-
self.assertEqual(writer.finish(), self.result_type(b'abc'))
339+
self.bytes_equal(writer.finish(), b'abc')
333340

334341
def test_finish_with_size(self):
335342
# Test PyBytesWriter_FinishWithSize()
336343
writer = self.create_writer(10, b'abc')
337344
self.assertEqual(writer.get_size(), 10)
338-
self.assertEqual(writer.finish_with_size(3), self.result_type(b'abc'))
345+
self.bytes_equal(writer.finish_with_size(3), b'abc')
339346

340347
writer = self.create_writer(3, b'abc')
341348
with self.assertRaises(SystemError):
342349
writer.finish_with_size(-3)
343350

344351
def test_write_bytes(self):
345-
# Test PyBytesWriter_WriteBytes()
346-
writer = self.create_writer()
347-
writer.write_bytes(b'Hello World!', -1)
348-
self.assertEqual(writer.finish(), self.result_type(b'Hello World!'))
352+
# Test PyBytesWriter_WriteBytes()
353+
writer = self.create_writer()
354+
writer.write_bytes(b'Hello World!', -1)
355+
self.bytes_equal(writer.finish(), b'Hello World!')
349356

350-
writer = self.create_writer()
351-
writer.write_bytes(b'Hello ', -1)
352-
writer.write_bytes(b'World! <truncated>', 6)
353-
self.assertEqual(writer.finish(), self.result_type(b'Hello World!'))
357+
writer = self.create_writer()
358+
writer.write_bytes(b'Hello ', -1)
359+
writer.write_bytes(b'World! <truncated>', 6)
360+
self.bytes_equal(writer.finish(), b'Hello World!')
354361

355362
def test_resize(self):
356363
# Test PyBytesWriter_Resize()
357364
writer = self.create_writer()
358365
writer.resize(len(b'number=123456'), b'number=123456')
359366
writer.resize(len(b'number=123456'), b'')
360367
self.assertEqual(writer.get_size(), len(b'number=123456'))
361-
self.assertEqual(writer.finish(), self.result_type(b'number=123456'))
368+
self.bytes_equal(writer.finish(), b'number=123456')
362369

363370
writer = self.create_writer()
364371
writer.resize(0, b'')
365372
writer.resize(len(b'number=123456'), b'number=123456')
366-
self.assertEqual(writer.finish(), self.result_type(b'number=123456'))
373+
self.bytes_equal(writer.finish(), b'number=123456')
367374

368375
writer = self.create_writer()
369376
writer.resize(len(b'number='), b'number=')
370377
writer.resize(len(b'number=123456'), b'123456')
371-
self.assertEqual(writer.finish(), self.result_type(b'number=123456'))
378+
self.bytes_equal(writer.finish(), b'number=123456')
372379

373380
writer = self.create_writer()
374381
writer.resize(len(b'number='), b'number=')
375382
writer.resize(len(b'number='), b'')
376383
writer.resize(len(b'number=123456'), b'123456')
377-
self.assertEqual(writer.finish(), self.result_type(b'number=123456'))
384+
self.bytes_equal(writer.finish(), b'number=123456')
378385

379386
writer = self.create_writer()
380387
writer.resize(len(b'number'), b'number')
381388
writer.resize(len(b'number='), b'=')
382389
writer.resize(len(b'number=123'), b'123')
383390
writer.resize(len(b'number=123456'), b'456')
384-
self.assertEqual(writer.finish(), self.result_type(b'number=123456'))
391+
self.bytes_equal(writer.finish(), b'number=123456')
385392

386393
def test_format_i(self):
387394
# Test PyBytesWriter_Format()
388395
writer = self.create_writer()
389396
writer.format_i(b'x=%i', 123456)
390-
self.assertEqual(writer.finish(), self.result_type(b'x=123456'))
397+
self.bytes_equal(writer.finish(), b'x=123456')
391398

392399
writer = self.create_writer()
393400
writer.format_i(b'x=%i, ', 123)
394401
writer.format_i(b'y=%i', 456)
395-
self.assertEqual(writer.finish(), self.result_type(b'x=123, y=456'))
402+
self.bytes_equal(writer.finish(), b'x=123, y=456')
403+
404+
def test_format_s(self):
405+
# Test PyBytesWriter_Format()
406+
writer = self.create_writer()
407+
writer.format_s(b's=%s', b'Hello World')
408+
self.bytes_equal(writer.finish(), b's=Hello World')
409+
410+
@support.nomemtest
411+
def test_format_s_memory_error(self):
412+
writer = self.create_writer()
413+
s = b'x' * 500
414+
with self.assertRaises(MemoryError):
415+
try:
416+
_testcapi.set_nomemory(0, 0)
417+
writer.format_s(b's=%s', s)
418+
finally:
419+
_testcapi.remove_mem_hooks()
420+
writer.write_bytes(b'after', -1)
421+
self.bytes_equal(writer.finish(), b'after')
396422

397423
def test_example_abc(self):
398424
self.assertEqual(_testcapi.byteswriter_abc(), b'abc')
@@ -405,7 +431,7 @@ def test_example_highlevel(self):
405431

406432

407433
class ByteArrayWriterTest(BytesWriterTest):
408-
result_type = bytearray
434+
RESULT_TYPE = bytearray
409435

410436
def create_writer(self, alloc=0, string=b''):
411437
return _testcapi.PyBytesWriter(alloc, string, 1)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :c:func:`PyBytesWriter_Format` error handling: restore the writer to its
2+
previous state on error. Patch by Victor Stinner.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :c:func:`PyBytesWriter_FinishWithSize` to return an empty bytearray when
2+
:c:func:`!_PyBytesWriter_CreateByteArray` is used, instead of returning an
3+
empty bytes string. Patch by Victor Stinner.

‎Modules/_testcapi/bytes.c‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,27 @@ writer_format_i(PyObject *self_raw, PyObject *args)
183183
}
184184

185185

186+
static PyObject*
187+
writer_format_s(PyObject *self_raw, PyObject *args)
188+
{
189+
WriterObject *self = (WriterObject *)self_raw;
190+
if (writer_check(self) < 0) {
191+
return NULL;
192+
}
193+
194+
char *format, *str;
195+
if (!PyArg_ParseTuple(args, "yy", &format, &str)) {
196+
return NULL;
197+
}
198+
199+
if (PyBytesWriter_Format(self->writer, format, str) < 0) {
200+
return NULL;
201+
}
202+
Py_RETURN_NONE;
203+
}
204+
205+
206+
// PyBytesWriter_Resize
186207
static PyObject*
187208
writer_resize(PyObject *self_raw, PyObject *args)
188209
{
@@ -262,6 +283,7 @@ writer_finish_with_size(PyObject *self_raw, PyObject *args)
262283
static PyMethodDef writer_methods[] = {
263284
{"write_bytes", _PyCFunction_CAST(writer_write_bytes), METH_VARARGS},
264285
{"format_i", _PyCFunction_CAST(writer_format_i), METH_VARARGS},
286+
{"format_s", _PyCFunction_CAST(writer_format_s), METH_VARARGS},
265287
{"resize", _PyCFunction_CAST(writer_resize), METH_VARARGS},
266288
{"get_size", _PyCFunction_CAST(writer_get_size), METH_NOARGS},
267289
{"finish", _PyCFunction_CAST(writer_finish), METH_NOARGS},

‎Objects/bytesobject.c‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ bytes_fromformat(PyBytesWriter *writer, Py_ssize_t writer_pos,
218218
size_t len = (len_expr); \
219219
s = PyBytesWriter_GrowAndUpdatePointer(writer, len, s); \
220220
if (s == NULL) { \
221-
goto error; \
221+
return NULL; \
222222
} \
223223
memcpy(s, (str), len); \
224224
s += len; \
@@ -272,7 +272,7 @@ bytes_fromformat(PyBytesWriter *writer, Py_ssize_t writer_pos,
272272
PyErr_SetString(PyExc_OverflowError,
273273
"PyBytes_FromFormatV(): %c format "
274274
"expects an integer in range [0; 255]");
275-
goto error;
275+
return NULL;
276276
}
277277
*s++ = (unsigned char)c;
278278
break;
@@ -365,9 +365,6 @@ bytes_fromformat(PyBytesWriter *writer, Py_ssize_t writer_pos,
365365
#undef WRITE_BYTES_LEN
366366

367367
return s;
368-
369-
error:
370-
return NULL;
371368
}
372369

373370

@@ -3746,7 +3743,7 @@ PyObject*
37463743
PyBytesWriter_FinishWithSize(PyBytesWriter *writer, Py_ssize_t size)
37473744
{
37483745
PyObject *result;
3749-
if (size == 0) {
3746+
if (size == 0 && !writer->use_bytearray) {
37503747
result = bytes_get_empty();
37513748
}
37523749
else if (writer->obj != NULL) {
@@ -3910,6 +3907,7 @@ int
39103907
PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...)
39113908
{
39123909
Py_ssize_t pos = writer->size;
3910+
Py_ssize_t old_pos = pos;
39133911
if (PyBytesWriter_Grow(writer, strlen(format)) < 0) {
39143912
return -1;
39153913
}
@@ -3919,6 +3917,12 @@ PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...)
39193917
char *buf = bytes_fromformat(writer, pos, format, vargs);
39203918
va_end(vargs);
39213919

3920+
if (buf == NULL) {
3921+
// On error, reset the writer to its previous state (undo any write)
3922+
writer->size = old_pos;
3923+
return -1;
3924+
}
3925+
39223926
Py_ssize_t size = buf - byteswriter_data(writer);
39233927
return PyBytesWriter_Resize(writer, size);
39243928
}

0 commit comments

Comments
 (0)