Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/afraid-chicken-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`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`.
31 changes: 31 additions & 0 deletions contracts/utils/Bytes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,37 @@ library Bytes {
return result;
}

/**
* @dev Moves the content of `buffer`, from `start` (included) to the end of `buffer` to the start of that buffer.
* The `end` argument is truncated to the length of the `buffer`.
*
* NOTE: This function modifies the provided buffer in place. If you need to preserve the original buffer, use {slice} instead
*/
function splice(bytes memory buffer, uint256 start) internal pure returns (bytes memory) {
return splice(buffer, start, buffer.length);
}

/**
* @dev Moves the content of `buffer`, from `start` (included) to end (excluded) to the start of that buffer.
* The `end` argument is truncated to the length of the `buffer`.
*
* NOTE: This function modifies the provided buffer in place. If you need to preserve the original buffer, use {slice} instead
*/
function splice(bytes memory buffer, uint256 start, uint256 end) internal pure returns (bytes memory) {
// sanitize
uint256 length = buffer.length;
end = Math.min(end, length);
start = Math.min(start, end);

// allocate and copy
assembly ("memory-safe") {
mcopy(add(buffer, 0x20), add(add(buffer, 0x20), start), sub(end, start))
mstore(buffer, sub(end, start))
}

return buffer;
}

/**
* @dev Reads a bytes32 from a bytes array without bounds checking.
*
Expand Down
87 changes: 87 additions & 0 deletions test/utils/Bytes.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// 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 {
using Bytes for bytes;

function testSliceWithStartOnly(bytes memory buffer, uint256 start) public pure {
start = bound(start, 0, buffer.length);
bytes memory result = buffer.slice(start);

// Should return bytes from start to end
assertEq(result.length, buffer.length - start);

// Verify content matches
for (uint256 i = 0; i < result.length; i++) {
assertEq(result[i], buffer[start + i]);
}

// Original buffer should remain unchanged
assertEq(buffer.length, buffer.length);
for (uint256 i = 0; i < buffer.length; i++) {
assertEq(buffer[i], buffer[i]);
}
}

function testSlice(bytes memory buffer, uint256 start, uint256 end) public pure {
bytes memory result = buffer.slice(start, end);

// Calculate expected bounds after sanitization
uint256 sanitizedEnd = end > buffer.length ? buffer.length : end;
uint256 sanitizedStart = start > sanitizedEnd ? sanitizedEnd : start;
uint256 expectedLength = sanitizedEnd - sanitizedStart;

assertEq(result.length, expectedLength);

// Verify content matches when there's content to verify
for (uint256 i = 0; i < result.length; i++) {
assertEq(result[i], buffer[sanitizedStart + i]);
}
}

function testSpliceWithStartOnly(bytes memory buffer, uint256 start) public pure {
start = bound(start, 0, buffer.length);
bytes memory originalBuffer = new bytes(buffer.length);
for (uint256 i = 0; i < buffer.length; i++) {
originalBuffer[i] = buffer[i];
}

bytes memory result = buffer.splice(start);

// Result should be the same object as input (modified in place)
assertEq(result.length == buffer.length, true);

// Should contain bytes from start to end, moved to beginning
assertEq(result.length, originalBuffer.length - start);

// Verify content matches moved content
for (uint256 i = 0; i < result.length; i++) {
assertEq(result[i], originalBuffer[start + i]);
}
}

function testSplice(bytes memory buffer, uint256 start, uint256 end) public pure {
bytes memory originalBuffer = new bytes(buffer.length);
for (uint256 i = 0; i < buffer.length; i++) {
originalBuffer[i] = buffer[i];
}

bytes memory result = buffer.splice(start, end);

// Calculate expected bounds after sanitization
uint256 sanitizedEnd = end > originalBuffer.length ? originalBuffer.length : end;
uint256 sanitizedStart = start > sanitizedEnd ? sanitizedEnd : start;
uint256 expectedLength = sanitizedEnd - sanitizedStart;

assertEq(result.length, expectedLength);

// Verify content matches moved content
for (uint256 i = 0; i < result.length; i++) {
assertEq(result[i], originalBuffer[sanitizedStart + i]);
}
}
}
8 changes: 5 additions & 3 deletions test/utils/Bytes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ describe('Bytes', function () {
});
});

describe('slice', function () {
describe('slice(bytes, uint256)', function () {
describe('slice & splice', function () {
describe('slice(bytes, uint256) & splice(bytes, uint256)', function () {
for (const [descr, start] of Object.entries({
'start = 0': 0,
'start within bound': 10,
Expand All @@ -66,11 +66,12 @@ describe('Bytes', function () {
it(descr, async function () {
const result = ethers.hexlify(lorem.slice(start));
expect(await this.mock.$slice(lorem, start)).to.equal(result);
expect(await this.mock.$splice(lorem, start)).to.equal(result);
});
}
});

describe('slice(bytes, uint256, uint256)', function () {
describe('slice(bytes, uint256, uint256) & splice(bytes, uint256, uint256)', function () {
for (const [descr, [start, end]] of Object.entries({
'start = 0': [0, 42],
'start and end within bound': [17, 42],
Expand All @@ -81,6 +82,7 @@ describe('Bytes', function () {
it(descr, async function () {
const result = ethers.hexlify(lorem.slice(start, end));
expect(await this.mock.$slice(lorem, start, ethers.Typed.uint256(end))).to.equal(result);
expect(await this.mock.$splice(lorem, start, ethers.Typed.uint256(end))).to.equal(result);
});
}
});
Expand Down