-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbase64.js
446 lines (395 loc) · 13 KB
/
base64.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
/* eslint-disable no-bitwise */
// https://datatracker.ietf.org/doc/html/rfc4648#section-4
import { uint8ArrayFromUtf8 } from './utf8.js';
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 BASE64URL_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|BufferSource} source
* @return {Iterable<number>}
*/
function toIterableUint8(source) {
if (typeof source === 'string') {
return uint8ArrayFromUtf8(source);
}
if (source instanceof Uint8Array) {
return source;
}
if (source instanceof ArrayBuffer) {
return new Uint8Array(source);
}
return new Uint8Array(source.buffer);
}
/**
* @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;
}
/**
* Strips any non Base64 characters
* @param {string} source
* @param {boolean} [url=false]
* @return {string}
*/
export function cleanBase64String(source, url) {
let result = '';
let count = 0;
const table = url ? BASE64URL_TABLE : BASE64_TABLE;
for (const char of source) {
if (table.includes(char)) {
count++;
result += char;
}
}
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 maskedValue = (tripleByte >> (8 * (2 - j))) & BIT_MASK_8;
array[writePosition++] = maskedValue;
}
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);
export const cleanBase64UrlString = (source) => cleanBase64String(source, true);