diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 73b40e82a96811f..fd996d5824dca9a 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1390,6 +1390,16 @@ def test_blob_get_slice_negative_index(self): def test_blob_get_slice_with_skip(self): self.assertEqual(self.blob[0:10:2], b"ti lb") + def test_blob_get_slice_with_negative_step(self): + # gh-150449: negative-step slices must not crash + self.assertEqual(self.blob[9:0:-2], self.data[9:0:-2]) + self.assertEqual(self.blob[9::-2], self.data[9::-2]) + self.assertEqual(self.blob[::-1], self.data[::-1]) + # When start <= stop with a negative step the slice is empty; this + # must return b"" rather than crashing or raising an exception. + self.assertEqual(self.blob[3:8:-1], self.data[3:8:-1]) # b"" + self.assertEqual(self.blob[5:5:-1], self.data[5:5:-1]) # b"" + def test_blob_set_slice(self): self.blob[0:5] = b"12345" expected = b"12345" + self.data[5:] @@ -1406,6 +1416,30 @@ def test_blob_set_slice_with_skip(self): expected = b"1h2s3b4o5 " + self.data[10:] self.assertEqual(actual, expected) + def test_blob_set_slice_with_negative_step(self): + # gh-150449: negative-step slice assignment must not crash + expected = bytearray(self.data) + expected[9:0:-2] = b"12345" + self.blob[9:0:-2] = b"12345" + actual = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual, bytes(expected)) + + # Also verify a slice that includes index 0 + expected2 = bytearray(self.data) + expected2[9::-2] = b"12345" + self.blob[9::-2] = b"12345" + actual2 = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual2, bytes(expected2)) + + # When start <= stop with a negative step the slice is empty; + # assigning b"" to it must be a no-op (blob contents unchanged). + state_before = bytes(self.blob[:]) + self.blob[3:8:-1] = b"" + self.assertEqual(bytes(self.blob[:]), state_before) + # Assigning a non-empty sequence to an empty slice must raise. + with self.assertRaisesRegex(IndexError, "wrong size"): + self.blob[3:8:-1] = b"abc" + def test_blob_mapping_invalid_index_type(self): msg = "indices must be integers" with self.assertRaisesRegex(TypeError, msg): diff --git a/Misc/NEWS.d/next/Library/2026-05-26-15-53-50.gh-issue-150449.GfDWxl.rst b/Misc/NEWS.d/next/Library/2026-05-26-15-53-50.gh-issue-150449.GfDWxl.rst new file mode 100644 index 000000000000000..d5f3227844fbbdf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-26-15-53-50.gh-issue-150449.GfDWxl.rst @@ -0,0 +1,3 @@ +Fix :class:`sqlite3.Blob` raising :exc:`SystemError` (or :exc:`ValueError` +on recent versions) when reading or writing with a negative-step slice such +as ``blob[9:0:-2]``. diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c index 2cc62751054278f..3f7ef23a529ce5a 100644 --- a/Modules/_sqlite/blob.c +++ b/Modules/_sqlite/blob.c @@ -445,7 +445,14 @@ subscript_slice(pysqlite_Blob *self, PyObject *item) return read_multiple(self, len, start); } - PyObject *blob = read_multiple(self, stop - start, start); + // Compute the contiguous blob region covering all slice elements, then + // copy each element using the standard size_t-cursor pattern that handles + // both positive and negative steps via unsigned arithmetic. + Py_ssize_t last = start + (len - 1) * step; + Py_ssize_t read_offset = Py_MIN(start, last); + Py_ssize_t read_length = Py_ABS(start - last) + 1; + + PyObject *blob = read_multiple(self, read_length, read_offset); if (blob == NULL) { return NULL; } @@ -456,10 +463,12 @@ subscript_slice(pysqlite_Blob *self, PyObject *item) return NULL; } char *res_buf = PyBytesWriter_GetData(writer); - char *blob_buf = PyBytes_AS_STRING(blob); - for (Py_ssize_t i = 0, j = 0; i < len; i++, j += step) { - res_buf[i] = blob_buf[j]; + + size_t cur; + Py_ssize_t i; + for (cur = (size_t)start, i = 0; i < len; cur += (size_t)step, i++) { + res_buf[i] = blob_buf[(Py_ssize_t)cur - read_offset]; } Py_DECREF(blob); return PyBytesWriter_Finish(writer); @@ -531,31 +540,48 @@ ass_subscript_slice(pysqlite_Blob *self, PyObject *item, PyObject *value) return -1; } - if (len == 0) { - return 0; - } - Py_buffer vbuf; if (PyObject_GetBuffer(value, &vbuf, PyBUF_SIMPLE) < 0) { return -1; } - int rc = -1; + // For extended slices the right-hand side must have the exact same + // element count as the slice, even when that count is zero. if (vbuf.len != len) { PyErr_SetString(PyExc_IndexError, "Blob slice assignment is wrong size"); + PyBuffer_Release(&vbuf); + return -1; } - else if (step == 1) { + + if (len == 0) { + PyBuffer_Release(&vbuf); + return 0; + } + + int rc; + if (step == 1) { rc = inner_write(self, vbuf.buf, len, start); } else { - PyObject *blob_bytes = read_multiple(self, stop - start, start); + rc = -1; + // Compute the contiguous blob region covering all slice elements, then + // update each element using the standard size_t-cursor pattern that + // handles both positive and negative steps via unsigned arithmetic. + Py_ssize_t last = start + (len - 1) * step; + Py_ssize_t read_offset = Py_MIN(start, last); + Py_ssize_t read_length = Py_ABS(start - last) + 1; + PyObject *blob_bytes = read_multiple(self, read_length, read_offset); if (blob_bytes != NULL) { char *blob_buf = PyBytes_AS_STRING(blob_bytes); - for (Py_ssize_t i = 0, j = 0; i < len; i++, j += step) { - blob_buf[j] = ((char *)vbuf.buf)[i]; + size_t cur; + Py_ssize_t i; + for (cur = (size_t)start, i = 0; i < len; + cur += (size_t)step, i++) { + blob_buf[(Py_ssize_t)cur - read_offset] = + ((char *)vbuf.buf)[i]; } - rc = inner_write(self, blob_buf, stop - start, start); + rc = inner_write(self, blob_buf, read_length, read_offset); Py_DECREF(blob_bytes); } }