diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index 8e1a4a204c5..74ebcb2fe70 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -432,8 +432,6 @@ def test_dot_new_with_str_key(self): with self.assertRaises(TypeError): h = hmac.new("key", digestmod='sha256') - # TODO: RUSTPYTHON - @unittest.expectedFailure @hashlib_helper.requires_hashdigest('sha256') def test_withtext(self): # Constructor call with text. @@ -443,8 +441,6 @@ def test_withtext(self): self.fail("Constructor call with text argument raised exception.") self.assertEqual(h.hexdigest(), self.expected) - # TODO: RUSTPYTHON - @unittest.expectedFailure @hashlib_helper.requires_hashdigest('sha256') def test_with_bytearray(self): try: @@ -454,8 +450,6 @@ def test_with_bytearray(self): self.fail("Constructor call with bytearray arguments raised exception.") self.assertEqual(h.hexdigest(), self.expected) - # TODO: RUSTPYTHON - @unittest.expectedFailure @hashlib_helper.requires_hashdigest('sha256') def test_with_memoryview_msg(self): try: @@ -464,8 +458,6 @@ def test_with_memoryview_msg(self): self.fail("Constructor call with memoryview msg raised exception.") self.assertEqual(h.hexdigest(), self.expected) - # TODO: RUSTPYTHON - @unittest.expectedFailure @hashlib_helper.requires_hashdigest('sha256') def test_withmodule(self): # Constructor call with text and digest module. diff --git a/crates/stdlib/src/hashlib.rs b/crates/stdlib/src/hashlib.rs index e7b03a2ff12..bfde58f43f7 100644 --- a/crates/stdlib/src/hashlib.rs +++ b/crates/stdlib/src/hashlib.rs @@ -7,11 +7,11 @@ pub mod _hashlib { use crate::common::lock::PyRwLock; use crate::vm::{ Py, PyObjectRef, PyPayload, PyResult, VirtualMachine, - builtins::{PyBytes, PyStrRef, PyTypeRef}, + builtins::{PyBytes, PyStrRef, PyTypeRef, PyValueError}, + class::StaticType, convert::ToPyObject, function::{ArgBytesLike, ArgStrOrBytesLike, FuncArgs, OptionalArg}, - protocol::PyBuffer, - types::Representable, + types::{Constructor, Initializer, Representable}, }; use blake2::{Blake2b512, Blake2s256}; use digest::{DynDigest, core_api::BlockSizeUser}; @@ -22,6 +22,12 @@ pub mod _hashlib { use sha2::{Sha224, Sha256, Sha384, Sha512}; use sha3::{Sha3_224, Sha3_256, Sha3_384, Sha3_512, Shake128, Shake256}; + #[pyattr] + #[pyexception(name = "UnsupportedDigestmodError", base = PyValueError, impl)] + #[derive(Debug)] + #[repr(transparent)] + pub struct UnsupportedDigestmodError(PyValueError); + #[derive(FromArgs, Debug)] #[allow(unused)] struct NewHashArgs { @@ -338,16 +344,21 @@ pub mod _hashlib { #[allow(unused)] pub struct NewHMACHashArgs { #[pyarg(positional)] - name: PyBuffer, + key: ArgBytesLike, #[pyarg(any, optional)] - data: OptionalArg, - #[pyarg(named, default = true)] - digestmod: bool, // TODO: RUSTPYTHON support functions & name functions + msg: OptionalArg, + #[pyarg(named, optional)] + digestmod: OptionalArg, } #[pyfunction] - fn hmac_new(_args: NewHMACHashArgs, vm: &VirtualMachine) -> PyResult { - Err(vm.new_type_error("cannot create 'hmac' instances")) // TODO: RUSTPYTHON support hmac + fn hmac_new(args: NewHMACHashArgs, vm: &VirtualMachine) -> PyResult { + // Raise UnsupportedDigestmodError so Python's hmac.py falls back to pure-Python implementation + let _ = args; + Err(vm.new_exception_msg( + UnsupportedDigestmodError::static_type().to_owned(), + "unsupported hash type".to_owned(), + )) } pub trait ThreadSafeDynDigest: DynClone + DynDigest + Sync + Send {} diff --git a/crates/stdlib/src/mmap.rs b/crates/stdlib/src/mmap.rs index 5309917a999..f1e2c2a039d 100644 --- a/crates/stdlib/src/mmap.rs +++ b/crates/stdlib/src/mmap.rs @@ -651,9 +651,10 @@ mod mmap { impl AsBuffer for PyMmap { fn as_buffer(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + let readonly = matches!(zelf.access, AccessMode::Read); let buf = PyBuffer::new( zelf.to_owned().into(), - BufferDescriptor::simple(zelf.__len__(), true), + BufferDescriptor::simple(zelf.__len__(), readonly), &BUFFER_METHODS, ); diff --git a/crates/vm/src/stdlib/os.rs b/crates/vm/src/stdlib/os.rs index 6849ea365df..6e2bb274fec 100644 --- a/crates/vm/src/stdlib/os.rs +++ b/crates/vm/src/stdlib/os.rs @@ -202,6 +202,15 @@ pub(super) mod _os { #[pyattr] pub(crate) const X_OK: u8 = 1 << 0; + // ST_RDONLY and ST_NOSUID flags for statvfs + #[cfg(all(unix, not(target_os = "redox")))] + #[pyattr] + const ST_RDONLY: libc::c_ulong = libc::ST_RDONLY; + + #[cfg(all(unix, not(target_os = "redox")))] + #[pyattr] + const ST_NOSUID: libc::c_ulong = libc::ST_NOSUID; + #[pyfunction] fn close(fileno: crt_fd::Owned) -> io::Result<()> { crt_fd::close(fileno) @@ -1701,6 +1710,103 @@ pub(super) mod _os { #[pyclass(with(PyStructSequence))] impl PyUnameResult {} + // statvfs_result: Result from statvfs or fstatvfs. + // = statvfs_result_fields + #[cfg(all(unix, not(target_os = "redox")))] + #[derive(Debug)] + #[pystruct_sequence_data] + pub(crate) struct StatvfsResultData { + pub f_bsize: libc::c_ulong, // filesystem block size + pub f_frsize: libc::c_ulong, // fragment size + pub f_blocks: libc::fsblkcnt_t, // size of fs in f_frsize units + pub f_bfree: libc::fsblkcnt_t, // free blocks + pub f_bavail: libc::fsblkcnt_t, // free blocks for unprivileged users + pub f_files: libc::fsfilcnt_t, // inodes + pub f_ffree: libc::fsfilcnt_t, // free inodes + pub f_favail: libc::fsfilcnt_t, // free inodes for unprivileged users + pub f_flag: libc::c_ulong, // mount flags + pub f_namemax: libc::c_ulong, // maximum filename length + #[pystruct_sequence(skip)] + pub f_fsid: libc::c_ulong, // filesystem ID (not in tuple but accessible as attribute) + } + + #[cfg(all(unix, not(target_os = "redox")))] + #[pyattr] + #[pystruct_sequence(name = "statvfs_result", module = "os", data = "StatvfsResultData")] + pub(crate) struct PyStatvfsResult; + + #[cfg(all(unix, not(target_os = "redox")))] + #[pyclass(with(PyStructSequence))] + impl PyStatvfsResult { + #[pyslot] + fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let seq: PyObjectRef = args.bind(vm)?; + crate::types::struct_sequence_new(cls, seq, vm) + } + } + + #[cfg(all(unix, not(target_os = "redox")))] + impl StatvfsResultData { + fn from_statvfs(st: libc::statvfs) -> Self { + // f_fsid is a struct on some platforms (e.g., Linux fsid_t) and a scalar on others. + // We extract raw bytes and interpret as a native-endian integer. + // Note: The value may differ across architectures due to endianness. + let f_fsid = { + let ptr = std::ptr::addr_of!(st.f_fsid) as *const u8; + let size = std::mem::size_of_val(&st.f_fsid); + if size >= 8 { + let bytes = unsafe { std::slice::from_raw_parts(ptr, 8) }; + u64::from_ne_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], + bytes[7], + ]) as libc::c_ulong + } else if size >= 4 { + let bytes = unsafe { std::slice::from_raw_parts(ptr, 4) }; + u32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as libc::c_ulong + } else { + 0 + } + }; + + Self { + f_bsize: st.f_bsize, + f_frsize: st.f_frsize, + f_blocks: st.f_blocks, + f_bfree: st.f_bfree, + f_bavail: st.f_bavail, + f_files: st.f_files, + f_ffree: st.f_ffree, + f_favail: st.f_favail, + f_flag: st.f_flag, + f_namemax: st.f_namemax, + f_fsid, + } + } + } + + /// Perform a statvfs system call on the given path. + #[cfg(all(unix, not(target_os = "redox")))] + #[pyfunction] + #[pyfunction(name = "fstatvfs")] + fn statvfs(path: OsPathOrFd<'_>, vm: &VirtualMachine) -> PyResult { + let mut st: libc::statvfs = unsafe { std::mem::zeroed() }; + let ret = match &path { + OsPathOrFd::Path(p) => { + let cpath = p.clone().into_cstring(vm)?; + unsafe { libc::statvfs(cpath.as_ptr(), &mut st) } + } + OsPathOrFd::Fd(fd) => unsafe { libc::fstatvfs(fd.as_raw(), &mut st) }, + }; + if ret != 0 { + return Err(OSErrorBuilder::with_filename( + &io::Error::last_os_error(), + path, + vm, + )); + } + Ok(StatvfsResultData::from_statvfs(st).to_pyobject(vm)) + } + pub(super) fn support_funcs() -> Vec { let mut supports = super::platform::module::support_funcs(); supports.extend(vec![ diff --git a/crates/vm/src/stdlib/thread.rs b/crates/vm/src/stdlib/thread.rs index 36252279397..fd300ac2f74 100644 --- a/crates/vm/src/stdlib/thread.rs +++ b/crates/vm/src/stdlib/thread.rs @@ -190,6 +190,7 @@ pub(crate) mod _thread { #[derive(PyPayload)] struct RLock { mu: RawRMutex, + count: std::sync::atomic::AtomicUsize, } impl fmt::Debug for RLock { @@ -204,6 +205,7 @@ pub(crate) mod _thread { fn slot_new(cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult { Self { mu: RawRMutex::INIT, + count: std::sync::atomic::AtomicUsize::new(0), } .into_ref_with_type(vm, cls) .map(Into::into) @@ -213,7 +215,12 @@ pub(crate) mod _thread { #[pymethod(name = "acquire_lock")] #[pymethod(name = "__enter__")] fn acquire(&self, args: AcquireArgs, vm: &VirtualMachine) -> PyResult { - acquire_lock_impl!(&self.mu, args, vm) + let result = acquire_lock_impl!(&self.mu, args, vm)?; + if result { + self.count + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + Ok(result) } #[pymethod] #[pymethod(name = "release_lock")] @@ -221,6 +228,12 @@ pub(crate) mod _thread { if !self.mu.is_locked() { return Err(vm.new_runtime_error("release unlocked lock")); } + debug_assert!( + self.count.load(std::sync::atomic::Ordering::Relaxed) > 0, + "RLock count underflow" + ); + self.count + .fetch_sub(1, std::sync::atomic::Ordering::Relaxed); unsafe { self.mu.unlock() }; Ok(()) } @@ -232,6 +245,7 @@ pub(crate) mod _thread { self.mu.unlock(); }; } + self.count.store(0, std::sync::atomic::Ordering::Relaxed); let new_mut = RawRMutex::INIT; let old_mutex: AtomicCell<&RawRMutex> = AtomicCell::new(&self.mu); @@ -245,6 +259,15 @@ pub(crate) mod _thread { self.mu.is_owned_by_current_thread() } + #[pymethod] + fn _recursion_count(&self) -> usize { + if self.mu.is_owned_by_current_thread() { + self.count.load(std::sync::atomic::Ordering::Relaxed) + } else { + 0 + } + } + #[pymethod] fn __exit__(&self, _args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { self.release(vm)