From 43dff3d9745d3fa937d175078f4796a81eec011e Mon Sep 17 00:00:00 2001 From: sumanjeet0012 Date: Mon, 13 Jul 2026 14:03:58 +0530 Subject: [PATCH 1/3] test: add comprehensive roundtrip tests and fix leading zeros bug --- multibase/converters.py | 28 ++++++++++++++++++++-------- tests/test_roundtrip.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 tests/test_roundtrip.py diff --git a/multibase/converters.py b/multibase/converters.py index 4202658..bfd9d5d 100644 --- a/multibase/converters.py +++ b/multibase/converters.py @@ -7,8 +7,17 @@ class BaseStringConverter(BaseConverter): def encode(self, bytes): + bytes = ensure_bytes(bytes) + if len(bytes) == 0: + return b"" + + leading_zeros = len(bytes) - len(bytes.lstrip(b"\x00")) + if leading_zeros == len(bytes): + return str(self.digits[0] * leading_zeros).encode("utf-8") + number = int.from_bytes(bytes, byteorder="big", signed=False) - return ensure_bytes(super().encode(number)) + encoded = super().encode(number) + return ensure_bytes(self.digits[0] * leading_zeros + encoded) def bytes_to_int(self, bytes): length = len(bytes) @@ -20,11 +29,19 @@ def bytes_to_int(self, bytes): return value def decode(self, bytes): + bytes_str = bytes.decode("utf-8") if isinstance(bytes, bytes) else bytes + if len(bytes_str) == 0: + return b"" + + leading_zeros = len(bytes_str) - len(bytes_str.lstrip(self.digits[0])) + if leading_zeros == len(bytes_str): + return b"\x00" * leading_zeros + decoded_int = self.bytes_to_int(bytes) # See https://docs.python.org/3.5/library/stdtypes.html#int.to_bytes for more about the magical expression # below decoded_data = decoded_int.to_bytes((decoded_int.bit_length() + 7) // 8, byteorder="big") - return decoded_data + return b"\x00" * leading_zeros + decoded_data class Base16StringConverter(BaseStringConverter): @@ -44,12 +61,7 @@ def decode(self, data): data_str = data.decode("utf-8") else: data_str = data - # Convert to match our digits case - if self.uppercase: - data_str = data_str.upper() - else: - data_str = data_str.lower() - return super().decode(data_str.encode("utf-8")) + return bytes.fromhex(data_str) class BaseByteStringConverter: diff --git a/tests/test_roundtrip.py b/tests/test_roundtrip.py new file mode 100644 index 0000000..59d3e8f --- /dev/null +++ b/tests/test_roundtrip.py @@ -0,0 +1,40 @@ +import os +import pytest +from multibase import encode, decode, ENCODINGS + +@pytest.mark.parametrize("encoding_info", ENCODINGS, ids=lambda e: e.encoding) +def test_random_data(encoding_info): + """Round-trip random data of various sizes.""" + for size in [1, 2, 7, 16, 32, 64, 137, 256, 1024]: + data = os.urandom(size) + encoded = encode(encoding_info.encoding, data) + decoded = decode(encoded) + assert decoded == data, f"Failed for {encoding_info.encoding} size={size}" + +@pytest.mark.parametrize("encoding_info", ENCODINGS, ids=lambda e: e.encoding) +def test_leading_zeros(encoding_info): + """Round-trip data with leading zero bytes.""" + for num_zeros in [1, 2, 4, 8, 16]: + data = b'\x00' * num_zeros + b'hello' + encoded = encode(encoding_info.encoding, data) + decoded = decode(encoded) + assert decoded == data, \ + f"Leading zeros lost for {encoding_info.encoding} zeros={num_zeros}" + +@pytest.mark.parametrize("encoding_info", ENCODINGS, ids=lambda e: e.encoding) +def test_all_zeros(encoding_info): + """Round-trip all-zero data.""" + for size in [1, 4, 16, 32]: + data = b'\x00' * size + encoded = encode(encoding_info.encoding, data) + decoded = decode(encoded) + assert decoded == data + +@pytest.mark.parametrize("encoding_info", ENCODINGS, ids=lambda e: e.encoding) +def test_all_ones(encoding_info): + """Round-trip all-0xFF data.""" + for size in [1, 4, 16, 32]: + data = b'\xff' * size + encoded = encode(encoding_info.encoding, data) + decoded = decode(encoded) + assert decoded == data From 4033614555044871820cce2ee3d9d5e6955c7d9b Mon Sep 17 00:00:00 2001 From: sumanjeet0012 Date: Mon, 13 Jul 2026 19:10:22 +0530 Subject: [PATCH 2/3] style: fix linting issues --- tests/test_roundtrip.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/test_roundtrip.py b/tests/test_roundtrip.py index 59d3e8f..4d2ddf5 100644 --- a/tests/test_roundtrip.py +++ b/tests/test_roundtrip.py @@ -1,6 +1,9 @@ import os + import pytest -from multibase import encode, decode, ENCODINGS + +from multibase import ENCODINGS, decode, encode + @pytest.mark.parametrize("encoding_info", ENCODINGS, ids=lambda e: e.encoding) def test_random_data(encoding_info): @@ -11,30 +14,32 @@ def test_random_data(encoding_info): decoded = decode(encoded) assert decoded == data, f"Failed for {encoding_info.encoding} size={size}" + @pytest.mark.parametrize("encoding_info", ENCODINGS, ids=lambda e: e.encoding) def test_leading_zeros(encoding_info): """Round-trip data with leading zero bytes.""" for num_zeros in [1, 2, 4, 8, 16]: - data = b'\x00' * num_zeros + b'hello' + data = b"\x00" * num_zeros + b"hello" encoded = encode(encoding_info.encoding, data) decoded = decode(encoded) - assert decoded == data, \ - f"Leading zeros lost for {encoding_info.encoding} zeros={num_zeros}" + assert decoded == data, f"Leading zeros lost for {encoding_info.encoding} zeros={num_zeros}" + @pytest.mark.parametrize("encoding_info", ENCODINGS, ids=lambda e: e.encoding) def test_all_zeros(encoding_info): """Round-trip all-zero data.""" for size in [1, 4, 16, 32]: - data = b'\x00' * size + data = b"\x00" * size encoded = encode(encoding_info.encoding, data) decoded = decode(encoded) assert decoded == data + @pytest.mark.parametrize("encoding_info", ENCODINGS, ids=lambda e: e.encoding) def test_all_ones(encoding_info): """Round-trip all-0xFF data.""" for size in [1, 4, 16, 32]: - data = b'\xff' * size + data = b"\xff" * size encoded = encode(encoding_info.encoding, data) decoded = decode(encoded) assert decoded == data From 6432a59e40841bf7db6857d355bdf3fde374388b Mon Sep 17 00:00:00 2001 From: sumanjeet0012 Date: Mon, 13 Jul 2026 19:39:41 +0530 Subject: [PATCH 3/3] fix: rename shadowed bytes variable causing isinstance TypeError --- multibase/converters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/multibase/converters.py b/multibase/converters.py index bfd9d5d..f4f0347 100644 --- a/multibase/converters.py +++ b/multibase/converters.py @@ -28,8 +28,8 @@ def bytes_to_int(self, bytes): value += self.digits.index(chr(x)) * base ** (length - (i + 1)) return value - def decode(self, bytes): - bytes_str = bytes.decode("utf-8") if isinstance(bytes, bytes) else bytes + def decode(self, data): + bytes_str = data.decode("utf-8") if isinstance(data, bytes) else data if len(bytes_str) == 0: return b"" @@ -37,7 +37,7 @@ def decode(self, bytes): if leading_zeros == len(bytes_str): return b"\x00" * leading_zeros - decoded_int = self.bytes_to_int(bytes) + decoded_int = self.bytes_to_int(data) # See https://docs.python.org/3.5/library/stdtypes.html#int.to_bytes for more about the magical expression # below decoded_data = decoded_int.to_bytes((decoded_int.bit_length() + 7) // 8, byteorder="big")