-
Notifications
You must be signed in to change notification settings - Fork 11.9k
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
Add Base64 library to utils #2884
Changes from 6 commits
157c32b
df3d838
7c187e0
c8a317c
9094dfa
5adf579
c1060ca
703bb20
c1f86e4
e48057c
f0fc459
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.0; | ||
|
||
import "../utils/Base64.sol"; | ||
|
||
contract Base64Mock { | ||
function encode(bytes memory value) external pure returns (string memory) { | ||
return Base64.encode(value); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.0; | ||
|
||
/** | ||
* @dev Provides a set of functions to operate with Base64 strings. | ||
*/ | ||
library Base64 { | ||
/** | ||
* @dev Base64 Encoding/Decoding Table | ||
*/ | ||
string internal constant _TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; | ||
|
||
/** | ||
* @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 | ||
uint256 encodedLen = 4 * ((data.length + 2) / 3); | ||
|
||
// Add some extra buffer at the end required for the writing | ||
string memory result = new string(encodedLen + 32); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need the additional space for ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's because the code operates on whole 32-byte slots rather than single bytes. It uses There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. then lets use mstore8 ! |
||
|
||
assembly { | ||
// Store the actual result length in memory | ||
mstore(result, encodedLen) | ||
|
||
// Prepare the lookup table | ||
let tablePtr := add(table, 1) | ||
|
||
// Prepare input pointer | ||
let dataPtr := data | ||
let endPtr := add(dataPtr, mload(data)) | ||
|
||
// Prepare result pointer, jump over length | ||
let resultPtr := add(result, 32) | ||
|
||
// Run over the input, 3 bytes at a time | ||
for { | ||
|
||
} 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 | ||
|
||
mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(18, input), 0x3F))))) | ||
resultPtr := add(resultPtr, 1) // Advance | ||
|
||
mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(12, input), 0x3F))))) | ||
resultPtr := add(resultPtr, 1) // Advance | ||
|
||
mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(6, input), 0x3F))))) | ||
resultPtr := add(resultPtr, 1) // Advance | ||
|
||
mstore(resultPtr, shl(248, 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 { | ||
mstore(sub(resultPtr, 2), shl(240, 0x3d3d)) | ||
} | ||
case 2 { | ||
mstore(sub(resultPtr, 1), shl(248, 0x3d)) | ||
} | ||
} | ||
|
||
return result; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
const { expect } = require('chai'); | ||
|
||
const Base64Mock = artifacts.require('Base64Mock'); | ||
|
||
contract('Strings', function () { | ||
beforeEach(async function () { | ||
this.base64 = await Base64Mock.new(); | ||
}); | ||
|
||
describe('from bytes - base64', function () { | ||
it('converts to base64 encoded string with double padding', async function () { | ||
const TEST_MESSAGE = 'test'; | ||
const input = web3.utils.asciiToHex(TEST_MESSAGE); | ||
expect(await this.base64.encode(input)).to.equal('dGVzdA=='); | ||
}); | ||
|
||
it('converts to base64 encoded string with single padding', async function () { | ||
const TEST_MESSAGE = 'test1'; | ||
const input = web3.utils.asciiToHex(TEST_MESSAGE); | ||
expect(await this.base64.encode(input)).to.equal('dGVzdDE='); | ||
}); | ||
|
||
it('converts to base64 encoded string without padding', async function () { | ||
const TEST_MESSAGE = 'test12'; | ||
const input = web3.utils.asciiToHex(TEST_MESSAGE); | ||
expect(await this.base64.encode(input)).to.equal('dGVzdDEy'); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just my 2 cents but as an alternative, you can have the following line at the top of the contract:
using Base64 for bytes;
and then in your encode:
value.encode();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree it looks better just using the
.encode
but I'm following the same pattern used for Strings mock. I'll wait if there's somebody from the OZ team who has input on this and why to use one or the other.