Skip to content

Commit 84a600b

Browse files
Amxxernestognw
andcommitted
Fix bug in Bytes.lastIndexOf when array is empty and position is not 2²⁵⁶-1 (#5797)
Co-authored-by: Ernesto García <ernestognw@gmail.com> Signed-off-by: Hadrien Croubois <hadrien.croubois@gmail.com>
1 parent fffade5 commit 84a600b

File tree

4 files changed

+251
-12
lines changed

4 files changed

+251
-12
lines changed

.changeset/witty-hats-flow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': patch
3+
---
4+
5+
`Bytes`: Fix `lastIndexOf(bytes,byte,uint256)` with empty buffers and finite position to correctly return `type(uint256).max` instead of accessing uninitialized memory sections.

contracts/utils/Bytes.sol

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ library Bytes {
5858
function lastIndexOf(bytes memory buffer, bytes1 s, uint256 pos) internal pure returns (uint256) {
5959
unchecked {
6060
uint256 length = buffer.length;
61-
// NOTE here we cannot do `i = Math.min(pos + 1, length)` because `pos + 1` could overflow
62-
for (uint256 i = Math.min(pos, length - 1) + 1; i > 0; --i) {
61+
for (uint256 i = Math.min(Math.saturatingAdd(pos, 1), length); i > 0; --i) {
6362
if (bytes1(_unsafeReadBytesOffset(buffer, i - 1)) == s) {
6463
return i - 1;
6564
}

test/utils/Bytes.t.sol

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {Test} from "forge-std/Test.sol";
6+
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
7+
import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol";
8+
9+
contract BytesTest is Test {
10+
using Bytes for bytes;
11+
12+
// INDEX OF
13+
function testIndexOf(bytes memory buffer, bytes1 s) public pure {
14+
uint256 result = Bytes.indexOf(buffer, s);
15+
16+
if (buffer.length == 0) {
17+
// Case 0: buffer is empty
18+
assertEq(result, type(uint256).max);
19+
} else if (result == type(uint256).max) {
20+
// Case 1: search value could not be found
21+
for (uint256 i = 0; i < buffer.length; ++i) assertNotEq(buffer[i], s);
22+
} else {
23+
// Case 2: search value was found
24+
assertEq(buffer[result], s);
25+
// search value is not present anywhere before the found location
26+
for (uint256 i = 0; i < result; ++i) assertNotEq(buffer[i], s);
27+
}
28+
}
29+
30+
function testIndexOf(bytes memory buffer, bytes1 s, uint256 pos) public pure {
31+
uint256 result = Bytes.indexOf(buffer, s, pos);
32+
33+
if (buffer.length == 0) {
34+
// Case 0: buffer is empty
35+
assertEq(result, type(uint256).max);
36+
} else if (result == type(uint256).max) {
37+
// Case 1: search value could not be found
38+
for (uint256 i = pos; i < buffer.length; ++i) assertNotEq(buffer[i], s);
39+
} else {
40+
// Case 2: search value was found
41+
assertEq(buffer[result], s);
42+
// search value is not present anywhere before the found location
43+
for (uint256 i = pos; i < result; ++i) assertNotEq(buffer[i], s);
44+
}
45+
}
46+
47+
function testLastIndexOf(bytes memory buffer, bytes1 s) public pure {
48+
uint256 result = Bytes.lastIndexOf(buffer, s);
49+
50+
if (buffer.length == 0) {
51+
// Case 0: buffer is empty
52+
assertEq(result, type(uint256).max);
53+
} else if (result == type(uint256).max) {
54+
// Case 1: search value could not be found
55+
for (uint256 i = 0; i < buffer.length; ++i) assertNotEq(buffer[i], s);
56+
} else {
57+
// Case 2: search value was found
58+
assertEq(buffer[result], s);
59+
// search value is not present anywhere after the found location
60+
for (uint256 i = result + 1; i < buffer.length; ++i) assertNotEq(buffer[i], s);
61+
}
62+
}
63+
64+
function testLastIndexOf(bytes memory buffer, bytes1 s, uint256 pos) public pure {
65+
uint256 result = Bytes.lastIndexOf(buffer, s, pos);
66+
67+
if (buffer.length == 0) {
68+
// Case 0: buffer is empty
69+
assertEq(result, type(uint256).max);
70+
} else if (result == type(uint256).max) {
71+
// Case 1: search value could not be found
72+
for (uint256 i = 0; i <= Math.min(pos, buffer.length - 1); ++i) assertNotEq(buffer[i], s);
73+
} else {
74+
// Case 2: search value was found
75+
assertEq(buffer[result], s);
76+
// search value is not present anywhere after the found location
77+
for (uint256 i = result + 1; i <= Math.min(pos, buffer.length - 1); ++i) assertNotEq(buffer[i], s);
78+
}
79+
}
80+
81+
// SLICES
82+
function testSliceWithStartOnly(bytes memory buffer, uint256 start) public pure {
83+
bytes memory originalBuffer = bytes.concat(buffer);
84+
bytes memory result = buffer.slice(start);
85+
86+
// Original buffer was not modified
87+
assertEq(buffer, originalBuffer);
88+
89+
// Should return bytes from start to end
90+
assertEq(result.length, Math.saturatingSub(buffer.length, start));
91+
92+
// Verify content matches
93+
for (uint256 i = 0; i < result.length; ++i) {
94+
assertEq(result[i], buffer[start + i]);
95+
}
96+
}
97+
98+
function testSlice(bytes memory buffer, uint256 start, uint256 end) public pure {
99+
bytes memory originalBuffer = bytes.concat(buffer);
100+
bytes memory result = buffer.slice(start, end);
101+
102+
// Original buffer was not modified
103+
assertEq(buffer, originalBuffer);
104+
105+
// Calculate expected bounds after sanitization
106+
uint256 sanitizedEnd = Math.min(end, buffer.length);
107+
uint256 sanitizedStart = Math.min(start, sanitizedEnd);
108+
uint256 expectedLength = sanitizedEnd - sanitizedStart;
109+
110+
assertEq(result.length, expectedLength);
111+
112+
// Verify content matches when there's content to verify
113+
for (uint256 i = 0; i < result.length; ++i) {
114+
assertEq(result[i], buffer[sanitizedStart + i]);
115+
}
116+
}
117+
118+
function testSpliceWithStartOnly(bytes memory buffer, uint256 start) public pure {
119+
bytes memory originalBuffer = bytes.concat(buffer);
120+
bytes memory result = buffer.splice(start);
121+
122+
// Result should be the same object as input (modified in place)
123+
assertEq(result, buffer);
124+
125+
// Should contain bytes from start to end, moved to beginning
126+
assertEq(result.length, Math.saturatingSub(originalBuffer.length, start));
127+
128+
// Verify content matches moved content
129+
for (uint256 i = 0; i < result.length; ++i) {
130+
assertEq(result[i], originalBuffer[start + i]);
131+
}
132+
}
133+
134+
function testSplice(bytes memory buffer, uint256 start, uint256 end) public pure {
135+
bytes memory originalBuffer = bytes.concat(buffer);
136+
bytes memory result = buffer.splice(start, end);
137+
138+
// Result should be the same object as input (modified in place)
139+
assertEq(result, buffer);
140+
141+
// Calculate expected bounds after sanitization
142+
uint256 sanitizedEnd = Math.min(end, originalBuffer.length);
143+
uint256 sanitizedStart = Math.min(start, sanitizedEnd);
144+
uint256 expectedLength = sanitizedEnd - sanitizedStart;
145+
146+
assertEq(result.length, expectedLength);
147+
148+
// Verify content matches moved content
149+
for (uint256 i = 0; i < result.length; ++i) {
150+
assertEq(result[i], originalBuffer[sanitizedStart + i]);
151+
}
152+
}
153+
154+
// REVERSE BITS
155+
function testSymbolicReverseBytes32(bytes32 value) public pure {
156+
assertEq(Bytes.reverseBytes32(Bytes.reverseBytes32(value)), value);
157+
}
158+
159+
function testSymbolicReverseBytes16(bytes16 value) public pure {
160+
assertEq(Bytes.reverseBytes16(Bytes.reverseBytes16(value)), value);
161+
}
162+
163+
function testSymbolicReverseBytes16Dirty(bytes16 value) public pure {
164+
assertEq(Bytes.reverseBytes16(Bytes.reverseBytes16(_dirtyBytes16(value))), value);
165+
assertEq(Bytes.reverseBytes16(_dirtyBytes16(Bytes.reverseBytes16(value))), value);
166+
}
167+
168+
function testSymbolicReverseBytes8(bytes8 value) public pure {
169+
assertEq(Bytes.reverseBytes8(Bytes.reverseBytes8(value)), value);
170+
}
171+
172+
function testSymbolicReverseBytes8Dirty(bytes8 value) public pure {
173+
assertEq(Bytes.reverseBytes8(Bytes.reverseBytes8(_dirtyBytes8(value))), value);
174+
assertEq(Bytes.reverseBytes8(_dirtyBytes8(Bytes.reverseBytes8(value))), value);
175+
}
176+
177+
function testSymbolicReverseBytes4(bytes4 value) public pure {
178+
assertEq(Bytes.reverseBytes4(Bytes.reverseBytes4(value)), value);
179+
}
180+
181+
function testSymbolicReverseBytes4Dirty(bytes4 value) public pure {
182+
assertEq(Bytes.reverseBytes4(Bytes.reverseBytes4(_dirtyBytes4(value))), value);
183+
assertEq(Bytes.reverseBytes4(_dirtyBytes4(Bytes.reverseBytes4(value))), value);
184+
}
185+
186+
function testSymbolicReverseBytes2(bytes2 value) public pure {
187+
assertEq(Bytes.reverseBytes2(Bytes.reverseBytes2(value)), value);
188+
}
189+
190+
function testSymbolicReverseBytes2Dirty(bytes2 value) public pure {
191+
assertEq(Bytes.reverseBytes2(Bytes.reverseBytes2(_dirtyBytes2(value))), value);
192+
assertEq(Bytes.reverseBytes2(_dirtyBytes2(Bytes.reverseBytes2(value))), value);
193+
}
194+
195+
// Helpers
196+
function _dirtyBytes16(bytes16 value) private pure returns (bytes16 dirty) {
197+
assembly ("memory-safe") {
198+
dirty := or(value, shr(128, not(0)))
199+
}
200+
}
201+
202+
function _dirtyBytes8(bytes8 value) private pure returns (bytes8 dirty) {
203+
assembly ("memory-safe") {
204+
dirty := or(value, shr(192, not(0)))
205+
}
206+
}
207+
208+
function _dirtyBytes4(bytes4 value) private pure returns (bytes4 dirty) {
209+
assembly ("memory-safe") {
210+
dirty := or(value, shr(224, not(0)))
211+
}
212+
}
213+
214+
function _dirtyBytes2(bytes2 value) private pure returns (bytes2 dirty) {
215+
assembly ("memory-safe") {
216+
dirty := or(value, shr(240, not(0)))
217+
}
218+
}
219+
}

test/utils/Bytes.test.js

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,39 +20,55 @@ describe('Bytes', function () {
2020

2121
describe('indexOf', function () {
2222
it('first', async function () {
23-
expect(await this.mock.$indexOf(lorem, ethers.toBeHex(present))).to.equal(lorem.indexOf(present));
23+
await expect(this.mock.$indexOf(lorem, ethers.toBeHex(present))).to.eventually.equal(lorem.indexOf(present));
2424
});
2525

2626
it('from index', async function () {
2727
for (const start in Array(lorem.length + 10).fill()) {
2828
const index = lorem.indexOf(present, start);
2929
const result = index === -1 ? ethers.MaxUint256 : index;
30-
expect(await this.mock.$indexOf(lorem, ethers.toBeHex(present), ethers.Typed.uint256(start))).to.equal(result);
30+
await expect(
31+
this.mock.$indexOf(lorem, ethers.toBeHex(present), ethers.Typed.uint256(start)),
32+
).to.eventually.equal(result);
3133
}
3234
});
3335

3436
it('absent', async function () {
35-
expect(await this.mock.$indexOf(lorem, ethers.toBeHex(absent))).to.equal(ethers.MaxUint256);
37+
await expect(this.mock.$indexOf(lorem, ethers.toBeHex(absent))).to.eventually.equal(ethers.MaxUint256);
38+
});
39+
40+
it('empty buffer', async function () {
41+
await expect(this.mock.$indexOf('0x', '0x00')).to.eventually.equal(ethers.MaxUint256);
42+
await expect(this.mock.$indexOf('0x', '0x00', ethers.Typed.uint256(17))).to.eventually.equal(ethers.MaxUint256);
3643
});
3744
});
3845

3946
describe('lastIndexOf', function () {
4047
it('first', async function () {
41-
expect(await this.mock.$lastIndexOf(lorem, ethers.toBeHex(present))).to.equal(lorem.lastIndexOf(present));
48+
await expect(this.mock.$lastIndexOf(lorem, ethers.toBeHex(present))).to.eventually.equal(
49+
lorem.lastIndexOf(present),
50+
);
4251
});
4352

4453
it('from index', async function () {
4554
for (const start in Array(lorem.length + 10).fill()) {
4655
const index = lorem.lastIndexOf(present, start);
4756
const result = index === -1 ? ethers.MaxUint256 : index;
48-
expect(await this.mock.$lastIndexOf(lorem, ethers.toBeHex(present), ethers.Typed.uint256(start))).to.equal(
49-
result,
50-
);
57+
await expect(
58+
this.mock.$lastIndexOf(lorem, ethers.toBeHex(present), ethers.Typed.uint256(start)),
59+
).to.eventually.equal(result);
5160
}
5261
});
5362

5463
it('absent', async function () {
55-
expect(await this.mock.$lastIndexOf(lorem, ethers.toBeHex(absent))).to.equal(ethers.MaxUint256);
64+
await expect(this.mock.$lastIndexOf(lorem, ethers.toBeHex(absent))).to.eventually.equal(ethers.MaxUint256);
65+
});
66+
67+
it('empty buffer', async function () {
68+
await expect(this.mock.$lastIndexOf('0x', '0x00')).to.eventually.equal(ethers.MaxUint256);
69+
await expect(this.mock.$lastIndexOf('0x', '0x00', ethers.Typed.uint256(17))).to.eventually.equal(
70+
ethers.MaxUint256,
71+
);
5672
});
5773
});
5874

@@ -65,7 +81,7 @@ describe('Bytes', function () {
6581
})) {
6682
it(descr, async function () {
6783
const result = ethers.hexlify(lorem.slice(start));
68-
expect(await this.mock.$slice(lorem, start)).to.equal(result);
84+
await expect(this.mock.$slice(lorem, start)).to.eventually.equal(result);
6985
});
7086
}
7187
});
@@ -80,7 +96,7 @@ describe('Bytes', function () {
8096
})) {
8197
it(descr, async function () {
8298
const result = ethers.hexlify(lorem.slice(start, end));
83-
expect(await this.mock.$slice(lorem, start, ethers.Typed.uint256(end))).to.equal(result);
99+
await expect(this.mock.$slice(lorem, start, ethers.Typed.uint256(end))).to.eventually.equal(result);
84100
});
85101
}
86102
});

0 commit comments

Comments
 (0)