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