Add Base64 library to utils (#2884)
* Add Base64 library to utils * Fix typo on Base64 padding * Added documentation for Base64 and references from ERC1155 and ERC721 * Updated Changelog * Fix typo in utilities doc * use mstore8 to improve memory accesses * use shorter strings with encodePacked * do not use using-for syntax, for clarity Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com> Co-authored-by: Francisco Giordano <frangio.1@gmail.com>pull/3063/head^2
parent
da3a9ae18b
commit
ef0273fde1
@ -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); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,88 @@ |
|||||||
|
// 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 |
||||||
|
string memory result = new string(4 * ((data.length + 2) / 3)); |
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
} |
@ -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'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
Loading…
Reference in new issue