diff --git a/crates/capi/src/bytearrayobject.rs b/crates/capi/src/bytearrayobject.rs index cc9db5dd5..a24c2890c 100644 --- a/crates/capi/src/bytearrayobject.rs +++ b/crates/capi/src/bytearrayobject.rs @@ -3,7 +3,7 @@ use crate::object::define_py_check; use crate::pystate::with_vm; use core::ffi::c_char; use rustpython_vm::builtins::PyByteArray; -use rustpython_vm::byte::bytes_from_object; +use rustpython_vm::byte::bytearray_from_object; define_py_check!(fn PyByteArray_Check, types.bytearray_type); @@ -41,7 +41,7 @@ pub unsafe extern "C" fn PyByteArray_FromStringAndSize( pub unsafe extern "C" fn PyByteArray_FromObject(obj: *mut PyObject) -> *mut PyObject { with_vm(|vm| { let obj = unsafe { &*obj }; - let data = bytes_from_object(vm, obj)?; + let data = bytearray_from_object(vm, obj)?; Ok(vm.ctx.new_bytearray(data)) }) } diff --git a/crates/vm/src/byte.rs b/crates/vm/src/byte.rs index b22fb54fa..d6ed983e8 100644 --- a/crates/vm/src/byte.rs +++ b/crates/vm/src/byte.rs @@ -7,9 +7,15 @@ use crate::{ protocol::{BufferFlags, PyBuffer}, }; +/// The element error `bytes` reports, which is the one entry point here that does +/// not name a single byte: `bytes([256])` says "bytes must be in range(0, 256)" +/// where every `bytearray` path says "byte". +const BYTES_ELEMENT_ERROR: &str = "bytes must be in range(0, 256)"; +const BYTE_ELEMENT_ERROR: &str = "byte must be in range(0, 256)"; + // PyBytes_FromObject pub fn bytes_from_object(vm: &VirtualMachine, obj: &PyObject) -> PyResult> { - collect_bytes(vm, obj, true, |name| { + collect_bytes(vm, obj, true, BYTES_ELEMENT_ERROR, |name| { format!("cannot convert '{name}' object to bytes") }) } @@ -18,7 +24,7 @@ pub fn bytes_from_object(vm: &VirtualMachine, obj: &PyObject) -> PyResult PyResult> { - collect_bytes(vm, obj, false, |name| { + collect_bytes(vm, obj, false, BYTE_ELEMENT_ERROR, |name| { format!("cannot convert '{name}' object to bytearray") }) } @@ -26,17 +32,19 @@ pub fn bytearray_from_object(vm: &VirtualMachine, obj: &PyObject) -> PyResult PyResult> { - collect_bytes(vm, obj, true, |name| { + collect_bytes(vm, obj, true, BYTE_ELEMENT_ERROR, |name| { format!("can't extend bytearray with {name}") }) } -/// `measured` is whether the object is asked how long it is; `unusable` names, -/// from the class name, what could not be done with one that is not iterable. +/// `measured` is whether the object is asked how long it is; `element` is the error +/// for a value outside `range(0, 256)`; `unusable` names, from the class name, what +/// could not be done with one that is not iterable. fn collect_bytes( vm: &VirtualMachine, obj: &PyObject, measured: bool, + element: &'static str, unusable: impl FnOnce(&str) -> String, ) -> PyResult> { if obj.check_buffer() { @@ -52,7 +60,7 @@ fn collect_bytes( if cls.slots.iter.load().is_none() && !cls.has_attr(identifier!(vm, __getitem__)) { return Err(vm.new_type_error(unusable(&cls.name()))); } - let value = |x: PyObjectRef| value_from_object(vm, &x); + let value = |x: PyObjectRef| value_from_object_with(vm, &x, element); let elements = if measured { vm.map_iterable_object_sized(obj, value) } else { @@ -67,8 +75,16 @@ fn collect_bytes( } pub fn value_from_object(vm: &VirtualMachine, obj: &PyObject) -> PyResult { + value_from_object_with(vm, obj, BYTE_ELEMENT_ERROR) +} + +fn value_from_object_with( + vm: &VirtualMachine, + obj: &PyObject, + element: &'static str, +) -> PyResult { obj.try_index(vm)? .as_bigint() .to_u8() - .ok_or_else(|| vm.new_value_error("byte must be in range(0, 256)")) + .ok_or_else(|| vm.new_value_error(element)) } diff --git a/extra_tests/snippets/builtin_bytes.py b/extra_tests/snippets/builtin_bytes.py index 0c45fc357..eb03698db 100644 --- a/extra_tests/snippets/builtin_bytes.py +++ b/extra_tests/snippets/builtin_bytes.py @@ -813,3 +813,22 @@ cannot( lambda: bytearray(b"ab").__setitem__(slice(0, 2), "ab"), "can assign only bytes, buffers, or iterables of ints in range(0, 256)", ) + + +def out_of_range(fn, message): + try: + fn() + except ValueError as e: + assert str(e) == message, e + else: + raise AssertionError(f"expected ValueError: {message}") + + +# `bytes` is the one entry point that does not name a single byte, and both the +# sized and unsized iterator paths report it that way. +out_of_range(lambda: bytes([256]), "bytes must be in range(0, 256)") +out_of_range(lambda: bytes(iter([256])), "bytes must be in range(0, 256)") +out_of_range(lambda: bytes([-1]), "bytes must be in range(0, 256)") +out_of_range(lambda: bytearray([256]), "byte must be in range(0, 256)") +out_of_range(lambda: bytearray(iter([256])), "byte must be in range(0, 256)") +out_of_range(lambda: bytearray().extend([256]), "byte must be in range(0, 256)")