Parcourir la source

Add Bytes.concat (#5882)

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Co-authored-by: Ernesto García <ernestognw@gmail.com>
Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com>
Hadrien Croubois il y a 1 mois
Parent
commit
005c9c9fa7
3 fichiers modifiés avec 78 ajouts et 0 suppressions
  1. 5 0
      .changeset/old-memes-dress.md
  2. 31 0
      contracts/utils/Bytes.sol
  3. 42 0
      test/utils/Bytes.test.js

+ 5 - 0
.changeset/old-memes-dress.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`Bytes`: Add `concat` that merges a `bytes[]` array of buffers into a single `bytes` buffer.

+ 31 - 0
contracts/utils/Bytes.sol

@@ -128,6 +128,37 @@ library Bytes {
         return buffer;
     }
 
+    /**
+     * @dev Concatenate an array of bytes into a single bytes object.
+     *
+     * For fixed bytes types, we recommend using the solidity built-in `bytes.concat` or (equivalent)
+     * `abi.encodePacked`.
+     *
+     * NOTE: this could be done in assembly with a single loop that expands starting at the FMP, but that would be
+     * significantly less readable. It might be worth benchmarking the savings of the full-assembly approach.
+     */
+    function concat(bytes[] memory buffers) internal pure returns (bytes memory) {
+        uint256 length = 0;
+        for (uint256 i = 0; i < buffers.length; ++i) {
+            length += buffers[i].length;
+        }
+
+        bytes memory result = new bytes(length);
+
+        uint256 offset = 0x20;
+        for (uint256 i = 0; i < buffers.length; ++i) {
+            bytes memory input = buffers[i];
+            assembly ("memory-safe") {
+                mcopy(add(result, offset), add(input, 0x20), mload(input))
+            }
+            unchecked {
+                offset += input.length;
+            }
+        }
+
+        return result;
+    }
+
     /**
      * @dev Returns true if the two byte buffers are equal.
      */

+ 42 - 0
test/utils/Bytes.test.js

@@ -2,6 +2,7 @@ const { ethers } = require('hardhat');
 const { expect } = require('chai');
 const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
 const { MAX_UINT128, MAX_UINT64, MAX_UINT32, MAX_UINT16 } = require('../helpers/constants');
+const { generators } = require('../helpers/random');
 
 // Helper functions for fixed bytes types
 const bytes32 = value => ethers.toBeHex(value, 32);
@@ -112,6 +113,47 @@ describe('Bytes', function () {
     });
   });
 
+  describe('concat', function () {
+    it('empty list', async function () {
+      await expect(this.mock.$concat([])).to.eventually.equal(generators.bytes.zero);
+    });
+
+    it('single item', async function () {
+      const item = generators.bytes();
+      await expect(this.mock.$concat([item])).to.eventually.equal(item);
+    });
+
+    it('multiple (non-empty) items', async function () {
+      const items = Array.from({ length: 17 }, generators.bytes);
+      await expect(this.mock.$concat(items)).to.eventually.equal(ethers.concat(items));
+    });
+
+    it('multiple (empty) items', async function () {
+      const items = Array.from({ length: 17 }).fill(generators.bytes.zero);
+      await expect(this.mock.$concat(items)).to.eventually.equal(ethers.concat(items));
+    });
+
+    it('multiple (variable length) items', async function () {
+      const items = [
+        generators.bytes.zero,
+        generators.bytes(17),
+        generators.bytes.zero,
+        generators.bytes(42),
+        generators.bytes(1),
+        generators.bytes(256),
+        generators.bytes(1024),
+        generators.bytes.zero,
+        generators.bytes(7),
+        generators.bytes(15),
+        generators.bytes(63),
+        generators.bytes.zero,
+        generators.bytes.zero,
+      ];
+
+      await expect(this.mock.$concat(items)).to.eventually.equal(ethers.concat(items));
+    });
+  });
+
   describe('clz bytes', function () {
     it('empty buffer', async function () {
       await expect(this.mock.$clz('0x')).to.eventually.equal(0);