Skip to content

Commit 4369449

Browse files
committed
Add Bytes.concat
1 parent efdc7cd commit 4369449

File tree

3 files changed

+51
-0
lines changed

3 files changed

+51
-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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,35 @@ 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 recommand 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 work benchmarking the saving 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+
offset += input.length;
155+
}
156+
157+
return result;
158+
}
159+
131160
/**
132161
* @dev Returns true if the two byte buffers are equal.
133162
*/

test/utils/Bytes.test.js

Lines changed: 17 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,22 @@ 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 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+
115132
describe('clz bytes', function () {
116133
it('empty buffer', async function () {
117134
await expect(this.mock.$clz('0x')).to.eventually.equal(0);

0 commit comments

Comments
 (0)