Skip to content

Commit c72ea53

Browse files
gh-150449: Support negative steps in sqlite3.Blob slices (GH-150450)
Reading or writing a slice with a negative step computed a negative length for sqlite3_blob_read() and sqlite3_blob_write(), so it failed instead of returning or storing the selected bytes. Compute the contiguous region which covers all selected bytes, and index it with a size_t cursor, so that a step of any sign and magnitude works. Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent 7385f48 commit c72ea53

5 files changed

Lines changed: 98 additions & 22 deletions

File tree

Doc/library/sqlite3.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,6 +1760,10 @@ Blob objects
17601760

17611761
.. versionadded:: 3.11
17621762

1763+
.. versionchanged:: next
1764+
:class:`Blob` now supports negative-step slices
1765+
(e.g. ``blob[9:0:-2]``) for both reading and writing.
1766+
17631767
A :class:`Blob` instance is a :term:`file-like object`
17641768
that can read and write data in an SQLite :abbr:`BLOB (Binary Large OBject)`.
17651769
Call :func:`len(blob) <len>` to get the size (number of bytes) of the blob.

Doc/whatsnew/3.16.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,13 @@ shlex
466466
a string, even if it is already safe for a shell without being quoted.
467467
(Contributed by Jay Berry in :gh:`148846`.)
468468

469+
sqlite3
470+
-------
471+
472+
* :class:`sqlite3.Blob` now supports negative-step slices for reading and
473+
writing (e.g. ``blob[9:0:-2]``). Previously, such slices would raise
474+
:exc:`SystemError` or :exc:`ValueError`.
475+
(Contributed by Jiseok CHOI in :gh:`150449`.)
469476

470477
symtable
471478
--------

Lib/test/test_sqlite3/test_dbapi.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,19 @@ def test_blob_get_slice_negative_index(self):
13901390
def test_blob_get_slice_with_skip(self):
13911391
self.assertEqual(self.blob[0:10:2], b"ti lb")
13921392

1393+
def test_blob_get_slice_with_negative_step(self):
1394+
# gh-150449: negative-step slices must not crash
1395+
self.assertEqual(self.blob[9:0:-2], self.data[9:0:-2])
1396+
self.assertEqual(self.blob[9::-2], self.data[9::-2])
1397+
self.assertEqual(self.blob[::-1], self.data[::-1])
1398+
# When start <= stop with a negative step the slice is empty; this
1399+
# must return b"" rather than crashing or raising an exception.
1400+
self.assertEqual(self.blob[3:8:-1], self.data[3:8:-1]) # b""
1401+
self.assertEqual(self.blob[5:5:-1], self.data[5:5:-1]) # b""
1402+
# Extreme step values: cur += (size_t)step must not overflow.
1403+
self.assertEqual(self.blob[5::sys.maxsize], self.data[5::sys.maxsize])
1404+
self.assertEqual(self.blob[::-sys.maxsize - 1], self.data[::-sys.maxsize - 1])
1405+
13931406
def test_blob_set_slice(self):
13941407
self.blob[0:5] = b"12345"
13951408
expected = b"12345" + self.data[5:]
@@ -1430,6 +1443,43 @@ def test_blob_set_slice_with_skip(self):
14301443
expected = b"1h2s3b4o5 " + self.data[10:]
14311444
self.assertEqual(actual, expected)
14321445

1446+
def test_blob_set_slice_with_negative_step(self):
1447+
# gh-150449: negative-step slice assignment must not crash
1448+
expected = bytearray(self.data)
1449+
expected[9:0:-2] = b"12345"
1450+
self.blob[9:0:-2] = b"12345"
1451+
actual = self.cx.execute("select b from test").fetchone()[0]
1452+
self.assertEqual(actual, bytes(expected))
1453+
1454+
# Also verify a slice that includes index 0
1455+
expected2 = bytearray(self.data)
1456+
expected2[9::-2] = b"12345"
1457+
self.blob[9::-2] = b"12345"
1458+
actual2 = self.cx.execute("select b from test").fetchone()[0]
1459+
self.assertEqual(actual2, bytes(expected2))
1460+
1461+
# When start <= stop with a negative step the slice is empty;
1462+
# assigning b"" to it must be a no-op (blob contents unchanged).
1463+
state_before = bytes(self.blob[:])
1464+
self.blob[3:8:-1] = b""
1465+
self.assertEqual(bytes(self.blob[:]), state_before)
1466+
1467+
def test_blob_set_slice_with_extreme_positive_step(self):
1468+
expected = bytearray(self.data)
1469+
expected[5::sys.maxsize] = b"\xab"
1470+
self.blob[5::sys.maxsize] = b"\xab"
1471+
actual = self.cx.execute("select b from test").fetchone()[0]
1472+
self.assertEqual(actual, bytes(expected))
1473+
self.assertEqual(actual[5], 0xab)
1474+
1475+
def test_blob_set_slice_with_extreme_negative_step(self):
1476+
expected = bytearray(self.data)
1477+
expected[::-sys.maxsize - 1] = b"\xcd"
1478+
self.blob[::-sys.maxsize - 1] = b"\xcd"
1479+
actual = self.cx.execute("select b from test").fetchone()[0]
1480+
self.assertEqual(actual, bytes(expected))
1481+
self.assertEqual(actual[-1], 0xcd)
1482+
14331483
def test_blob_mapping_invalid_index_type(self):
14341484
msg = "indices must be integers"
14351485
with self.assertRaisesRegex(TypeError, msg):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:class:`sqlite3.Blob` now supports negative-step slices for reading and
2+
writing (e.g. ``blob[9:0:-2]``). Previously, such slices would raise
3+
:exc:`SystemError` or :exc:`ValueError`.

Modules/_sqlite/blob.c

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,14 @@ subscript_slice(pysqlite_Blob *self, PyObject *item)
454454
return read_multiple(self, len, start);
455455
}
456456

457-
PyObject *blob = read_multiple(self, stop - start, start);
457+
// Compute the contiguous blob region covering all slice elements, then
458+
// copy each element using the standard size_t-cursor pattern that handles
459+
// both positive and negative steps via unsigned arithmetic.
460+
Py_ssize_t last = start + (len - 1) * step;
461+
Py_ssize_t read_offset = Py_MIN(start, last);
462+
Py_ssize_t read_length = Py_ABS(start - last) + 1;
463+
464+
PyObject *blob = read_multiple(self, read_length, read_offset);
458465
if (blob == NULL) {
459466
return NULL;
460467
}
@@ -465,10 +472,12 @@ subscript_slice(pysqlite_Blob *self, PyObject *item)
465472
return NULL;
466473
}
467474
char *res_buf = PyBytesWriter_GetData(writer);
468-
469475
char *blob_buf = PyBytes_AS_STRING(blob);
470-
for (Py_ssize_t i = 0, j = 0; i < len; i++, j += step) {
471-
res_buf[i] = blob_buf[j];
476+
477+
size_t cur;
478+
Py_ssize_t i;
479+
for (cur = (size_t)start, i = 0; i < len; cur += (size_t)step, i++) {
480+
res_buf[i] = blob_buf[(Py_ssize_t)cur - read_offset];
472481
}
473482
Py_DECREF(blob);
474483
return PyBytesWriter_Finish(writer);
@@ -562,28 +571,31 @@ ass_subscript_slice(pysqlite_Blob *self, PyObject *item, PyObject *value)
562571
rc = inner_write(self, vbuf.buf, len, start);
563572
}
564573
else {
565-
/* Read the affected region, patch it and write it back. The
566-
object returned by read_multiple() cannot be used as the buffer,
567-
because for a single byte it is an immortal singleton. */
568-
Py_ssize_t length = stop - start;
569-
if (length <= 0) {
570-
/* start > stop for a negative step; see gh-150449. */
571-
PyErr_SetString(PyExc_ValueError, "size must be >= 0");
574+
/* Compute the contiguous blob region covering all slice elements,
575+
read it, patch each element and write it back. The object
576+
returned by read_multiple() cannot be used as the buffer, because
577+
for a single byte it is an immortal singleton. */
578+
Py_ssize_t last = start + (len - 1) * step;
579+
Py_ssize_t write_offset = Py_MIN(start, last);
580+
Py_ssize_t write_length = Py_ABS(start - last) + 1;
581+
char *buf = PyMem_Malloc(write_length);
582+
if (buf == NULL) {
583+
PyErr_NoMemory();
572584
}
573585
else {
574-
char *buf = PyMem_Malloc(length);
575-
if (buf == NULL) {
576-
PyErr_NoMemory();
577-
}
578-
else {
579-
if (inner_read(self, buf, length, start) == 0) {
580-
for (Py_ssize_t i = 0, j = 0; i < len; i++, j += step) {
581-
buf[j] = ((char *)vbuf.buf)[i];
582-
}
583-
rc = inner_write(self, buf, length, start);
586+
if (inner_read(self, buf, write_length, write_offset) == 0) {
587+
/* The size_t cursor handles both positive and negative steps
588+
via unsigned arithmetic. */
589+
size_t cur;
590+
Py_ssize_t i;
591+
for (cur = (size_t)start, i = 0; i < len;
592+
cur += (size_t)step, i++) {
593+
buf[(Py_ssize_t)cur - write_offset] =
594+
((char *)vbuf.buf)[i];
584595
}
585-
PyMem_Free(buf);
596+
rc = inner_write(self, buf, write_length, write_offset);
586597
}
598+
PyMem_Free(buf);
587599
}
588600
}
589601
PyBuffer_Release(&vbuf);

0 commit comments

Comments
 (0)