Skip to content

Commit ce79cd4

Browse files
ever0deyouknowone
andauthored
sqlite3: fix Blob.__setitem__ value range validation (RustPython#7981)
* sqlite3: fix Blob.__setitem__ value range validation Previously, assigning an out-of-range integer (negative or > 255) or an integer too large for i64 (e.g. 2**65) to a Blob index raised OverflowError instead of ValueError. Mirror CPython's ass_subscript_index logic: - Convert the value via to_i64(), treating any overflow as -1 - Validate the result is in [0, 255], raising ValueError("byte must be in range(0, 256)") otherwise - Separate deletion error messages: "item deletion" for index, "slice deletion" for slice * sqlite3: fix Blob.__setitem__ negative-step slice write In the step != 1 branch of Blob.ass_subscript, the loop used i_in_temp += step as usize where step is isize. For negative steps (e.g. step = -2), (-2isize) as usize = 18446744073709551614 causing an out-of-bounds panic whenever slice_len >= 2. Fix: use SaturatedSliceIter (already used by the read path) to iterate over the correct absolute blob indices, then map each index back to a temp buffer offset via abs_idx - range_start. Also fix a Clippy lint: replace val < 0 || val > 255 with the idiomatic !(0..=255).contains(&val) Add a regression test in extra_tests/snippets/stdlib_sqlite.py that exercises blob[9:0:-2] (negative step, slice_len=5). * fix: guard blob negative-step snippet from CPython 3.11 bug * style: add blank line after import sys in stdlib_sqlite snippet (ruff) * Update extra_tests/snippets/stdlib_sqlite.py --------- Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
1 parent dcb273b commit ce79cd4

3 files changed

Lines changed: 38 additions & 14 deletions

File tree

Lib/test/test_sqlite3/test_dbapi.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1444,7 +1444,6 @@ def test_blob_get_item_error_bigint(self):
14441444
with self.assertRaisesRegex(IndexError, "cannot fit 'int'"):
14451445
self.blob[_testcapi.ULLONG_MAX]
14461446

1447-
@unittest.expectedFailure # TODO: RUSTPYTHON
14481447
def test_blob_set_item_error(self):
14491448
with self.assertRaisesRegex(TypeError, "cannot be interpreted"):
14501449
self.blob[0] = b"multiple"

crates/stdlib/src/_sqlite3.rs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ mod _sqlite3 {
4444
sqlite3_value_text, sqlite3_value_type,
4545
};
4646
use malachite_bigint::Sign;
47+
use num_traits::ToPrimitive;
4748
use rustpython_common::{
4849
atomic::{Ordering, PyAtomic, Radium},
4950
hash::PyHash,
@@ -2551,28 +2552,35 @@ mod _sqlite3 {
25512552
value: Option<PyObjectRef>,
25522553
vm: &VirtualMachine,
25532554
) -> PyResult<()> {
2554-
let Some(value) = value else {
2555-
return Err(vm.new_type_error("Blob doesn't support slice deletion"));
2556-
};
25572555
self.ensure_connection_open(vm)?;
25582556
let inner = self.inner(vm)?;
25592557

25602558
if let Some(index) = needle.try_index_opt(vm) {
25612559
// Handle single item assignment: blob[i] = b
2562-
let Some(value) = value.downcast_ref::<PyInt>() else {
2560+
let Some(value) = value else {
2561+
return Err(vm.new_type_error("Blob doesn't support item deletion"));
2562+
};
2563+
let Some(int_val) = value.downcast_ref::<PyInt>() else {
25632564
return Err(vm.new_type_error(format!(
25642565
"'{}' object cannot be interpreted as an integer",
25652566
value.class()
25662567
)));
25672568
};
2568-
let value = value.try_to_primitive::<u8>(vm)?;
25692569
let blob_len = inner.blob.bytes();
25702570
let index = Self::wrapped_index(index?, blob_len, vm)?;
2571-
Self::expect_write(blob_len, 1, index, vm)?;
2572-
let ret = inner.blob.write_single(value, index);
2571+
// Mirror CPython ass_subscript_index: use PyLong_AsLong, treat any
2572+
// overflow (e.g. 2**65) as -1, then validate the [0, 255] range.
2573+
let val = int_val.as_bigint().to_i64().unwrap_or(-1);
2574+
if !(0..=255).contains(&val) {
2575+
return Err(vm.new_value_error("byte must be in range(0, 256)"));
2576+
}
2577+
let ret = inner.blob.write_single(val as u8, index);
25732578
self.check(ret, vm)
25742579
} else if let Some(slice) = needle.downcast_ref::<PySlice>() {
25752580
// Handle slice assignment: blob[a:b:c] = b"..."
2581+
let Some(value) = value else {
2582+
return Err(vm.new_type_error("Blob doesn't support slice deletion"));
2583+
};
25762584
let value_buf = PyBuffer::try_from_borrowed_object(vm, &value)?;
25772585

25782586
let buf = value_buf
@@ -2604,25 +2612,25 @@ mod _sqlite3 {
26042612
self.check(ret, vm)
26052613
} else {
26062614
let span_len = range.end - range.start;
2615+
let range_start = range.start;
26072616
let mut temp_buf = vec![0u8; span_len];
26082617

26092618
let ret = inner.blob.read(
26102619
temp_buf.as_mut_ptr().cast(),
26112620
span_len as c_int,
2612-
range.start as c_int,
2621+
range_start as c_int,
26132622
);
26142623
self.check(ret, vm)?;
26152624

2616-
let mut i_in_temp: usize = 0;
2617-
for i_in_src in 0..slice_len {
2618-
temp_buf[i_in_temp] = buf[i_in_src];
2619-
i_in_temp += step as usize;
2625+
let iter = SaturatedSliceIter::from_adjust_indices(range, step, slice_len);
2626+
for (i_in_src, abs_idx) in iter.enumerate() {
2627+
temp_buf[abs_idx - range_start] = buf[i_in_src];
26202628
}
26212629

26222630
let ret = inner.blob.write(
26232631
temp_buf.as_ptr().cast(),
26242632
span_len as c_int,
2625-
range.start as c_int,
2633+
range_start as c_int,
26262634
);
26272635
self.check(ret, vm)
26282636
}

extra_tests/snippets/stdlib_sqlite.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,20 @@ def finalize(self):
5353
cx.create_aggregate("aggtxt", 1, AggrText)
5454
cur.execute("select aggtxt(key) from foo")
5555
assert cur.fetchone()[0] == "341011"
56+
57+
# Blob extended-slice assignment with negative step
58+
# Guard: CPython 3.11 has a SystemError bug with negative-step Blob slicing;
59+
# this test only runs on RustPython where the fix is being validated.
60+
# TODO: remove this once https://github.com/python/cpython/pull/150450 is released and RustPython CI uses it.
61+
import sys
62+
63+
if sys.implementation.name == "rustpython":
64+
cx.execute("CREATE TABLE blobtest(b BLOB)")
65+
data = b"this blob data string is exactly fifty bytes long!"
66+
cx.execute("INSERT INTO blobtest(b) VALUES (?)", (data,))
67+
blob = cx.blobopen("blobtest", "b", 1)
68+
blob[9:0:-2] = b"12345" # writes to indices 9, 7, 5, 3, 1
69+
actual = cx.execute("select b from blobtest").fetchone()[0]
70+
expected = b"t5i4 3l2b1" + data[10:]
71+
assert actual == expected, f"got {actual!r}, expected {expected!r}"
72+
blob.close()

0 commit comments

Comments
 (0)