Skip to content

Commit

Permalink
Switch to custom base64 implementation
Browse files Browse the repository at this point in the history
Previously we used Node's API for converting to/from base64
but it limits portability of this library to defirent enviroments
include Deno

Fixes #152
  • Loading branch information
IvanGoncharov committed Jun 25, 2021
1 parent cde9d0e commit b290c74
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 7 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ parserOptions:
ecmaVersion: 2020
env:
es6: true
node: true
reportUnusedDisableDirectives: true
plugins:
- node
Expand Down Expand Up @@ -659,6 +658,8 @@ overrides:
node/no-unpublished-import: off
import/no-extraneous-dependencies: [error, { devDependencies: true }]
- files: 'resources/**'
env:
node: true
rules:
node/no-unpublished-require: off
node/no-sync: off
Expand Down
4 changes: 2 additions & 2 deletions src/utils/__tests__/base64-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { expect } from 'chai';

import { base64, unbase64 } from '../base64';

const exampleUtf8 = 'Some examples: ❤😀';
const exampleBase64 = 'U29tZSBleGFtcGxlczog4p2k8J+YgA==';
const exampleUtf8 = 'Some examples: ͢❤😀';
const exampleBase64 = 'U29tZSBleGFtcGxlczogIM2i4p2k8J+YgA==';

describe('base64 conversion', () => {
it('converts from utf-8 to base64', () => {
Expand Down
152 changes: 148 additions & 4 deletions src/utils/base64.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,153 @@
export type Base64String = string;

export function base64(i: string): Base64String {
return Buffer.from(i, 'utf8').toString('base64');
export function base64(input: string): Base64String {
const utf8Array = stringToUTF8Array(input);
let result = '';

const length = utf8Array.length;
const rest = length % 3;
for (let i = 0; i < length - rest; i += 3) {
const a = utf8Array[i];
const b = utf8Array[i + 1];
const c = utf8Array[i + 2];

result += first6Bits(a);
result += last2BitsAndFirst4Bits(a, b);
result += last4BitsAndFirst2Bits(b, c);
result += last6Bits(c);
}

if (rest === 1) {
const a = utf8Array[length - 1];
result += first6Bits(a) + last2BitsAndFirst4Bits(a, 0) + '==';
} else if (rest === 2) {
const a = utf8Array[length - 2];
const b = utf8Array[length - 1];
result +=
first6Bits(a) +
last2BitsAndFirst4Bits(a, b) +
last4BitsAndFirst2Bits(b, 0) +
'=';
}

return result;
}

function first6Bits(a: number): string {
return toBase64Char((a >> 2) & 63);
}

function last2BitsAndFirst4Bits(a: number, b: number): string {
return toBase64Char(((a << 4) | (b >> 4)) & 63);
}

function last4BitsAndFirst2Bits(b: number, c: number): string {
return toBase64Char(((b << 2) | (c >> 6)) & 63);
}

function last6Bits(c: number): string {
return toBase64Char(c & 63);
}

export function unbase64(input: Base64String): string {
const utf8Array = [];

for (let i = 0; i < input.length; i += 4) {
const a = fromBase64Char(input[i]);
const b = fromBase64Char(input[i + 1]);
const c = fromBase64Char(input[i + 2]);
const d = fromBase64Char(input[i + 3]);

if (a === -1 || b === -1 || c === -1 || d === -1) {
/*
* Previously we used Node's API for parsing Base64 and following code
* Buffer.from(i, 'utf8').toString('base64')
* That silently ignored incorrect input and returned empty string instead
* Let's keep this behaviour for a time being and hopefully fix it in the future.
*/
return '';
}

const bitmap24 = (a << 18) | (b << 12) | (c << 6) | d;
utf8Array.push((bitmap24 >> 16) & 255);
utf8Array.push((bitmap24 >> 8) & 255);
utf8Array.push(bitmap24 & 255);
}

let paddingIndex = input.length - 1;
while (input[paddingIndex] === '=') {
--paddingIndex;
utf8Array.pop();
}

return utf8ArrayToString(utf8Array);
}

export function unbase64(i: Base64String): string {
return Buffer.from(i, 'base64').toString('utf8');
const b64CharacterSet =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

function toBase64Char(bitMap6: number): string {
return b64CharacterSet.charAt(bitMap6);
}

function fromBase64Char(base64Char: string | void): number {
if (base64Char === undefined) {
return -1;
}
return base64Char === '=' ? 0 : b64CharacterSet.indexOf(base64Char);
}

function stringToUTF8Array(input: string): Array<number> {
const result = [];
for (const utfChar of input) {
const code = utfChar.codePointAt(0);
if (code < 0x80) {
result.push(code);
} else if (code < 0x800) {
result.push(0xc0 | (code >> 6));
result.push(0x80 | (code & 0x3f));
} else if (code < 0x10000) {
result.push(0xe0 | (code >> 12));
result.push(0x80 | ((code >> 6) & 0x3f));
result.push(0x80 | (code & 0x3f));
} else {
result.push(0xf0 | (code >> 18));
result.push(0x80 | ((code >> 12) & 0x3f));
result.push(0x80 | ((code >> 6) & 0x3f));
result.push(0x80 | (code & 0x3f));
}
}
return result;
}

function utf8ArrayToString(input: Array<number>) {
let result = '';
for (let i = 0; i < input.length; ) {
const a = input[i++];
if ((a & 0x80) === 0) {
result += String.fromCodePoint(a);
continue;
}

const b = input[i++];
if ((a & 0xe0) === 0xc0) {
result += String.fromCodePoint(((a & 0x1f) << 6) | (b & 0x3f));
continue;
}

const c = input[i++];
if ((a & 0xf0) === 0xe0) {
result += String.fromCodePoint(
((a & 0x0f) << 12) | ((b & 0x3f) << 6) | (c & 0x3f),
);
continue;
}

const d = input[i++];
result += String.fromCodePoint(
((a & 0x07) << 18) | ((b & 0x3f) << 12) | ((c & 0x3f) << 6) | (d & 0x3f),
);
}

return result;
}

0 comments on commit b290c74

Please sign in to comment.