From ffcc3608ef17136774f2dc314b3c20b39047eddd Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 8 Jan 2024 18:19:47 +0100 Subject: [PATCH 01/14] add encode url to the Base64 library --- contracts/utils/Base64.sol | 66 ++++++++++++++++++++++++++++++++++++++ test/utils/Base64.test.js | 29 ++++++++++++++--- 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index f8547d1cc88..8ea9ffdd42a 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -9,8 +9,10 @@ pragma solidity ^0.8.20; library Base64 { /** * @dev Base64 Encoding/Decoding Table + * See sections 4 and 5 of https://datatracker.ietf.org/doc/html/rfc4648 */ string internal constant _TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + string internal constant _TABLE_URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; /** * @dev Converts a `bytes` to its Bytes64 `string` representation. @@ -87,4 +89,68 @@ library Base64 { return result; } + + /** + * @dev Converts a `bytes` to its Bytes64Url `string` representation. + */ + function encodeUrl(bytes memory data) internal pure returns (string memory) { + /** + * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence + * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol + */ + if (data.length == 0) return ""; + + // Loads the table into memory + string memory table = _TABLE_URL; + + // Encoding takes 3 bytes chunks of binary data from `bytes` data parameter + // and split into 4 numbers of 6 bits. + // Rounding here is different from the one performed in {encode} because we don't want padding + string memory result = new string((4 * data.length + 2) / 3); + + /// @solidity memory-safe-assembly + assembly { + // Prepare the lookup table (skip the first "length" byte) + let tablePtr := add(table, 1) + + // Prepare result pointer, jump over length + let resultPtr := add(result, 32) + + // Run over the input, 3 bytes at a time + for { + let dataPtr := data + let endPtr := add(data, mload(data)) + } lt(dataPtr, endPtr) { + + } { + // Advance 3 bytes + dataPtr := add(dataPtr, 3) + let input := mload(dataPtr) + + // To write each character, shift the 3 bytes (18 bits) chunk + // 4 times in blocks of 6 bits for each character (18, 12, 6, 0) + // and apply logical AND with 0x3F which is the number of + // the previous character in the ASCII table prior to the Base64 Table + // The result is then added to the table to get the character to write, + // and finally write it in the result pointer but with a left shift + // of 256 (1 byte) - 8 (1 ASCII char) = 248 bits + + mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + + mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + + mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + + mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + } + + // No `=` padding here + } + + return result; + } } diff --git a/test/utils/Base64.test.js b/test/utils/Base64.test.js index 4707db0c349..e6463b5a1dc 100644 --- a/test/utils/Base64.test.js +++ b/test/utils/Base64.test.js @@ -2,6 +2,10 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +// Replace "+/" with "-_" in the char table, and remove the padding +// see https://datatracker.ietf.org/doc/html/rfc4648#section-5 +const base64toBase64Url = str => str.replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', ''); + async function fixture() { const mock = await ethers.deployContract('$Base64'); return { mock }; @@ -12,18 +16,33 @@ describe('Strings', function () { Object.assign(this, await loadFixture(fixture)); }); - describe('from bytes - base64', function () { + describe('base64', function () { for (const { title, input, expected } of [ { title: 'converts to base64 encoded string with double padding', input: 'test', expected: 'dGVzdA==' }, { title: 'converts to base64 encoded string with single padding', input: 'test1', expected: 'dGVzdDE=' }, { title: 'converts to base64 encoded string without padding', input: 'test12', expected: 'dGVzdDEy' }, - { title: 'empty bytes', input: '0x', expected: '' }, + { title: 'converts to base64 encoded string (special case)', input: 'où', expected: 'b/k=' }, + { title: 'empty bytes', input: '', expected: '' }, ]) it(title, async function () { - const raw = ethers.isBytesLike(input) ? input : ethers.toUtf8Bytes(input); + const buffer = Buffer.from(input, 'ascii'); + expect(await this.mock.$encode(buffer)).to.equal(ethers.encodeBase64(buffer)); + expect(await this.mock.$encode(buffer)).to.equal(expected); + }); + }); - expect(await this.mock.$encode(raw)).to.equal(ethers.encodeBase64(raw)); - expect(await this.mock.$encode(raw)).to.equal(expected); + describe('base64url', function () { + for (const { title, input, expected } of [ + { title: 'converts to base64url encoded string with double padding', input: 'test', expected: 'dGVzdA' }, + { title: 'converts to base64url encoded string with single padding', input: 'test1', expected: 'dGVzdDE' }, + { title: 'converts to base64url encoded string without padding', input: 'test12', expected: 'dGVzdDEy' }, + { title: 'converts to base64url encoded string (special case)', input: 'où', expected: 'b_k' }, + { title: 'empty bytes', input: '', expected: '' }, + ]) + it(title, async function () { + const buffer = Buffer.from(input, 'ascii'); + expect(await this.mock.$encodeUrl(buffer)).to.equal(base64toBase64Url(ethers.encodeBase64(buffer))); + expect(await this.mock.$encodeUrl(buffer)).to.equal(expected); }); }); }); From c3ef73ad85b6109a2494f68e02514ce4bb1c7037 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 8 Jan 2024 18:20:57 +0100 Subject: [PATCH 02/14] add changeset --- .changeset/twenty-feet-grin.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/twenty-feet-grin.md diff --git a/.changeset/twenty-feet-grin.md b/.changeset/twenty-feet-grin.md new file mode 100644 index 00000000000..8e33a6e7768 --- /dev/null +++ b/.changeset/twenty-feet-grin.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Base64`: And `encodeUrl` following section 5 of rfc4648 for URL encoding From b217af39eee2ac2f4c386f0004fe3d4c1fcf74b3 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 10 Jan 2024 10:47:07 +0100 Subject: [PATCH 03/14] refactor base64 encoding --- contracts/utils/Base64.sol | 107 ++++++++++--------------------------- 1 file changed, 29 insertions(+), 78 deletions(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index 8ea9ffdd42a..247f036520c 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.20; +import "hardhat/console.sol"; + /** * @dev Provides a set of functions to operate with Base64 strings. */ @@ -18,95 +20,33 @@ library Base64 { * @dev Converts a `bytes` to its Bytes64 `string` representation. */ function encode(bytes memory data) internal pure returns (string memory) { - /** - * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence - * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol - */ - if (data.length == 0) return ""; - - // Loads the table into memory - string memory table = _TABLE; - - // Encoding takes 3 bytes chunks of binary data from `bytes` data parameter - // and split into 4 numbers of 6 bits. - // The final Base64 length should be `bytes` data length multiplied by 4/3 rounded up - // - `data.length + 2` -> Round up - // - `/ 3` -> Number of 3-bytes chunks - // - `4 *` -> 4 characters for each chunk - string memory result = new string(4 * ((data.length + 2) / 3)); - - /// @solidity memory-safe-assembly - assembly { - // Prepare the lookup table (skip the first "length" byte) - let tablePtr := add(table, 1) - - // Prepare result pointer, jump over length - let resultPtr := add(result, 32) - - // Run over the input, 3 bytes at a time - for { - let dataPtr := data - let endPtr := add(data, mload(data)) - } lt(dataPtr, endPtr) { - - } { - // Advance 3 bytes - dataPtr := add(dataPtr, 3) - let input := mload(dataPtr) - - // To write each character, shift the 3 bytes (18 bits) chunk - // 4 times in blocks of 6 bits for each character (18, 12, 6, 0) - // and apply logical AND with 0x3F which is the number of - // the previous character in the ASCII table prior to the Base64 Table - // The result is then added to the table to get the character to write, - // and finally write it in the result pointer but with a left shift - // of 256 (1 byte) - 8 (1 ASCII char) = 248 bits - - mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F)))) - resultPtr := add(resultPtr, 1) // Advance - - mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F)))) - resultPtr := add(resultPtr, 1) // Advance - - mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F)))) - resultPtr := add(resultPtr, 1) // Advance - - mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F)))) - resultPtr := add(resultPtr, 1) // Advance - } - - // When data `bytes` is not exactly 3 bytes long - // it is padded with `=` characters at the end - switch mod(mload(data), 3) - case 1 { - mstore8(sub(resultPtr, 1), 0x3d) - mstore8(sub(resultPtr, 2), 0x3d) - } - case 2 { - mstore8(sub(resultPtr, 1), 0x3d) - } - } - - return result; + return _encode(data, _TABLE, true); } /** * @dev Converts a `bytes` to its Bytes64Url `string` representation. */ function encodeUrl(bytes memory data) internal pure returns (string memory) { + return _encode(data, _TABLE_URL, false); + } + + /** + * @dev Internal table-agnostic conversion + */ + function _encode(bytes memory data, string memory table, bool withPadding) private pure returns (string memory) { /** * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol */ if (data.length == 0) return ""; - // Loads the table into memory - string memory table = _TABLE_URL; - - // Encoding takes 3 bytes chunks of binary data from `bytes` data parameter - // and split into 4 numbers of 6 bits. - // Rounding here is different from the one performed in {encode} because we don't want padding - string memory result = new string((4 * data.length + 2) / 3); + // Encoding takes 3 bytes chunks of binary data from `bytes` data parameter and split into 4 numbers of 6 bits. + // If padding is enabled, the final Base64 length should be `bytes` data length multiplied by 4/3 rounded up + // - `data.length + 2` -> Round up + // - `/ 3` -> Number of 3-bytes chunks + // - `4 *` -> 4 characters for each chunk + // If padding is not enable, then the length is rounded up differently + string memory result = new string(withPadding ? 4 * ((data.length + 2) / 3) : (4 * data.length + 2) / 3); /// @solidity memory-safe-assembly assembly { @@ -148,7 +88,18 @@ library Base64 { resultPtr := add(resultPtr, 1) // Advance } - // No `=` padding here + if withPadding { + // When data `bytes` is not exactly 3 bytes long + // it is padded with `=` characters at the end + switch mod(mload(data), 3) + case 1 { + mstore8(sub(resultPtr, 1), 0x3d) + mstore8(sub(resultPtr, 2), 0x3d) + } + case 2 { + mstore8(sub(resultPtr, 1), 0x3d) + } + } } return result; From ea29b55d8d705c47451b6cce1a38a0f5aa0aa4ed Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 10 Jan 2024 10:47:23 +0100 Subject: [PATCH 04/14] typo --- contracts/utils/Base64.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index 247f036520c..04a11903c10 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -45,7 +45,7 @@ library Base64 { // - `data.length + 2` -> Round up // - `/ 3` -> Number of 3-bytes chunks // - `4 *` -> 4 characters for each chunk - // If padding is not enable, then the length is rounded up differently + // If padding is not enabled, then the length is rounded up differently string memory result = new string(withPadding ? 4 * ((data.length + 2) / 3) : (4 * data.length + 2) / 3); /// @solidity memory-safe-assembly From 551ad07e90cdcb3a7d57159eb4c144b3da0b3a0e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 10 Jan 2024 11:24:54 +0100 Subject: [PATCH 05/14] cleanup --- contracts/utils/Base64.sol | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index 04a11903c10..e34e11d2c9a 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -3,8 +3,6 @@ pragma solidity ^0.8.20; -import "hardhat/console.sol"; - /** * @dev Provides a set of functions to operate with Base64 strings. */ @@ -40,13 +38,21 @@ library Base64 { */ if (data.length == 0) return ""; - // Encoding takes 3 bytes chunks of binary data from `bytes` data parameter and split into 4 numbers of 6 bits. - // If padding is enabled, the final Base64 length should be `bytes` data length multiplied by 4/3 rounded up - // - `data.length + 2` -> Round up - // - `/ 3` -> Number of 3-bytes chunks - // - `4 *` -> 4 characters for each chunk - // If padding is not enabled, then the length is rounded up differently - string memory result = new string(withPadding ? 4 * ((data.length + 2) / 3) : (4 * data.length + 2) / 3); + uint256 resultLength = withPadding + // The final length should be `bytes` data length divided by 3 rounded up and then multiplied by 4 + // so that it leaves room for padding the last chunk + // - `data.length + 2` -> Round up + // - `/ 3` -> Number of 3-bytes chunks + // - `4 *` -> 4 characters for each chunk + ? 4 * ((data.length + 2) / 3) + // The final length should be `bytes` data length multiplied by 4/3 rounded up + // as opposed to when padding is required to fill the last chunk. + // - `4 *` -> 4 characters for each chunk + // - `data.length + 2` -> Round up + // - `/ 3` -> Number of 3-bytes chunks + : (4 * data.length + 2) / 3; + + string memory result = new string(resultLength); /// @solidity memory-safe-assembly assembly { From fb5dade78ddd95ebb5f712c4b290bf0c8f068a16 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 10 Jan 2024 11:25:29 +0100 Subject: [PATCH 06/14] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- .changeset/twenty-feet-grin.md | 2 +- contracts/utils/Base64.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/twenty-feet-grin.md b/.changeset/twenty-feet-grin.md index 8e33a6e7768..69b4fe63b2e 100644 --- a/.changeset/twenty-feet-grin.md +++ b/.changeset/twenty-feet-grin.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`Base64`: And `encodeUrl` following section 5 of rfc4648 for URL encoding +`Base64`: Add `encodeURL` following section 5 of RFC4648 for URL encoding diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index e34e11d2c9a..0414b6c12ef 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -24,7 +24,7 @@ library Base64 { /** * @dev Converts a `bytes` to its Bytes64Url `string` representation. */ - function encodeUrl(bytes memory data) internal pure returns (string memory) { + function encodeURL(bytes memory data) internal pure returns (string memory) { return _encode(data, _TABLE_URL, false); } From 6e3d9731b85a3ee0563da3f861ab258858b99ec9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 10 Jan 2024 11:26:17 +0100 Subject: [PATCH 07/14] fix tests --- test/utils/Base64.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/utils/Base64.test.js b/test/utils/Base64.test.js index e6463b5a1dc..3c756acea9f 100644 --- a/test/utils/Base64.test.js +++ b/test/utils/Base64.test.js @@ -41,8 +41,8 @@ describe('Strings', function () { ]) it(title, async function () { const buffer = Buffer.from(input, 'ascii'); - expect(await this.mock.$encodeUrl(buffer)).to.equal(base64toBase64Url(ethers.encodeBase64(buffer))); - expect(await this.mock.$encodeUrl(buffer)).to.equal(expected); + expect(await this.mock.$encodeURL(buffer)).to.equal(base64toBase64Url(ethers.encodeBase64(buffer))); + expect(await this.mock.$encodeURL(buffer)).to.equal(expected); }); }); }); From 1f1ad9ec65fe9c96c16ef449cb3bde1de2800221 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 10 Jan 2024 11:27:04 +0100 Subject: [PATCH 08/14] fix lint --- contracts/utils/Base64.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index 0414b6c12ef..f8dfacc5f61 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -39,18 +39,18 @@ library Base64 { if (data.length == 0) return ""; uint256 resultLength = withPadding - // The final length should be `bytes` data length divided by 3 rounded up and then multiplied by 4 + ? // The final length should be `bytes` data length divided by 3 rounded up and then multiplied by 4 // so that it leaves room for padding the last chunk // - `data.length + 2` -> Round up // - `/ 3` -> Number of 3-bytes chunks // - `4 *` -> 4 characters for each chunk - ? 4 * ((data.length + 2) / 3) - // The final length should be `bytes` data length multiplied by 4/3 rounded up + 4 * ((data.length + 2) / 3) + : // The final length should be `bytes` data length multiplied by 4/3 rounded up // as opposed to when padding is required to fill the last chunk. // - `4 *` -> 4 characters for each chunk // - `data.length + 2` -> Round up // - `/ 3` -> Number of 3-bytes chunks - : (4 * data.length + 2) / 3; + (4 * data.length + 2) / 3; string memory result = new string(resultLength); From edcdf5b148cf965df2d210fe553e09f600a8266d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 10 Jan 2024 11:32:40 +0100 Subject: [PATCH 09/14] stable lint --- contracts/utils/Base64.sol | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index f8dfacc5f61..3f2d7c134b3 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -38,19 +38,17 @@ library Base64 { */ if (data.length == 0) return ""; - uint256 resultLength = withPadding - ? // The final length should be `bytes` data length divided by 3 rounded up and then multiplied by 4 - // so that it leaves room for padding the last chunk - // - `data.length + 2` -> Round up - // - `/ 3` -> Number of 3-bytes chunks - // - `4 *` -> 4 characters for each chunk - 4 * ((data.length + 2) / 3) - : // The final length should be `bytes` data length multiplied by 4/3 rounded up - // as opposed to when padding is required to fill the last chunk. - // - `4 *` -> 4 characters for each chunk - // - `data.length + 2` -> Round up - // - `/ 3` -> Number of 3-bytes chunks - (4 * data.length + 2) / 3; + // If padding is enabled, the final length should be `bytes` data length divided by 3 rounded up and then + // multiplied by 4 so that it leaves room for padding the last chunk + // - `data.length + 2` -> Round up + // - `/ 3` -> Number of 3-bytes chunks + // - `4 *` -> 4 characters for each chunk + // If padding is disabled, the final length should be `bytes` data length multiplied by 4/3 rounded up as + // opposed to when padding is required to fill the last chunk. + // - `4 *` -> 4 characters for each chunk + // - `data.length + 2` -> Round up + // - `/ 3` -> Number of 3-bytes chunks + uint256 resultLength = withPadding ? 4 * ((data.length + 2) / 3) : (4 * data.length + 2) / 3; string memory result = new string(resultLength); From b9e47dc919c4e25e67abc314f008aa0602856418 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 10 Jan 2024 12:52:45 -0600 Subject: [PATCH 10/14] Add fuzzing tests --- scripts/tests/base64.sh | 31 +++++++++++++++++++++++++++++++ test/utils/Base64.t.sol | 29 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 scripts/tests/base64.sh create mode 100644 test/utils/Base64.t.sol diff --git a/scripts/tests/base64.sh b/scripts/tests/base64.sh new file mode 100644 index 00000000000..f51cb4002e2 --- /dev/null +++ b/scripts/tests/base64.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -euo pipefail + +_encode() { + # - Print the input to stdout + # - Remove the first two characters + # - Convert from hex to binary + # - Convert from binary to base64 + # - Remove newlines from `base64` output + echo -n "$1" | cut -c 3- | xxd -r -p | base64 | tr -d \\n +} + +encode() { + # - Convert from base64 to hex + # - Remove newlines from `xxd` output + _encode "$1" | xxd -p | tr -d \\n +} + +encodeURL() { + # - Remove padding from `base64` output + # - Replace `+` with `-` + # - Replace `/` with `_` + # - Convert from base64 to hex + # - Remove newlines from `xxd` output + _encode "$1" | sed 's/=//g' | sed 's/+/-/g' | sed 's/\//_/g' | xxd -p | tr -d \\n +} + +# $1: function name +# $2: input +$1 $2 diff --git a/test/utils/Base64.t.sol b/test/utils/Base64.t.sol new file mode 100644 index 00000000000..41c61023143 --- /dev/null +++ b/test/utils/Base64.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {Base64} from "@openzeppelin/contracts/utils/Base64.sol"; + +contract ShortStringsTest is Test { + function testEncode(bytes memory input) external { + string memory output = Base64.encode(input); + assertEq(output, _base64Ffi(input, "encode")); + } + + function testEncodeURL(bytes memory input) external { + string memory output = Base64.encodeURL(input); + assertEq(output, _base64Ffi(input, "encodeURL")); + } + + function _base64Ffi(bytes memory input, string memory fn) internal returns (string memory) { + string[] memory command = new string[](4); + command[0] = "bash"; + command[1] = "scripts/tests/base64.sh"; + command[2] = fn; + command[3] = vm.toString(input); + bytes memory retData = vm.ffi(command); + return string(retData); + } +} From ea6d5bbc45349c93c881e9b518899322489914c0 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 10 Jan 2024 13:07:13 -0600 Subject: [PATCH 11/14] Enable ffi --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 7a44045a11b..0e78aa94d9d 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -82,7 +82,7 @@ jobs: - name: Set up environment uses: ./.github/actions/setup - name: Run tests - run: forge test -vv + run: forge test -vv --ffi coverage: runs-on: ubuntu-latest From 66305d9636597c46f7acb9aa1fddbb5f3d2dd9df Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 10 Jan 2024 13:32:34 -0600 Subject: [PATCH 12/14] Change test name lol --- test/utils/Base64.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/Base64.t.sol b/test/utils/Base64.t.sol index 41c61023143..dc98f4fa8c9 100644 --- a/test/utils/Base64.t.sol +++ b/test/utils/Base64.t.sol @@ -6,7 +6,7 @@ import {Test} from "forge-std/Test.sol"; import {Base64} from "@openzeppelin/contracts/utils/Base64.sol"; -contract ShortStringsTest is Test { +contract Base64Test is Test { function testEncode(bytes memory input) external { string memory output = Base64.encode(input); assertEq(output, _base64Ffi(input, "encode")); From 6a86d82a8ea077fc077255dc7565f2487df9f996 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 11 Jan 2024 09:55:28 -0600 Subject: [PATCH 13/14] Avoid running Base64 fuzzing in CI --- .github/workflows/checks.yml | 3 ++- test/utils/Base64.t.sol | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 0e78aa94d9d..167b64a8425 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -82,7 +82,8 @@ jobs: - name: Set up environment uses: ./.github/actions/setup - name: Run tests - run: forge test -vv --ffi + # Base64Test requires `--ffi`. See test/utils/Base64.t.sol + run: forge test -vv --no-match-contract Base64Test coverage: runs-on: ubuntu-latest diff --git a/test/utils/Base64.t.sol b/test/utils/Base64.t.sol index dc98f4fa8c9..80e4f49df9c 100644 --- a/test/utils/Base64.t.sol +++ b/test/utils/Base64.t.sol @@ -6,6 +6,9 @@ import {Test} from "forge-std/Test.sol"; import {Base64} from "@openzeppelin/contracts/utils/Base64.sol"; +/// NOTE: This test requires `ffi` to be enabled. It does not run in the CI +/// environment given `ffi` is not recommended. +/// See: https://github.com/foundry-rs/foundry/issues/6744 contract Base64Test is Test { function testEncode(bytes memory input) external { string memory output = Base64.encode(input); From 7cfe5071a4be858446d92ac5c18df929a99bcf19 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 11 Jan 2024 09:59:39 -0600 Subject: [PATCH 14/14] Improve Base64 unit tests --- test/utils/Base64.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/utils/Base64.test.js b/test/utils/Base64.test.js index 3c756acea9f..3f3e9fc8beb 100644 --- a/test/utils/Base64.test.js +++ b/test/utils/Base64.test.js @@ -21,7 +21,8 @@ describe('Strings', function () { { title: 'converts to base64 encoded string with double padding', input: 'test', expected: 'dGVzdA==' }, { title: 'converts to base64 encoded string with single padding', input: 'test1', expected: 'dGVzdDE=' }, { title: 'converts to base64 encoded string without padding', input: 'test12', expected: 'dGVzdDEy' }, - { title: 'converts to base64 encoded string (special case)', input: 'où', expected: 'b/k=' }, + { title: 'converts to base64 encoded string (/ case)', input: 'où', expected: 'b/k=' }, + { title: 'converts to base64 encoded string (+ case)', input: 'zs~1t8', expected: 'enN+MXQ4' }, { title: 'empty bytes', input: '', expected: '' }, ]) it(title, async function () { @@ -36,7 +37,8 @@ describe('Strings', function () { { title: 'converts to base64url encoded string with double padding', input: 'test', expected: 'dGVzdA' }, { title: 'converts to base64url encoded string with single padding', input: 'test1', expected: 'dGVzdDE' }, { title: 'converts to base64url encoded string without padding', input: 'test12', expected: 'dGVzdDEy' }, - { title: 'converts to base64url encoded string (special case)', input: 'où', expected: 'b_k' }, + { title: 'converts to base64url encoded string (_ case)', input: 'où', expected: 'b_k' }, + { title: 'converts to base64url encoded string (- case)', input: 'zs~1t8', expected: 'enN-MXQ4' }, { title: 'empty bytes', input: '', expected: '' }, ]) it(title, async function () {