Add reverseBits operations to Bytes.sol (#5724)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
This commit is contained in:
5
.changeset/major-feet-write.md
Normal file
5
.changeset/major-feet-write.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'openzeppelin-solidity': minor
|
||||
---
|
||||
|
||||
`Bytes`: Add `reverseBytes32`, `reverseBytes16`, `reverseBytes8`, `reverseBytes4`, and `reverseBytes2` functions to reverse byte order for converting between little-endian and big-endian representations.
|
||||
@ -99,6 +99,58 @@ library Bytes {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Reverses the byte order of a bytes32 value, converting between little-endian and big-endian.
|
||||
* Inspired in https://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel[Reverse Parallel]
|
||||
*/
|
||||
function reverseBytes32(bytes32 value) internal pure returns (bytes32) {
|
||||
value = // swap bytes
|
||||
((value >> 8) & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) |
|
||||
((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8);
|
||||
value = // swap 2-byte long pairs
|
||||
((value >> 16) & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) |
|
||||
((value & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16);
|
||||
value = // swap 4-byte long pairs
|
||||
((value >> 32) & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) |
|
||||
((value & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32);
|
||||
value = // swap 8-byte long pairs
|
||||
((value >> 64) & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) |
|
||||
((value & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64);
|
||||
return (value >> 128) | (value << 128); // swap 16-byte long pairs
|
||||
}
|
||||
|
||||
/// @dev Same as {reverseBytes32} but optimized for 128-bit values.
|
||||
function reverseBytes16(bytes16 value) internal pure returns (bytes16) {
|
||||
value = // swap bytes
|
||||
((value & 0xFF00FF00FF00FF00FF00FF00FF00FF00) >> 8) |
|
||||
((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF) << 8);
|
||||
value = // swap 2-byte long pairs
|
||||
((value & 0xFFFF0000FFFF0000FFFF0000FFFF0000) >> 16) |
|
||||
((value & 0x0000FFFF0000FFFF0000FFFF0000FFFF) << 16);
|
||||
value = // swap 4-byte long pairs
|
||||
((value & 0xFFFFFFFF00000000FFFFFFFF00000000) >> 32) |
|
||||
((value & 0x00000000FFFFFFFF00000000FFFFFFFF) << 32);
|
||||
return (value >> 64) | (value << 64); // swap 8-byte long pairs
|
||||
}
|
||||
|
||||
/// @dev Same as {reverseBytes32} but optimized for 64-bit values.
|
||||
function reverseBytes8(bytes8 value) internal pure returns (bytes8) {
|
||||
value = ((value & 0xFF00FF00FF00FF00) >> 8) | ((value & 0x00FF00FF00FF00FF) << 8); // swap bytes
|
||||
value = ((value & 0xFFFF0000FFFF0000) >> 16) | ((value & 0x0000FFFF0000FFFF) << 16); // swap 2-byte long pairs
|
||||
return (value >> 32) | (value << 32); // swap 4-byte long pairs
|
||||
}
|
||||
|
||||
/// @dev Same as {reverseBytes32} but optimized for 32-bit values.
|
||||
function reverseBytes4(bytes4 value) internal pure returns (bytes4) {
|
||||
value = ((value & 0xFF00FF00) >> 8) | ((value & 0x00FF00FF) << 8); // swap bytes
|
||||
return (value >> 16) | (value << 16); // swap 2-byte long pairs
|
||||
}
|
||||
|
||||
/// @dev Same as {reverseBytes32} but optimized for 16-bit values.
|
||||
function reverseBytes2(bytes2 value) internal pure returns (bytes2) {
|
||||
return (value >> 8) | (value << 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Reads a bytes32 from a bytes array without bounds checking.
|
||||
*
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
module.exports = {
|
||||
MAX_UINT16: 2n ** 16n - 1n,
|
||||
MAX_UINT32: 2n ** 32n - 1n,
|
||||
MAX_UINT48: 2n ** 48n - 1n,
|
||||
MAX_UINT64: 2n ** 64n - 1n,
|
||||
MAX_UINT128: 2n ** 128n - 1n,
|
||||
};
|
||||
|
||||
74
test/utils/Bytes.t.sol
Normal file
74
test/utils/Bytes.t.sol
Normal file
@ -0,0 +1,74 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol";
|
||||
|
||||
contract BytesTest is Test {
|
||||
// REVERSE BITS
|
||||
function testSymbolicReverseBytes32(bytes32 value) public pure {
|
||||
assertEq(Bytes.reverseBytes32(Bytes.reverseBytes32(value)), value);
|
||||
}
|
||||
|
||||
function testSymbolicReverseBytes16(bytes16 value) public pure {
|
||||
assertEq(Bytes.reverseBytes16(Bytes.reverseBytes16(value)), value);
|
||||
}
|
||||
|
||||
function testSymbolicReverseBytes16Dirty(bytes16 value) public pure {
|
||||
assertEq(Bytes.reverseBytes16(Bytes.reverseBytes16(_dirtyBytes16(value))), value);
|
||||
assertEq(Bytes.reverseBytes16(_dirtyBytes16(Bytes.reverseBytes16(value))), value);
|
||||
}
|
||||
|
||||
function testSymbolicReverseBytes8(bytes8 value) public pure {
|
||||
assertEq(Bytes.reverseBytes8(Bytes.reverseBytes8(value)), value);
|
||||
}
|
||||
|
||||
function testSymbolicReverseBytes8Dirty(bytes8 value) public pure {
|
||||
assertEq(Bytes.reverseBytes8(Bytes.reverseBytes8(_dirtyBytes8(value))), value);
|
||||
assertEq(Bytes.reverseBytes8(_dirtyBytes8(Bytes.reverseBytes8(value))), value);
|
||||
}
|
||||
|
||||
function testSymbolicReverseBytes4(bytes4 value) public pure {
|
||||
assertEq(Bytes.reverseBytes4(Bytes.reverseBytes4(value)), value);
|
||||
}
|
||||
|
||||
function testSymbolicReverseBytes4Dirty(bytes4 value) public pure {
|
||||
assertEq(Bytes.reverseBytes4(Bytes.reverseBytes4(_dirtyBytes4(value))), value);
|
||||
assertEq(Bytes.reverseBytes4(_dirtyBytes4(Bytes.reverseBytes4(value))), value);
|
||||
}
|
||||
|
||||
function testSymbolicReverseBytes2(bytes2 value) public pure {
|
||||
assertEq(Bytes.reverseBytes2(Bytes.reverseBytes2(value)), value);
|
||||
}
|
||||
|
||||
function testSymbolicReverseBytes2Dirty(bytes2 value) public pure {
|
||||
assertEq(Bytes.reverseBytes2(Bytes.reverseBytes2(_dirtyBytes2(value))), value);
|
||||
assertEq(Bytes.reverseBytes2(_dirtyBytes2(Bytes.reverseBytes2(value))), value);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
function _dirtyBytes16(bytes16 value) private pure returns (bytes16 dirty) {
|
||||
assembly ("memory-safe") {
|
||||
dirty := or(value, shr(128, not(0)))
|
||||
}
|
||||
}
|
||||
|
||||
function _dirtyBytes8(bytes8 value) private pure returns (bytes8 dirty) {
|
||||
assembly ("memory-safe") {
|
||||
dirty := or(value, shr(192, not(0)))
|
||||
}
|
||||
}
|
||||
|
||||
function _dirtyBytes4(bytes4 value) private pure returns (bytes4 dirty) {
|
||||
assembly ("memory-safe") {
|
||||
dirty := or(value, shr(224, not(0)))
|
||||
}
|
||||
}
|
||||
|
||||
function _dirtyBytes2(bytes2 value) private pure returns (bytes2 dirty) {
|
||||
assembly ("memory-safe") {
|
||||
dirty := or(value, shr(240, not(0)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,14 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
const { MAX_UINT128, MAX_UINT64, MAX_UINT32, MAX_UINT16 } = require('../helpers/constants');
|
||||
|
||||
// Helper functions for fixed bytes types
|
||||
const bytes32 = value => ethers.toBeHex(value, 32);
|
||||
const bytes16 = value => ethers.toBeHex(value, 16);
|
||||
const bytes8 = value => ethers.toBeHex(value, 8);
|
||||
const bytes4 = value => ethers.toBeHex(value, 4);
|
||||
const bytes2 = value => ethers.toBeHex(value, 2);
|
||||
|
||||
async function fixture() {
|
||||
const mock = await ethers.deployContract('$Bytes');
|
||||
@ -85,4 +93,120 @@ describe('Bytes', function () {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('reverseBits', function () {
|
||||
describe('reverseBytes32', function () {
|
||||
it('reverses bytes correctly', async function () {
|
||||
await expect(this.mock.$reverseBytes32(bytes32(0))).to.eventually.equal(bytes32(0));
|
||||
await expect(this.mock.$reverseBytes32(bytes32(ethers.MaxUint256))).to.eventually.equal(
|
||||
bytes32(ethers.MaxUint256),
|
||||
);
|
||||
|
||||
// Test complex pattern that clearly shows byte reversal
|
||||
await expect(
|
||||
this.mock.$reverseBytes32('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'),
|
||||
).to.eventually.equal('0xefcdab8967452301efcdab8967452301efcdab8967452301efcdab8967452301');
|
||||
});
|
||||
|
||||
it('double reverse returns original', async function () {
|
||||
const values = [0n, 1n, 0x12345678n, ethers.MaxUint256];
|
||||
for (const value of values) {
|
||||
const reversed = await this.mock.$reverseBytes32(bytes32(value));
|
||||
await expect(this.mock.$reverseBytes32(reversed)).to.eventually.equal(bytes32(value));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('reverseBytes16', function () {
|
||||
it('reverses bytes correctly', async function () {
|
||||
await expect(this.mock.$reverseBytes16(bytes16(0))).to.eventually.equal(bytes16(0));
|
||||
await expect(this.mock.$reverseBytes16(bytes16(MAX_UINT128))).to.eventually.equal(bytes16(MAX_UINT128));
|
||||
|
||||
// Test complex pattern that clearly shows byte reversal
|
||||
await expect(this.mock.$reverseBytes16('0x0123456789abcdef0123456789abcdef')).to.eventually.equal(
|
||||
'0xefcdab8967452301efcdab8967452301',
|
||||
);
|
||||
});
|
||||
|
||||
it('double reverse returns original', async function () {
|
||||
const values = [0n, 1n, 0x12345678n, MAX_UINT128];
|
||||
for (const value of values) {
|
||||
const reversed = await this.mock.$reverseBytes16(bytes16(value));
|
||||
// Cast back to uint128 for comparison since function returns uint256
|
||||
await expect(this.mock.$reverseBytes16(reversed)).to.eventually.equal(bytes16(value & MAX_UINT128));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('reverseBytes8', function () {
|
||||
it('reverses bytes correctly', async function () {
|
||||
await expect(this.mock.$reverseBytes8(bytes8(0))).to.eventually.equal(bytes8(0));
|
||||
await expect(this.mock.$reverseBytes8(bytes8(MAX_UINT64))).to.eventually.equal(bytes8(MAX_UINT64));
|
||||
|
||||
// Test known pattern: 0x123456789ABCDEF0 -> 0xF0DEBC9A78563412
|
||||
await expect(this.mock.$reverseBytes8('0x123456789abcdef0')).to.eventually.equal('0xf0debc9a78563412');
|
||||
});
|
||||
|
||||
it('double reverse returns original', async function () {
|
||||
const values = [0n, 1n, 0x12345678n, MAX_UINT64];
|
||||
for (const value of values) {
|
||||
const reversed = await this.mock.$reverseBytes8(bytes8(value));
|
||||
// Cast back to uint64 for comparison since function returns uint256
|
||||
await expect(this.mock.$reverseBytes8(reversed)).to.eventually.equal(bytes8(value & MAX_UINT64));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('reverseBytes4', function () {
|
||||
it('reverses bytes correctly', async function () {
|
||||
await expect(this.mock.$reverseBytes4(bytes4(0))).to.eventually.equal(bytes4(0));
|
||||
await expect(this.mock.$reverseBytes4(bytes4(MAX_UINT32))).to.eventually.equal(bytes4(MAX_UINT32));
|
||||
|
||||
// Test known pattern: 0x12345678 -> 0x78563412
|
||||
await expect(this.mock.$reverseBytes4(bytes4(0x12345678))).to.eventually.equal(bytes4(0x78563412));
|
||||
});
|
||||
|
||||
it('double reverse returns original', async function () {
|
||||
const values = [0n, 1n, 0x12345678n, MAX_UINT32];
|
||||
for (const value of values) {
|
||||
const reversed = await this.mock.$reverseBytes4(bytes4(value));
|
||||
// Cast back to uint32 for comparison since function returns uint256
|
||||
await expect(this.mock.$reverseBytes4(reversed)).to.eventually.equal(bytes4(value & MAX_UINT32));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('reverseBytes2', function () {
|
||||
it('reverses bytes correctly', async function () {
|
||||
await expect(this.mock.$reverseBytes2(bytes2(0))).to.eventually.equal(bytes2(0));
|
||||
await expect(this.mock.$reverseBytes2(bytes2(MAX_UINT16))).to.eventually.equal(bytes2(MAX_UINT16));
|
||||
|
||||
// Test known pattern: 0x1234 -> 0x3412
|
||||
await expect(this.mock.$reverseBytes2(bytes2(0x1234))).to.eventually.equal(bytes2(0x3412));
|
||||
});
|
||||
|
||||
it('double reverse returns original', async function () {
|
||||
const values = [0n, 1n, 0x1234n, MAX_UINT16];
|
||||
for (const value of values) {
|
||||
const reversed = await this.mock.$reverseBytes2(bytes2(value));
|
||||
// Cast back to uint16 for comparison since function returns uint256
|
||||
await expect(this.mock.$reverseBytes2(reversed)).to.eventually.equal(bytes2(value & MAX_UINT16));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', function () {
|
||||
it('handles single byte values', async function () {
|
||||
await expect(this.mock.$reverseBytes2(bytes2(0x00ff))).to.eventually.equal(bytes2(0xff00));
|
||||
await expect(this.mock.$reverseBytes4(bytes4(0x000000ff))).to.eventually.equal(bytes4(0xff000000));
|
||||
});
|
||||
|
||||
it('handles alternating patterns', async function () {
|
||||
await expect(this.mock.$reverseBytes2(bytes2(0xaaaa))).to.eventually.equal(bytes2(0xaaaa));
|
||||
await expect(this.mock.$reverseBytes2(bytes2(0x5555))).to.eventually.equal(bytes2(0x5555));
|
||||
await expect(this.mock.$reverseBytes4(bytes4(0xaaaaaaaa))).to.eventually.equal(bytes4(0xaaaaaaaa));
|
||||
await expect(this.mock.$reverseBytes4(bytes4(0x55555555))).to.eventually.equal(bytes4(0x55555555));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user