-
Notifications
You must be signed in to change notification settings - Fork 0
/
encoding.ts
392 lines (379 loc) · 12 KB
/
encoding.ts
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
import type { BufferType } from "./internal.ts";
import { bufferToDataView } from "./internal.ts";
const DECODER = new TextDecoder();
const ENCODER = new TextEncoder();
const HEX = ENCODER.encode("0123456789ABCDEF");
const HEX_CACHE = new Uint8Array(256);
function encodeHexFromView(view: DataView): string {
if (view.byteLength == 0) {
return "";
}
const length = view.byteLength;
const hexBytes = new Uint8Array(view.byteLength * 2);
for (let index = 0, outIndex = 0; index < length; index++, outIndex += 2) {
const byte = view.getUint8(index);
hexBytes[outIndex] = HEX[(byte & 0xF0) >> 4];
hexBytes[outIndex + 1] = HEX[byte & 0x0F];
}
return DECODER.decode(hexBytes);
}
/**
* Encode a data view, typed array, or array buffer as a hex string
*
* Note that multi-byte types will be encoded according to the process's
* endianness. Therefore, for portability, it is recommended to use
* `arrayToEndian` on multi-byte typed arrays, such as Uint32Array before
* encoding as hex.
*
* @param array input array to encode
* @returns a string which when decoded as hex will have the same bytes as was
* input.
*/
export function encodeHex(array: BufferType): string {
if (
array instanceof Uint8Array ||
array instanceof Int8Array ||
array instanceof Int16Array ||
array instanceof Uint16Array ||
array instanceof Int32Array ||
array instanceof Uint32Array ||
array instanceof Uint8ClampedArray ||
array instanceof Float32Array ||
array instanceof Float64Array ||
array instanceof BigInt64Array ||
array instanceof BigUint64Array
) {
return encodeHexFromView(new DataView(array.buffer));
} else if (array instanceof ArrayBuffer) {
return encodeHexFromView(new DataView(array));
} else if (array instanceof DataView) {
return encodeHexFromView(array);
} else {
throw new Error("Bad input to encodeHex");
}
}
const BAD_INPUT_HEX = "Bad input to decodeHex";
/**
* Decode a hex string into a Uint8Array
*
* Will throw when the input text has non hex characters, including spaces,
* or when the input text has a length which is not even.
*
* Upper and lower A-F are accepted.
* Characters outside of 0-9, A-F, a-f will throw.
*
* @param text input text of hex characters
* @returns a decoded Uint8Array
*/
export function decodeHex(text: string): Uint8Array {
if (text == "") {
return new Uint8Array();
}
const hexBytes = ENCODER.encode(text);
let index = 0;
let badHex = false;
if (hexBytes.length & 1) {
// Only even lengths
throw new Error(BAD_INPUT_HEX);
}
if (HEX_CACHE[0] == 0) {
for (let i = 0; i < 256; i++) {
if (i >= 48 && i <= 57) {
HEX_CACHE[i] = i - 48;
} else if (i >= 65 && i <= 70) {
HEX_CACHE[i] = i - 55;
} else if (i >= 97 && i <= 102) {
HEX_CACHE[i] = i - 87;
} else {
HEX_CACHE[i] = 255;
}
}
}
const bytes = new Uint8Array(Math.ceil(text.length / 2));
for (let i = 0; i < hexBytes.length; i += 2, index++) {
const leftHex = hexBytes[i];
const rightHex = hexBytes[i + 1];
const left = HEX_CACHE[leftHex];
const right = HEX_CACHE[rightHex];
bytes[index] = left << 4 | right;
if (left == 255 || right == 255) {
badHex = true;
break;
}
}
if (badHex) {
throw new Error(BAD_INPUT_HEX);
}
return bytes;
}
function encodeBase64Length(
bufferLength: number,
padding: boolean,
): [number, number, number] {
const chunks = Math.ceil(bufferLength / 3);
const withPadding = chunks * 4;
const mod3 = bufferLength % 3;
const completeChunks = mod3 == 0 ? bufferLength : (bufferLength - mod3);
if (padding || mod3 == 0) {
return [withPadding, completeChunks, mod3];
}
if (mod3 == 1) {
return [withPadding - 2, completeChunks, mod3];
}
// mod3 == 2
return [withPadding - 1, completeChunks, mod3];
}
const BASE64_ALPHABET =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const BASE64_OUT = ENCODER.encode(BASE64_ALPHABET);
const BASE64_CACHE = new Uint8Array(256);
const BASE64_URL_ALPHABET =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
const BASE64_URL_OUT = ENCODER.encode(BASE64_URL_ALPHABET);
const BASE64_URL_CACHE = new Uint8Array(256);
function encodeBase64Alphabet(
array: BufferType,
out: Uint8Array,
padding: boolean,
) {
const view = bufferToDataView(array);
const [encodedLength, encodedChunkableLength, mod3] = encodeBase64Length(
view.byteLength,
padding,
);
const output = new Uint8Array(encodedLength);
let outputIndex = 0;
for (let i = 0; i < encodedChunkableLength; i += 3) {
const a = view.getUint8(i);
const b = view.getUint8(i + 1);
const c = view.getUint8(i + 2);
output[outputIndex] = out[(a & 0b1111_1100) >> 2];
output[outputIndex + 1] = out[(a & 0b11) << 4 | (b & 0b1111_0000) >> 4];
output[outputIndex + 2] = out[(b & 0b1111) << 2 | (c & 0b1100_0000) >> 6];
output[outputIndex + 3] = out[c & 0b0011_1111];
outputIndex += 4;
}
if (mod3 == 1) {
const a = view.getUint8(encodedChunkableLength);
output[outputIndex] = out[(a & 0b1111_1100) >> 2];
output[outputIndex + 1] = out[(a & 0b0000_0011) << 4];
if (padding) {
output[outputIndex + 2] = 61;
output[outputIndex + 3] = 61;
}
} else if (mod3 == 2) {
const a = view.getUint8(encodedChunkableLength);
const b = view.getUint8(encodedChunkableLength + 1);
output[outputIndex] = out[(a & 0b1111_1100) >> 2];
output[outputIndex + 1] =
out[(a & 0b0000_0011) << 4 | (b & 0b1111_0000) >> 4];
output[outputIndex + 2] = out[(b & 0b1111) << 2];
if (padding) {
output[outputIndex + 3] = 61;
}
}
return DECODER.decode(output);
}
/**
* A base64 standard implementation, see RFC4648
* https://datatracker.ietf.org/doc/html/rfc4648
* section Base 64 Encoding
*
* This function will encode input bytes into a string using the characters
* A-Z, a-z, 0-9, +, /, and = as padding at the end.
*
* Note that multi-byte types will be encoded according to the process's
* endianness. Therefore, for portability, it is recommended to use
* `arrayToEndian` on multi-byte typed arrays, such as Uint32Array before
* encoding as hex.
*
* @param array Input data to encode
* @returns a text string matching Base64
*/
export function encodeBase64(
array: BufferType,
): string {
return encodeBase64Alphabet(array, BASE64_OUT, true);
}
/**
* A base64 url implementation, see RFC4648
* https://datatracker.ietf.org/doc/html/rfc4648 section
* Base 64 Encoding with URL and Filename Safe Alphabet
*
* This function will encode input bytes into a string using the characters
* A-Z, a-z, 0-9, -, _, and with no padding.
*
* Note that multi-byte types will be encoded according to the process's
* endianness. Therefore, for portability, it is recommended to use
* `arrayToEndian` on multi-byte typed arrays, such as Uint32Array before
* encoding as hex.
*
* @param array Input data to encode
* @returns a text string matching Base64
*/
export function encodeBase64Url(
array: BufferType,
): string {
return encodeBase64Alphabet(array, BASE64_URL_OUT, false);
}
function decodeBase64Alphabet(
input: Uint8Array,
length: number,
lengthMod4: number,
output: Uint8Array,
alphabet: Uint8Array,
) {
let index = 0;
let unsupported: number | null = null;
const totalLength = length - lengthMod4;
for (let i = 0; i < totalLength; i += 4, index += 3) {
const aValue = alphabet[input[i]];
const bValue = alphabet[input[i + 1]];
const cValue = alphabet[input[i + 2]];
const dValue = alphabet[input[i + 3]];
output[index] = (aValue << 2) | ((bValue & 0b110000) >> 4);
output[index + 1] = ((bValue & 0xF) << 4) | ((cValue & 0b111100) >> 2);
output[index + 2] = ((cValue & 0b11) << 6) | dValue;
if (aValue == 255 || bValue == 255 || cValue == 255 || dValue == 255) {
unsupported = i;
break;
}
}
if (unsupported != null) {
throw new Error(
"Unsupported characters in base64: " + DECODER.decode(
new Uint8Array([
input[unsupported],
input[unsupported + 1],
input[unsupported + 2],
input[unsupported + 3],
]),
),
);
}
if (lengthMod4 == 2) {
const a = input[length - 2];
const b = input[length - 1];
const aValue = alphabet[a];
const bValue = alphabet[b];
if ((aValue | bValue) == 255) {
throw new Error(
"Unsupported characters in base64: " +
DECODER.decode(new Uint8Array([a, b])) +
JSON.stringify([aValue, bValue]),
);
}
output[index] = (aValue << 2) | ((bValue & 0b110000) >> 4);
if ((bValue & 0b1111) != 0) {
throw new Error("Mangled Base64 padding");
}
} else if (lengthMod4 == 3) {
const a = input[length - 3];
const b = input[length - 2];
const c = input[length - 1];
const aValue = alphabet[a];
const bValue = alphabet[b];
const cValue = alphabet[c];
if ((aValue | bValue | cValue) == 255) {
throw new Error(
"Unsupported characters in base64: " +
DECODER.decode(new Uint8Array([a, b, c])) +
JSON.stringify([aValue, bValue, cValue]),
);
}
output[index] = (aValue << 2) | ((bValue & 0b110000) >> 4);
output[index + 1] = ((bValue & 0xF) << 4) | ((cValue & 0b111100) >> 2);
if ((cValue & 0b11) != 0) {
throw new Error("Mangled Base64 padding");
}
}
}
function calculateLength(text: Uint8Array) {
let length = text.length;
// Subtract padding
for (let i = length - 1; i >= 0; i--) {
if (text[i] == 61) {
length = i;
}
}
const lengthMod4 = length % 4;
let byteLength: number;
if (lengthMod4 == 2) {
byteLength = ((length - 2) / 4) * 3 + 1;
} else if (lengthMod4 == 3) {
byteLength = ((length - 3) / 4) * 3 + 2;
} else if (lengthMod4 == 0) {
byteLength = length / 4 * 3;
} else {
throw new Error("Invalid base64 length");
}
return [length, lengthMod4, byteLength];
}
/**
* Decode a base64 standard input into a Uint8Array
*
* This function will throw when characters other than A-Z, a-z, 0-9, +, and
* / are used, with the exception of allowing = at the end.
*
* This function will throw when the base64 input padding is mangled.
*
* This function will throw when the length does not meet expectations.
*
* @param text Input base64 standard string
* @returns an output Uint8Array of decoded bytes
*/
export function decodeBase64(text: string): Uint8Array {
if (typeof text != "string") {
throw new Error("Expecting a string");
}
if (text.length == 0) {
return new Uint8Array([]);
}
if (BASE64_CACHE[0] == 0) {
for (let i = 0; i < 256; i++) {
BASE64_CACHE[i] = 255;
}
for (let i = 0; i < BASE64_ALPHABET.length; i++) {
BASE64_CACHE[BASE64_ALPHABET.charCodeAt(i)] = i;
}
}
const input = ENCODER.encode(text);
const [length, lengthMod4, byteLength] = calculateLength(input);
const output = new Uint8Array(byteLength);
decodeBase64Alphabet(input, length, lengthMod4, output, BASE64_CACHE);
return output;
}
/**
* Decode a base64 url input into a Uint8Array
*
* This function will throw when characters other than A-Z, a-z, 0-9, -, and
* _ are used. No padding is expected.
*
* This function will throw when the base64 input padding is mangled.
*
* This function will throw when the length does not meet expectations.
*
* @param text Input base64 standard string
* @returns an output Uint8Array of decoded bytes
*/
export function decodeBase64Url(text: string): Uint8Array {
if (typeof text != "string") {
throw new Error("Expecting a string");
}
if (text.length == 0) {
return new Uint8Array([]);
}
if (BASE64_URL_CACHE[0] == 0) {
for (let i = 0; i < 256; i++) {
BASE64_URL_CACHE[i] = 255;
}
for (let i = 0; i < BASE64_URL_ALPHABET.length; i++) {
BASE64_URL_CACHE[BASE64_URL_ALPHABET.charCodeAt(i)] = i;
}
}
const input = ENCODER.encode(text);
const [length, lengthMod4, byteLength] = calculateLength(input);
const output = new Uint8Array(byteLength);
decodeBase64Alphabet(input, length, lengthMod4, output, BASE64_URL_CACHE);
return output;
}