Skip to content

Commit 08a81df

Browse files
bpo-33817: Fix _PyString_Resize() and _PyUnicode_Resize() for empty strings. (GH-11515)
1 parent 0167c08 commit 08a81df

File tree

5 files changed

+147
-4
lines changed

5 files changed

+147
-4
lines changed

Lib/test/test_str.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,8 +474,106 @@ def __rmod__(self, other):
474474
self.assertEqual('lhs %% %r' % SubclassedStr('rhs'),
475475
"Success, self.__rmod__('lhs %% %r') was called")
476476

477+
478+
class CAPITest(unittest.TestCase):
479+
480+
# Test PyString_FromFormat()
481+
def test_from_format(self):
482+
ctypes = test_support.import_module('ctypes')
483+
_testcapi = test_support.import_module('_testcapi')
484+
from ctypes import pythonapi, py_object
485+
from ctypes import (
486+
c_int, c_uint,
487+
c_long, c_ulong,
488+
c_size_t, c_ssize_t,
489+
c_char_p)
490+
491+
PyString_FromFormat = pythonapi.PyString_FromFormat
492+
PyString_FromFormat.restype = py_object
493+
494+
# basic tests
495+
self.assertEqual(PyString_FromFormat(b'format'),
496+
b'format')
497+
self.assertEqual(PyString_FromFormat(b'Hello %s !', b'world'),
498+
b'Hello world !')
499+
500+
# test formatters
501+
self.assertEqual(PyString_FromFormat(b'c=%c', c_int(0)),
502+
b'c=\0')
503+
self.assertEqual(PyString_FromFormat(b'c=%c', c_int(ord('@'))),
504+
b'c=@')
505+
self.assertEqual(PyString_FromFormat(b'c=%c', c_int(255)),
506+
b'c=\xff')
507+
self.assertEqual(PyString_FromFormat(b'd=%d ld=%ld zd=%zd',
508+
c_int(1), c_long(2),
509+
c_size_t(3)),
510+
b'd=1 ld=2 zd=3')
511+
self.assertEqual(PyString_FromFormat(b'd=%d ld=%ld zd=%zd',
512+
c_int(-1), c_long(-2),
513+
c_size_t(-3)),
514+
b'd=-1 ld=-2 zd=-3')
515+
self.assertEqual(PyString_FromFormat(b'u=%u lu=%lu zu=%zu',
516+
c_uint(123), c_ulong(456),
517+
c_size_t(789)),
518+
b'u=123 lu=456 zu=789')
519+
self.assertEqual(PyString_FromFormat(b'i=%i', c_int(123)),
520+
b'i=123')
521+
self.assertEqual(PyString_FromFormat(b'i=%i', c_int(-123)),
522+
b'i=-123')
523+
self.assertEqual(PyString_FromFormat(b'x=%x', c_int(0xabc)),
524+
b'x=abc')
525+
526+
self.assertEqual(PyString_FromFormat(b's=%s', c_char_p(b'cstr')),
527+
b's=cstr')
528+
529+
# test minimum and maximum integer values
530+
size_max = c_size_t(-1).value
531+
for formatstr, ctypes_type, value, py_formatter in (
532+
(b'%d', c_int, _testcapi.INT_MIN, str),
533+
(b'%d', c_int, _testcapi.INT_MAX, str),
534+
(b'%ld', c_long, _testcapi.LONG_MIN, str),
535+
(b'%ld', c_long, _testcapi.LONG_MAX, str),
536+
(b'%lu', c_ulong, _testcapi.ULONG_MAX, str),
537+
(b'%zd', c_ssize_t, _testcapi.PY_SSIZE_T_MIN, str),
538+
(b'%zd', c_ssize_t, _testcapi.PY_SSIZE_T_MAX, str),
539+
(b'%zu', c_size_t, size_max, str),
540+
):
541+
self.assertEqual(PyString_FromFormat(formatstr, ctypes_type(value)),
542+
py_formatter(value).encode('ascii')),
543+
544+
# width and precision (width is currently ignored)
545+
self.assertEqual(PyString_FromFormat(b'%5s', b'a'),
546+
b'a')
547+
self.assertEqual(PyString_FromFormat(b'%.3s', b'abcdef'),
548+
b'abc')
549+
550+
# '%%' formatter
551+
self.assertEqual(PyString_FromFormat(b'%%'),
552+
b'%')
553+
self.assertEqual(PyString_FromFormat(b'[%%]'),
554+
b'[%]')
555+
self.assertEqual(PyString_FromFormat(b'%%%c', c_int(ord('_'))),
556+
b'%_')
557+
self.assertEqual(PyString_FromFormat(b'%%s'),
558+
b'%s')
559+
560+
# Invalid formats and partial formatting
561+
self.assertEqual(PyString_FromFormat(b'%'), b'%')
562+
self.assertEqual(PyString_FromFormat(b'x=%i y=%', c_int(2), c_int(3)),
563+
b'x=2 y=%')
564+
565+
self.assertEqual(PyString_FromFormat(b'%c', c_int(-1)), b'\xff')
566+
self.assertEqual(PyString_FromFormat(b'%c', c_int(256)), b'\0')
567+
568+
# Issue #33817: empty strings
569+
self.assertEqual(PyString_FromFormat(b''),
570+
b'')
571+
self.assertEqual(PyString_FromFormat(b'%s', b''),
572+
b'')
573+
574+
477575
def test_main():
478-
test_support.run_unittest(StrTest)
576+
test_support.run_unittest(StrTest, CAPITest)
479577

480578
if __name__ == "__main__":
481579
test_main()

Lib/test/test_unicode.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,6 +1824,12 @@ def check_format(expected, format, *args):
18241824
check_format(u'%s',
18251825
b'%.%s', b'abc')
18261826

1827+
# Issue #33817: empty strings
1828+
check_format(u'',
1829+
b'')
1830+
check_format(u'',
1831+
b'%s', b'')
1832+
18271833
@test_support.cpython_only
18281834
def test_encode_decimal(self):
18291835
from _testcapi import unicode_encodedecimal
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fixed :c:func:`_PyString_Resize` and :c:func:`_PyUnicode_Resize` for empty
2+
strings. This fixed also :c:func:`PyString_FromFormat` and
3+
:c:func:`PyUnicode_FromFormat` when they return an empty string (e.g.
4+
``PyString_FromFormat("%s", "")``).

Objects/stringobject.c

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3893,13 +3893,31 @@ _PyString_Resize(PyObject **pv, Py_ssize_t newsize)
38933893
register PyObject *v;
38943894
register PyStringObject *sv;
38953895
v = *pv;
3896-
if (!PyString_Check(v) || Py_REFCNT(v) != 1 || newsize < 0 ||
3897-
PyString_CHECK_INTERNED(v)) {
3896+
if (!PyString_Check(v) || newsize < 0) {
38983897
*pv = 0;
38993898
Py_DECREF(v);
39003899
PyErr_BadInternalCall();
39013900
return -1;
39023901
}
3902+
if (Py_SIZE(v) == 0) {
3903+
if (newsize == 0) {
3904+
return 0;
3905+
}
3906+
*pv = PyString_FromStringAndSize(NULL, newsize);
3907+
Py_DECREF(v);
3908+
return (*pv == NULL) ? -1 : 0;
3909+
}
3910+
if (Py_REFCNT(v) != 1 || PyString_CHECK_INTERNED(v)) {
3911+
*pv = 0;
3912+
Py_DECREF(v);
3913+
PyErr_BadInternalCall();
3914+
return -1;
3915+
}
3916+
if (newsize == 0) {
3917+
*pv = PyString_FromStringAndSize(NULL, 0);
3918+
Py_DECREF(v);
3919+
return (*pv == NULL) ? -1 : 0;
3920+
}
39033921
/* XXX UNREF/NEWREF interface should be more symmetrical */
39043922
_Py_DEC_REFTOTAL;
39053923
_Py_ForgetReference(v);

Objects/unicodeobject.c

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,10 +421,27 @@ int _PyUnicode_Resize(PyUnicodeObject **unicode, Py_ssize_t length)
421421
return -1;
422422
}
423423
v = *unicode;
424-
if (v == NULL || !PyUnicode_Check(v) || Py_REFCNT(v) != 1 || length < 0) {
424+
if (v == NULL || !PyUnicode_Check(v) || length < 0) {
425425
PyErr_BadInternalCall();
426426
return -1;
427427
}
428+
if (v->length == 0) {
429+
if (length == 0) {
430+
return 0;
431+
}
432+
*unicode = _PyUnicode_New(length);
433+
Py_DECREF(v);
434+
return (*unicode == NULL) ? -1 : 0;
435+
}
436+
if (Py_REFCNT(v) != 1) {
437+
PyErr_BadInternalCall();
438+
return -1;
439+
}
440+
if (length == 0) {
441+
*unicode = _PyUnicode_New(0);
442+
Py_DECREF(v);
443+
return (*unicode == NULL) ? -1 : 0;
444+
}
428445

429446
/* Resizing unicode_empty and single character objects is not
430447
possible since these are being shared. We simply return a fresh

0 commit comments

Comments
 (0)