diff --git a/CHANGELOG.md b/CHANGELOG.md index dbdb990a6..f68ff7c92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased +## Fixed +* Erroring on constructing an odd length hex string. ([#539](https://github.com/algorand/pyteal/pull/539)) + # 0.18.1 ## Fixed diff --git a/pyteal/ast/bytes.py b/pyteal/ast/bytes.py index 4c4fb4d6e..1c7bd09b4 100644 --- a/pyteal/ast/bytes.py +++ b/pyteal/ast/bytes.py @@ -1,4 +1,4 @@ -from typing import Union, cast, overload, TYPE_CHECKING +from typing import cast, overload, TYPE_CHECKING from pyteal.types import TealType, valid_base16, valid_base32, valid_base64 from pyteal.util import escapeStr @@ -14,14 +14,14 @@ class Bytes(LeafExpr): """An expression that represents a byte string.""" @overload - def __init__(self, arg1: Union[str, bytes, bytearray]) -> None: + def __init__(self, arg1: str | bytes | bytearray) -> None: pass @overload def __init__(self, arg1: str, arg2: str) -> None: pass - def __init__(self, arg1: Union[str, bytes, bytearray], arg2: str = None) -> None: + def __init__(self, arg1: str | bytes | bytearray, arg2: str = None) -> None: """ __init__(arg1: Union[str, bytes, bytearray]) -> None __init__(self, arg1: str, arg2: str) -> None @@ -49,15 +49,15 @@ def __init__(self, arg1: Union[str, bytes, bytearray], arg2: str = None) -> None self.byte_str = escapeStr(arg1) elif type(arg1) in (bytes, bytearray): self.base = "base16" - self.byte_str = cast(Union[bytes, bytearray], arg1).hex() + self.byte_str = cast(bytes | bytearray, arg1).hex() else: - raise TealInputError("Unknown argument type: {}".format(type(arg1))) + raise TealInputError(f"Unknown argument type: {type(arg1)}") else: if type(arg1) is not str: - raise TealInputError("Unknown type for base: {}".format(type(arg1))) + raise TealInputError(f"Unknown type for base: {type(arg1)}") if type(arg2) is not str: - raise TealInputError("Unknown type for value: {}".format(type(arg2))) + raise TealInputError(f"Unknown type for value: {type(arg2)}") self.base = arg1 @@ -75,9 +75,7 @@ def __init__(self, arg1: Union[str, bytes, bytearray], arg2: str = None) -> None valid_base16(self.byte_str) else: raise TealInputError( - "invalid base {}, need to be base32, base64, or base16.".format( - self.base - ) + f"invalid base {self.base}, need to be base32, base64, or base16." ) def __teal__(self, options: "CompileOptions"): @@ -86,12 +84,12 @@ def __teal__(self, options: "CompileOptions"): elif self.base == "base16": payload = "0x" + self.byte_str else: - payload = "{}({})".format(self.base, self.byte_str) + payload = f"{self.base}({self.byte_str})" op = TealOp(self, Op.byte, payload) return TealBlock.FromOp(options, op) def __str__(self): - return "({} bytes: {})".format(self.base, self.byte_str) + return f"({self.base} bytes: {self.byte_str})" def type_of(self): return TealType.bytes diff --git a/pyteal/ast/bytes_test.py b/pyteal/ast/bytes_test.py index 2ad5382a5..b14544737 100644 --- a/pyteal/ast/bytes_test.py +++ b/pyteal/ast/bytes_test.py @@ -1,6 +1,7 @@ import pytest import pyteal as pt +from pyteal.errors import TealInputError options = pt.CompileOptions() @@ -88,6 +89,15 @@ def test_bytes_base16_empty(): assert actual == expected +B16_ODD_LEN_TESTCASES = ["F", "0c1"] + + +@pytest.mark.parametrize("testcase", B16_ODD_LEN_TESTCASES) +def test_bytes_base16_odd_len(testcase): + with pytest.raises(TealInputError): + pt.Bytes("base16", testcase) + + def test_bytes_utf8(): expr = pt.Bytes("hello world") assert expr.type_of() == pt.TealType.bytes diff --git a/pyteal/types.py b/pyteal/types.py index 63b61bd54..a31ecbc0f 100644 --- a/pyteal/types.py +++ b/pyteal/types.py @@ -84,6 +84,11 @@ def valid_base64(s: str): def valid_base16(s: str): """check if s is a valid hex encoding string""" + if len(s) % 2: + raise TealInputError( + f"{s} is of odd length, not a valid RFC 4648 base16 string" + ) + pattern = re.compile(r"[0-9A-Fa-f]*") if pattern.fullmatch(s) is None: