Created
July 11, 2022 00:35
-
-
Save clshortfuse/0936c1056eb82724ae413d09138578af to your computer and use it in GitHub Desktop.
es6 base64 encoder and decoder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const BIT_MASK_6 = 0b11_1111; | |
const BIT_MASK_8 = 0b1111_1111; | |
const CODEPOINT_SINGLE_LIMIT = 0x00_80; | |
const CODEPOINT_DOUBLE_LIMIT = 0x08_00; | |
const CODEPOINT_TRIPLE_LIMIT = 0x1_00_00; | |
const CODEPOINT_QUAD_LIMIT = 0x11_00_00; | |
const CODEPOINT_EXTRA_BYTE = 0b10 << 6; | |
const BASE64_CHAR_62 = '+'; | |
const BASE64_CHAR_63 = '/'; | |
const BASE64_CHAR_PAD = '='; | |
const BASE64_CODEPOINT_62 = BASE64_CHAR_62.codePointAt(0); | |
const BASE64_CODEPOINT_63 = BASE64_CHAR_63.codePointAt(0); | |
const BASE64_CODEPOINT_PAD = BASE64_CHAR_PAD.codePointAt(0); | |
const BASE64URL_CHAR_62 = '-'; | |
const BASE64URL_CHAR_63 = '_'; | |
const BASE64URL_CHAR_PAD = ''; // Optional | |
const BASE64URL_CODEPOINT_62 = BASE64URL_CHAR_62.codePointAt(0); | |
const BASE64URL_CODEPOINT_63 = BASE64URL_CHAR_63.codePointAt(0); | |
const BASE64_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; | |
const BASE64_CHAR_TO_SEXTET_INDEX = new Map(Array.from(BASE64_TABLE).map((c, index) => [c, index])); | |
const BASE64_CODEPOINT_TO_SEXTET_INDEX = new Map(Array.from(BASE64_TABLE).map((c, index) => [c.codePointAt(0), index])); | |
const BASE64_SEXTET_TO_CHAR_INDEX = new Map(Array.from(BASE64_TABLE).map((c, index) => [index, c])); | |
const BASE64_SEXTET_TO_CODEPOINT_INDEX = new Map(Array.from(BASE64_TABLE).map((c, index) => [index, c.codePointAt(0)])); | |
/** @type {Map<number,number>} */ | |
const BIT_MASKS = new Map([1, 2, 3, 4, 5, 6, 7, 8].map((v) => [v, 2 ** v - 1])); | |
const BIT_MASK_1 = BIT_MASKS.get(1); | |
const BIT_MASK_2 = BIT_MASKS.get(2); | |
const BIT_MASK_3 = BIT_MASKS.get(3); | |
const BIT_MASK_4 = BIT_MASKS.get(4); | |
const BIT_MASK_5 = BIT_MASKS.get(5); | |
const BIT_MASK_6 = BIT_MASKS.get(6); | |
const BIT_MASK_7 = BIT_MASKS.get(7); | |
const BIT_MASK_8 = BIT_MASKS.get(8); | |
/** | |
* @param {string} utf8 | |
* @yields {number} octet | |
*/ | |
export function* octetFromUtf8(utf8) { | |
for (let i = 0; i < utf8.length; i++) { | |
const codePoint = utf8.codePointAt(i); | |
let charBytes = 0; | |
if (codePoint < CODEPOINT_SINGLE_LIMIT) { | |
yield (codePoint); | |
continue; | |
} | |
if (codePoint < CODEPOINT_DOUBLE_LIMIT) { | |
charBytes = 2; | |
} else if (codePoint < CODEPOINT_TRIPLE_LIMIT) { | |
charBytes = 3; | |
} else if (codePoint < CODEPOINT_QUAD_LIMIT) { | |
charBytes = 4; | |
i++; | |
} | |
// First byte is has `charBytes` amount of leading 1 bits | |
const firstByteValue = (0b1111_1111 << (8 - charBytes)) & BIT_MASK_8; | |
for (let j = 0; j < charBytes; j++) { | |
const position = 6 * (charBytes - 1 - j); | |
// Push target into last 6 bits and mask | |
const sextet = (codePoint >> position) & BIT_MASK_6; | |
const initalValue = j === 0 ? firstByteValue : CODEPOINT_EXTRA_BYTE; | |
const octet = initalValue | sextet; | |
yield octet; | |
} | |
} | |
} | |
/** | |
* @param {string|BufferSource} source | |
* @return {Iterable<number>} | |
*/ | |
function toIterableUint8(source) { | |
/* eslint-disable indent */ | |
/* eslint-disable unicorn/no-nested-ternary */ | |
return (typeof source === 'string') ? octetFromUtf8(source) | |
: (source instanceof Uint8Array ? source | |
: source instanceof ArrayBuffer ? new Uint8Array(source) | |
: new Uint8Array(source.buffer)); | |
/* eslint-enable indent */ | |
/* eslint-enable unicorn/no-nested-ternary */ | |
} | |
/** | |
* @param {number} sextet | |
* @param {boolean} [url=false] | |
* @return {number} | |
*/ | |
function base64CodepointFromSextet(sextet, url) { | |
if (url) { | |
if (sextet === 63) return BASE64URL_CODEPOINT_63; | |
if (sextet === 62) return BASE64URL_CODEPOINT_62; | |
} | |
const value = BASE64_SEXTET_TO_CODEPOINT_INDEX.get(sextet); | |
if (value === null) throw new Error(`Invalid value: ${sextet}`); | |
return value; | |
} | |
/** | |
* @param {number} codepoint | |
* @param {boolean} [url=false] | |
* @return {number} | |
*/ | |
function sextetFromBase64Codepoint(codepoint, url) { | |
if (url) { | |
if (codepoint === BASE64URL_CODEPOINT_62) return 62; | |
if (codepoint === BASE64URL_CODEPOINT_63) return 63; | |
} | |
const value = BASE64_CODEPOINT_TO_SEXTET_INDEX.get(codepoint); | |
if (value === null) throw new Error(`Invalid value: ${codepoint}`); | |
return value; | |
} | |
/** | |
* @param {number} sextet | |
* @param {boolean} [url=false] | |
* @return {string} | |
*/ | |
function base64CharFromSextet(sextet, url) { | |
if (url) { | |
if (sextet === 62) return BASE64URL_CHAR_62; | |
if (sextet === 63) return BASE64URL_CHAR_63; | |
} | |
const value = BASE64_SEXTET_TO_CHAR_INDEX.get(sextet); | |
if (value == null) throw new Error(`Invalid value: ${sextet}`); | |
return value; | |
} | |
/** | |
* @param {string} char | |
* @param {boolean} [url=false] | |
* @return {number} sextet | |
*/ | |
function sextetFromBase64Char(char, url) { | |
if (url) { | |
if (char === BASE64URL_CHAR_62) return 62; | |
if (char === BASE64URL_CHAR_63) return 63; | |
} | |
const value = BASE64_CHAR_TO_SEXTET_INDEX.get(char); | |
if (value == null) throw new Error(`Invalid value: ${char}`); | |
return value; | |
} | |
/** | |
* @param {Iterable<number>} source | |
* @yields {number} sextet | |
*/ | |
function* sextetsFromOctets(source) { | |
let storedBits = 0; | |
let bitStore = 0; | |
for (const octet of source) { | |
const bitsNeeded = 6 - storedBits; | |
const partial = octet >> (8 - bitsNeeded); | |
const sextet = bitStore | partial; | |
yield sextet; | |
const remainingBits = 8 - bitsNeeded; | |
const mask = BIT_MASKS.get(remainingBits) ?? (2 ** remainingBits - 1); | |
const remainder = octet & mask; | |
bitStore = remainder << (6 - remainingBits); | |
storedBits = remainingBits; | |
if (storedBits === 6) { | |
yield bitStore; | |
bitStore = 0; | |
storedBits = 0; | |
} | |
} | |
if (storedBits) { | |
yield bitStore; | |
} | |
} | |
/** | |
* @param {string|BufferSource} source | |
* @param {boolean} [url=false] | |
* @return {string} | |
*/ | |
export function encodeBase64AsString(source, url) { | |
const iterableSource = toIterableUint8(source); | |
let result = ''; | |
let count = 0; | |
for (const sextet of sextetsFromOctets(iterableSource)) { | |
count++; | |
result += base64CharFromSextet(sextet, url); | |
} | |
if (!url) { | |
switch (count % 4) { | |
case 3: | |
return `${result}=`; | |
case 2: | |
return `${result}==`; | |
default: | |
} | |
} | |
return result; | |
} | |
/** | |
* @param {string|Uint8Array|BufferSource} source | |
* @param {boolean} [url=false] | |
* @return {Uint8Array} | |
*/ | |
export function encodeBase64AsArray(source, url) { | |
// Every 24bits is 4 (characters) of data | |
// Javascript underreports 3byte utf8 as single length, therefore byte count could be upto 3x utf8 length | |
// TODO: Implement optimistic size prediction | |
const iterableSource = toIterableUint8(source); | |
const destinationSize = (typeof source === 'string') ? (source.length * 4) | |
: Math.ceil((/** @type Uint8Array */ (iterableSource).length * 8) / 6); | |
const padSize = 4 - (destinationSize % 4); | |
const array = new Uint8Array(destinationSize + padSize); | |
let count = 0; | |
for (const sextet of sextetsFromOctets(iterableSource)) { | |
array[count++] = base64CodepointFromSextet(sextet, url); | |
} | |
if (!url) { | |
switch (count % 4) { | |
case 2: | |
array[count++] = BASE64_CODEPOINT_PAD; | |
// Fallthrough | |
case 3: | |
array[count++] = BASE64_CODEPOINT_PAD; | |
break; | |
default: | |
} | |
} | |
// Maintains memory footprint, but faster than slice | |
return array.subarray(0, count); | |
} | |
/** | |
* @param {string|Uint8Array} source | |
* @param {?boolean} [url] Explicit base64url processing (auto if omitted) | |
* @return {Uint8Array} | |
*/ | |
export function decodeBase64AsArray(source, url) { | |
// Base64 has 24bits of data for every 32bits | |
let sourceBits = source.length * 6; | |
const unalignedBytes = source.length % 4; | |
if (unalignedBytes) { | |
if (url === false) throw new Error(`Invalid Base64 source: ${source.toString()}`); | |
switch (unalignedBytes) { | |
case 2: | |
sourceBits += 6; | |
// Fallthrough | |
case 3: | |
sourceBits += 6; | |
break; | |
default: | |
case 1: | |
throw new Error(`Invalid Base64 source: ${source.toString()}`); | |
} | |
} | |
const destinationSize = (url === false) ? sourceBits / 8 : Math.ceil(sourceBits / 8); | |
const array = new Uint8Array(destinationSize); | |
const isString = typeof source === 'string'; | |
const decoder = isString ? sextetFromBase64Char : sextetFromBase64Codepoint; | |
const decodeURL = (url !== false); | |
let tripleByte = 0b0000_0000_0000_0000_0000_0000; | |
let writePosition = 0; | |
for (let i = 0; i < source.length; i++) { | |
const offset = i % 4; | |
const value = source[i]; | |
const isPadding = isString ? value === BASE64_CHAR_PAD : value === BASE64_CODEPOINT_PAD; | |
if (!isPadding) { | |
const sextet = decoder(value, decodeURL); | |
const shiftedData = sextet << (6 * (3 - offset)); | |
tripleByte |= shiftedData; | |
} | |
let bytesToWrite = 0; | |
if (isPadding) { | |
bytesToWrite = offset - 1; | |
} else if ((offset === 3) || (i === source.length - 1)) { | |
bytesToWrite = offset; | |
} | |
if (bytesToWrite) { | |
for (let j = 0; j < bytesToWrite; j++) { | |
const value = (tripleByte >> (8 * (2 - j))) & BIT_MASK_8; | |
array[writePosition++] = value; | |
} | |
tripleByte = 0; | |
} | |
if (isPadding) break; | |
} | |
if (array.length === writePosition) return array; | |
return array.subarray(0, writePosition); | |
} | |
/** | |
* @param {string|Uint8Array} source | |
* @param {?boolean} [url] Explicit base64url processing (auto if omitted) | |
* @return {string} | |
*/ | |
export function decodeBase64AsASCII(source, url) { | |
// Base64 has 24bits of data for every 32bits | |
const decodeURL = (url !== false); | |
let output = ''; | |
const isString = typeof source === 'string'; | |
const decoder = isString ? sextetFromBase64Char : sextetFromBase64Codepoint; | |
let tripleByte = 0b0000_0000_0000_0000_0000_0000; | |
for (let i = 0; i < source.length; i++) { | |
const offset = i % 4; | |
const value = source[i]; | |
const isPadding = isString ? value === BASE64_CHAR_PAD : value === BASE64_CODEPOINT_PAD; | |
if (!isPadding) { | |
const sextet = decoder(value, decodeURL); | |
const shiftedData = sextet << (6 * (3 - offset)); | |
tripleByte |= shiftedData; | |
} | |
let bytesToWrite = 0; | |
if (isPadding) { | |
bytesToWrite = offset - 1; | |
} else if ((offset === 3) || (i === source.length - 1)) { | |
bytesToWrite = offset; | |
} | |
if (bytesToWrite) { | |
for (let j = 0; j < bytesToWrite; j++) { | |
const charCode = (tripleByte >> (8 * (2 - j))) & BIT_MASK_8; | |
// eslint-disable-next-line unicorn/prefer-code-point | |
output += String.fromCharCode(charCode); | |
} | |
tripleByte = 0; | |
} | |
if (isPadding) break; | |
} | |
return output; | |
} | |
/** | |
* @param {string|Uint8Array|BufferSource} source | |
* @param {?boolean} [url] Explicit base64url processing (auto if omitted) | |
* @return {string} | |
*/ | |
export function decodeBase64AsUtf8(source, url) { | |
// Base64 has 24bits of data for every 32bits | |
const decodeURL = (url !== false); | |
let output = ''; | |
const isString = typeof source === 'string'; | |
const decoder = isString ? sextetFromBase64Char : sextetFromBase64Codepoint; | |
let tripleByte = 0b0000_0000_0000_0000_0000_0000; | |
let codePoint = 0; | |
let codePointLength = 0; | |
let codePointIndex = 0; | |
for (let i = 0; i < source.length; i++) { | |
const offset = i % 4; | |
const value = source[i]; | |
const isPadding = isString ? value === BASE64_CHAR_PAD : value === BASE64_CODEPOINT_PAD; | |
if (!isPadding) { | |
const sextet = decoder(value, decodeURL); | |
const shiftedData = sextet << (6 * (3 - offset)); | |
tripleByte |= shiftedData; | |
} | |
let bytesToWrite = 0; | |
if (isPadding) { | |
bytesToWrite = offset - 1; | |
} else if ((offset === 3) || (i === source.length - 1)) { | |
bytesToWrite = offset; | |
} | |
if (bytesToWrite) { | |
for (let j = 0; j < bytesToWrite; j++) { | |
const codePointOctet = (tripleByte >> (8 * (2 - j))) & BIT_MASK_8; | |
if (!codePointLength) { | |
if (codePointOctet >> 7 === 0) { | |
codePointLength = 1; | |
codePoint = codePointOctet; | |
} else if (codePointOctet >> 5 === 0b110) { | |
codePointLength = 2; | |
codePoint = codePointOctet & BIT_MASK_5; | |
} else if (codePointOctet >> 4 === 0b1110) { | |
codePointLength = 3; | |
codePoint = codePointOctet & BIT_MASK_4; | |
} else if (codePointOctet >> 3 === 0b1_1110) { | |
codePointLength = 4; | |
codePoint = codePointOctet & BIT_MASK_3; | |
} else { | |
throw new Error('Invalid source data'); | |
} | |
} | |
if (codePointIndex) { | |
if (codePointOctet >> 6 !== 0b10) throw new Error('Invalid source data'); | |
// Shift 6 and mask | |
codePoint <<= 6; | |
codePoint |= (codePointOctet & BIT_MASK_6); | |
} | |
codePointIndex++; | |
if (codePointIndex === codePointLength) { | |
output += String.fromCodePoint(codePoint); | |
codePointIndex = 0; | |
codePointLength = 0; | |
} | |
} | |
tripleByte = 0; | |
} | |
if (isPadding) break; | |
} | |
if (codePointLength) { | |
output += String.fromCodePoint(codePoint); | |
} | |
return output; | |
} | |
/** @alias encodeBase64AsString */ | |
export const encodeBase64AsASCII = encodeBase64AsString; | |
/** @alias encodeBase64AsString */ | |
export const encodeBase64AsUtf8 = encodeBase64AsString; | |
/** @alias decodeBase64AsUtf8 */ | |
export const decodeBase64AsString = decodeBase64AsUtf8; | |
export const encodeBase64UrlAsArray = (source) => encodeBase64AsArray(source, true); | |
export const encodeBase64UrlAsString = (source) => encodeBase64AsString(source, true); | |
export const decodeBase64UrlAsArray = (source) => decodeBase64AsArray(source, true); | |
export const decodeBase64UrlAsString = (source) => decodeBase64AsString(source, true); | |
export const decodeBase64UrlAsUtf8 = (source) => decodeBase64AsUtf8(source, true); | |
export const decodeBase64UrlAsASCII = (source) => decodeBase64AsASCII(source, true); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment