diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 8962fe00ed8..2174e14e7cb 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1444,7 +1444,6 @@ def test_blob_get_item_error_bigint(self): with self.assertRaisesRegex(IndexError, "cannot fit 'int'"): self.blob[_testcapi.ULLONG_MAX] - @unittest.expectedFailure # TODO: RUSTPYTHON def test_blob_set_item_error(self): with self.assertRaisesRegex(TypeError, "cannot be interpreted"): self.blob[0] = b"multiple" diff --git a/crates/stdlib/src/_sqlite3.rs b/crates/stdlib/src/_sqlite3.rs index fd7fec4feb3..5dfdf6f4f1e 100644 --- a/crates/stdlib/src/_sqlite3.rs +++ b/crates/stdlib/src/_sqlite3.rs @@ -44,6 +44,7 @@ mod _sqlite3 { sqlite3_value_text, sqlite3_value_type, }; use malachite_bigint::Sign; + use num_traits::ToPrimitive; use rustpython_common::{ atomic::{Ordering, PyAtomic, Radium}, hash::PyHash, @@ -2551,28 +2552,35 @@ mod _sqlite3 { value: Option, vm: &VirtualMachine, ) -> PyResult<()> { - let Some(value) = value else { - return Err(vm.new_type_error("Blob doesn't support slice deletion")); - }; self.ensure_connection_open(vm)?; let inner = self.inner(vm)?; if let Some(index) = needle.try_index_opt(vm) { // Handle single item assignment: blob[i] = b - let Some(value) = value.downcast_ref::() else { + let Some(value) = value else { + return Err(vm.new_type_error("Blob doesn't support item deletion")); + }; + let Some(int_val) = value.downcast_ref::() else { return Err(vm.new_type_error(format!( "'{}' object cannot be interpreted as an integer", value.class() ))); }; - let value = value.try_to_primitive::(vm)?; let blob_len = inner.blob.bytes(); let index = Self::wrapped_index(index?, blob_len, vm)?; - Self::expect_write(blob_len, 1, index, vm)?; - let ret = inner.blob.write_single(value, index); + // Mirror CPython ass_subscript_index: use PyLong_AsLong, treat any + // overflow (e.g. 2**65) as -1, then validate the [0, 255] range. + let val = int_val.as_bigint().to_i64().unwrap_or(-1); + if !(0..=255).contains(&val) { + return Err(vm.new_value_error("byte must be in range(0, 256)")); + } + let ret = inner.blob.write_single(val as u8, index); self.check(ret, vm) } else if let Some(slice) = needle.downcast_ref::() { // Handle slice assignment: blob[a:b:c] = b"..." + let Some(value) = value else { + return Err(vm.new_type_error("Blob doesn't support slice deletion")); + }; let value_buf = PyBuffer::try_from_borrowed_object(vm, &value)?; let buf = value_buf @@ -2604,25 +2612,25 @@ mod _sqlite3 { self.check(ret, vm) } else { let span_len = range.end - range.start; + let range_start = range.start; let mut temp_buf = vec![0u8; span_len]; let ret = inner.blob.read( temp_buf.as_mut_ptr().cast(), span_len as c_int, - range.start as c_int, + range_start as c_int, ); self.check(ret, vm)?; - let mut i_in_temp: usize = 0; - for i_in_src in 0..slice_len { - temp_buf[i_in_temp] = buf[i_in_src]; - i_in_temp += step as usize; + let iter = SaturatedSliceIter::from_adjust_indices(range, step, slice_len); + for (i_in_src, abs_idx) in iter.enumerate() { + temp_buf[abs_idx - range_start] = buf[i_in_src]; } let ret = inner.blob.write( temp_buf.as_ptr().cast(), span_len as c_int, - range.start as c_int, + range_start as c_int, ); self.check(ret, vm) } diff --git a/extra_tests/snippets/stdlib_sqlite.py b/extra_tests/snippets/stdlib_sqlite.py index f2e02b48cf1..e23c61146f3 100644 --- a/extra_tests/snippets/stdlib_sqlite.py +++ b/extra_tests/snippets/stdlib_sqlite.py @@ -53,3 +53,19 @@ def finalize(self): cx.create_aggregate("aggtxt", 1, AggrText) cur.execute("select aggtxt(key) from foo") assert cur.fetchone()[0] == "341011" + +# Blob extended-slice assignment with negative step +# Guard: CPython 3.11 has a SystemError bug with negative-step Blob slicing; +# this test only runs on RustPython where the fix is being validated. +import sys + +if sys.implementation.name == "rustpython": + cx.execute("CREATE TABLE blobtest(b BLOB)") + data = b"this blob data string is exactly fifty bytes long!" + cx.execute("INSERT INTO blobtest(b) VALUES (?)", (data,)) + blob = cx.blobopen("blobtest", "b", 1) + blob[9:0:-2] = b"12345" # writes to indices 9, 7, 5, 3, 1 + actual = cx.execute("select b from blobtest").fetchone()[0] + expected = b"t5i4 3l2b1" + data[10:] + assert actual == expected, f"got {actual!r}, expected {expected!r}" + blob.close()