From a8678066c324cac4d9ee73dd8fd82e5b748c0e6d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 23 Jun 2025 19:08:23 +0200 Subject: [PATCH 1/9] Add Base64.decode --- contracts/utils/Base64.sol | 81 +++++++++++++++++++++++++++++++++++--- test/utils/Base64.test.js | 17 +++++--- 2 files changed, 87 insertions(+), 11 deletions(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index c6ee6a524aa..b81ee9822d2 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -3,22 +3,26 @@ pragma solidity ^0.8.20; +import {SafeCast} from "./math/SafeCast.sol"; + /** * @dev Provides a set of functions to operate with Base64 strings. */ library Base64 { + using SafeCast for bool; + /** * @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-_"; + bytes internal constant _TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + bytes internal constant _TABLE_URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; /** * @dev Converts a `bytes` to its Bytes64 `string` representation. */ function encode(bytes memory data) internal pure returns (string memory) { - return _encode(data, _TABLE, true); + return string(_encode(data, _TABLE, true)); } /** @@ -26,13 +30,24 @@ library Base64 { * Output is not padded with `=` as specified in https://www.rfc-editor.org/rfc/rfc4648[rfc4648]. */ function encodeURL(bytes memory data) internal pure returns (string memory) { - return _encode(data, _TABLE_URL, false); + return string(_encode(data, _TABLE_URL, false)); + } + + /** + * @dev Converts a Base64 `string` to the `bytes` it represents. + * + * * Supports padded an unpadded inputs. + * * Supports both encoding ({encode} and {encodeURL}) seamlessly. + * * Does NOT revert if the input is not a valid Base64 string. + */ + function decode(string memory data) internal pure returns (bytes memory) { + return _decode(bytes(data)); } /** * @dev Internal table-agnostic conversion */ - function _encode(bytes memory data, string memory table, bool withPadding) private pure returns (string memory) { + function _encode(bytes memory data, bytes memory table, bool withPadding) private pure returns (bytes memory) { /** * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol @@ -54,7 +69,7 @@ library Base64 { // This is equivalent to: Math.ceil((4 * data.length) / 3) uint256 resultLength = withPadding ? 4 * ((data.length + 2) / 3) : (4 * data.length + 2) / 3; - string memory result = new string(resultLength); + bytes memory result = new bytes(resultLength); assembly ("memory-safe") { // Prepare the lookup table (skip the first "length" byte) @@ -116,4 +131,58 @@ library Base64 { return result; } + + function _decode(bytes memory data) private pure returns (bytes memory) { + if (data.length == 0) return ""; + + uint256 resultLength = (data.length / 4) * 3; + if (data.length % 4 == 0) { + resultLength -= (data[data.length - 1] == "=").toUint() + (data[data.length - 2] == "=").toUint(); + } else { + resultLength += (data.length % 4) - 1; + } + + bytes memory result = new bytes(resultLength); + + assembly ("memory-safe") { + // Magic values. This writes over FMP (0x40) and zero slot (0x60) that will have to be reset. + let m := 0xfc000000fc00686c7074787c8084888c9094989ca0a4a8acb0b4b8bcc0c4c8cc + mstore(0x5b, m) + mstore(0x3b, 0x04080c1014181c2024282c3034383c4044484c5054585c6064) + mstore(0x1a, 0xf8fcf800fcd0d4d8dce0e4e8ecf0f4) + + // Prepare result pointer, jump over length + let dataPtr := data + let resultPtr := add(result, 0x20) + let endPtr := add(resultPtr, resultLength) + + for {} lt(resultPtr, endPtr) {} { + // Advance 4 bytes + dataPtr := add(dataPtr, 4) + let input := mload(dataPtr) + + mstore( + resultPtr, + or( + and(m, mload(byte(28, input))), + shr( + 6, + or( + and(m, mload(byte(29, input))), + shr(6, or(and(m, mload(byte(30, input))), shr(6, and(m, mload(byte(31, input)))))) + ) + ) + ) + ) + + resultPtr := add(resultPtr, 3) + } + + // Restore FMP and zero slot. + mstore(0x40, endPtr) + mstore(0x60, 0) + } + + return result; + } } diff --git a/test/utils/Base64.test.js b/test/utils/Base64.test.js index 5c427466671..b0fcfe01325 100644 --- a/test/utils/Base64.test.js +++ b/test/utils/Base64.test.js @@ -11,7 +11,7 @@ async function fixture() { return { mock }; } -describe('Strings', function () { +describe('Base64', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); @@ -27,8 +27,9 @@ describe('Strings', function () { ]) it(title, async function () { 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); + await expect(this.mock.$encode(buffer)).to.eventually.equal(ethers.encodeBase64(buffer)); + await expect(this.mock.$encode(buffer)).to.eventually.equal(expected); + await expect(this.mock.$decode(expected)).to.eventually.equal(ethers.hexlify(buffer)); }); }); @@ -43,11 +44,17 @@ 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); + await expect(this.mock.$encodeURL(buffer)).to.eventually.equal(base64toBase64Url(ethers.encodeBase64(buffer))); + await expect(this.mock.$encodeURL(buffer)).to.eventually.equal(expected); + await expect(this.mock.$decode(expected)).to.eventually.equal(ethers.hexlify(buffer)); }); }); + // TODO ? + it.skip('Decode invalid base64 string', async function () { + await expect(this.mock.$decode('dGVzd$==')).to.be.reverted; + }); + it('Encode reads beyond the input buffer into dirty memory', async function () { const mock = await ethers.deployContract('Base64Dirty'); const buffer32 = ethers.id('example'); From 77335781ca0bddb8a79f62d64edd87ae71ed94b5 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 23 Jun 2025 23:20:06 +0200 Subject: [PATCH 2/9] change method for one I understand fully --- contracts/utils/Base64.sol | 91 ++++++++++++++++++++++---------------- test/utils/Base64.t.sol | 2 + test/utils/Base64.test.js | 8 +++- 3 files changed, 62 insertions(+), 39 deletions(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index b81ee9822d2..87cd20a133e 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -45,9 +45,13 @@ library Base64 { } /** - * @dev Internal table-agnostic conversion + * @dev Internal table-agnostic encoding */ - function _encode(bytes memory data, bytes memory table, bool withPadding) private pure returns (bytes memory) { + function _encode( + bytes memory data, + bytes memory table, + bool withPadding + ) private pure returns (bytes memory result) { /** * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol @@ -69,9 +73,9 @@ library Base64 { // This is equivalent to: Math.ceil((4 * data.length) / 3) uint256 resultLength = withPadding ? 4 * ((data.length + 2) / 3) : (4 * data.length + 2) / 3; - bytes memory result = new bytes(resultLength); - assembly ("memory-safe") { + result := mload(0x40) + // Prepare the lookup table (skip the first "length" byte) let tablePtr := add(table, 1) @@ -122,67 +126,80 @@ library Base64 { case 1 { mstore8(sub(resultPtr, 1), 0x3d) mstore8(sub(resultPtr, 2), 0x3d) + resultPtr := add(resultPtr, 2) } case 2 { mstore8(sub(resultPtr, 1), 0x3d) + resultPtr := add(resultPtr, 1) } } - } - return result; + // Store result length and update FMP to reserve allocated space + mstore(result, resultLength) + mstore(0x40, resultPtr) + } } - function _decode(bytes memory data) private pure returns (bytes memory) { - if (data.length == 0) return ""; + /** + * @dev Internal decoding + */ + function _decode(bytes memory data) private pure returns (bytes memory result) { + uint256 dataLength = data.length; + if (dataLength == 0) return ""; - uint256 resultLength = (data.length / 4) * 3; - if (data.length % 4 == 0) { - resultLength -= (data[data.length - 1] == "=").toUint() + (data[data.length - 2] == "=").toUint(); + uint256 resultLength = (dataLength / 4) * 3; + if (dataLength % 4 == 0) { + resultLength -= (data[dataLength - 1] == "=").toUint() + (data[dataLength - 2] == "=").toUint(); } else { - resultLength += (data.length % 4) - 1; + resultLength += (dataLength % 4) - 1; } - bytes memory result = new bytes(resultLength); - assembly ("memory-safe") { - // Magic values. This writes over FMP (0x40) and zero slot (0x60) that will have to be reset. - let m := 0xfc000000fc00686c7074787c8084888c9094989ca0a4a8acb0b4b8bcc0c4c8cc - mstore(0x5b, m) - mstore(0x3b, 0x04080c1014181c2024282c3034383c4044484c5054585c6064) - mstore(0x1a, 0xf8fcf800fcd0d4d8dce0e4e8ecf0f4) + result := mload(0x40) + + // Temporarily store the reverse lookup table between in memory. This spans from 0x00 to 0x50, Using: + // - all 64bytes of scratch space + // - part of the FMP (at location 0x40) + mstore(0x30, 0x2425262728292a2b2c2d2e2f30313233) + mstore(0x20, 0x0a0b0c0d0e0f10111213141516171819ffffffff3fff1a1b1c1d1e1f20212223) + mstore(0x00, 0x3eff3eff3f3435363738393a3b3c3dffffffffffffff00010203040506070809) + + // decode function + function decodeChr(chr, filter) -> decoded { + if and(filter, or(lt(chr, 43), gt(chr, 122))) { + revert(0, 0) + } + decoded := byte(0, mload(mul(filter, sub(chr, 43)))) + if gt(decoded, 63) { + revert(0, 0) + } + } // Prepare result pointer, jump over length let dataPtr := data let resultPtr := add(result, 0x20) let endPtr := add(resultPtr, resultLength) + // loop while not everything is decoded for {} lt(resultPtr, endPtr) {} { - // Advance 4 bytes dataPtr := add(dataPtr, 4) + + // Read a 4 bytes chunk of data let input := mload(dataPtr) - mstore( - resultPtr, - or( - and(m, mload(byte(28, input))), - shr( - 6, - or( - and(m, mload(byte(29, input))), - shr(6, or(and(m, mload(byte(30, input))), shr(6, and(m, mload(byte(31, input)))))) - ) - ) - ) - ) + // Decode each byte in the chunk as a 6 bit block, and align them to form a block of 3 bytes + let b0 := shl(250, decodeChr(byte(28, input), 1)) + let b1 := shl(244, decodeChr(byte(29, input), 1)) + let b2 := shl(238, decodeChr(byte(30, input), lt(add(resultPtr, 1), endPtr))) + let b3 := shl(232, decodeChr(byte(31, input), lt(add(resultPtr, 2), endPtr))) + mstore(resultPtr, or(b0, or(b1, or(b2, b3)))) resultPtr := add(resultPtr, 3) } - // Restore FMP and zero slot. + // Store result length and update FMP to reserve allocated space + mstore(result, resultLength) mstore(0x40, endPtr) - mstore(0x60, 0) } - - return result; } } diff --git a/test/utils/Base64.t.sol b/test/utils/Base64.t.sol index b8aa7ac6121..fc866ea96aa 100644 --- a/test/utils/Base64.t.sol +++ b/test/utils/Base64.t.sol @@ -8,10 +8,12 @@ import {Base64} from "@openzeppelin/contracts/utils/Base64.sol"; contract Base64Test is Test { function testEncode(bytes memory input) external pure { assertEq(Base64.encode(input), vm.toBase64(input)); + assertEq(Base64.decode(Base64.encode(input)), input); } function testEncodeURL(bytes memory input) external pure { assertEq(Base64.encodeURL(input), _removePadding(vm.toBase64URL(input))); + assertEq(Base64.decode(Base64.encodeURL(input)), input); } function _removePadding(string memory inputStr) internal pure returns (string memory) { diff --git a/test/utils/Base64.test.js b/test/utils/Base64.test.js index b0fcfe01325..d1ee7c32685 100644 --- a/test/utils/Base64.test.js +++ b/test/utils/Base64.test.js @@ -50,9 +50,13 @@ describe('Base64', function () { }); }); - // TODO ? - it.skip('Decode invalid base64 string', async function () { + it('Decode invalid base64 string', async function () { + // ord('$') < 43 await expect(this.mock.$decode('dGVzd$==')).to.be.reverted; + // ord('~') > 122 + await expect(this.mock.$decode('dGVzd~==')).to.be.reverted; + // ord('@') in range, but '@' not in the dictionary + await expect(this.mock.$decode('dGVzd@==')).to.be.reverted; }); it('Encode reads beyond the input buffer into dirty memory', async function () { From 4c9fa29fdfb38a6baf3268b5404e0d63d963ce9b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 23 Jun 2025 23:27:09 +0200 Subject: [PATCH 3/9] up --- contracts/utils/Base64.sol | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index 87cd20a133e..a5339a262ae 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -188,11 +188,19 @@ library Base64 { let input := mload(dataPtr) // Decode each byte in the chunk as a 6 bit block, and align them to form a block of 3 bytes - let b0 := shl(250, decodeChr(byte(28, input), 1)) - let b1 := shl(244, decodeChr(byte(29, input), 1)) - let b2 := shl(238, decodeChr(byte(30, input), lt(add(resultPtr, 1), endPtr))) - let b3 := shl(232, decodeChr(byte(31, input), lt(add(resultPtr, 2), endPtr))) - mstore(resultPtr, or(b0, or(b1, or(b2, b3)))) + mstore( + resultPtr, + or( + shl(250, decodeChr(byte(28, input), 1)), + or( + shl(244, decodeChr(byte(29, input), 1)), + or( + shl(238, decodeChr(byte(30, input), lt(add(resultPtr, 1), endPtr))), + shl(232, decodeChr(byte(31, input), lt(add(resultPtr, 2), endPtr))) + ) + ) + ) + ) resultPtr := add(resultPtr, 3) } From b02069f70b768310636cf1a3f7941b76d1dbdc54 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 24 Jun 2025 13:43:09 +0200 Subject: [PATCH 4/9] optimize encode, avoiding memory allocation for the table --- contracts/utils/Base64.sol | 43 +++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index a5339a262ae..c261af4054b 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -11,18 +11,11 @@ import {SafeCast} from "./math/SafeCast.sol"; library Base64 { using SafeCast for bool; - /** - * @dev Base64 Encoding/Decoding Table - * See sections 4 and 5 of https://datatracker.ietf.org/doc/html/rfc4648 - */ - bytes internal constant _TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - bytes internal constant _TABLE_URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - /** * @dev Converts a `bytes` to its Bytes64 `string` representation. */ function encode(bytes memory data) internal pure returns (string memory) { - return string(_encode(data, _TABLE, true)); + return string(_encode(data, true)); } /** @@ -30,7 +23,7 @@ library Base64 { * Output is not padded with `=` as specified in https://www.rfc-editor.org/rfc/rfc4648[rfc4648]. */ function encodeURL(bytes memory data) internal pure returns (string memory) { - return string(_encode(data, _TABLE_URL, false)); + return string(_encode(data, false)); } /** @@ -46,12 +39,11 @@ library Base64 { /** * @dev Internal table-agnostic encoding + * + * If padding is enabled, uses the Base64 table, otherwise use the Base64Url table. + * See sections 4 and 5 of https://datatracker.ietf.org/doc/html/rfc4648 */ - function _encode( - bytes memory data, - bytes memory table, - bool withPadding - ) private pure returns (bytes memory result) { + function _encode(bytes memory data, bool withPadding) private pure returns (bytes memory result) { /** * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol @@ -76,8 +68,15 @@ library Base64 { assembly ("memory-safe") { result := mload(0x40) - // Prepare the lookup table (skip the first "length" byte) - let tablePtr := add(table, 1) + // Store the encoding table in the scratch space (and fmp ptr) to avoid memory allocation + // + // Base64 (ascii) A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9 + / + // Base64 (hex) 4142434445464748494a4b4c4d4e4f505152535455565758595a6162636465666768696a6b6c6d6e6f707172737475767778797a303132333435363738392b2f + // Base64Url (ascii) A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9 - _ + // Base64Url (hex) 4142434445464748494a4b4c4d4e4f505152535455565758595a6162636465666768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f + // xor (hex) 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000670 + mstore(0x1f, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef") + mstore(0x3f, xor("ghijklmnopqrstuvwxyz0123456789-_", mul(withPadding, 0x670))) // Prepare result pointer, jump over length let resultPtr := add(result, 0x20) @@ -102,17 +101,13 @@ library Base64 { // Use this as an index into the lookup table, mload an entire word // so the desired character is in the least significant byte, and // mstore8 this least significant byte into the result and continue. - - mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F)))) + mstore8(resultPtr, mload(and(shr(18, input), 0x3F))) resultPtr := add(resultPtr, 1) // Advance - - mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F)))) + mstore8(resultPtr, mload(and(shr(12, input), 0x3F))) resultPtr := add(resultPtr, 1) // Advance - - mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F)))) + mstore8(resultPtr, mload(and(shr(6, input), 0x3F))) resultPtr := add(resultPtr, 1) // Advance - - mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F)))) + mstore8(resultPtr, mload(and(input, 0x3F))) resultPtr := add(resultPtr, 1) // Advance } From b407d58e9476b6677ee6075f7812cfb8d9079c38 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 24 Jun 2025 16:06:53 +0200 Subject: [PATCH 5/9] avoid filter --- contracts/utils/Base64.sol | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index c261af4054b..cdc5e2a5676 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -157,14 +157,14 @@ library Base64 { // - part of the FMP (at location 0x40) mstore(0x30, 0x2425262728292a2b2c2d2e2f30313233) mstore(0x20, 0x0a0b0c0d0e0f10111213141516171819ffffffff3fff1a1b1c1d1e1f20212223) - mstore(0x00, 0x3eff3eff3f3435363738393a3b3c3dffffffffffffff00010203040506070809) + mstore(0x00, 0x3eff3eff3f3435363738393a3b3c3dffffff00ffffff00010203040506070809) // decode function - function decodeChr(chr, filter) -> decoded { - if and(filter, or(lt(chr, 43), gt(chr, 122))) { + function decodeChr(chr) -> decoded { + if or(lt(chr, 43), gt(chr, 122)) { revert(0, 0) } - decoded := byte(0, mload(mul(filter, sub(chr, 43)))) + decoded := byte(0, mload(sub(chr, 43))) if gt(decoded, 63) { revert(0, 0) } @@ -175,6 +175,12 @@ library Base64 { let resultPtr := add(result, 0x20) let endPtr := add(resultPtr, resultLength) + // In some cases, the last iteration will read bytes after the end of the data. We cache the value, and + // set it to "==" (fake padding) to make sure no dirty bytes are read in that section. + let afterPtr := add(add(data, 0x20), dataLength) + let afterCache := mload(afterPtr) + mstore(afterPtr, shl(240, 0x3d3d)) + // loop while not everything is decoded for {} lt(resultPtr, endPtr) {} { dataPtr := add(dataPtr, 4) @@ -186,13 +192,10 @@ library Base64 { mstore( resultPtr, or( - shl(250, decodeChr(byte(28, input), 1)), + shl(250, decodeChr(byte(28, input))), or( - shl(244, decodeChr(byte(29, input), 1)), - or( - shl(238, decodeChr(byte(30, input), lt(add(resultPtr, 1), endPtr))), - shl(232, decodeChr(byte(31, input), lt(add(resultPtr, 2), endPtr))) - ) + shl(244, decodeChr(byte(29, input))), + or(shl(238, decodeChr(byte(30, input))), shl(232, decodeChr(byte(31, input)))) ) ) ) @@ -200,6 +203,9 @@ library Base64 { resultPtr := add(resultPtr, 3) } + // Reset the value that was cached + mstore(afterPtr, afterCache) + // Store result length and update FMP to reserve allocated space mstore(result, resultLength) mstore(0x40, endPtr) From f3945e2b2120021d82bde4af46a70a48e61de7a0 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 24 Jun 2025 16:51:18 +0200 Subject: [PATCH 6/9] add changeset --- .changeset/solid-cobras-talk.md | 5 +++++ contracts/utils/Base64.sol | 7 ++----- 2 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 .changeset/solid-cobras-talk.md diff --git a/.changeset/solid-cobras-talk.md b/.changeset/solid-cobras-talk.md new file mode 100644 index 00000000000..8d79402566c --- /dev/null +++ b/.changeset/solid-cobras-talk.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Base64`: Add a new `decode` function that parses base64 encoded strings. diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index cdc5e2a5676..1360ae57c18 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -192,11 +192,8 @@ library Base64 { mstore( resultPtr, or( - shl(250, decodeChr(byte(28, input))), - or( - shl(244, decodeChr(byte(29, input))), - or(shl(238, decodeChr(byte(30, input))), shl(232, decodeChr(byte(31, input)))) - ) + or(shl(250, decodeChr(byte(28, input))), shl(244, decodeChr(byte(29, input)))), + or(shl(238, decodeChr(byte(30, input))), shl(232, decodeChr(byte(31, input)))) ) ) From 5cbe251b1cf640bafec8ddf1c24da3ef4de8420a Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 25 Jun 2025 16:39:50 +0200 Subject: [PATCH 7/9] rename bool --- contracts/utils/Base64.sol | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index 1360ae57c18..84d5290d6c0 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -15,7 +15,7 @@ library Base64 { * @dev Converts a `bytes` to its Bytes64 `string` representation. */ function encode(bytes memory data) internal pure returns (string memory) { - return string(_encode(data, true)); + return string(_encode(data, false)); } /** @@ -23,7 +23,7 @@ library Base64 { * Output is not padded with `=` as specified in https://www.rfc-editor.org/rfc/rfc4648[rfc4648]. */ function encodeURL(bytes memory data) internal pure returns (string memory) { - return string(_encode(data, false)); + return string(_encode(data, true)); } /** @@ -43,13 +43,15 @@ library Base64 { * If padding is enabled, uses the Base64 table, otherwise use the Base64Url table. * See sections 4 and 5 of https://datatracker.ietf.org/doc/html/rfc4648 */ - function _encode(bytes memory data, bool withPadding) private pure returns (bytes memory result) { + function _encode(bytes memory data, bool urlAndFilenameSafe) private pure returns (bytes memory result) { /** * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol */ if (data.length == 0) return ""; + // Padding is enabled by default, but disabled when the "urlAndFilenameSafe" alphabet is used + // // 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` -> Prepare for division rounding up @@ -63,7 +65,7 @@ library Base64 { // - ` + 2` -> Prepare for division rounding up // - `/ 3` -> Number of 3-bytes chunks (rounded up) // This is equivalent to: Math.ceil((4 * data.length) / 3) - uint256 resultLength = withPadding ? 4 * ((data.length + 2) / 3) : (4 * data.length + 2) / 3; + uint256 resultLength = urlAndFilenameSafe ? (4 * data.length + 2) / 3 : 4 * ((data.length + 2) / 3); assembly ("memory-safe") { result := mload(0x40) @@ -76,7 +78,7 @@ library Base64 { // Base64Url (hex) 4142434445464748494a4b4c4d4e4f505152535455565758595a6162636465666768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f // xor (hex) 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000670 mstore(0x1f, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef") - mstore(0x3f, xor("ghijklmnopqrstuvwxyz0123456789-_", mul(withPadding, 0x670))) + mstore(0x3f, xor("ghijklmnopqrstuvwxyz0123456789+/", mul(urlAndFilenameSafe, 0x670))) // Prepare result pointer, jump over length let resultPtr := add(result, 0x20) @@ -114,7 +116,7 @@ library Base64 { // Reset the value that was cached mstore(afterPtr, afterCache) - if withPadding { + if iszero(urlAndFilenameSafe) { // When data `bytes` is not exactly 3 bytes long // it is padded with `=` characters at the end switch mod(mload(data), 3) From 7e4b037b0db34fc5d9b4a633b776626ad2b3ad0e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 26 Jun 2025 22:19:59 +0200 Subject: [PATCH 8/9] char valdity filter --- contracts/utils/Base64.sol | 44 +++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index 84d5290d6c0..6052e8e6bbe 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -11,6 +11,8 @@ import {SafeCast} from "./math/SafeCast.sol"; library Base64 { using SafeCast for bool; + error InvalidBase64Digit(uint8); + /** * @dev Converts a `bytes` to its Bytes64 `string` representation. */ @@ -141,6 +143,8 @@ library Base64 { * @dev Internal decoding */ function _decode(bytes memory data) private pure returns (bytes memory result) { + bytes4 errorSelector = InvalidBase64Digit.selector; + uint256 dataLength = data.length; if (dataLength == 0) return ""; @@ -161,17 +165,6 @@ library Base64 { mstore(0x20, 0x0a0b0c0d0e0f10111213141516171819ffffffff3fff1a1b1c1d1e1f20212223) mstore(0x00, 0x3eff3eff3f3435363738393a3b3c3dffffff00ffffff00010203040506070809) - // decode function - function decodeChr(chr) -> decoded { - if or(lt(chr, 43), gt(chr, 122)) { - revert(0, 0) - } - decoded := byte(0, mload(sub(chr, 43))) - if gt(decoded, 63) { - revert(0, 0) - } - } - // Prepare result pointer, jump over length let dataPtr := data let resultPtr := add(result, 0x20) @@ -191,11 +184,36 @@ library Base64 { let input := mload(dataPtr) // Decode each byte in the chunk as a 6 bit block, and align them to form a block of 3 bytes + let a := sub(byte(28, input), 43) + if iszero(and(shl(a, 1), 0xffffffd0ffffffc47ff5)) { + mstore(0, errorSelector) + mstore(4, add(a, 49)) + revert(0, 0x24) + } + let b := sub(byte(29, input), 43) + if iszero(and(shl(b, 1), 0xffffffd0ffffffc47ff5)) { + mstore(0, errorSelector) + mstore(4, add(b, 49)) + revert(0, 0x24) + } + let c := sub(byte(30, input), 43) + if iszero(and(shl(c, 1), 0xffffffd0ffffffc47ff5)) { + mstore(0, errorSelector) + mstore(4, add(c, 49)) + revert(0, 0x24) + } + let d := sub(byte(31, input), 43) + if iszero(and(shl(d, 1), 0xffffffd0ffffffc47ff5)) { + mstore(0, errorSelector) + mstore(4, add(d, 49)) + revert(0, 0x24) + } + mstore( resultPtr, or( - or(shl(250, decodeChr(byte(28, input))), shl(244, decodeChr(byte(29, input)))), - or(shl(238, decodeChr(byte(30, input))), shl(232, decodeChr(byte(31, input)))) + or(shl(250, byte(0, mload(a))), shl(244, byte(0, mload(b)))), + or(shl(238, byte(0, mload(c))), shl(232, byte(0, mload(d)))) ) ) From 59a2df8d8efe73ad3fc8e620016e4e3e6e4b38d5 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 27 Jun 2025 10:00:06 +0200 Subject: [PATCH 9/9] Update Base64.sol --- contracts/utils/Base64.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index 6052e8e6bbe..fda1387c3a0 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -185,24 +185,28 @@ library Base64 { // Decode each byte in the chunk as a 6 bit block, and align them to form a block of 3 bytes let a := sub(byte(28, input), 43) + // slither-disable-next-line incorrect-shift if iszero(and(shl(a, 1), 0xffffffd0ffffffc47ff5)) { mstore(0, errorSelector) mstore(4, add(a, 49)) revert(0, 0x24) } let b := sub(byte(29, input), 43) + // slither-disable-next-line incorrect-shift if iszero(and(shl(b, 1), 0xffffffd0ffffffc47ff5)) { mstore(0, errorSelector) mstore(4, add(b, 49)) revert(0, 0x24) } let c := sub(byte(30, input), 43) + // slither-disable-next-line incorrect-shift if iszero(and(shl(c, 1), 0xffffffd0ffffffc47ff5)) { mstore(0, errorSelector) mstore(4, add(c, 49)) revert(0, 0x24) } let d := sub(byte(31, input), 43) + // slither-disable-next-line incorrect-shift if iszero(and(shl(d, 1), 0xffffffd0ffffffc47ff5)) { mstore(0, errorSelector) mstore(4, add(d, 49))