Skip to content

Commit 190d2ff

Browse files
gh-85943: Fix BytesWarning in the struct format cache under -bb (GH-153627)
Normalize bytes format strings to str before using them as the cache key, so that equal str and bytes formats no longer collide and get compared. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2931f20 commit 190d2ff

3 files changed

Lines changed: 36 additions & 4 deletions

File tree

Lib/test/test_struct.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,16 @@ def test_calcsize(self):
182182
self.assertGreaterEqual(struct.calcsize('n'), struct.calcsize('i'))
183183
self.assertGreaterEqual(struct.calcsize('n'), struct.calcsize('P'))
184184

185+
def test_cache_bytes_vs_str_bb(self):
186+
# Mixing str and bytes formats must not raise BytesWarning under -bb.
187+
code = (
188+
'import struct\n'
189+
'struct.calcsize(b"!d"); struct.calcsize("!d")\n'
190+
'struct.calcsize(">d"); struct.calcsize(b">d")\n'
191+
'struct.Struct(b"i"); struct.Struct("i")\n'
192+
)
193+
assert_python_ok('-bb', '-c', code)
194+
185195
def test_integers(self):
186196
# Integer tests (bBhHiIlLqQnN).
187197
import binascii
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :mod:`struct` functions raising :exc:`BytesWarning` under the ``-bb``
2+
command line option when a :class:`str` format is used after an equal
3+
:class:`bytes` format (or vice versa). The internal format cache no longer
4+
mixes :class:`str` and :class:`bytes` keys.

Modules/_struct.c

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2642,32 +2642,50 @@ static PyType_Spec PyStructType_spec = {
26422642
static int
26432643
cache_struct_converter(PyObject *module, PyObject *fmt, PyStructObject **ptr)
26442644
{
2645-
PyObject * s_object;
2645+
PyObject *s_object;
2646+
PyObject *key;
26462647
_structmodulestate *state = get_struct_state(module);
26472648

26482649
if (fmt == NULL) {
26492650
Py_SETREF(*ptr, NULL);
26502651
return 1;
26512652
}
26522653

2653-
if (PyDict_GetItemRef(state->cache, fmt, &s_object) < 0) {
2654+
/* Use a str cache key: an equal str and bytes would collide and be
2655+
compared, raising BytesWarning under -bb. */
2656+
if (PyBytes_Check(fmt)) {
2657+
key = PyUnicode_DecodeASCII(PyBytes_AS_STRING(fmt),
2658+
PyBytes_GET_SIZE(fmt), "surrogateescape");
2659+
if (key == NULL) {
2660+
return 0;
2661+
}
2662+
}
2663+
else {
2664+
key = Py_NewRef(fmt);
2665+
}
2666+
2667+
if (PyDict_GetItemRef(state->cache, key, &s_object) < 0) {
2668+
Py_DECREF(key);
26542669
return 0;
26552670
}
26562671
if (s_object != NULL) {
2672+
Py_DECREF(key);
26572673
*ptr = PyStructObject_CAST(s_object);
26582674
return Py_CLEANUP_SUPPORTED;
26592675
}
26602676

2661-
s_object = PyObject_CallOneArg(state->PyStructType, fmt);
2677+
s_object = PyObject_CallOneArg(state->PyStructType, key);
26622678
if (s_object != NULL) {
26632679
if (PyDict_GET_SIZE(state->cache) >= MAXCACHE)
26642680
PyDict_Clear(state->cache);
26652681
/* Attempt to cache the result */
2666-
if (PyDict_SetItem(state->cache, fmt, s_object) == -1)
2682+
if (PyDict_SetItem(state->cache, key, s_object) == -1)
26672683
PyErr_Clear();
2684+
Py_DECREF(key);
26682685
*ptr = (PyStructObject *)s_object;
26692686
return Py_CLEANUP_SUPPORTED;
26702687
}
2688+
Py_DECREF(key);
26712689
return 0;
26722690
}
26732691

0 commit comments

Comments
 (0)