Skip to content

Commit 32397f2

Browse files
Amxxjames-toussainternestognw
authored
Add Bytes.splice, an inplace variant of Buffer.slice (#5733)
Co-authored-by: James Toussaint <33313130+james-toussaint@users.noreply.github.com> Co-authored-by: ernestognw <ernestognw@gmail.com>
1 parent 2ea54a1 commit 32397f2

File tree

4 files changed

+116
-4
lines changed

4 files changed

+116
-4
lines changed

.changeset/afraid-chicken-attack.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`Bytes`: Add `splice(bytes,uint256)` and `splice(bytes,uint256,uint256)` functions that move a specified range of bytes to the start of the buffer and truncate it in place, as an alternative to `slice`.

contracts/utils/Bytes.sol

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ library Bytes {
8080

8181
/**
8282
* @dev Copies the content of `buffer`, from `start` (included) to `end` (excluded) into a new bytes object in
83-
* memory.
83+
* memory. The `end` argument is truncated to the length of the `buffer`.
8484
*
8585
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's `Array.slice`]
8686
*/
@@ -100,6 +100,36 @@ library Bytes {
100100
}
101101

102102
/**
103+
* @dev Moves the content of `buffer`, from `start` (included) to the end of `buffer` to the start of that buffer.
104+
*
105+
* NOTE: This function modifies the provided buffer in place. If you need to preserve the original buffer, use {slice} instead
106+
*/
107+
function splice(bytes memory buffer, uint256 start) internal pure returns (bytes memory) {
108+
return splice(buffer, start, buffer.length);
109+
}
110+
111+
/**
112+
* @dev Moves the content of `buffer`, from `start` (included) to end (excluded) to the start of that buffer. The
113+
* `end` argument is truncated to the length of the `buffer`.
114+
*
115+
* NOTE: This function modifies the provided buffer in place. If you need to preserve the original buffer, use {slice} instead
116+
*/
117+
function splice(bytes memory buffer, uint256 start, uint256 end) internal pure returns (bytes memory) {
118+
// sanitize
119+
uint256 length = buffer.length;
120+
end = Math.min(end, length);
121+
start = Math.min(start, end);
122+
123+
// allocate and copy
124+
assembly ("memory-safe") {
125+
mcopy(add(buffer, 0x20), add(add(buffer, 0x20), start), sub(end, start))
126+
mstore(buffer, sub(end, start))
127+
}
128+
129+
return buffer;
130+
}
131+
132+
/*
103133
* @dev Reverses the byte order of a bytes32 value, converting between little-endian and big-endian.
104134
* Inspired in https://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel[Reverse Parallel]
105135
*/

test/utils/Bytes.t.sol

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,84 @@
33
pragma solidity ^0.8.20;
44

55
import {Test} from "forge-std/Test.sol";
6+
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
67
import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol";
78

89
contract BytesTest is Test {
10+
using Bytes for bytes;
11+
12+
function testSliceWithStartOnly(bytes memory buffer, uint256 start) public pure {
13+
bytes memory originalBuffer = bytes.concat(buffer);
14+
bytes memory result = buffer.slice(start);
15+
16+
// Original buffer was not modified
17+
assertEq(buffer, originalBuffer);
18+
19+
// Should return bytes from start to end
20+
assertEq(result.length, Math.saturatingSub(buffer.length, start));
21+
22+
// Verify content matches
23+
for (uint256 i = 0; i < result.length; ++i) {
24+
assertEq(result[i], buffer[start + i]);
25+
}
26+
}
27+
28+
function testSlice(bytes memory buffer, uint256 start, uint256 end) public pure {
29+
bytes memory originalBuffer = bytes.concat(buffer);
30+
bytes memory result = buffer.slice(start, end);
31+
32+
// Original buffer was not modified
33+
assertEq(buffer, originalBuffer);
34+
35+
// Calculate expected bounds after sanitization
36+
uint256 sanitizedEnd = Math.min(end, buffer.length);
37+
uint256 sanitizedStart = Math.min(start, sanitizedEnd);
38+
uint256 expectedLength = sanitizedEnd - sanitizedStart;
39+
40+
assertEq(result.length, expectedLength);
41+
42+
// Verify content matches when there's content to verify
43+
for (uint256 i = 0; i < result.length; ++i) {
44+
assertEq(result[i], buffer[sanitizedStart + i]);
45+
}
46+
}
47+
48+
function testSpliceWithStartOnly(bytes memory buffer, uint256 start) public pure {
49+
bytes memory originalBuffer = bytes.concat(buffer);
50+
bytes memory result = buffer.splice(start);
51+
52+
// Result should be the same object as input (modified in place)
53+
assertEq(result, buffer);
54+
55+
// Should contain bytes from start to end, moved to beginning
56+
assertEq(result.length, Math.saturatingSub(originalBuffer.length, start));
57+
58+
// Verify content matches moved content
59+
for (uint256 i = 0; i < result.length; ++i) {
60+
assertEq(result[i], originalBuffer[start + i]);
61+
}
62+
}
63+
64+
function testSplice(bytes memory buffer, uint256 start, uint256 end) public pure {
65+
bytes memory originalBuffer = bytes.concat(buffer);
66+
bytes memory result = buffer.splice(start, end);
67+
68+
// Result should be the same object as input (modified in place)
69+
assertEq(result, buffer);
70+
71+
// Calculate expected bounds after sanitization
72+
uint256 sanitizedEnd = Math.min(end, originalBuffer.length);
73+
uint256 sanitizedStart = Math.min(start, sanitizedEnd);
74+
uint256 expectedLength = sanitizedEnd - sanitizedStart;
75+
76+
assertEq(result.length, expectedLength);
77+
78+
// Verify content matches moved content
79+
for (uint256 i = 0; i < result.length; ++i) {
80+
assertEq(result[i], originalBuffer[sanitizedStart + i]);
81+
}
82+
}
83+
984
// REVERSE BITS
1085
function testSymbolicReverseBytes32(bytes32 value) public pure {
1186
assertEq(Bytes.reverseBytes32(Bytes.reverseBytes32(value)), value);

test/utils/Bytes.test.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ describe('Bytes', function () {
6464
});
6565
});
6666

67-
describe('slice', function () {
68-
describe('slice(bytes, uint256)', function () {
67+
describe('slice & splice', function () {
68+
describe('slice(bytes, uint256) & splice(bytes, uint256)', function () {
6969
for (const [descr, start] of Object.entries({
7070
'start = 0': 0,
7171
'start within bound': 10,
@@ -74,11 +74,12 @@ describe('Bytes', function () {
7474
it(descr, async function () {
7575
const result = ethers.hexlify(lorem.slice(start));
7676
expect(await this.mock.$slice(lorem, start)).to.equal(result);
77+
expect(await this.mock.$splice(lorem, start)).to.equal(result);
7778
});
7879
}
7980
});
8081

81-
describe('slice(bytes, uint256, uint256)', function () {
82+
describe('slice(bytes, uint256, uint256) & splice(bytes, uint256, uint256)', function () {
8283
for (const [descr, [start, end]] of Object.entries({
8384
'start = 0': [0, 42],
8485
'start and end within bound': [17, 42],
@@ -89,6 +90,7 @@ describe('Bytes', function () {
8990
it(descr, async function () {
9091
const result = ethers.hexlify(lorem.slice(start, end));
9192
expect(await this.mock.$slice(lorem, start, ethers.Typed.uint256(end))).to.equal(result);
93+
expect(await this.mock.$splice(lorem, start, ethers.Typed.uint256(end))).to.equal(result);
9294
});
9395
}
9496
});

0 commit comments

Comments
 (0)