Merge pull request #21 from propeller-heads/router/hr/ENG-4056-Bytes-Array-Conversion
feat: add LibPrefixLengthEncodedByteArray
This commit is contained in:
73
foundry/lib/bytes/LibPrefixLengthEncodedByteArray.sol
Normal file
73
foundry/lib/bytes/LibPrefixLengthEncodedByteArray.sol
Normal file
@@ -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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
foundry/test/LibPrefixLengthEncodedByteArray.t.sol
Normal file
108
foundry/test/LibPrefixLengthEncodedByteArray.t.sol
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user