Skip to content

Commit 20b16ec

Browse files
Switch to custom base64 implementation (#346)
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
1 parent cde9d0e commit 20b16ec

File tree

3 files changed

+152
-7
lines changed

3 files changed

+152
-7
lines changed

.eslintrc.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ parserOptions:
33
ecmaVersion: 2020
44
env:
55
es6: true
6-
node: true
76
reportUnusedDisableDirectives: true
87
plugins:
98
- node
@@ -659,6 +658,8 @@ overrides:
659658
node/no-unpublished-import: off
660659
import/no-extraneous-dependencies: [error, { devDependencies: true }]
661660
- files: 'resources/**'
661+
env:
662+
node: true
662663
rules:
663664
node/no-unpublished-require: off
664665
node/no-sync: off

src/utils/__tests__/base64-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { expect } from 'chai';
33

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

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

99
describe('base64 conversion', () => {
1010
it('converts from utf-8 to base64', () => {

src/utils/base64.js

Lines changed: 148 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,153 @@
11
export type Base64String = string;
22

3-
export function base64(i: string): Base64String {
4-
return Buffer.from(i, 'utf8').toString('base64');
3+
export function base64(input: string): Base64String {
4+
const utf8Array = stringToUTF8Array(input);
5+
let result = '';
6+
7+
const length = utf8Array.length;
8+
const rest = length % 3;
9+
for (let i = 0; i < length - rest; i += 3) {
10+
const a = utf8Array[i];
11+
const b = utf8Array[i + 1];
12+
const c = utf8Array[i + 2];
13+
14+
result += first6Bits(a);
15+
result += last2BitsAndFirst4Bits(a, b);
16+
result += last4BitsAndFirst2Bits(b, c);
17+
result += last6Bits(c);
18+
}
19+
20+
if (rest === 1) {
21+
const a = utf8Array[length - 1];
22+
result += first6Bits(a) + last2BitsAndFirst4Bits(a, 0) + '==';
23+
} else if (rest === 2) {
24+
const a = utf8Array[length - 2];
25+
const b = utf8Array[length - 1];
26+
result +=
27+
first6Bits(a) +
28+
last2BitsAndFirst4Bits(a, b) +
29+
last4BitsAndFirst2Bits(b, 0) +
30+
'=';
31+
}
32+
33+
return result;
34+
}
35+
36+
function first6Bits(a: number): string {
37+
return toBase64Char((a >> 2) & 63);
38+
}
39+
40+
function last2BitsAndFirst4Bits(a: number, b: number): string {
41+
return toBase64Char(((a << 4) | (b >> 4)) & 63);
42+
}
43+
44+
function last4BitsAndFirst2Bits(b: number, c: number): string {
45+
return toBase64Char(((b << 2) | (c >> 6)) & 63);
46+
}
47+
48+
function last6Bits(c: number): string {
49+
return toBase64Char(c & 63);
50+
}
51+
52+
export function unbase64(input: Base64String): string {
53+
const utf8Array = [];
54+
55+
for (let i = 0; i < input.length; i += 4) {
56+
const a = fromBase64Char(input[i]);
57+
const b = fromBase64Char(input[i + 1]);
58+
const c = fromBase64Char(input[i + 2]);
59+
const d = fromBase64Char(input[i + 3]);
60+
61+
if (a === -1 || b === -1 || c === -1 || d === -1) {
62+
/*
63+
* Previously we used Node's API for parsing Base64 and following code
64+
* Buffer.from(i, 'utf8').toString('base64')
65+
* That silently ignored incorrect input and returned empty string instead
66+
* Let's keep this behaviour for a time being and hopefully fix it in the future.
67+
*/
68+
return '';
69+
}
70+
71+
const bitmap24 = (a << 18) | (b << 12) | (c << 6) | d;
72+
utf8Array.push((bitmap24 >> 16) & 255);
73+
utf8Array.push((bitmap24 >> 8) & 255);
74+
utf8Array.push(bitmap24 & 255);
75+
}
76+
77+
let paddingIndex = input.length - 1;
78+
while (input[paddingIndex] === '=') {
79+
--paddingIndex;
80+
utf8Array.pop();
81+
}
82+
83+
return utf8ArrayToString(utf8Array);
584
}
685

7-
export function unbase64(i: Base64String): string {
8-
return Buffer.from(i, 'base64').toString('utf8');
86+
const b64CharacterSet =
87+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
88+
89+
function toBase64Char(bitMap6: number): string {
90+
return b64CharacterSet.charAt(bitMap6);
91+
}
92+
93+
function fromBase64Char(base64Char: string | void): number {
94+
if (base64Char === undefined) {
95+
return -1;
96+
}
97+
return base64Char === '=' ? 0 : b64CharacterSet.indexOf(base64Char);
98+
}
99+
100+
function stringToUTF8Array(input: string): Array<number> {
101+
const result = [];
102+
for (const utfChar of input) {
103+
const code = utfChar.codePointAt(0);
104+
if (code < 0x80) {
105+
result.push(code);
106+
} else if (code < 0x800) {
107+
result.push(0xc0 | (code >> 6));
108+
result.push(0x80 | (code & 0x3f));
109+
} else if (code < 0x10000) {
110+
result.push(0xe0 | (code >> 12));
111+
result.push(0x80 | ((code >> 6) & 0x3f));
112+
result.push(0x80 | (code & 0x3f));
113+
} else {
114+
result.push(0xf0 | (code >> 18));
115+
result.push(0x80 | ((code >> 12) & 0x3f));
116+
result.push(0x80 | ((code >> 6) & 0x3f));
117+
result.push(0x80 | (code & 0x3f));
118+
}
119+
}
120+
return result;
121+
}
122+
123+
function utf8ArrayToString(input: Array<number>) {
124+
let result = '';
125+
for (let i = 0; i < input.length; ) {
126+
const a = input[i++];
127+
if ((a & 0x80) === 0) {
128+
result += String.fromCodePoint(a);
129+
continue;
130+
}
131+
132+
const b = input[i++];
133+
if ((a & 0xe0) === 0xc0) {
134+
result += String.fromCodePoint(((a & 0x1f) << 6) | (b & 0x3f));
135+
continue;
136+
}
137+
138+
const c = input[i++];
139+
if ((a & 0xf0) === 0xe0) {
140+
result += String.fromCodePoint(
141+
((a & 0x0f) << 12) | ((b & 0x3f) << 6) | (c & 0x3f),
142+
);
143+
continue;
144+
}
145+
146+
const d = input[i++];
147+
result += String.fromCodePoint(
148+
((a & 0x07) << 18) | ((b & 0x3f) << 12) | ((c & 0x3f) << 6) | (d & 0x3f),
149+
);
150+
}
151+
152+
return result;
9153
}

0 commit comments

Comments
 (0)