diff --git a/foundry/lib/bytes/LibPrefixLengthEncodedByteArray.sol b/foundry/lib/bytes/LibPrefixLengthEncodedByteArray.sol new file mode 100644 index 0000000..b5773fd --- /dev/null +++ b/foundry/lib/bytes/LibPrefixLengthEncodedByteArray.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +/** + * @title Propellerheads PrefixLengthEncoded Byte Array Library + * @author PropellerHeads Developers + * @dev Provide a gas efficient encoding for bytes array. + * + * Array of bytes are encoded as a single bytes like this : + * 16 bits ??bits 16bits ??bits ... + * [length(elem1)] [elem1] [length(elem2)] [elem2] ... + * + * This is much more efficient and compact than default solidity encoding. + */ +library LibPrefixLengthEncodedByteArray { + /** + * @dev Pop the first element of an array and returns it with the remaining data. + */ + function next(bytes calldata encoded) + internal + pure + returns (bytes calldata elem, bytes calldata res) + { + assembly { + switch iszero(encoded.length) + case 1 { + elem.offset := 0 + elem.length := 0 + res.offset := 0 + res.length := 0 + } + default { + let l := shr(240, calldataload(encoded.offset)) + elem.offset := add(encoded.offset, 2) + elem.length := l + res.offset := add(elem.offset, l) + res.length := sub(sub(encoded.length, l), 2) + } + } + } + + /** + * @dev Gets the size of the encoded array. + */ + function size(bytes calldata encoded) internal pure returns (uint256 s) { + assembly { + let offset := encoded.offset + let end := add(encoded.offset, encoded.length) + for {} lt(offset, end) {} { + offset := add(offset, add(shr(240, calldataload(offset)), 2)) + s := add(s, 1) + } + } + } + + /** + * @dev Cast an encoded array into a Solidity array. + */ + function toArray(bytes calldata encoded) + internal + pure + returns (bytes[] memory arr) + { + bytes calldata elem; + uint256 idx = 0; + arr = new bytes[](LibPrefixLengthEncodedByteArray.size(encoded)); + while (encoded.length > 0) { + (elem, encoded) = LibPrefixLengthEncodedByteArray.next(encoded); + arr[idx] = elem; + idx++; + } + } +} diff --git a/foundry/test/LibPrefixLengthEncodedByteArray.t.sol b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol new file mode 100644 index 0000000..c06eb47 --- /dev/null +++ b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {LibPrefixLengthEncodedByteArray} from + "../lib/bytes/LibPrefixLengthEncodedByteArray.sol"; + +contract LibPrefixLengthEncodedByteArrayTest is Test { + using LibPrefixLengthEncodedByteArray for bytes; + + function testNextEmpty() public { + bytes memory encoded = ""; + (bytes memory elem, bytes memory remaining) = this.next(encoded); + assertEq(elem.length, 0); + assertEq(remaining.length, 0); + } + + function testNextSingleElement() public { + // Create encoded data: length prefix (0003) followed by "ABC" + bytes memory encoded = hex"0003414243"; + (bytes memory elem, bytes memory remaining) = this.next(encoded); + + assertEq(elem.length, 3); + assertEq(elem, hex"414243"); // "ABC" + assertEq(remaining.length, 0); + } + + function testNextMultipleElements() public { + // Encoded data: [0003]ABC[0002]DE + bytes memory encoded = hex"000341424300024445"; + + // First next() + (bytes memory elem1, bytes memory remaining1) = this.next(encoded); + assertEq(elem1, hex"414243"); // "ABC" + assertEq(remaining1, hex"00024445"); + + // Second next() + (bytes memory elem2, bytes memory remaining2) = this.next(remaining1); + assertEq(elem2, hex"4445"); // "DE" + assertEq(remaining2.length, 0); + } + + function testSize() public { + bytes memory empty = ""; + assertEq(this.size(empty), 0); + + bytes memory single = hex"0003414243"; + assertEq(this.size(single), 1); + + bytes memory multiple = hex"0003414243000244450001FF"; + assertEq(this.size(multiple), 3); + } + + function testFailInvalidLength() public { + // Length prefix larger than remaining data + bytes memory invalid = hex"0004414243"; + (bytes memory elem, bytes memory remaining) = this.next(invalid); + } + + function testFailIncompletePrefix() public { + // Only 1 byte instead of 2 bytes prefix + bytes memory invalid = hex"01"; + (bytes memory elem, bytes memory remaining) = this.next(invalid); + } + + function testLargeElement() public { + // Test with a large but manageable size (1000 bytes) + bytes memory large = new bytes(1002); // 2 bytes prefix + 1000 bytes data + large[0] = bytes1(uint8(0x03)); // 03 + large[1] = bytes1(uint8(0xe8)); // E8 (1000 in hex) + + // Fill data bytes + for (uint256 i = 2; i < large.length; i++) { + large[i] = bytes1(uint8(0x01)); + } + + (bytes memory elem, bytes memory remaining) = this.next(large); + assertEq(elem.length, 1000); + assertEq(remaining.length, 0); + } + + function testSizeWithLargeElements() public { + // Two elements: 1000 bytes + 500 bytes + bytes memory data = new bytes(1504); // 1000 + 2 + 500 + 2 + + // First element (1000 bytes) + data[0] = bytes1(uint8(0x03)); // 03 + data[1] = bytes1(uint8(0xe8)); // E8 (1000 in hex) + + // Second element (500 bytes) + data[1002] = bytes1(uint8(0x01)); // 01 + data[1003] = bytes1(uint8(0xf4)); // F4 (500 in hex) + + assertEq(this.size(data), 2); + } + + function next(bytes calldata data) + external + pure + returns (bytes memory elem, bytes memory remaining) + { + (elem, remaining) = data.next(); + } + + function size(bytes calldata data) external pure returns (uint256 s) { + s = data.size(); + } +}