diff --git a/docs/utils/libbytes.md b/docs/utils/libbytes.md index f630c5faa0..173b1cb40b 100644 --- a/docs/utils/libbytes.md +++ b/docs/utils/libbytes.md @@ -89,6 +89,17 @@ function get(BytesStorage storage $) Returns the value stored in `$`. +### uint8At(BytesStorage,uint256) + +```solidity +function uint8At(BytesStorage storage $, uint256 i) + internal + view + returns (uint8 result) +``` + +Returns the uint8 at index `i`. If out-of-bounds, returns 0. + ## Bytes Operations ### replace(bytes,bytes,bytes) diff --git a/docs/utils/libstring.md b/docs/utils/libstring.md index edca63d3aa..8aa1a7437a 100644 --- a/docs/utils/libstring.md +++ b/docs/utils/libstring.md @@ -203,6 +203,17 @@ function get(StringStorage storage $) Returns the value stored in `$`. +### uint8At(StringStorage,uint256) + +```solidity +function uint8At(StringStorage storage $, uint256 i) + internal + view + returns (uint8) +``` + +Returns the uint8 at index `i`. If out-of-bounds, returns 0. + ### bytesStorage(StringStorage) ```solidity diff --git a/package.json b/package.json index a4838b786e..d632b0b5bf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "solady", "license": "MIT", - "version": "0.1.14", + "version": "0.1.15", "description": "Optimized Solidity snippets.", "files": [ "src/**/*.sol", diff --git a/src/utils/LibBytes.sol b/src/utils/LibBytes.sol index cef4fba515..a26bcc6611 100644 --- a/src/utils/LibBytes.sol +++ b/src/utils/LibBytes.sol @@ -123,6 +123,32 @@ library LibBytes { } } + /// @dev Returns the uint8 at index `i`. If out-of-bounds, returns 0. + function uint8At(BytesStorage storage $, uint256 i) internal view returns (uint8 result) { + /// @solidity memory-safe-assembly + assembly { + for { let packed := sload($.slot) } 1 {} { + if iszero(eq(or(packed, 0xff), packed)) { + if iszero(gt(i, 0x1e)) { + result := byte(i, packed) + break + } + if iszero(gt(i, and(0xff, packed))) { + mstore(0x00, $.slot) + let j := sub(i, 0x1f) + result := byte(and(j, 0x1f), sload(add(keccak256(0x00, 0x20), shr(5, j)))) + } + break + } + if iszero(gt(i, shr(8, packed))) { + mstore(0x00, $.slot) + result := byte(and(i, 0x1f), sload(add(keccak256(0x00, 0x20), shr(5, i)))) + } + break + } + } + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* BYTES OPERATIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/src/utils/LibString.sol b/src/utils/LibString.sol index cff5beec78..3d40155025 100644 --- a/src/utils/LibString.sol +++ b/src/utils/LibString.sol @@ -108,6 +108,11 @@ library LibString { return string(LibBytes.get(bytesStorage($))); } + /// @dev Returns the uint8 at index `i`. If out-of-bounds, returns 0. + function uint8At(StringStorage storage $, uint256 i) internal view returns (uint8) { + return LibBytes.uint8At(bytesStorage($), i); + } + /// @dev Helper to cast `$` to a `BytesStorage`. function bytesStorage(StringStorage storage $) internal diff --git a/src/utils/LibZip.sol b/src/utils/LibZip.sol index df208982ef..b72eef0e6a 100644 --- a/src/utils/LibZip.sol +++ b/src/utils/LibZip.sol @@ -167,36 +167,49 @@ library LibZip { function cdCompress(bytes memory data) internal pure returns (bytes memory result) { /// @solidity memory-safe-assembly assembly { - function rle(v_, o_, d_) -> _o, _d { - mstore(o_, shl(240, or(and(0xff, add(d_, 0xff)), and(0x80, v_)))) - _o := add(o_, 2) + function countLeadingZeroBytes(x_) -> _r { + _r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x_)) + _r := or(_r, shl(6, lt(0xffffffffffffffff, shr(_r, x_)))) + _r := or(_r, shl(5, lt(0xffffffff, shr(_r, x_)))) + _r := or(_r, shl(4, lt(0xffff, shr(_r, x_)))) + _r := xor(31, or(shr(3, _r), lt(0xff, shr(_r, x_)))) + } + function min(x_, y_) -> _z { + _z := xor(x_, mul(xor(x_, y_), lt(y_, x_))) } result := mload(0x40) let o := add(result, 0x20) - let z := 0 // Number of consecutive 0x00. - let y := 0 // Number of consecutive 0xff. for { let end := add(data, mload(data)) } iszero(eq(data, end)) {} { data := add(data, 1) let c := byte(31, mload(data)) if iszero(c) { - if y { o, y := rle(0xff, o, y) } - z := add(z, 1) - if eq(z, 0x80) { o, z := rle(0x00, o, 0x80) } + let z := 0 + for {} 1 {} { + let r := 0x20 + let x := mload(add(data, r)) + if x { r := countLeadingZeroBytes(x) } + r := min(min(sub(end, data), r), sub(0x7f, z)) + data := add(data, r) + z := add(z, r) + if iszero(gt(r, 0x1f)) { break } + } + mstore(o, shl(240, z)) + o := add(o, 2) continue } if eq(c, 0xff) { - if z { o, z := rle(0x00, o, z) } - y := add(y, 1) - if eq(y, 0x20) { o, y := rle(0xff, o, 0x20) } + let r := 0x20 + let x := not(mload(add(data, r))) + if x { r := countLeadingZeroBytes(x) } + r := min(min(sub(end, data), r), 0x1f) + data := add(data, r) + mstore(o, shl(240, or(r, 0x80))) + o := add(o, 2) continue } - if y { o, y := rle(0xff, o, y) } - if z { o, z := rle(0x00, o, z) } mstore8(o, c) o := add(o, 1) } - if y { o, y := rle(0xff, o, y) } - if z { o, z := rle(0x00, o, z) } // Bitwise negate the first 4 bytes. mstore(add(result, 4), not(mload(add(result, 4)))) mstore(result, sub(o, add(result, 0x20))) // Store the length. diff --git a/src/utils/g/LibBytes.sol b/src/utils/g/LibBytes.sol index acaf1c9954..84fa661f0d 100644 --- a/src/utils/g/LibBytes.sol +++ b/src/utils/g/LibBytes.sol @@ -127,6 +127,32 @@ library LibBytes { } } + /// @dev Returns the uint8 at index `i`. If out-of-bounds, returns 0. + function uint8At(BytesStorage storage $, uint256 i) internal view returns (uint8 result) { + /// @solidity memory-safe-assembly + assembly { + for { let packed := sload($.slot) } 1 {} { + if iszero(eq(or(packed, 0xff), packed)) { + if iszero(gt(i, 0x1e)) { + result := byte(i, packed) + break + } + if iszero(gt(i, and(0xff, packed))) { + mstore(0x00, $.slot) + let j := sub(i, 0x1f) + result := byte(and(j, 0x1f), sload(add(keccak256(0x00, 0x20), shr(5, j)))) + } + break + } + if iszero(gt(i, shr(8, packed))) { + mstore(0x00, $.slot) + result := byte(and(i, 0x1f), sload(add(keccak256(0x00, 0x20), shr(5, i)))) + } + break + } + } + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* BYTES OPERATIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/src/utils/g/LibString.sol b/src/utils/g/LibString.sol index 6ca1ed1ab2..8dd862bd3f 100644 --- a/src/utils/g/LibString.sol +++ b/src/utils/g/LibString.sol @@ -112,6 +112,11 @@ library LibString { return string(LibBytes.get(bytesStorage($))); } + /// @dev Returns the uint8 at index `i`. If out-of-bounds, returns 0. + function uint8At(StringStorage storage $, uint256 i) internal view returns (uint8) { + return LibBytes.uint8At(bytesStorage($), i); + } + /// @dev Helper to cast `$` to a `BytesStorage`. function bytesStorage(StringStorage storage $) internal diff --git a/test/LibString.t.sol b/test/LibString.t.sol index e761351cb0..9afbdf0b39 100644 --- a/test/LibString.t.sol +++ b/test/LibString.t.sol @@ -1678,6 +1678,23 @@ contract LibStringTest is SoladyTest { } } + function testUint8AtStringStorage(bytes calldata s, uint256 i) public { + LibString.setCalldata(_getStringStorage(0), string(s)); + uint8 retrieved = LibString.uint8At(_getStringStorage(0), i); + assertEq(retrieved, i < s.length ? uint8(s[i]) : 0); + } + + function testUint8AtStringStorage(bytes32) public { + uint256 i = _bound(_random(), 0, 1000); + this.testUint8AtStringStorage(_randomBytes(), i); + } + + function testUint8AtStringStorage() public { + this.testUint8AtStringStorage(hex"1122334455", 2); + this.testUint8AtStringStorage(new bytes(200), 2); + this.testUint8AtStringStorage(new bytes(500), 2); + } + function _lowerOriginal(string memory subject) internal pure returns (string memory result) { unchecked { uint256 n = bytes(subject).length; diff --git a/test/LibZip.t.sol b/test/LibZip.t.sol index 897fdf08f8..fdd8ba5f8b 100644 --- a/test/LibZip.t.sol +++ b/test/LibZip.t.sol @@ -7,11 +7,112 @@ import {LibClone} from "../src/utils/LibClone.sol"; import {ERC1967Factory} from "../src/utils/ERC1967Factory.sol"; import {LibString} from "../src/utils/LibString.sol"; import {DynamicBufferLib} from "../src/utils/DynamicBufferLib.sol"; +import {LibBytes} from "../src/utils/LibBytes.sol"; import {LibZip} from "../src/utils/LibZip.sol"; contract LibZipTest is SoladyTest { + using LibBytes for LibBytes.BytesStorage; using DynamicBufferLib for DynamicBufferLib.DynamicBuffer; + LibBytes.BytesStorage internal _bytesStorage; + + struct ABC { + uint256 a; + uint256 b; + uint256 c; + } + + struct ABCPacked { + uint32 a; + uint64 b; + uint32 c; + } + + uint256 internal constant _A = 0x112233; + uint256 internal constant _B = 0x0102030405060708; + uint256 internal constant _C = 0xf1f2f3; + + ABC internal _abc; + ABCPacked internal _abcPacked; + + bytes internal constant _CD_COMPRESS_INPUT = + hex"00000000000000000000000000000000000000000000000000000000000ae11c0000000000000000000000000000000000000000000000000000002b9cdca0ab0000000000000000000000000000000000003961790f8baa365051889e4c367d00000000000000000000000000000000000026d85539440bc844167ac0cc42320000000000000000000000000000000000000000000000007b55939986433925"; + + function testCdCompressGas() public { + bytes memory data = _CD_COMPRESS_INPUT; + assertLt(LibZip.cdCompress(data).length, data.length); + } + + function testCdCompressOriginalGas() public { + bytes memory data = _CD_COMPRESS_INPUT; + assertLt(_cdCompressOriginal(data).length, data.length); + } + + function testStoreABCWithCdCompressGas() public { + _bytesStorage.set(LibZip.cdCompress(abi.encode(_A, _B, _C))); + } + + function testStoreABCWithCdCompressOriginalGas() public { + _bytesStorage.set(_cdCompressOriginal(abi.encode(_A, _B, _C))); + } + + function testStoreABCWithFlzCompressGas() public { + _bytesStorage.set(LibZip.flzCompress(abi.encode(_A, _B, _C))); + } + + function testStoreABCGas() public { + _abc.a = _A; + _abc.b = _B; + _abc.c = _C; + } + + function testStoreABCPackedGas() public { + _abcPacked.a = uint32(_A); + _abcPacked.b = uint64(_B); + _abcPacked.c = uint32(_C); + } + + function _cdCompressOriginal(bytes memory data) internal pure returns (bytes memory result) { + /// @solidity memory-safe-assembly + assembly { + function rle(v_, o_, d_) -> _o, _d { + mstore(o_, shl(240, or(and(0xff, add(d_, 0xff)), and(0x80, v_)))) + _o := add(o_, 2) + } + result := mload(0x40) + let o := add(result, 0x20) + let z := 0 // Number of consecutive 0x00. + let y := 0 // Number of consecutive 0xff. + for { let end := add(data, mload(data)) } iszero(eq(data, end)) {} { + data := add(data, 1) + let c := byte(31, mload(data)) + if iszero(c) { + if y { o, y := rle(0xff, o, y) } + z := add(z, 1) + if eq(z, 0x80) { o, z := rle(0x00, o, 0x80) } + continue + } + if eq(c, 0xff) { + if z { o, z := rle(0x00, o, z) } + y := add(y, 1) + if eq(y, 0x20) { o, y := rle(0xff, o, 0x20) } + continue + } + if y { o, y := rle(0xff, o, y) } + if z { o, z := rle(0x00, o, z) } + mstore8(o, c) + o := add(o, 1) + } + if y { o, y := rle(0xff, o, y) } + if z { o, z := rle(0x00, o, z) } + // Bitwise negate the first 4 bytes. + mstore(add(result, 4), not(mload(add(result, 4)))) + mstore(result, sub(o, add(result, 0x20))) // Store the length. + mstore(o, 0) // Zeroize the slot after the string. + mstore(0x40, add(o, 0x20)) // Allocate the memory. + } + } + function testFlzCompressDecompress() public brutalizeMemory { assertEq(LibZip.flzCompress(""), ""); assertEq(LibZip.flzDecompress(""), ""); @@ -110,6 +211,17 @@ contract LibZipTest is SoladyTest { } } } + if (_randomChance(16)) { + uint256 r = _randomUniform(); + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, r) + for { let i := 0 } lt(i, n) { i := add(i, 0x20) } { + mstore(0x20, i) + if and(1, keccak256(0x00, 0x40)) { mstore(add(add(data, 0x20), i), 0) } + } + } + } if (n != 0) { uint256 m = _random() % 8; for (uint256 j; j < m; ++j) {