diff --git a/src/utils/LibBytes.sol b/src/utils/LibBytes.sol index 62b65f43a..ae3fad927 100644 --- a/src/utils/LibBytes.sol +++ b/src/utils/LibBytes.sol @@ -22,6 +22,114 @@ library LibBytes { /// @dev The constant returned when the `search` is not found in the bytes. uint256 internal constant NOT_FOUND = type(uint256).max; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* NATIVE BYTES OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Sets the value of the bytes array storage reference `$` to `s`. + /// A bytes array in memory cannot be assigned to a local bytes array storage reference directly. + function set(bytes storage $, bytes memory s) internal { + /// @solidity memory-safe-assembly + assembly { + let len := mload(s) + let packed := shl(1, len) + for {} 1 {} { + if iszero(gt(len, 0x1f)) { + let right_aligned := mload(add(s, len)) + let zeros := shl(3, sub(0x20, len)) + let left_aligned := shl(zeros, right_aligned) + packed := or(packed, left_aligned) + break + } + packed := or(packed, 1) + mstore(0x00, $.slot) + let ptr := keccak256(0x00, 0x20) + // the number of words minus 1 + let words := shr(5, sub(len, 1)) + let end := add(ptr, words) + let o := add(s, 0x20) + for {} 1 {} { + if eq(ptr, end) { break } + sstore(ptr, mload(o)) + ptr := add(ptr, 1) + o := add(o, 0x20) + } + // clean and store the last word + let right_aligned := mload(add(s, len)) + let zeros := shl(3, sub(sub(o, s), len)) + let left_aligned := shl(zeros, right_aligned) + sstore(ptr, left_aligned) + break + } + sstore($.slot, packed) + } + } + + /// @dev Sets the value of the bytes array storage reference `$` to `s`. + /// A bytes array in calldata cannot be assigned to a local bytes array storage reference directly. + function setCalldata(bytes storage $, bytes calldata s) internal { + /// @solidity memory-safe-assembly + assembly { + let len_ptr := sub(s.offset, 0x20) + let packed := shl(1, s.length) + for {} 1 {} { + if iszero(gt(s.length, 0x1f)) { + let right_aligned := calldataload(add(len_ptr, s.length)) + let zeros := shl(3, sub(0x20, s.length)) + let left_aligned := shl(zeros, right_aligned) + packed := or(packed, left_aligned) + break + } + packed := or(packed, 1) + mstore(0x00, $.slot) + let ptr := keccak256(0x00, 0x20) + // the number of words minus 1 + let words := shr(5, sub(s.length, 1)) + let end := add(ptr, words) + let o := s.offset + for {} 1 {} { + if eq(ptr, end) { break } + sstore(ptr, calldataload(o)) + ptr := add(ptr, 1) + o := add(o, 0x20) + } + // clean and store the last word + let right_aligned := calldataload(add(len_ptr, s.length)) + let zeros := shl(3, sub(sub(o, len_ptr), s.length)) + let left_aligned := shl(zeros, right_aligned) + sstore(ptr, left_aligned) + break + } + sstore($.slot, packed) + } + } + + /// @dev Deletes a bytes array from storage. + /// The `delete` keyword is not applicable to local bytes array storage references. + function delete_(bytes storage $) internal { + /// @solidity memory-safe-assembly + assembly { + let packed := sload($.slot) + let is_long_string := and(packed, 1) + for {} 1 {} { + sstore($.slot, 0) + if iszero(is_long_string) { break } + mstore(0, $.slot) + let ptr := keccak256(0x00, 0x20) + let len := shr(1, packed) + // the number of words used to store the string + let words := shr(5, add(len, 0x1f)) + let end := add(ptr, words) + for {} 1 {} { + sstore(ptr, 0) + ptr := add(ptr, 1) + if eq(ptr, end) { break } + } + break + } + } + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* BYTE STORAGE OPERATIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/src/utils/LibString.sol b/src/utils/LibString.sol index 3bfc96b3c..b4ca49fd6 100644 --- a/src/utils/LibString.sol +++ b/src/utils/LibString.sol @@ -74,6 +74,36 @@ library LibString { /// @dev Lookup for ' \t\n\r\x0b\x0c'. uint128 internal constant WHITESPACE_7_BIT_ASCII = 0x100003e00; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* NATIVE STRING OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Sets the value of the string storage reference `$` to `s`. + /// A string in memory cannot be assigned to a local string storage reference directly. + function set(string storage $, string memory s) internal { + LibBytes.set(bytesStorage($), bytes(s)); + } + + /// @dev Sets the value of the string storage reference `$` to `s`. + /// A string in calldata cannot be assigned to a local string storage reference directly. + function setCalldata(string storage $, string calldata s) internal { + LibBytes.setCalldata(bytesStorage($), bytes(s)); + } + + /// @dev Deletes a string from storage. + /// The `delete` keyword is not applicable to local string storage references. + function delete_(string storage $) internal { + LibBytes.delete_(bytesStorage($)); + } + + /// @dev Helper to cast `$` to a `bytes`. + function bytesStorage(string storage $) internal pure returns (bytes storage casted) { + /// @solidity memory-safe-assembly + assembly { + casted.slot := $.slot + } + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* STRING STORAGE OPERATIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/src/utils/g/LibBytes.sol b/src/utils/g/LibBytes.sol index 7d3b100a4..060c251da 100644 --- a/src/utils/g/LibBytes.sol +++ b/src/utils/g/LibBytes.sol @@ -26,6 +26,114 @@ library LibBytes { /// @dev The constant returned when the `search` is not found in the bytes. uint256 internal constant NOT_FOUND = type(uint256).max; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* NATIVE BYTES OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Sets the value of the bytes array storage reference `$` to `s`. + /// A bytes array in memory cannot be assigned to a local bytes array storage reference directly. + function set(bytes storage $, bytes memory s) internal { + /// @solidity memory-safe-assembly + assembly { + let len := mload(s) + let packed := shl(1, len) + for {} 1 {} { + if iszero(gt(len, 0x1f)) { + let right_aligned := mload(add(s, len)) + let zeros := shl(3, sub(0x20, len)) + let left_aligned := shl(zeros, right_aligned) + packed := or(packed, left_aligned) + break + } + packed := or(packed, 1) + mstore(0x00, $.slot) + let ptr := keccak256(0x00, 0x20) + // the number of words minus 1 + let words := shr(5, sub(len, 1)) + let end := add(ptr, words) + let o := add(s, 0x20) + for {} 1 {} { + if eq(ptr, end) { break } + sstore(ptr, mload(o)) + ptr := add(ptr, 1) + o := add(o, 0x20) + } + // clean and store the last word + let right_aligned := mload(add(s, len)) + let zeros := shl(3, sub(sub(o, s), len)) + let left_aligned := shl(zeros, right_aligned) + sstore(ptr, left_aligned) + break + } + sstore($.slot, packed) + } + } + + /// @dev Sets the value of the bytes array storage reference `$` to `s`. + /// A bytes array in calldata cannot be assigned to a local bytes array storage reference directly. + function setCalldata(bytes storage $, bytes calldata s) internal { + /// @solidity memory-safe-assembly + assembly { + let len_ptr := sub(s.offset, 0x20) + let packed := shl(1, s.length) + for {} 1 {} { + if iszero(gt(s.length, 0x1f)) { + let right_aligned := calldataload(add(len_ptr, s.length)) + let zeros := shl(3, sub(0x20, s.length)) + let left_aligned := shl(zeros, right_aligned) + packed := or(packed, left_aligned) + break + } + packed := or(packed, 1) + mstore(0x00, $.slot) + let ptr := keccak256(0x00, 0x20) + // the number of words minus 1 + let words := shr(5, sub(s.length, 1)) + let end := add(ptr, words) + let o := s.offset + for {} 1 {} { + if eq(ptr, end) { break } + sstore(ptr, calldataload(o)) + ptr := add(ptr, 1) + o := add(o, 0x20) + } + // clean and store the last word + let right_aligned := calldataload(add(len_ptr, s.length)) + let zeros := shl(3, sub(sub(o, len_ptr), s.length)) + let left_aligned := shl(zeros, right_aligned) + sstore(ptr, left_aligned) + break + } + sstore($.slot, packed) + } + } + + /// @dev Deletes a bytes array from storage. + /// The `delete` keyword is not applicable to local bytes array storage references. + function delete_(bytes storage $) internal { + /// @solidity memory-safe-assembly + assembly { + let packed := sload($.slot) + let is_long_string := and(packed, 1) + for {} 1 {} { + sstore($.slot, 0) + if iszero(is_long_string) { break } + mstore(0, $.slot) + let ptr := keccak256(0x00, 0x20) + let len := shr(1, packed) + // the number of words used to store the string + let words := shr(5, add(len, 0x1f)) + let end := add(ptr, words) + for {} 1 {} { + sstore(ptr, 0) + ptr := add(ptr, 1) + if eq(ptr, end) { break } + } + break + } + } + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* BYTE STORAGE OPERATIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/src/utils/g/LibString.sol b/src/utils/g/LibString.sol index e89faec06..d56a4c05b 100644 --- a/src/utils/g/LibString.sol +++ b/src/utils/g/LibString.sol @@ -78,6 +78,36 @@ library LibString { /// @dev Lookup for ' \t\n\r\x0b\x0c'. uint128 internal constant WHITESPACE_7_BIT_ASCII = 0x100003e00; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* NATIVE STRING OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Sets the value of the string storage reference `$` to `s`. + /// A string in memory cannot be assigned to a local string storage reference directly. + function set(string storage $, string memory s) internal { + LibBytes.set(bytesStorage($), bytes(s)); + } + + /// @dev Sets the value of the string storage reference `$` to `s`. + /// A string in calldata cannot be assigned to a local string storage reference directly. + function setCalldata(string storage $, string calldata s) internal { + LibBytes.setCalldata(bytesStorage($), bytes(s)); + } + + /// @dev Deletes a string from storage. + /// The `delete` keyword is not applicable to local string storage references. + function delete_(string storage $) internal { + LibBytes.delete_(bytesStorage($)); + } + + /// @dev Helper to cast `$` to a `bytes`. + function bytesStorage(string storage $) internal pure returns (bytes storage casted) { + /// @solidity memory-safe-assembly + assembly { + casted.slot := $.slot + } + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* STRING STORAGE OPERATIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/test/LibString.t.sol b/test/LibString.t.sol index e761351cb..f5c18cfca 100644 --- a/test/LibString.t.sol +++ b/test/LibString.t.sol @@ -12,6 +12,14 @@ contract SimpleStringSetAndGet { } } +contract SimpleStringSetAndGetWithNativeStorageString { + string public x; + + function setX(string calldata x_) public { + LibString.setCalldata(x, x_); + } +} + contract SimpleStringSetAndGetWithStringStorage { LibString.StringStorage internal _x; @@ -25,8 +33,19 @@ contract SimpleStringSetAndGetWithStringStorage { } contract LibStringTest is SoladyTest { + function testDelete() public brutalizeMemory { + string storage $ = _getNativeStorageString(); + _set($, "Milady"); + _testDelete($); + _set($, "MiladyMiladyMiladyMiladyMiladyMiladyMilady"); + _testDelete($); + } + function testSimpleStringSetAndGetGas() public { _testSimpleStringSetAndGet(new SimpleStringSetAndGet()); + _testSimpleStringSetAndGet( + SimpleStringSetAndGet(address(new SimpleStringSetAndGetWithNativeStorageString())) + ); _testSimpleStringSetAndGet( SimpleStringSetAndGet(address(new SimpleStringSetAndGetWithStringStorage())) ); @@ -336,10 +355,10 @@ contract LibStringTest is SoladyTest { let c0 := byte(0, mload(p)) let c1 := byte(1, mload(p)) if and(gt(c1, 58), gt(and(temp, 15), 7)) { - mstore8(add(p, 1), sub(c1, 32)) + mstore8(add(p, 1), sub(c1, 32)) } if and(gt(c0, 58), gt(shr(4, temp), 7)) { - mstore8(p, sub(c0, 32)) + mstore8(p, sub(c0, 32)) } } } @@ -1580,6 +1599,19 @@ contract LibStringTest is SoladyTest { return LibString.toSmallString(s); } + function testSetAndGetNativeStorageString() public { + string memory emptyString; + _testSetAndGetNativeStorageString(emptyString); + _testSetAndGetNativeStorageString(""); + _testSetAndGetNativeStorageString("a"); + _testSetAndGetNativeStorageString("ab"); + unchecked { + for (uint256 i = 0; i != 300; ++i) { + _testSetAndGetNativeStorageString(_randomUniformString(i), false); + } + } + } + function testSetAndGetStringStorage() public { string memory emptyString; _testSetAndGetStringStorage(emptyString); @@ -1593,6 +1625,20 @@ contract LibStringTest is SoladyTest { } } + function testSetAndGetNativeStorageString(bytes32) public { + vm.pauseGasMetering(); + if (_randomChance(32)) { + assertEq(bytes(_getNativeStorageString()).length, 0); + assertEq(_get(_getNativeStorageString()), ""); + } + if (_randomChance(2)) _testSetAndGetNativeStorageString(string(_randomBytes())); + if (_randomChance(16)) _testSetAndGetNativeStorageString(string(_randomBytes())); + if (_randomChance(32)) { + _testSetAndGetNativeStorageString(_randomUniformString(_randomUniform() & 0xfff)); + } + vm.resumeGasMetering(); + } + function testSetAndGetStringStorage(bytes32) public { vm.pauseGasMetering(); if (_randomChance(32)) { @@ -1608,19 +1654,51 @@ contract LibStringTest is SoladyTest { vm.resumeGasMetering(); } + function testSetAndGetNativeStorageString2(string memory s) public { + _testSetAndGetNativeStorageString(s); + } + function testSetAndGetStringStorage2(string memory s) public { _testSetAndGetStringStorage(s); } + function testSetAndGetNativeStorageStringCalldata(string calldata s) public { + LibString.setCalldata(_getNativeStorageString(), s); + assertEq(_getNativeStorageString(), s); + } + function testSetAndGetStringStorageCalldata(string calldata s) public { LibString.setCalldata(_getStringStorage(), s); assertEq(LibString.get(_getStringStorage()), s); } + function _testSetAndGetNativeStorageString(string memory s) internal { + _testSetAndGetNativeStorageString(s, _randomChance(8)); + } + function _testSetAndGetStringStorage(string memory s) internal { _testSetAndGetStringStorage(s, _randomChance(8)); } + function _testSetAndGetNativeStorageString(string memory s0, bool writeTo1) internal { + _set(_getNativeStorageString(0), s0); + string memory s1; + if (writeTo1) { + s1 = string(_randomBytes()); + _set(_getNativeStorageString(1), s1); + if (_randomChance(16)) { + _misalignFreeMemoryPointer(); + _brutalizeMemory(); + } + } + assertEq(_get(_getNativeStorageString(0)), s0); + if (writeTo1) { + assertEq(_get(_getNativeStorageString(1)), s1); + if (_randomChance(16)) _testDelete(_getNativeStorageString(0)); + if (_randomChance(16)) _testDelete(_getNativeStorageString(1)); + } + } + function _testSetAndGetStringStorage(string memory s0, bool writeTo1) internal { _set(_getStringStorage(0), s0); string memory s1; @@ -1640,6 +1718,34 @@ contract LibStringTest is SoladyTest { } } + function _testDelete(string storage $) internal { + uint256 length = bytes($).length; + LibString.delete_($); + assertEq($, ""); + assertEq(bytes($).length, 0); + uint256 packed; + assembly { + packed := sload($.slot) + } + assertEq(packed, 0, "Expected the length slot to be zero"); + if (length >= 32) { + uint256 p; + /// @solidity memory-safe-assembly + assembly { + mstore(0, $.slot) + p := keccak256(0, 0x20) + } + uint256 words = (length + 31) / 32; + for (uint256 i; i < words; ++i) { + uint256 word; + assembly { + word := sload(add(p, i)) + } + assertEq(word, 0, "Expected every word to be zero"); + } + } + } + function _testClear(LibString.StringStorage storage $) internal { if (_randomChance(2)) { LibString.clear($); @@ -1650,12 +1756,23 @@ contract LibStringTest is SoladyTest { assertTrue(LibString.isEmpty($)); } + function _set(string storage $, string memory s) internal { + LibString.set($, s); + assertEq(bytes($).length, bytes(s).length); + } + function _set(LibString.StringStorage storage $, string memory s) internal { LibString.set($, s); assertEq(LibString.length($), bytes(s).length); assertEq(LibString.isEmpty($), bytes(s).length == 0); } + function _get(string storage $) internal returns (string memory result) { + result = $; + _checkMemory(result); + assertEq(bytes($).length, bytes(result).length); + } + function _get(LibString.StringStorage storage $) internal returns (string memory result) { result = LibString.get($); _checkMemory(result); @@ -1663,10 +1780,21 @@ contract LibStringTest is SoladyTest { assertEq(LibString.length($), bytes(result).length); } + function _getNativeStorageString() internal pure returns (string storage) { + return _getNativeStorageString(0); + } + function _getStringStorage() internal pure returns (LibString.StringStorage storage) { return _getStringStorage(0); } + function _getNativeStorageString(uint256 o) internal pure returns (string storage $) { + /// @solidity memory-safe-assembly + assembly { + $.slot := add(0x39be4c398aefe47a0e, o) + } + } + function _getStringStorage(uint256 o) internal pure