Skip to content

Commit 005c9c9

Browse files
Amxxsourcery-ai[bot]ernestognwarr00
authored
Add Bytes.concat (#5882)
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: Ernesto García <[email protected]> Co-authored-by: Arr00 <[email protected]>
1 parent be8ee86 commit 005c9c9

File tree

3 files changed

+78
-0
lines changed

3 files changed

+78
-0
lines changed

.changeset/old-memes-dress.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 `concat` that merges a `bytes[]` array of buffers into a single `bytes` buffer.

contracts/utils/Bytes.sol

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,37 @@ library Bytes {
128128
return buffer;
129129
}
130130

131+
/**
132+
* @dev Concatenate an array of bytes into a single bytes object.
133+
*
134+
* For fixed bytes types, we recommend using the solidity built-in `bytes.concat` or (equivalent)
135+
* `abi.encodePacked`.
136+
*
137+
* NOTE: this could be done in assembly with a single loop that expands starting at the FMP, but that would be
138+
* significantly less readable. It might be worth benchmarking the savings of the full-assembly approach.
139+
*/
140+
function concat(bytes[] memory buffers) internal pure returns (bytes memory) {
141+
uint256 length = 0;
142+
for (uint256 i = 0; i < buffers.length; ++i) {
143+
length += buffers[i].length;
144+
}
145+
146+
bytes memory result = new bytes(length);
147+
148+
uint256 offset = 0x20;
149+
for (uint256 i = 0; i < buffers.length; ++i) {
150+
bytes memory input = buffers[i];
151+
assembly ("memory-safe") {
152+
mcopy(add(result, offset), add(input, 0x20), mload(input))
153+
}
154+
unchecked {
155+
offset += input.length;
156+
}
157+
}
158+
159+
return result;
160+
}
161+
131162
/**
132163
* @dev Returns true if the two byte buffers are equal.
133164
*/

test/utils/Bytes.test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const { ethers } = require('hardhat');
22
const { expect } = require('chai');
33
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
44
const { MAX_UINT128, MAX_UINT64, MAX_UINT32, MAX_UINT16 } = require('../helpers/constants');
5+
const { generators } = require('../helpers/random');
56

67
// Helper functions for fixed bytes types
78
const bytes32 = value => ethers.toBeHex(value, 32);
@@ -112,6 +113,47 @@ describe('Bytes', function () {
112113
});
113114
});
114115

116+
describe('concat', function () {
117+
it('empty list', async function () {
118+
await expect(this.mock.$concat([])).to.eventually.equal(generators.bytes.zero);
119+
});
120+
121+
it('single item', async function () {
122+
const item = generators.bytes();
123+
await expect(this.mock.$concat([item])).to.eventually.equal(item);
124+
});
125+
126+
it('multiple (non-empty) items', async function () {
127+
const items = Array.from({ length: 17 }, generators.bytes);
128+
await expect(this.mock.$concat(items)).to.eventually.equal(ethers.concat(items));
129+
});
130+
131+
it('multiple (empty) items', async function () {
132+
const items = Array.from({ length: 17 }).fill(generators.bytes.zero);
133+
await expect(this.mock.$concat(items)).to.eventually.equal(ethers.concat(items));
134+
});
135+
136+
it('multiple (variable length) items', async function () {
137+
const items = [
138+
generators.bytes.zero,
139+
generators.bytes(17),
140+
generators.bytes.zero,
141+
generators.bytes(42),
142+
generators.bytes(1),
143+
generators.bytes(256),
144+
generators.bytes(1024),
145+
generators.bytes.zero,
146+
generators.bytes(7),
147+
generators.bytes(15),
148+
generators.bytes(63),
149+
generators.bytes.zero,
150+
generators.bytes.zero,
151+
];
152+
153+
await expect(this.mock.$concat(items)).to.eventually.equal(ethers.concat(items));
154+
});
155+
});
156+
115157
describe('clz bytes', function () {
116158
it('empty buffer', async function () {
117159
await expect(this.mock.$clz('0x')).to.eventually.equal(0);

0 commit comments

Comments
 (0)