diff --git a/.changeset/fluffy-facts-brake.md b/.changeset/fluffy-facts-brake.md new file mode 100644 index 00000000000..e8705337675 --- /dev/null +++ b/.changeset/fluffy-facts-brake.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Arrays`: Add `splice` function variants with `replacement` parameter for `address[]`, `bytes32[]` and `uint256[]` arrays, enabling in-place array modification with new content. diff --git a/.changeset/forty-ads-design.md b/.changeset/forty-ads-design.md new file mode 100644 index 00000000000..f0ebfca4ca7 --- /dev/null +++ b/.changeset/forty-ads-design.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Bytes`: Add `splice(bytes,uint256,bytes)` function that replaces a portion of a bytes buffer with replacement content, copying only what fits within the original buffer bounds. diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index e8ea749fb61..830cfaf11d7 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -496,6 +496,40 @@ library Arrays { return array; } + /** + * @dev Replaces the content of `array` with the content of `replacement`. The replacement is truncated to fit within the bounds of the array. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] + */ + function splice(address[] memory array, address[] memory replacement) internal pure returns (address[] memory) { + return splice(array, 0, replacement); + } + + /** + * @dev Replaces the content of `array` starting at position `start` with the content of `replacement`. The + * replacement is truncated to fit within the bounds of the array. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] + */ + function splice( + address[] memory array, + uint256 start, + address[] memory replacement + ) internal pure returns (address[] memory) { + // sanitize + start = Math.min(start, array.length); + uint256 copyLength = Math.min(replacement.length, array.length - start); + + // allocate and copy + assembly ("memory-safe") { + mcopy(add(add(array, 0x20), mul(start, 0x20)), add(replacement, 0x20), mul(copyLength, 0x20)) + } + + return array; + } + /** * @dev Moves the content of `array`, from `start` (included) to the end of `array` to the start of that array. * @@ -527,6 +561,40 @@ library Arrays { return array; } + /** + * @dev Replaces the content of `array` with the content of `replacement`. The replacement is truncated to fit within the bounds of the array. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] + */ + function splice(bytes32[] memory array, bytes32[] memory replacement) internal pure returns (bytes32[] memory) { + return splice(array, 0, replacement); + } + + /** + * @dev Replaces the content of `array` starting at position `start` with the content of `replacement`. The + * replacement is truncated to fit within the bounds of the array. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] + */ + function splice( + bytes32[] memory array, + uint256 start, + bytes32[] memory replacement + ) internal pure returns (bytes32[] memory) { + // sanitize + start = Math.min(start, array.length); + uint256 copyLength = Math.min(replacement.length, array.length - start); + + // allocate and copy + assembly ("memory-safe") { + mcopy(add(add(array, 0x20), mul(start, 0x20)), add(replacement, 0x20), mul(copyLength, 0x20)) + } + + return array; + } + /** * @dev Moves the content of `array`, from `start` (included) to the end of `array` to the start of that array. * @@ -558,6 +626,40 @@ library Arrays { return array; } + /** + * @dev Replaces the content of `array` with the content of `replacement`. The replacement is truncated to fit within the bounds of the array. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] + */ + function splice(uint256[] memory array, uint256[] memory replacement) internal pure returns (uint256[] memory) { + return splice(array, 0, replacement); + } + + /** + * @dev Replaces the content of `array` starting at position `start` with the content of `replacement`. The + * replacement is truncated to fit within the bounds of the array. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] + */ + function splice( + uint256[] memory array, + uint256 start, + uint256[] memory replacement + ) internal pure returns (uint256[] memory) { + // sanitize + start = Math.min(start, array.length); + uint256 copyLength = Math.min(replacement.length, array.length - start); + + // allocate and copy + assembly ("memory-safe") { + mcopy(add(add(array, 0x20), mul(start, 0x20)), add(replacement, 0x20), mul(copyLength, 0x20)) + } + + return array; + } + /** * @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check. * diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index 36574d81533..59f5779682b 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -128,6 +128,36 @@ library Bytes { return buffer; } + /** + * @dev Replaces the content of `buffer` with the content of `replacement`. The replacement is truncated to fit within the bounds of the buffer. + * + * NOTE: This function modifies the provided buffer in place. If you need to preserve the original buffer, use {slice} instead + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] + */ + function splice(bytes memory buffer, bytes memory replacement) internal pure returns (bytes memory) { + return splice(buffer, 0, replacement); + } + + /** + * @dev Replaces the content of `buffer` starting at position `start` with the content of `replacement`. The + * replacement is truncated to fit within the bounds of the buffer. + * + * NOTE: This function modifies the provided buffer in place. If you need to preserve the original buffer, use {slice} instead + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's `Array.splice`] + */ + function splice(bytes memory buffer, uint256 start, bytes memory replacement) internal pure returns (bytes memory) { + // sanitize + start = Math.min(start, buffer.length); + uint256 copyLength = Math.min(replacement.length, buffer.length - start); + + // allocate and copy + assembly ("memory-safe") { + mcopy(add(add(buffer, 0x20), start), add(replacement, 0x20), copyLength) + } + + return buffer; + } + /** * @dev Concatenate an array of bytes into a single bytes object. * diff --git a/scripts/generate/templates/Arrays.js b/scripts/generate/templates/Arrays.js index ff618025c8f..d634120f8b1 100644 --- a/scripts/generate/templates/Arrays.js +++ b/scripts/generate/templates/Arrays.js @@ -422,6 +422,40 @@ function splice(${type.name}[] memory array, uint256 start, uint256 end) interna return array; } + +/** + * @dev Replaces the content of \`array\` with the content of \`replacement\`. The replacement is truncated to fit within the bounds of the array. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's \`Array.splice\`] + */ +function splice(${type.name}[] memory array, ${type.name}[] memory replacement) internal pure returns (${type.name}[] memory) { + return splice(array, 0, replacement); +} + +/** + * @dev Replaces the content of \`array\` starting at position \`start\` with the content of \`replacement\`. The + * replacement is truncated to fit within the bounds of the array. + * + * NOTE: This function modifies the provided array in place. If you need to preserve the original array, use {slice} instead + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice[Javascript's \`Array.splice\`] + */ +function splice( + ${type.name}[] memory array, + uint256 start, + ${type.name}[] memory replacement +) internal pure returns (${type.name}[] memory) { + // sanitize + start = Math.min(start, array.length); + uint256 copyLength = Math.min(replacement.length, array.length - start); + + // allocate and copy + assembly ("memory-safe") { + mcopy(add(add(array, 0x20), mul(start, 0x20)), add(replacement, 0x20), mul(copyLength, 0x20)) + } + + return array; +} `; // GENERATE diff --git a/test/utils/Arrays.t.sol b/test/utils/Arrays.t.sol index 0daac5e3713..3310f1a9970 100644 --- a/test/utils/Arrays.t.sol +++ b/test/utils/Arrays.t.sol @@ -182,6 +182,149 @@ contract ArraysTest is Test, SymTest { _assertSliceOf(result, originalValues, sanitizedStart, expectedLength); } + function testSpliceAddressWithReplacementFromStart( + address[] memory values, + address[] memory replacement + ) public pure { + address[] memory originalValues = _copyArray(values); + address[] memory result = Arrays.splice(values, replacement); + + bytes32[] memory valuesBytes; + bytes32[] memory originalValuesBytes; + bytes32[] memory replacementBytes; + bytes32[] memory resultBytes; + + assembly { + valuesBytes := values + originalValuesBytes := originalValues + replacementBytes := replacement + resultBytes := result + } + + _validateSplice(valuesBytes, originalValuesBytes, 0, replacementBytes, resultBytes); + } + + function testSpliceAddressWithReplacement( + address[] memory values, + uint256 start, + address[] memory replacement + ) public pure { + address[] memory originalValues = _copyArray(values); + address[] memory result = Arrays.splice(values, start, replacement); + + bytes32[] memory valuesBytes; + bytes32[] memory originalValuesBytes; + bytes32[] memory replacementBytes; + bytes32[] memory resultBytes; + + assembly { + valuesBytes := values + originalValuesBytes := originalValues + replacementBytes := replacement + resultBytes := result + } + + _validateSplice(valuesBytes, originalValuesBytes, start, replacementBytes, resultBytes); + } + + function testSpliceBytes32WithReplacementFromStart( + bytes32[] memory values, + bytes32[] memory replacement + ) public pure { + bytes32[] memory originalValues = _copyArray(values); + bytes32[] memory result = Arrays.splice(values, replacement); + + _validateSplice(values, originalValues, 0, replacement, result); + } + + function testSpliceBytes32WithReplacement( + bytes32[] memory values, + uint256 start, + bytes32[] memory replacement + ) public pure { + bytes32[] memory originalValues = _copyArray(values); + bytes32[] memory result = Arrays.splice(values, start, replacement); + + _validateSplice(values, originalValues, start, replacement, result); + } + + function testSpliceUint256WithReplacementFromStart( + uint256[] memory values, + uint256[] memory replacement + ) public pure { + uint256[] memory originalValues = _copyArray(values); + uint256[] memory result = Arrays.splice(values, replacement); + + bytes32[] memory valuesBytes; + bytes32[] memory originalValuesBytes; + bytes32[] memory replacementBytes; + bytes32[] memory resultBytes; + + assembly { + valuesBytes := values + originalValuesBytes := originalValues + replacementBytes := replacement + resultBytes := result + } + + _validateSplice(valuesBytes, originalValuesBytes, 0, replacementBytes, resultBytes); + } + + function testSpliceUint256WithReplacement( + uint256[] memory values, + uint256 start, + uint256[] memory replacement + ) public pure { + uint256[] memory originalValues = _copyArray(values); + uint256[] memory result = Arrays.splice(values, start, replacement); + + bytes32[] memory valuesBytes; + bytes32[] memory originalValuesBytes; + bytes32[] memory replacementBytes; + bytes32[] memory resultBytes; + assembly { + valuesBytes := values + originalValuesBytes := originalValues + replacementBytes := replacement + resultBytes := result + } + + _validateSplice(valuesBytes, originalValuesBytes, start, replacementBytes, resultBytes); + } + + function _validateSplice( + bytes32[] memory values, + bytes32[] memory originalValues, + uint256 start, + bytes32[] memory replacement, + bytes32[] memory result + ) internal pure { + // Result should be the same object as input (modified in place) + assertEq(result, values); + + // Array length should remain unchanged + assertEq(result.length, originalValues.length); + + // Calculate expected bounds after sanitization + uint256 sanitizedStart = Math.min(start, originalValues.length); + uint256 copyLength = Math.min(replacement.length, originalValues.length - sanitizedStart); + + // Verify content before start position is unchanged + for (uint256 i = 0; i < sanitizedStart; ++i) { + assertEq(result[i], originalValues[i]); + } + + // Verify replacement content was copied correctly + for (uint256 i = 0; i < copyLength; ++i) { + assertEq(result[sanitizedStart + i], replacement[i]); + } + + // Verify content after replacement is unchanged + for (uint256 i = sanitizedStart + copyLength; i < result.length; ++i) { + assertEq(result[i], originalValues[i]); + } + } + /// Asserts function _assertSort(uint256[] memory values) internal pure { diff --git a/test/utils/Arrays.test.js b/test/utils/Arrays.test.js index c3bee1492c9..ea13d3b91d6 100644 --- a/test/utils/Arrays.test.js +++ b/test/utils/Arrays.test.js @@ -232,6 +232,149 @@ describe('Arrays', function () { }); }); } + + describe('splice with replacement from start', function () { + const array = Array.from({ length: 10 }, generators[name]); + const replacementFromStartFragment = `$splice(${name}[] arr, ${name}[] replacement)`; + + it('replacement longer than array', async function () { + const longReplacement = Array.from({ length: 15 }, generators[name]); + const expected = [...array]; + const copyLength = Math.min(longReplacement.length, array.length); + for (let i = 0; i < copyLength; i++) { + expected[i] = longReplacement[i]; + } + await expect(this.mock[replacementFromStartFragment](array, longReplacement)).to.eventually.deep.equal( + expected, + ); + }); + + it('replacement shorter than array', async function () { + const shortReplacement = Array.from({ length: 3 }, generators[name]); + const expected = [...array]; + for (let i = 0; i < shortReplacement.length; i++) { + expected[i] = shortReplacement[i]; + } + await expect(this.mock[replacementFromStartFragment](array, shortReplacement)).to.eventually.deep.equal( + expected, + ); + }); + + it('empty replacement', async function () { + const emptyReplacement = []; + await expect(this.mock[replacementFromStartFragment](array, emptyReplacement)).to.eventually.deep.equal( + array, + ); + }); + + it('replace entire array with same size', async function () { + const sameSize = Array.from({ length: array.length }, generators[name]); + const expected = [...sameSize]; + await expect(this.mock[replacementFromStartFragment](array, sameSize)).to.eventually.deep.equal(expected); + }); + + it('single element replacement', async function () { + const singleReplacement = [generators[name]()]; + const expected = [...array]; + expected[0] = singleReplacement[0]; + await expect(this.mock[replacementFromStartFragment](array, singleReplacement)).to.eventually.deep.equal( + expected, + ); + }); + + it('empty array', async function () { + const emptyArray = []; + const replacement = Array.from({ length: 3 }, generators[name]); + await expect(this.mock[replacementFromStartFragment](emptyArray, replacement)).to.eventually.deep.equal( + emptyArray, + ); + }); + }); + + describe('splice with replacement', function () { + const array = Array.from({ length: 10 }, generators[name]); + const replacement = Array.from({ length: 3 }, generators[name]); + const replacementFragment = `$splice(${name}[] arr, uint256 start, ${name}[] replacement)`; + + it('replace at start', async function () { + const start = 0; + const expected = [...array]; + const copyLength = Math.min(replacement.length, array.length - start); + for (let i = 0; i < copyLength; i++) { + expected[start + i] = replacement[i]; + } + await expect(this.mock[replacementFragment](array, start, replacement)).to.eventually.deep.equal(expected); + }); + + it('replace in middle', async function () { + const start = 3; + const expected = [...array]; + const copyLength = Math.min(replacement.length, array.length - start); + for (let i = 0; i < copyLength; i++) { + expected[start + i] = replacement[i]; + } + await expect(this.mock[replacementFragment](array, start, replacement)).to.eventually.deep.equal(expected); + }); + + it('replace at end', async function () { + const start = array.length - 2; + const expected = [...array]; + const copyLength = Math.min(replacement.length, array.length - start); + for (let i = 0; i < copyLength; i++) { + expected[start + i] = replacement[i]; + } + await expect(this.mock[replacementFragment](array, start, replacement)).to.eventually.deep.equal(expected); + }); + + it('start out of bounds', async function () { + const start = array.length + 5; + await expect(this.mock[replacementFragment](array, start, replacement)).to.eventually.deep.equal(array); + }); + + it('replacement longer than remaining space', async function () { + const longReplacement = Array.from({ length: 8 }, generators[name]); + const start = array.length - 3; + const expected = [...array]; + const copyLength = Math.min(longReplacement.length, array.length - start); + for (let i = 0; i < copyLength; i++) { + expected[start + i] = longReplacement[i]; + } + await expect(this.mock[replacementFragment](array, start, longReplacement)).to.eventually.deep.equal( + expected, + ); + }); + + it('empty replacement', async function () { + const emptyReplacement = []; + const start = 3; + await expect(this.mock[replacementFragment](array, start, emptyReplacement)).to.eventually.deep.equal( + array, + ); + }); + + it('replace entire array', async function () { + const shortArray = Array.from({ length: 2 }, generators[name]); + const longReplacement = Array.from({ length: 5 }, generators[name]); + const expected = [...shortArray]; + const copyLength = Math.min(longReplacement.length, shortArray.length); + for (let i = 0; i < copyLength; i++) { + expected[i] = longReplacement[i]; + } + await expect(this.mock[replacementFragment](shortArray, 0, longReplacement)).to.eventually.deep.equal( + expected, + ); + }); + + it('single element replacement', async function () { + const singleReplacement = [generators[name]()]; + const start = 2; + const expected = [...array]; + expected[start] = singleReplacement[0]; + await expect(this.mock[replacementFragment](array, start, singleReplacement)).to.eventually.deep.equal( + expected, + ); + }); + }); } describe('unsafeAccess', function () { diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol index 9412ed53c98..daf362083a1 100644 --- a/test/utils/Bytes.t.sol +++ b/test/utils/Bytes.t.sol @@ -155,6 +155,60 @@ contract BytesTest is Test { } } + function testSpliceWithReplacementFromStart(bytes memory buffer, bytes memory replacement) public pure { + bytes memory originalBuffer = bytes.concat(buffer); + bytes memory result = buffer.splice(replacement); + + // Result should be the same object as input (modified in place) + assertEq(result, buffer); + + // Buffer length should remain unchanged + assertEq(result.length, originalBuffer.length); + + // Calculate copy length (replacement is applied from start=0) + uint256 copyLength = Math.min(replacement.length, originalBuffer.length); + + // Verify replacement content was copied correctly from start + for (uint256 i = 0; i < copyLength; ++i) { + assertEq(result[i], replacement[i]); + } + + // Verify content after replacement is unchanged + for (uint256 i = copyLength; i < result.length; ++i) { + assertEq(result[i], originalBuffer[i]); + } + } + + function testSpliceWithReplacement(bytes memory buffer, uint256 start, bytes memory replacement) public pure { + bytes memory originalBuffer = bytes.concat(buffer); + bytes memory result = buffer.splice(start, replacement); + + // Result should be the same object as input (modified in place) + assertEq(result, buffer); + + // Buffer length should remain unchanged + assertEq(result.length, originalBuffer.length); + + // Calculate expected bounds after sanitization + uint256 sanitizedStart = Math.min(start, originalBuffer.length); + uint256 copyLength = Math.min(replacement.length, originalBuffer.length - sanitizedStart); + + // Verify content before start position is unchanged + for (uint256 i = 0; i < sanitizedStart; ++i) { + assertEq(result[i], originalBuffer[i]); + } + + // Verify replacement content was copied correctly + for (uint256 i = 0; i < copyLength; ++i) { + assertEq(result[sanitizedStart + i], replacement[i]); + } + + // Verify content after replacement is unchanged + for (uint256 i = sanitizedStart + copyLength; i < result.length; ++i) { + assertEq(result[i], originalBuffer[i]); + } + } + // REVERSE BITS function testSymbolicReverseBytes32(bytes32 value) public pure { assertEq(Bytes.reverseBytes32(Bytes.reverseBytes32(value)), value); diff --git a/test/utils/Bytes.test.js b/test/utils/Bytes.test.js index 9eb439cf9c9..95ac4b96e8c 100644 --- a/test/utils/Bytes.test.js +++ b/test/utils/Bytes.test.js @@ -91,7 +91,7 @@ describe('Bytes', function () { it(descr, async function () { const result = ethers.hexlify(lorem.slice(start)); await expect(this.mock.$slice(lorem, start)).to.eventually.equal(result); - await expect(this.mock.$splice(lorem, start)).to.eventually.equal(result); + await expect(this.mock.$splice(lorem, ethers.Typed.uint256(start))).to.eventually.equal(result); }); } }); @@ -111,6 +111,176 @@ describe('Bytes', function () { }); } }); + + describe('splice(bytes, bytes)', function () { + const replacement = ethers.toUtf8Bytes('REPLACEMENT'); + + it('replace from start', async function () { + const buffer = new Uint8Array(lorem); + const expected = new Uint8Array(buffer.length); + expected.set(replacement.slice(0, Math.min(replacement.length, buffer.length)), 0); + expected.set( + buffer.slice(Math.min(replacement.length, buffer.length)), + Math.min(replacement.length, buffer.length), + ); + + await expect(this.mock.$splice(lorem, ethers.Typed.bytes(replacement))).to.eventually.equal( + ethers.hexlify(expected), + ); + }); + + it('replacement longer than buffer', async function () { + const longReplacement = ethers.toUtf8Bytes( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi placerat elit non felis scelerisque faucibus. Donec eget blandit.', + ); + const buffer = new Uint8Array(lorem); + const expected = new Uint8Array(buffer); + expected.set(longReplacement.slice(0, Math.min(longReplacement.length, buffer.length)), 0); + + await expect(this.mock.$splice(lorem, ethers.Typed.bytes(longReplacement))).to.eventually.equal( + ethers.hexlify(expected), + ); + }); + + it('replacement shorter than buffer', async function () { + const shortReplacement = ethers.toUtf8Bytes('SHORT'); + const buffer = new Uint8Array(lorem); + const expected = new Uint8Array(buffer); + expected.set(shortReplacement, 0); + + await expect(this.mock.$splice(lorem, ethers.Typed.bytes(shortReplacement))).to.eventually.equal( + ethers.hexlify(expected), + ); + }); + + it('empty replacement', async function () { + const emptyReplacement = new Uint8Array(0); + await expect(this.mock.$splice(lorem, ethers.Typed.bytes(emptyReplacement))).to.eventually.equal( + ethers.hexlify(lorem), + ); + }); + + it('replace entire buffer with same size', async function () { + const sameSize = ethers.toUtf8Bytes('A'.repeat(lorem.length)); + const expected = new Uint8Array(sameSize); + + await expect(this.mock.$splice(lorem, ethers.Typed.bytes(sameSize))).to.eventually.equal( + ethers.hexlify(expected), + ); + }); + + it('single byte replacement', async function () { + const singleByte = new Uint8Array([0xff]); + const buffer = new Uint8Array(lorem); + const expected = new Uint8Array(buffer); + expected[0] = 0xff; + + await expect(this.mock.$splice(lorem, ethers.Typed.bytes(singleByte))).to.eventually.equal( + ethers.hexlify(expected), + ); + }); + + it('empty buffer', async function () { + const emptyBuffer = new Uint8Array(0); + const replacement = ethers.toUtf8Bytes('REPLACEMENT'); + + await expect(this.mock.$splice(emptyBuffer, ethers.Typed.bytes(replacement))).to.eventually.equal( + ethers.hexlify(emptyBuffer), + ); + }); + }); + + describe('splice(bytes, uint256, bytes)', function () { + const replacement = ethers.toUtf8Bytes('REPLACEMENT'); + + it('replace at start', async function () { + const buffer = new Uint8Array(lorem); + const expected = new Uint8Array(buffer.length); + expected.set(replacement.slice(0, Math.min(replacement.length, buffer.length)), 0); + expected.set( + buffer.slice(Math.min(replacement.length, buffer.length)), + Math.min(replacement.length, buffer.length), + ); + + await expect(this.mock.$splice(lorem, 0, ethers.Typed.bytes(replacement))).to.eventually.equal( + ethers.hexlify(expected), + ); + }); + + it('replace in middle', async function () { + const start = 10; + const buffer = new Uint8Array(lorem); + const copyLength = Math.min(replacement.length, buffer.length - start); + const expected = new Uint8Array(buffer); + expected.set(replacement.slice(0, copyLength), start); + + await expect(this.mock.$splice(lorem, start, ethers.Typed.bytes(replacement))).to.eventually.equal( + ethers.hexlify(expected), + ); + }); + + it('replace at end', async function () { + const start = lorem.length - 5; + const buffer = new Uint8Array(lorem); + const copyLength = Math.min(replacement.length, buffer.length - start); + const expected = new Uint8Array(buffer); + expected.set(replacement.slice(0, copyLength), start); + + await expect(this.mock.$splice(lorem, start, ethers.Typed.bytes(replacement))).to.eventually.equal( + ethers.hexlify(expected), + ); + }); + + it('start out of bounds', async function () { + const start = lorem.length + 10; + await expect(this.mock.$splice(lorem, start, ethers.Typed.bytes(replacement))).to.eventually.equal( + ethers.hexlify(lorem), + ); + }); + + it('replacement longer than remaining buffer', async function () { + const longReplacement = ethers.toUtf8Bytes('THIS IS A VERY LONG REPLACEMENT THAT EXCEEDS THE BUFFER SIZE'); + const start = lorem.length - 5; + const buffer = new Uint8Array(lorem); + const copyLength = Math.min(longReplacement.length, buffer.length - start); + const expected = new Uint8Array(buffer); + expected.set(longReplacement.slice(0, copyLength), start); + + await expect(this.mock.$splice(lorem, start, ethers.Typed.bytes(longReplacement))).to.eventually.equal( + ethers.hexlify(expected), + ); + }); + + it('empty replacement', async function () { + const emptyReplacement = new Uint8Array(0); + await expect(this.mock.$splice(lorem, 10, ethers.Typed.bytes(emptyReplacement))).to.eventually.equal( + ethers.hexlify(lorem), + ); + }); + + it('replace entire buffer', async function () { + const shortBuffer = ethers.toUtf8Bytes('SHORT'); + const longReplacement = ethers.toUtf8Bytes('LONGER_REPLACEMENT'); + const expected = new Uint8Array(shortBuffer); + expected.set(longReplacement.slice(0, Math.min(longReplacement.length, shortBuffer.length)), 0); + + await expect(this.mock.$splice(shortBuffer, 0, ethers.Typed.bytes(longReplacement))).to.eventually.equal( + ethers.hexlify(expected), + ); + }); + + it('single byte replacement', async function () { + const singleByte = new Uint8Array([0xff]); + const start = 5; + const buffer = new Uint8Array(lorem); + const expected = new Uint8Array(buffer); + expected[start] = 0xff; + + await expect(this.mock.$splice(lorem, start, ethers.Typed.bytes(singleByte))).to.eventually.equal( + ethers.hexlify(expected), + ); + }); + }); }); describe('concat', function () {