Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Lib/test/test_sqlite3/test_dbapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
34 changes: 21 additions & 13 deletions crates/stdlib/src/_sqlite3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -2551,28 +2552,35 @@ mod _sqlite3 {
value: Option<PyObjectRef>,
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::<PyInt>() 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::<PyInt>() else {
return Err(vm.new_type_error(format!(
"'{}' object cannot be interpreted as an integer",
value.class()
)));
};
let value = value.try_to_primitive::<u8>(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::<PySlice>() {
// 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
Expand Down Expand Up @@ -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)
}
Expand Down
16 changes: 16 additions & 0 deletions extra_tests/snippets/stdlib_sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
youknowone marked this conversation as resolved.
Resolved
import sys

if sys.implementation.name == "rustpython":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we usually don't add rustpython-only code. is this CPython bug? Then is this reported to CPython?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the negative-step slice crash is a CPython bug — I've filed it upstream as python/cpython#150449 and submitted a fix PR (python/cpython#150450).

However, this PR also contains a few pre-existing RustPython-only bugs unrelated to that CPython issue:

  • blob[i] = v was using try_to_primitive::<u8> which silently gave the wrong error for out-of-range values (e.g. 256 or -1), instead of CPython's ValueError: byte must be in range(0, 256)
  • The item-deletion vs. slice-deletion error messages were swapped

Should I split this into two PRs — one for the CPython-mirrored negative-step fix (which could be reverted or marked TODO: RUSTPYTHON until upstream lands), and one for the RustPython-specific behavior alignment?

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()
Loading