From 71a88e6b8401ef9144c27e3c4669db46e076424e Mon Sep 17 00:00:00 2001 From: 0xIryna Date: Wed, 25 Mar 2026 21:25:22 -0700 Subject: [PATCH] feat: add BytesParser library, update TypeConverter --- src/libs/BytesParser.sol | 130 +++++++++++++++++++++++++++++++++++++ src/libs/TypeConverter.sol | 23 +++++++ test/BytesParserTest.t.sol | 129 ++++++++++++++++++++++++++++++++++++ test/TypeConverter.t.sol | 84 ++++++++++++++++++++++++ 4 files changed, 366 insertions(+) create mode 100644 src/libs/BytesParser.sol create mode 100644 test/BytesParserTest.t.sol diff --git a/src/libs/BytesParser.sol b/src/libs/BytesParser.sol new file mode 100644 index 0000000..92d0431 --- /dev/null +++ b/src/libs/BytesParser.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.20 <0.9.0; + +/// @title BytesParser +/// @author Wormhole Labs +/// @notice Parses tightly packed data. +/// @dev Modified from +/// https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/libraries/BytesParsing.sol +library BytesParser { + error LengthMismatch(uint256 encodedLength, uint256 expectedLength); + error InvalidBool(uint8 value); + + /// @notice Reverts if the encoded byte array length does not match the expected length. + /// @param encoded The byte array to check. + /// @param expected The expected length. + function checkLength(bytes memory encoded, uint256 expected) internal pure { + if (encoded.length != expected) revert LengthMismatch(encoded.length, expected); + } + + /// @notice Reads a uint8 from `encoded` at the given byte `offset` without bounds checking. + /// @param encoded The byte array to read from. + /// @param offset The byte offset to start reading from. + /// @return value The decoded uint8 value. + /// @return nextOffset The offset immediately after the read bytes. + function asUint8Unchecked( + bytes memory encoded, + uint256 offset + ) internal pure returns (uint8 value, uint256 nextOffset) { + assembly ("memory-safe") { + nextOffset := add(offset, 1) + value := mload(add(encoded, nextOffset)) + } + } + + /// @notice Reads a bool from `encoded` at the given byte `offset` without bounds checking. + /// @dev Reverts with `InvalidBool` if the underlying uint8 is not 0 or 1. + /// @param encoded The byte array to read from. + /// @param offset The byte offset to start reading from. + /// @return value The decoded bool value. + /// @return nextOffset The offset immediately after the read bytes. + function asBoolUnchecked( + bytes memory encoded, + uint256 offset + ) internal pure returns (bool value, uint256 nextOffset) { + uint8 uint8Value; + (uint8Value, nextOffset) = asUint8Unchecked(encoded, offset); + + if (uint8Value & 0xfe != 0) revert InvalidBool(uint8Value); + + uint256 cleanedValue = uint256(uint8Value); + // skip 2x iszero opcode + assembly ("memory-safe") { + value := cleanedValue + } + } + + /// @notice Reads a uint256 from `encoded` at the given byte `offset` without bounds checking. + /// @param encoded The byte array to read from. + /// @param offset The byte offset to start reading from. + /// @return value The decoded uint256 value. + /// @return nextOffset The offset immediately after the read bytes. + function asUint256Unchecked( + bytes memory encoded, + uint256 offset + ) internal pure returns (uint256 value, uint256 nextOffset) { + assembly ("memory-safe") { + nextOffset := add(offset, 32) + value := mload(add(encoded, nextOffset)) + } + } + + /// @notice Reads a uint128 from `encoded` at the given byte `offset` without bounds checking. + /// @param encoded The byte array to read from. + /// @param offset The byte offset to start reading from. + /// @return value The decoded uint128 value. + /// @return nextOffset The offset immediately after the read bytes. + function asUint128Unchecked( + bytes memory encoded, + uint256 offset + ) internal pure returns (uint128 value, uint256 nextOffset) { + assembly ("memory-safe") { + nextOffset := add(offset, 16) + value := mload(add(encoded, nextOffset)) + } + } + + /// @notice Reads a uint32 from `encoded` at the given byte `offset` without bounds checking. + /// @param encoded The byte array to read from. + /// @param offset The byte offset to start reading from. + /// @return value The decoded uint32 value. + /// @return nextOffset The offset immediately after the read bytes. + function asUint32Unchecked( + bytes memory encoded, + uint256 offset + ) internal pure returns (uint32 value, uint256 nextOffset) { + assembly ("memory-safe") { + nextOffset := add(offset, 4) + value := mload(add(encoded, nextOffset)) + } + } + + /// @notice Reads a bytes32 from `encoded` at the given byte `offset` without bounds checking. + /// @param encoded The byte array to read from. + /// @param offset The byte offset to start reading from. + /// @return value The decoded bytes32 value. + /// @return nextOffset The offset immediately after the read bytes. + function asBytes32Unchecked( + bytes memory encoded, + uint256 offset + ) internal pure returns (bytes32 value, uint256 nextOffset) { + uint256 uint256Value; + (uint256Value, nextOffset) = asUint256Unchecked(encoded, offset); + value = bytes32(uint256Value); + } + + /// @notice Reads an address from `encoded` at the given byte `offset` without bounds checking. + /// @param encoded The byte array to read from. + /// @param offset The byte offset to start reading from. + /// @return value The decoded address value. + /// @return nextOffset The offset immediately after the read bytes. + function asAddressUnchecked( + bytes memory encoded, + uint256 offset + ) internal pure returns (address value, uint256 nextOffset) { + assembly ("memory-safe") { + nextOffset := add(offset, 20) + value := mload(add(encoded, nextOffset)) + } + } +} diff --git a/src/libs/TypeConverter.sol b/src/libs/TypeConverter.sol index 6ff4600..fc89826 100644 --- a/src/libs/TypeConverter.sol +++ b/src/libs/TypeConverter.sol @@ -5,14 +5,37 @@ pragma solidity >=0.8.20 <0.9.0; /// @author M0 Labs /// @notice Utilities for converting between different data types. library TypeConverter { + /// @notice Thrown when a uint256 value exceeds the max uint16 value. + error Uint16Overflow(); + + /// @notice Thrown when a uint256 value exceeds the max uint32 value. + error Uint32Overflow(); + /// @notice Thrown when a uint256 value exceeds the max uint64 value. error Uint64Overflow(); + /// @notice Thrown when a uint256 value exceeds the max uint128 value. error Uint128Overflow(); /// @notice Thrown when a bytes32 value doesn't represent a valid Ethereum address. error InvalidAddress(bytes32 value); + /// @notice Converts a uint256 to uint16, reverting if the value overflows. + /// @param value The uint256 value to convert. + /// @return The uint16 representation of the value. + function toUint16(uint256 value) internal pure returns (uint16) { + if (value > type(uint16).max) revert Uint16Overflow(); + return uint16(value); + } + + /// @notice Converts a uint256 to uint32, reverting if the value overflows. + /// @param value The uint256 value to convert. + /// @return The uint32 representation of the value. + function toUint32(uint256 value) internal pure returns (uint32) { + if (value > type(uint32).max) revert Uint32Overflow(); + return uint32(value); + } + /// @notice Converts a uint256 to uint64, reverting if the value overflows. /// @param value The uint256 value to convert. /// @return The uint64 representation of the value. diff --git a/test/BytesParserTest.t.sol b/test/BytesParserTest.t.sol new file mode 100644 index 0000000..2f7f179 --- /dev/null +++ b/test/BytesParserTest.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import { Test } from "forge-std/Test.sol"; + +import { BytesParser } from "../src/libs/BytesParser.sol"; + +contract BytesParserTest is Test { + using BytesParser for bytes; + + function test_asUint8Unchecked() external pure { + bytes memory data = hex"0203"; + + (uint8 value, uint256 nextOffset) = data.asUint8Unchecked(0); + assertEq(value, 2); + assertEq(nextOffset, 1); + + (value, nextOffset) = data.asUint8Unchecked(nextOffset); + assertEq(value, 3); + assertEq(nextOffset, 2); + } + + function testFuzz_asUint8Unchecked(uint8 inputValue) external pure { + bytes memory data = abi.encodePacked(uint8(inputValue)); + + (uint8 value, uint256 nextOffset) = data.asUint8Unchecked(0); + assertEq(value, inputValue); + assertEq(nextOffset, 1); + } + + function test_asBoolUnchecked() external pure { + bytes memory trueData = abi.encodePacked(true); + bytes memory falseData = abi.encodePacked(false); + + (bool trueValue,) = trueData.asBoolUnchecked(0); + (bool falseValue,) = falseData.asBoolUnchecked(0); + + assertTrue(trueValue); + assertFalse(falseValue); + } + + /// forge-config: default.allow_internal_expect_revert = true + function test_asBoolUnchecked_invalidValue() external { + bytes memory invalidData = abi.encodePacked(uint8(2)); + + vm.expectRevert(abi.encodeWithSelector(BytesParser.InvalidBool.selector, 0x02)); + invalidData.asBoolUnchecked(0); + } + + function test_asUint256Unchecked() external pure { + bytes memory data = abi.encodePacked(uint256(1)); + + (uint256 value, uint256 nextOffset) = data.asUint256Unchecked(0); + assertEq(value, 1); + assertEq(nextOffset, 32); + } + + function testFuzz_asUint256Unchecked(uint256 inputValue) external pure { + bytes memory data = abi.encodePacked(inputValue); + + (uint256 value, uint256 nextOffset) = data.asUint256Unchecked(0); + assertEq(value, inputValue); + assertEq(nextOffset, 32); + } + + function test_asUint128Unchecked() external pure { + bytes memory data = abi.encodePacked(uint128(1)); + + (uint128 value, uint256 nextOffset) = data.asUint128Unchecked(0); + assertEq(value, 1); + assertEq(nextOffset, 16); + } + + function testFuzz_asUint128Unchecked(uint128 inputValue) external pure { + bytes memory data = abi.encodePacked(inputValue); + + (uint128 value, uint256 nextOffset) = data.asUint128Unchecked(0); + assertEq(value, inputValue); + assertEq(nextOffset, 16); + } + + function test_asUint32Unchecked() external pure { + bytes memory data = abi.encodePacked(uint32(1)); + + (uint32 value, uint256 nextOffset) = data.asUint32Unchecked(0); + assertEq(value, 1); + assertEq(nextOffset, 4); + } + + function testFuzz_asUint32Unchecked(uint32 inputValue) external pure { + bytes memory data = abi.encodePacked(inputValue); + + (uint32 value, uint256 nextOffset) = data.asUint32Unchecked(0); + assertEq(value, inputValue); + assertEq(nextOffset, 4); + } + + function test_asBytes32Unchecked() external pure { + bytes memory data = abi.encodePacked(bytes32(uint256(1))); + + (bytes32 value, uint256 nextOffset) = data.asBytes32Unchecked(0); + assertEq(value, bytes32(uint256(1))); + assertEq(nextOffset, 32); + } + + function testFuzz_asBytes32Unchecked(bytes32 inputValue) external pure { + bytes memory data = abi.encodePacked(inputValue); + + (bytes32 value, uint256 nextOffset) = data.asBytes32Unchecked(0); + assertEq(value, inputValue); + assertEq(nextOffset, 32); + } + + function test_asAddressUnchecked() external pure { + bytes memory data = abi.encodePacked(address(1)); + + (address value, uint256 nextOffset) = data.asAddressUnchecked(0); + assertEq(value, address(1)); + assertEq(nextOffset, 20); + } + + function testFuzz_asAddressUnchecked(address inputValue) external pure { + bytes memory data = abi.encodePacked(inputValue); + + (address value, uint256 nextOffset) = data.asAddressUnchecked(0); + assertEq(value, inputValue); + assertEq(nextOffset, 20); + } +} diff --git a/test/TypeConverter.t.sol b/test/TypeConverter.t.sol index 0440ec5..bf53872 100644 --- a/test/TypeConverter.t.sol +++ b/test/TypeConverter.t.sol @@ -6,6 +6,90 @@ import { Test } from "../lib/forge-std/src/Test.sol"; import { TypeConverter } from "../src/libs/TypeConverter.sol"; contract TypeConverterTest is Test { + /////////////////////////////////////////////////////////////////////////// + // toUint16 // + /////////////////////////////////////////////////////////////////////////// + + function test_toUint16_basic() external pure { + uint256 value = 100; + uint16 result = TypeConverter.toUint16(value); + assertEq(result, uint16(100)); + } + + function test_toUint16_maxUint16() external pure { + uint256 value = type(uint16).max; + uint16 result = TypeConverter.toUint16(value); + assertEq(result, type(uint16).max); + } + + function test_toUint16_zero() external pure { + uint256 value = 0; + uint16 result = TypeConverter.toUint16(value); + assertEq(result, 0); + } + + function testFuzz_toUint16(uint16 value) external pure { + uint256 uint256Value = uint256(value); + uint16 result = TypeConverter.toUint16(uint256Value); + assertEq(result, value); + } + + /// forge-config: default.allow_internal_expect_revert = true + function test_toUint16_overflow() external { + uint256 value = uint256(type(uint16).max) + 1; + vm.expectRevert(TypeConverter.Uint16Overflow.selector); + TypeConverter.toUint16(value); + } + + /// forge-config: default.allow_internal_expect_revert = true + function testFuzz_toUint16_overflow(uint256 value) external { + vm.assume(value > type(uint16).max); + vm.expectRevert(TypeConverter.Uint16Overflow.selector); + TypeConverter.toUint16(value); + } + + /////////////////////////////////////////////////////////////////////////// + // toUint32 // + /////////////////////////////////////////////////////////////////////////// + + function test_toUint32_basic() external pure { + uint256 value = 100; + uint32 result = TypeConverter.toUint32(value); + assertEq(result, uint32(100)); + } + + function test_toUint32_maxUint32() external pure { + uint256 value = type(uint32).max; + uint32 result = TypeConverter.toUint32(value); + assertEq(result, type(uint32).max); + } + + function test_toUint32_zero() external pure { + uint256 value = 0; + uint32 result = TypeConverter.toUint32(value); + assertEq(result, 0); + } + + function testFuzz_toUint32(uint32 value) external pure { + uint256 uint256Value = uint256(value); + uint32 result = TypeConverter.toUint32(uint256Value); + assertEq(result, value); + } + + /// forge-config: default.allow_internal_expect_revert = true + function test_toUint32_overflow() external { + uint256 value = uint256(type(uint32).max) + 1; + vm.expectRevert(TypeConverter.Uint32Overflow.selector); + TypeConverter.toUint32(value); + } + + /// forge-config: default.allow_internal_expect_revert = true + function testFuzz_toUint32_overflow(uint256 value) external { + vm.assume(value > type(uint32).max); + vm.expectRevert(TypeConverter.Uint32Overflow.selector); + TypeConverter.toUint32(value); + } + /////////////////////////////////////////////////////////////////////////// // toUint64 // ///////////////////////////////////////////////////////////////////////////