Skip to content

Base64.decode #5765

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 95 additions & 9 deletions contracts/utils/Base64.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,55 @@

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));
}

/**
* @dev Converts a `bytes` to its Bytes64Url `string` representation.
* 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
* @dev Internal table-agnostic encoding
*/
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 result) {
/**
* Inspired by Brecht Devos (Brechtpd) implementation - MIT licence
* https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol
Expand All @@ -54,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;

string memory result = new string(resultLength);

assembly ("memory-safe") {
result := mload(0x40)

// Prepare the lookup table (skip the first "length" byte)
let tablePtr := add(table, 1)

Expand Down Expand Up @@ -107,13 +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)
}
}

// Store result length and update FMP to reserve allocated space
mstore(result, resultLength)
mstore(0x40, resultPtr)
}
}

/**
* @dev Internal decoding
*/
function _decode(bytes memory data) private pure returns (bytes memory result) {
uint256 dataLength = data.length;
if (dataLength == 0) return "";

uint256 resultLength = (dataLength / 4) * 3;
if (dataLength % 4 == 0) {
resultLength -= (data[dataLength - 1] == "=").toUint() + (data[dataLength - 2] == "=").toUint();
} else {
resultLength += (dataLength % 4) - 1;
}

assembly ("memory-safe") {
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)

return result;
// loop while not everything is decoded
for {} lt(resultPtr, endPtr) {} {
dataPtr := add(dataPtr, 4)

// Read a 4 bytes chunk of data
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))))

resultPtr := add(resultPtr, 3)
}

// Store result length and update FMP to reserve allocated space
mstore(result, resultLength)
mstore(0x40, endPtr)
}
}
}
2 changes: 2 additions & 0 deletions test/utils/Base64.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
21 changes: 16 additions & 5 deletions test/utils/Base64.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ async function fixture() {
return { mock };
}

describe('Strings', function () {
describe('Base64', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
Expand All @@ -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));
});
});

Expand All @@ -43,11 +44,21 @@ 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));
});
});

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 () {
const mock = await ethers.deployContract('Base64Dirty');
const buffer32 = ethers.id('example');
Expand Down
Loading