Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions multihash/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
)
from .funcs import Func, FuncReg, IdentityHash, ShakeHash
from .multihash import (
Cast,
MHFromBytes,
Multihash,
MultihashSet,
cast,
coerce_code,
decode,
digest,
Expand All @@ -26,24 +29,28 @@
is_app_code,
is_valid,
is_valid_code,
mh_from_bytes,
sum,
sum_stream,
to_b58_string,
to_hex_string,
)

__all__ = [
"Cast",
"Func",
"FuncReg",
"HashComputationError",
"IdentityHash",
"InvalidMultihashError",
"MHFromBytes",
"Multihash",
"MultihashError",
"MultihashSet",
"ShakeHash",
"TruncationError",
"UnsupportedCodeError",
"cast",
"coerce_code",
"decode",
"digest",
Expand All @@ -55,6 +62,7 @@
"is_app_code",
"is_valid",
"is_valid_code",
"mh_from_bytes",
"sum",
"sum_stream",
"to_b58_string",
Expand Down
47 changes: 47 additions & 0 deletions multihash/multihash.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,53 @@ def decode(multihash):
return Multihash(code=code, name=constants.CODE_HASHES.get(code, code), length=length, digest=buf)


def cast(buf: bytes) -> Multihash:
"""Validate ``buf`` as a multihash and return a :class:`Multihash`.

This is the Python equivalent of go-multihash ``Cast``: the entire buffer
must be a valid multihash (no trailing data).

Args:
buf: Raw multihash bytes

Returns:
Multihash: Decoded multihash

Raises:
TypeError: If ``buf`` is not bytes
ValueError: If ``buf`` is not a valid multihash
"""
return decode(buf)


Cast = cast


def mh_from_bytes(buf: bytes) -> tuple[int, Multihash]:
"""Read a multihash from the start of ``buf``, allowing trailing data.

This is the Python equivalent of go-multihash ``MHFromBytes``.

Args:
buf: Buffer that begins with a multihash (may contain trailing bytes)

Returns:
Tuple of ``(bytes_consumed, Multihash)``

Raises:
TypeError: If ``buf`` is not bytes
ValueError: If the leading bytes are not a valid multihash
"""
if not isinstance(buf, bytes):
raise TypeError(f"buf should be bytes, not {type(buf)}")
stream = BytesIO(buf)
mh = Multihash.read(stream)
return stream.tell(), mh


MHFromBytes = mh_from_bytes


def encode(digest, code, length=None):
"""
Encode a hash digest along with the specified function code
Expand Down
1 change: 1 addition & 0 deletions newsfragments/56.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add :func:`~multihash.cast` and :func:`~multihash.mh_from_bytes` for go-multihash ``Cast`` / ``MHFromBytes`` parity.
42 changes: 42 additions & 0 deletions tests/test_new_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
MultihashSet,
ShakeHash,
TruncationError,
cast,
decode,
digest,
from_json,
mh_from_bytes,
sum,
sum_stream,
)
Expand Down Expand Up @@ -919,3 +921,43 @@ def test_read_write_app_code(self):
finally:
# Clean up
FuncReg.unregister(app_code)


class TestCastAndMhFromBytesTestCase:
"""Tests for cast() and mh_from_bytes()."""

def test_cast_valid(self):
mh = sum(b"hello", Func.sha2_256)
encoded = mh.encode()
casted = cast(encoded)
assert casted == decode(encoded)
assert casted.digest == mh.digest

def test_cast_rejects_trailing_data(self):
mh = sum(b"hello", Func.sha2_256)
with pytest.raises(ValueError):
cast(mh.encode() + b"extra")

def test_mh_from_bytes_with_trailing(self):
mh = sum(b"hello", Func.sha2_256)
encoded = mh.encode()
buf = encoded + b"trailing-bytes"
consumed, parsed = mh_from_bytes(buf)
assert consumed == len(encoded)
assert parsed.digest == mh.digest
assert parsed.code == mh.code

def test_mh_from_bytes_exact(self):
mh = sum(b"hello", Func.sha2_256)
encoded = mh.encode()
consumed, parsed = mh_from_bytes(encoded)
assert consumed == len(encoded)
assert parsed == decode(encoded)

def test_go_aliases(self):
from multihash import Cast, MHFromBytes

mh = sum(b"hello", Func.sha2_256)
encoded = mh.encode()
assert Cast(encoded) == cast(encoded)
assert MHFromBytes(encoded) == mh_from_bytes(encoded)
Loading