diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index 3c91a1083..1506bb798 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -1066,26 +1066,10 @@ class OpenSSLConstructorTestCase(ThroughOpenSSLAPIMixin, ): self.hmac_digest(b'key', b'msg', value) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module '_hashlib' has no attribute 'HMAC'. Did you mean: 'exc_type'? - def test_internal_types(self): - return super().test_internal_types() - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module '_hashlib' has no attribute 'hmac_digest' def test_digest(self): return super().test_digest() - @unittest.expectedFailure # TODO: RUSTPYTHON - def test_constructor(self): - return super().test_constructor() - - @unittest.expectedFailure # TODO: RUSTPYTHON - def test_constructor_missing_digestmod(self): - return super().test_constructor_missing_digestmod() - - @unittest.expectedFailure # TODO: RUSTPYTHON - def test_constructor_unknown_digestmod(self): - return super().test_constructor_unknown_digestmod() - class BuiltinConstructorTestCase(ThroughBuiltinAPIMixin, ExtensionConstructorTestCaseMixin, @@ -1265,18 +1249,6 @@ class OpenSSLUpdateTestCase(UpdateTestCaseMixin, unittest.TestCase): def gil_minsize(self): return _hashlib._GIL_MINSIZE - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module '_hashlib' has no attribute '_GIL_MINSIZE' - def test_update_large(self): - return super().test_update_large() - - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'NoneType' - def test_update_exceptions(self): - return super().test_update_exceptions() - - @unittest.expectedFailure # TODO: RUSTPYTHON - def test_update(self): - return super().test_update() - class BuiltinUpdateTestCase(BuiltinModuleMixin, UpdateTestCaseMixin, unittest.TestCase): @@ -1328,7 +1300,6 @@ class PythonCopyTestCase(CopyBaseTestCase, unittest.TestCase): self.assertNotEqual(id(h1._inner), id(h2._inner)) self.assertNotEqual(id(h1._outer), id(h2._outer)) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'NoneType' def test_equality(self): # Testing if the copy has the same digests. h1 = hmac.HMAC(b"key", digestmod="sha256") @@ -1337,7 +1308,6 @@ class PythonCopyTestCase(CopyBaseTestCase, unittest.TestCase): self.assertEqual(h1.digest(), h2.digest()) self.assertEqual(h1.hexdigest(), h2.hexdigest()) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'NoneType' def test_equality_new(self): # Testing if the copy has the same digests with hmac.new(). h1 = hmac.new(b"key", digestmod="sha256") @@ -1383,14 +1353,6 @@ class OpenSSLCopyTestCase(ExtensionCopyTestCase, unittest.TestCase): def init(self, h): h._init_openssl_hmac(b"key", b"msg", digestmod="sha256") - @unittest.expectedFailure # TODO: RUSTPYTHON; _hashlib.UnsupportedDigestmodError: unsupported hash type - def test_attributes(self): - return super().test_attributes() - - @unittest.expectedFailure # TODO: RUSTPYTHON; _hashlib.UnsupportedDigestmodError: unsupported hash type - def test_realcopy(self): - return super().test_realcopy() - @hashlib_helper.requires_builtin_hmac() class BuiltinCopyTestCase(ExtensionCopyTestCase, unittest.TestCase): diff --git a/crates/stdlib/src/hashlib.rs b/crates/stdlib/src/hashlib.rs index c66e4a8cd..5097d804a 100644 --- a/crates/stdlib/src/hashlib.rs +++ b/crates/stdlib/src/hashlib.rs @@ -19,7 +19,7 @@ pub mod _hashlib { types::{Constructor, Representable}, }; use blake2::{Blake2b512, Blake2s256}; - use digest::{DynDigest, core_api::BlockSizeUser}; + use digest::{DynDigest, OutputSizeUser, core_api::BlockSizeUser}; use digest::{ExtendableOutput, Update}; use dyn_clone::{DynClone, clone_trait_object}; use hmac::Mac; @@ -258,6 +258,105 @@ pub mod _hashlib { ) } + // Object-safe HMAC trait for type-erased dispatch + trait DynHmac: Send + Sync { + fn dyn_update(&mut self, data: &[u8]); + fn dyn_finalize(&self) -> Vec; + fn dyn_clone(&self) -> Box; + } + + struct TypedHmac(D); + + impl DynHmac for TypedHmac + where + D: Mac + Clone + Send + Sync + 'static, + { + fn dyn_update(&mut self, data: &[u8]) { + Mac::update(&mut self.0, data); + } + + fn dyn_finalize(&self) -> Vec { + self.0.clone().finalize().into_bytes().to_vec() + } + + fn dyn_clone(&self) -> Box { + Box::new(TypedHmac(self.0.clone())) + } + } + + #[pyattr] + #[pyclass(module = "_hashlib", name = "HMAC")] + #[derive(PyPayload)] + pub struct PyHmac { + algo_name: String, + digest_size: usize, + block_size: usize, + ctx: PyRwLock>, + } + + impl core::fmt::Debug for PyHmac { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "HMAC {}", self.algo_name) + } + } + + #[pyclass(with(Representable), flags(IMMUTABLETYPE))] + impl PyHmac { + #[pyslot] + fn slot_new(_cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult { + Err(vm.new_type_error("cannot create '_hashlib.HMAC' instances".to_owned())) + } + + #[pygetset] + fn name(&self) -> String { + format!("hmac-{}", self.algo_name) + } + + #[pygetset] + fn digest_size(&self) -> usize { + self.digest_size + } + + #[pygetset] + fn block_size(&self) -> usize { + self.block_size + } + + #[pymethod] + fn update(&self, msg: ArgBytesLike) { + msg.with_ref(|bytes| self.ctx.write().dyn_update(bytes)); + } + + #[pymethod] + fn digest(&self) -> PyBytes { + self.ctx.read().dyn_finalize().into() + } + + #[pymethod] + fn hexdigest(&self) -> String { + hex::encode(self.ctx.read().dyn_finalize()) + } + + #[pymethod] + fn copy(&self) -> Self { + Self { + algo_name: self.algo_name.clone(), + digest_size: self.digest_size, + block_size: self.block_size, + ctx: PyRwLock::new(self.ctx.read().dyn_clone()), + } + } + } + + impl Representable for PyHmac { + fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + Ok(format!( + "<{} HMAC object @ {:#x}>", + zelf.algo_name, zelf as *const _ as usize + )) + } + } + #[pyattr] #[pyclass(module = "_hashlib", name = "HASH")] #[derive(PyPayload)] @@ -646,18 +745,50 @@ pub mod _hashlib { #[pyarg(positional)] key: ArgBytesLike, #[pyarg(any, optional)] - msg: OptionalArg, + msg: OptionalArg>, #[pyarg(named, optional)] digestmod: OptionalArg, } #[pyfunction] - fn hmac_new(args: NewHMACHashArgs, vm: &VirtualMachine) -> PyResult { - let _ = args; - Err(vm.new_exception_msg( - UnsupportedDigestmodError::static_type().to_owned(), - "unsupported hash type".to_owned(), - )) + fn hmac_new(args: NewHMACHashArgs, vm: &VirtualMachine) -> PyResult { + let digestmod = args.digestmod.into_option().ok_or_else(|| { + vm.new_type_error("Missing required parameter 'digestmod'.".to_owned()) + })?; + let name = resolve_digestmod(&digestmod, vm)?; + + let key_buf = args.key.borrow_buf(); + let msg_data = args.msg.flatten(); + + macro_rules! make_hmac { + ($hash_ty:ty) => {{ + let mut mac = as Mac>::new_from_slice(&key_buf) + .map_err(|_| vm.new_value_error("invalid key length".to_owned()))?; + if let Some(ref m) = msg_data { + m.with_ref(|bytes| Mac::update(&mut mac, bytes)); + } + Ok(PyHmac { + algo_name: name, + digest_size: <$hash_ty as OutputSizeUser>::output_size(), + block_size: <$hash_ty as BlockSizeUser>::block_size(), + ctx: PyRwLock::new(Box::new(TypedHmac(mac))), + }) + }}; + } + + match name.as_str() { + "md5" => make_hmac!(Md5), + "sha1" => make_hmac!(Sha1), + "sha224" => make_hmac!(Sha224), + "sha256" => make_hmac!(Sha256), + "sha384" => make_hmac!(Sha384), + "sha512" => make_hmac!(Sha512), + "sha3_224" => make_hmac!(Sha3_224), + "sha3_256" => make_hmac!(Sha3_256), + "sha3_384" => make_hmac!(Sha3_384), + "sha3_512" => make_hmac!(Sha3_512), + _ => Err(unsupported_hash(&name, vm)), + } } #[pyfunction]