Merge pull request #21 from propeller-heads/router/hr/ENG-4056-Bytes-Array-Conversion

feat: add LibPrefixLengthEncodedByteArray
This commit is contained in:
Harsh Vardhan Roy
2025-01-23 20:12:38 +05:30
committed by GitHub
2 changed files with 181 additions and 0 deletions

View 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++;
}
}
}

View 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();
}
}