Skip to content

Commit f19bf29

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 f19bf29

File tree

4 files changed

+149
-12
lines changed

4 files changed

+149
-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: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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+
}

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)