From 1cadf53a7670bc03436e742c3caa59cad3880b4a Mon Sep 17 00:00:00 2001 From: acul71 Date: Fri, 2 Oct 2026 04:54:11 -0400 Subject: [PATCH] Support variable-length BLAKE3 digests up to 128 bytes. Co-authored-by: Cursor --- multihash/funcs.py | 30 ++++++++++++++++++++---------- newsfragments/49.feature.rst | 1 + tests/test_funcs.py | 19 ++++++++++++++++++- 3 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 newsfragments/49.feature.rst diff --git a/multihash/funcs.py b/multihash/funcs.py index 7ff9e96..6f6469d 100644 --- a/multihash/funcs.py +++ b/multihash/funcs.py @@ -254,8 +254,7 @@ def copy(self) -> "ShakeHash": class Blake3Hash: """hashlib-compatible wrapper for Blake3 using official blake3 library. - BLAKE3 is a cryptographic hash function that is much faster than MD5, SHA-1, SHA-2, - and SHA-3, yet is just as secure as the latest standard SHA-3. + BLAKE3 supports variable-length output up to 128 bytes (default 32). Example: >>> from multihash import digest @@ -270,28 +269,32 @@ class Blake3Hash: '...' """ + MAX_SIZE = 128 name: str = "blake3" - digest_size: int = 32 block_size: int = 64 - def __init__(self) -> None: + def __init__(self, length: int = 32) -> None: + if length <= 0 or length > self.MAX_SIZE: + raise ValueError(f"blake3 length must be 1..{self.MAX_SIZE}, got {length}") self._hasher = blake3.blake3() + self._length = length + self.digest_size = length def update(self, data: bytes) -> None: """Update the hash with data.""" self._hasher.update(data) def digest(self) -> bytes: - """Return digest.""" - return self._hasher.digest() + """Return digest of the configured length.""" + return self._hasher.digest(self._length) def hexdigest(self) -> str: """Return hex digest.""" - return self._hasher.hexdigest() + return self.digest().hex() def copy(self) -> "Blake3Hash": """Create a copy of the hash state.""" - c = Blake3Hash() + c = Blake3Hash(self._length) c._hasher = self._hasher.copy() return c @@ -573,8 +576,7 @@ class FuncReg(metaclass=_FuncRegMeta): # SHA2-512 truncated variants (Python 3.6+) (Func.sha2_512_224, "sha512_224", getattr(hashlib, "sha512_224", None)), (Func.sha2_512_256, "sha512_256", getattr(hashlib, "sha512_256", None)), - # Blake3 (using official blake3 library) - (Func.blake3, "blake3", Blake3Hash), + # Blake3 registered via register_variable_size in reset() # Murmur3 variants (using official mmh3 library) (Func.murmur3_128, "murmur3-128", Murmur3_128Hash), (Func.murmur3_32, "murmur3-32", Murmur3_32Hash), @@ -677,6 +679,14 @@ def shake_256_factory(size_hint: int): cls.register_variable_size(Func.shake_128, "shake_128", shake_128_factory, "shake_128") cls.register_variable_size(Func.shake_256, "shake_256", shake_256_factory, "shake_256") + def blake3_factory(size_hint: int): + length = 32 if size_hint < 0 else size_hint + if length <= 0 or length > Blake3Hash.MAX_SIZE: + return None + return Blake3Hash(length) + + cls.register_variable_size(Func.blake3, "blake3", blake3_factory, "blake3") + @classmethod def get(cls, func_hint: Func | str | int) -> Func | int: """Return a registered hash function matching the given hint.""" diff --git a/newsfragments/49.feature.rst b/newsfragments/49.feature.rst new file mode 100644 index 0000000..fc1ea09 --- /dev/null +++ b/newsfragments/49.feature.rst @@ -0,0 +1 @@ +Support variable-length BLAKE3 digests up to 128 bytes (default 32), matching go-multihash. diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 715e869..5c7d163 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -6,7 +6,7 @@ import pytest -from multihash import Func, FuncReg, IdentityHash, Multihash, digest +from multihash import Func, FuncReg, HashComputationError, IdentityHash, Multihash, digest from multihash.constants import HASH_CODES @@ -104,6 +104,23 @@ def test_blake3_available(self): assert mh.code == Func.blake3 assert len(mh.digest) == 32 # Blake3 default is 32 bytes + def test_blake3_variable_lengths(self): + """BLAKE3 supports variable output up to 128 bytes.""" + import blake3 as blake3_mod + + data = b"foo" + for length in (32, 64, 128): + mh = digest(data, Func.blake3, length=length) + assert len(mh.digest) == length + expected = blake3_mod.blake3(data).digest(length) + assert mh.digest == expected + + assert FuncReg.is_variable_size(Func.blake3) is True + assert FuncReg.default_lengths[Func.blake3] == 32 + + with pytest.raises(HashComputationError): + digest(data, Func.blake3, length=129) + def test_murmur3_128_available(self): """Test that MurmurHash3-128 is available and works.""" assert hasattr(Func, "murmur3_128")