Skip to content
Open
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
30 changes: 22 additions & 8 deletions src/_algopy_testing/arc4.py
Original file line number Diff line number Diff line change
Expand Up @@ -1227,15 +1227,28 @@ def _update_backing_value(self) -> None:

@classmethod
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
# AVM does not validate total encoded length on construction; it errors later
# when an operation reads past the backing bytes (see algorand-python-testing#45).
raw = value.value if isinstance(value, Bytes) else bytes(value)
tuple_type = _tuple_type_from_struct(cls)
tuple_value = tuple_type.from_bytes(value)
if not tuple_value:
return typing.cast("typing.Self", tuple_value)
# convert each decoded ARC-4 item to the declared field type (ARC-4
# annotations pass through, native annotations go through arc4_to_native)
child_types = tuple_type._type_info.child_types
try:
decoded = _decode_tuple_items(raw, child_types, strict=True)
except ValueError:
decoded = _decode_tuple_items(raw, child_types, strict=False)
items = [
_to_native_type(field.type, arc4_item)
for arc4_item, field in zip(decoded, cls._type_info.fields, strict=True)
]
instance = object.__new__(cls)
MutableBytes.__init__(instance)
instance._value = raw
for field, item in zip(cls._type_info.fields, items, strict=True):
object.__setattr__(instance, field.name, item)
return instance
items = [
_to_native_type(field.type, arc4_item)
for arc4_item, field in zip(tuple_value.native, cls._type_info.fields, strict=True)
for arc4_item, field in zip(decoded, cls._type_info.fields, strict=True)
]
return cls(*items)

Expand Down Expand Up @@ -1548,7 +1561,7 @@ def _encode( # noqa: PLR0912


def _decode_tuple_items( # noqa: PLR0912, PLR0915
value: bytes, child_types: list[_TypeInfo]
value: bytes, child_types: list[_TypeInfo], *, strict: bool = True
) -> list[typing.Any]:
dynamic_segments: list[list[int]] = [] # Store the start and end of a dynamic element
value_partitions: list[bytes] = []
Expand Down Expand Up @@ -1597,7 +1610,8 @@ def _decode_tuple_items( # noqa: PLR0912, PLR0915
array_index += curr_len

if array_index >= len(value) and i != len(child_types) - 1:
raise ValueError(f"input string is not long enough to be decoded: {value!r}")
if strict:
raise ValueError(f"input string is not long enough to be decoded: {value!r}")

i += 1

Expand Down
13 changes: 13 additions & 0 deletions tests/arc4/test_struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,19 @@ def test_replace() -> None:
assert x != y


class FixedArrayUInt64(arc4.Struct):
length: arc4.UInt16
arr: arc4.StaticArray[arc4.UInt64, typing.Literal[4095]]


def test_struct_from_bytes_allows_short_input() -> None:
"""AVM construction from undersized bytes succeeds; OOB field access errors later."""
foo = FixedArrayUInt64.from_bytes(b"\x00\x00")
assert foo.length == 0
with pytest.raises(ValueError, match="not long enough"):
_ = foo.arr[0]


def _compare_abi_and_arc4_values(
arc4_value: typing.Any,
abi_value: typing.Any,
Expand Down