diff --git a/crates/stdlib/src/hashlib.rs b/crates/stdlib/src/hashlib.rs index f28f27b10..ceca00d0b 100644 --- a/crates/stdlib/src/hashlib.rs +++ b/crates/stdlib/src/hashlib.rs @@ -8,7 +8,7 @@ pub(crate) use _hashlib::module_def; #[pymodule] pub(crate) mod _hashlib { use crate::vm::{ - Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine, + Py, PyObject, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine, builtins::{ PyBaseExceptionRef, PyBytes, PyFrozenSet, PyStr, PyType, PyTypeRef, PyUtf8StrRef, PyValueError, @@ -67,11 +67,35 @@ pub(crate) mod _hashlib { dict.into() } + #[derive(Debug)] + struct HashName(PyUtf8StrRef); + + impl TryFromObject for HashName { + fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { + if !obj.downcastable::() { + let name = if vm.is_none(&obj) { + "None".into() + } else { + obj.class().slot_name() + }; + let mut end = name.len().min(50); + while !name.is_char_boundary(end) { + end -= 1; + } + return Err(vm.new_type_error(format!( + "new() argument 'name' must be str, not {}", + &name[..end] + ))); + } + PyUtf8StrRef::try_from_object(vm, obj).map(Self) + } + } + #[derive(FromArgs, Debug)] #[allow(unused)] struct NewHashArgs { #[pyarg(any)] - name: PyUtf8StrRef, + name: HashName, // Missing still allows the string keyword; b'' does not. #[pyarg(any, optional, py_default = "b''")] data: OptionalArg, @@ -785,7 +809,7 @@ pub(crate) mod _hashlib { #[pyfunction(name = "new")] fn hashlib_new(args: NewHashArgs, vm: &VirtualMachine) -> PyResult { let data = resolve_data(args.data, args.string, vm)?; - match args.name.as_str().to_lowercase().as_str() { + match args.name.0.as_str().to_lowercase().as_str() { "md5" => Ok(new_fixed_hasher("md5", data).into_pyobject(vm)), "sha1" => Ok(new_fixed_hasher("sha1", data).into_pyobject(vm)), "sha224" => Ok(new_fixed_hasher("sha224", data).into_pyobject(vm)), diff --git a/extra_tests/snippets/stdlib_hashlib.py b/extra_tests/snippets/stdlib_hashlib.py index f3400aed5..9cbc6bffc 100644 --- a/extra_tests/snippets/stdlib_hashlib.py +++ b/extra_tests/snippets/stdlib_hashlib.py @@ -1,5 +1,7 @@ +import _hashlib import _md5 import _sha1 +import array import hashlib from testutils import assert_raises @@ -64,3 +66,17 @@ assert _sha1.sha1(b"").hexdigest() == "da39a3ee5e6b4b0d3255bfef95601890afd80709" # narrower than the length asked for, converting the argument fails first. with assert_raises(OverflowError): hashlib.pbkdf2_hmac("sha256", b"password", b"salt", 1, 2**62) + + +# Invalid names use the native constructor's bounded type diagnostic. +for value, name in ( + (None, "None"), + (42, "int"), + (b"sha256", "bytes"), + (array.array("b"), "array.array"), + (type("x" * 60, (), {})(), "x" * 50), + (type("€" * 17, (), {})(), "€" * 16), +): + with assert_raises(TypeError) as caught: + _hashlib.new(value) + assert str(caught.exception) == f"new() argument 'name' must be str, not {name}"