diff --git a/.changeset/khaki-hats-leave.md b/.changeset/khaki-hats-leave.md new file mode 100644 index 00000000000..021df0ff083 --- /dev/null +++ b/.changeset/khaki-hats-leave.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Bytes`: Add a `nibbles` function to split each byte into two nibbles. diff --git a/.changeset/ten-steaks-try.md b/.changeset/ten-steaks-try.md new file mode 100644 index 00000000000..a734f5fdb45 --- /dev/null +++ b/.changeset/ten-steaks-try.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Bytes`: Add an `equal` function to compare byte buffers. diff --git a/.changeset/whole-cats-find.md b/.changeset/whole-cats-find.md new file mode 100644 index 00000000000..e5ba8df6e5d --- /dev/null +++ b/.changeset/whole-cats-find.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Bytes`: Add a `clz` function to count the leading zero bytes in a `uint256` value. diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index b76198e8032..42d9402726d 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -99,6 +99,28 @@ library Bytes { return result; } + /// @dev Split each byte in `value` into two nibbles (4 bits each). + function nibbles(bytes memory value) internal pure returns (bytes memory) { + uint256 length = value.length; + bytes memory nibbles_ = new bytes(length * 2); + for (uint256 i = 0; i < length; i++) { + (nibbles_[i * 2], nibbles_[i * 2 + 1]) = (value[i] & 0xf0, value[i] & 0x0f); + } + return nibbles_; + } + + /** + * @dev Returns true if the two byte buffers are equal. + */ + function equal(bytes memory a, bytes memory b) internal pure returns (bool) { + return a.length == b.length && keccak256(a) == keccak256(b); + } + + /// @dev Counts the number of leading zeros in a uint256. + function clz(uint256 x) internal pure returns (uint256) { + return Math.ternary(x == 0, 32, 31 - Math.log256(x)); + } + /** * @dev Moves the content of `buffer`, from `start` (included) to the end of `buffer` to the start of that buffer. * diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index c67bff5a1b1..bebaaa0a691 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -6,6 +6,7 @@ pragma solidity ^0.8.20; import {Math} from "./math/Math.sol"; import {SafeCast} from "./math/SafeCast.sol"; import {SignedMath} from "./math/SignedMath.sol"; +import {Bytes} from "./Bytes.sol"; /** * @dev String operations. @@ -149,7 +150,7 @@ library Strings { * @dev Returns true if the two strings are equal. */ function equal(string memory a, string memory b) internal pure returns (bool) { - return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); + return Bytes.equal(bytes(a), bytes(b)); } /** diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol index be0bf783e95..690b06d2fed 100644 --- a/test/utils/Bytes.t.sol +++ b/test/utils/Bytes.t.sol @@ -81,6 +81,64 @@ contract BytesTest is Test { } } + function testIndexOf(bytes memory buffer, bytes1 s) public pure { + testIndexOf(buffer, s, 0); + } + + function testIndexOf(bytes memory buffer, bytes1 s, uint256 pos) public pure { + uint256 result = Bytes.indexOf(buffer, s, pos); + + // Should not be found before result + for (uint256 i = pos; result != type(uint256).max && i < result; i++) assertNotEq(buffer[i], s); + if (result != type(uint256).max) assertEq(buffer[result], s); + } + + function testLastIndexOf(bytes memory buffer, bytes1 s) public pure { + testLastIndexOf(buffer, s, 0); + } + + function testLastIndexOf(bytes memory buffer, bytes1 s, uint256 pos) public pure { + pos = bound(pos, 0, buffer.length); + uint256 result = Bytes.lastIndexOf(buffer, s, pos); + + // Should not be found before result + for (uint256 i = pos; result != type(uint256).max && i < result; i++) assertNotEq(buffer[i], s); + if (result != type(uint256).max) assertEq(buffer[result], s); + } + + function testNibbles(bytes memory value) public pure { + bytes memory result = Bytes.nibbles(value); + assertEq(result.length, value.length * 2); + for (uint256 i = 0; i < value.length; i++) { + bytes1 originalByte = value[i]; + bytes1 highNibble = result[i * 2]; + bytes1 lowNibble = result[i * 2 + 1]; + + assertEq(highNibble, originalByte & 0xf0); + assertEq(lowNibble, originalByte & 0x0f); + } + } + + function testSymbolicEqual(bytes memory a, bytes memory b) public pure { + assertEq(Bytes.equal(a, b), Bytes.equal(a, b)); + } + + function testSymbolicCountLeadingZeroes(uint256 x) public pure { + uint256 result = Bytes.clz(x); + assertLe(result, 32); // [0, 32] + + if (x != 0) { + uint256 firstNonZeroBytePos = 32 - result - 1; + uint256 byteValue = (x >> (firstNonZeroBytePos * 8)) & 0xff; + assertNotEq(byteValue, 0); + + // x != 0 implies result < 32 + // most significant byte should be non-zero + uint256 msbValue = (x >> (248 - result * 8)) & 0xff; + assertNotEq(msbValue, 0); + } + } + // REVERSE BITS function testSymbolicReverseBytes32(bytes32 value) public pure { assertEq(Bytes.reverseBytes32(Bytes.reverseBytes32(value)), value); diff --git a/test/utils/Bytes.test.js b/test/utils/Bytes.test.js index 0357cac90d1..505117eed19 100644 --- a/test/utils/Bytes.test.js +++ b/test/utils/Bytes.test.js @@ -96,6 +96,100 @@ describe('Bytes', function () { }); }); + describe('nibbles', function () { + it('converts single byte', async function () { + await expect(this.mock.$nibbles('0xab')).to.eventually.equal('0xa00b'); + }); + + it('converts multiple bytes', async function () { + await expect(this.mock.$nibbles('0x1234')).to.eventually.equal('0x10023004'); + }); + + it('handles empty bytes', async function () { + await expect(this.mock.$nibbles('0x')).to.eventually.equal('0x'); + }); + + it('converts lorem text', async function () { + const result = await this.mock.$nibbles(lorem); + expect(ethers.dataLength(result)).to.equal(lorem.length * 2); + + // Check nibble extraction for first few bytes + for (let i = 0; i < Math.min(lorem.length, 5); i++) { + const originalByte = lorem[i]; + const highNibble = ethers.dataSlice(result, i * 2, i * 2 + 1); + const lowNibble = ethers.dataSlice(result, i * 2 + 1, i * 2 + 2); + + expect(highNibble).to.equal(ethers.toBeHex(originalByte & 0xf0, 1)); + expect(lowNibble).to.equal(ethers.toBeHex(originalByte & 0x0f, 1)); + } + }); + }); + + describe('equal', function () { + it('identical arrays', async function () { + await expect(this.mock.$equal(lorem, lorem)).to.eventually.be.true; + }); + + it('same content', async function () { + const copy = new Uint8Array(lorem); + await expect(this.mock.$equal(lorem, copy)).to.eventually.be.true; + }); + + it('different content', async function () { + const different = ethers.toUtf8Bytes('Different content'); + await expect(this.mock.$equal(lorem, different)).to.eventually.be.false; + }); + + it('different lengths', async function () { + const shorter = lorem.slice(0, 10); + await expect(this.mock.$equal(lorem, shorter)).to.eventually.be.false; + }); + + it('empty arrays', async function () { + const empty1 = new Uint8Array(0); + const empty2 = new Uint8Array(0); + await expect(this.mock.$equal(empty1, empty2)).to.eventually.be.true; + }); + + it('one empty one not', async function () { + const empty = new Uint8Array(0); + await expect(this.mock.$equal(lorem, empty)).to.eventually.be.false; + }); + }); + + describe('clz', function () { + it('zero value', async function () { + await expect(this.mock.$clz(0)).to.eventually.equal(32); + }); + + it('small values', async function () { + await expect(this.mock.$clz(1)).to.eventually.equal(31); + await expect(this.mock.$clz(255)).to.eventually.equal(31); + }); + + it('larger values', async function () { + await expect(this.mock.$clz(256)).to.eventually.equal(30); + await expect(this.mock.$clz(0xff00)).to.eventually.equal(30); + await expect(this.mock.$clz(0x10000)).to.eventually.equal(29); + }); + + it('max value', async function () { + await expect(this.mock.$clz(ethers.MaxUint256)).to.eventually.equal(0); + }); + + it('specific patterns', async function () { + await expect( + this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000000100'), + ).to.eventually.equal(30); + await expect( + this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000010000'), + ).to.eventually.equal(29); + await expect( + this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000001000000'), + ).to.eventually.equal(28); + }); + }); + describe('reverseBits', function () { describe('reverseBytes32', function () { it('reverses bytes correctly', async function () {