generated from MetaMask/metamask-module-template
-
-
Notifications
You must be signed in to change notification settings - Fork 7
/
bytes.ts
430 lines (373 loc) · 12.9 KB
/
bytes.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
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
import { assert } from './assert';
import { add0x, assertIsHexString, Hex, remove0x } from './hex';
// '0'.charCodeAt(0) === 48
const HEX_MINIMUM_NUMBER_CHARACTER = 48;
// '9'.charCodeAt(0) === 57
const HEX_MAXIMUM_NUMBER_CHARACTER = 58;
const HEX_CHARACTER_OFFSET = 87;
export type Bytes = bigint | number | string | Uint8Array;
/**
* Memoized function that returns an array to be used as a lookup table for
* converting bytes to hexadecimal values.
*
* The array is created lazily and then cached for future use. The benefit of
* this approach is that the performance of converting bytes to hex is much
* better than if we were to call `toString(16)` on each byte.
*
* The downside is that the array is created once and then never garbage
* collected. This is not a problem in practice because the array is only 256
* elements long.
*
* @returns A function that returns the lookup table.
*/
function getPrecomputedHexValuesBuilder(): () => string[] {
// To avoid issues with tree shaking, we need to use a function to return the
// array. This is because the array is only used in the `bytesToHex` function
// and if we were to use a global variable, the array might be removed by the
// tree shaker.
const lookupTable: string[] = [];
return () => {
if (lookupTable.length === 0) {
for (let i = 0; i < 256; i++) {
lookupTable.push(i.toString(16).padStart(2, '0'));
}
}
return lookupTable;
};
}
/**
* Function implementation of the {@link getPrecomputedHexValuesBuilder}
* function.
*/
const getPrecomputedHexValues = getPrecomputedHexValuesBuilder();
/**
* Check if a value is a `Uint8Array`.
*
* @param value - The value to check.
* @returns Whether the value is a `Uint8Array`.
*/
export function isBytes(value: unknown): value is Uint8Array {
return value instanceof Uint8Array;
}
/**
* Assert that a value is a `Uint8Array`.
*
* @param value - The value to check.
* @throws If the value is not a `Uint8Array`.
*/
export function assertIsBytes(value: unknown): asserts value is Uint8Array {
assert(isBytes(value), 'Value must be a Uint8Array.');
}
/**
* Convert a `Uint8Array` to a hexadecimal string.
*
* @param bytes - The bytes to convert to a hexadecimal string.
* @returns The hexadecimal string.
*/
export function bytesToHex(bytes: Uint8Array): Hex {
assertIsBytes(bytes);
if (bytes.length === 0) {
return '0x';
}
const lookupTable = getPrecomputedHexValues();
const hex = new Array(bytes.length);
for (let i = 0; i < bytes.length; i++) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
hex[i] = lookupTable[bytes[i]!];
}
return add0x(hex.join(''));
}
/**
* Convert a `Uint8Array` to a `bigint`.
*
* To convert a `Uint8Array` to a `number` instead, use {@link bytesToNumber}.
* To convert a two's complement encoded `Uint8Array` to a `bigint`, use
* {@link bytesToSignedBigInt}.
*
* @param bytes - The bytes to convert to a `bigint`.
* @returns The `bigint`.
*/
export function bytesToBigInt(bytes: Uint8Array): bigint {
assertIsBytes(bytes);
const hex = bytesToHex(bytes);
return BigInt(hex);
}
/**
* Convert a `Uint8Array` to a signed `bigint`. This assumes that the bytes are
* encoded in two's complement.
*
* To convert a `Uint8Array` to an unsigned `bigint` instead, use
* {@link bytesToBigInt}.
*
* @see https://en.wikipedia.org/wiki/Two%27s_complement
* @param bytes - The bytes to convert to a signed `bigint`.
* @returns The signed `bigint`.
*/
export function bytesToSignedBigInt(bytes: Uint8Array): bigint {
assertIsBytes(bytes);
let value = BigInt(0);
for (const byte of bytes) {
// eslint-disable-next-line no-bitwise
value = (value << BigInt(8)) + BigInt(byte);
}
return BigInt.asIntN(bytes.length * 8, value);
}
/**
* Convert a `Uint8Array` to a `number`.
*
* To convert a `Uint8Array` to a `bigint` instead, use {@link bytesToBigInt}.
*
* @param bytes - The bytes to convert to a number.
* @returns The number.
* @throws If the resulting number is not a safe integer.
*/
export function bytesToNumber(bytes: Uint8Array): number {
assertIsBytes(bytes);
const bigint = bytesToBigInt(bytes);
assert(
bigint <= BigInt(Number.MAX_SAFE_INTEGER),
'Number is not a safe integer. Use `bytesToBigInt` instead.',
);
return Number(bigint);
}
/**
* Convert a UTF-8 encoded `Uint8Array` to a `string`.
*
* @param bytes - The bytes to convert to a string.
* @returns The string.
*/
export function bytesToString(bytes: Uint8Array): string {
assertIsBytes(bytes);
return new TextDecoder().decode(bytes);
}
/**
* Convert a hexadecimal string to a `Uint8Array`. The string can optionally be
* prefixed with `0x`. It accepts even and odd length strings.
*
* If the value is "0x", an empty `Uint8Array` is returned.
*
* @param value - The hexadecimal string to convert to bytes.
* @returns The bytes as `Uint8Array`.
*/
export function hexToBytes(value: string): Uint8Array {
// "0x" is often used as empty byte array.
if (value?.toLowerCase?.() === '0x') {
return new Uint8Array();
}
assertIsHexString(value);
// Remove the `0x` prefix if it exists, and pad the string to have an even
// number of characters.
const strippedValue = remove0x(value).toLowerCase();
const normalizedValue =
strippedValue.length % 2 === 0 ? strippedValue : `0${strippedValue}`;
const bytes = new Uint8Array(normalizedValue.length / 2);
for (let i = 0; i < bytes.length; i++) {
// While this is not the prettiest way to convert a hexadecimal string to a
// `Uint8Array`, it is a lot faster than using `parseInt` to convert each
// character.
const c1 = normalizedValue.charCodeAt(i * 2);
const c2 = normalizedValue.charCodeAt(i * 2 + 1);
const n1 =
c1 -
(c1 < HEX_MAXIMUM_NUMBER_CHARACTER
? HEX_MINIMUM_NUMBER_CHARACTER
: HEX_CHARACTER_OFFSET);
const n2 =
c2 -
(c2 < HEX_MAXIMUM_NUMBER_CHARACTER
? HEX_MINIMUM_NUMBER_CHARACTER
: HEX_CHARACTER_OFFSET);
bytes[i] = n1 * 16 + n2;
}
return bytes;
}
/**
* Convert a `bigint` to a `Uint8Array`.
*
* This assumes that the `bigint` is an unsigned integer. To convert a signed
* `bigint` instead, use {@link signedBigIntToBytes}.
*
* @param value - The bigint to convert to bytes.
* @returns The bytes as `Uint8Array`.
*/
export function bigIntToBytes(value: bigint): Uint8Array {
assert(typeof value === 'bigint', 'Value must be a bigint.');
assert(value >= BigInt(0), 'Value must be a non-negative bigint.');
const hex = value.toString(16);
return hexToBytes(hex);
}
/**
* Check if a `bigint` fits in a certain number of bytes.
*
* @param value - The `bigint` to check.
* @param bytes - The number of bytes.
* @returns Whether the `bigint` fits in the number of bytes.
*/
function bigIntFits(value: bigint, bytes: number): boolean {
assert(bytes > 0);
/* eslint-disable no-bitwise */
const mask = value >> BigInt(31);
return !(((~value & mask) + (value & ~mask)) >> BigInt(bytes * 8 + ~0));
/* eslint-enable no-bitwise */
}
/**
* Convert a signed `bigint` to a `Uint8Array`. This uses two's complement
* encoding to represent negative numbers.
*
* To convert an unsigned `bigint` to a `Uint8Array` instead, use
* {@link bigIntToBytes}.
*
* @see https://en.wikipedia.org/wiki/Two%27s_complement
* @param value - The number to convert to bytes.
* @param byteLength - The length of the resulting `Uint8Array`. If the number
* is larger than the maximum value that can be represented by the given length,
* an error is thrown.
* @returns The bytes as `Uint8Array`.
*/
export function signedBigIntToBytes(
value: bigint,
byteLength: number,
): Uint8Array {
assert(typeof value === 'bigint', 'Value must be a bigint.');
assert(typeof byteLength === 'number', 'Byte length must be a number.');
assert(byteLength > 0, 'Byte length must be greater than 0.');
assert(
bigIntFits(value, byteLength),
'Byte length is too small to represent the given value.',
);
// ESLint doesn't like mutating function parameters, so to avoid having to
// disable the rule, we create a new variable.
let numberValue = value;
const bytes = new Uint8Array(byteLength);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = Number(BigInt.asUintN(8, numberValue));
// eslint-disable-next-line no-bitwise
numberValue >>= BigInt(8);
}
return bytes.reverse();
}
/**
* Convert a `number` to a `Uint8Array`.
*
* @param value - The number to convert to bytes.
* @returns The bytes as `Uint8Array`.
* @throws If the number is not a safe integer.
*/
export function numberToBytes(value: number): Uint8Array {
assert(typeof value === 'number', 'Value must be a number.');
assert(value >= 0, 'Value must be a non-negative number.');
assert(
Number.isSafeInteger(value),
'Value is not a safe integer. Use `bigIntToBytes` instead.',
);
const hex = value.toString(16);
return hexToBytes(hex);
}
/**
* Convert a `string` to a UTF-8 encoded `Uint8Array`.
*
* @param value - The string to convert to bytes.
* @returns The bytes as `Uint8Array`.
*/
export function stringToBytes(value: string): Uint8Array {
assert(typeof value === 'string', 'Value must be a string.');
return new TextEncoder().encode(value);
}
/**
* Convert a byte-like value to a `Uint8Array`. The value can be a `Uint8Array`,
* a `bigint`, a `number`, or a `string`.
*
* This will attempt to guess the type of the value based on its type and
* contents. For more control over the conversion, use the more specific
* conversion functions, such as {@link hexToBytes} or {@link stringToBytes}.
*
* If the value is a `string`, and it is prefixed with `0x`, it will be
* interpreted as a hexadecimal string. Otherwise, it will be interpreted as a
* UTF-8 string. To convert a hexadecimal string to bytes without interpreting
* it as a UTF-8 string, use {@link hexToBytes} instead.
*
* If the value is a `bigint`, it is assumed to be unsigned. To convert a signed
* `bigint` to bytes, use {@link signedBigIntToBytes} instead.
*
* If the value is a `Uint8Array`, it will be returned as-is.
*
* @param value - The value to convert to bytes.
* @returns The bytes as `Uint8Array`.
*/
export function valueToBytes(value: Bytes): Uint8Array {
if (typeof value === 'bigint') {
return bigIntToBytes(value);
}
if (typeof value === 'number') {
return numberToBytes(value);
}
if (typeof value === 'string') {
if (value.startsWith('0x')) {
return hexToBytes(value);
}
return stringToBytes(value);
}
if (isBytes(value)) {
return value;
}
throw new TypeError(`Unsupported value type: "${typeof value}".`);
}
/**
* Concatenate multiple byte-like values into a single `Uint8Array`. The values
* can be `Uint8Array`, `bigint`, `number`, or `string`. This uses
* {@link valueToBytes} under the hood to convert each value to bytes. Refer to
* the documentation of that function for more information.
*
* @param values - The values to concatenate.
* @returns The concatenated bytes as `Uint8Array`.
*/
export function concatBytes(values: Bytes[]): Uint8Array {
const normalizedValues = new Array(values.length);
let byteLength = 0;
for (let i = 0; i < values.length; i++) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const value = valueToBytes(values[i]!);
normalizedValues[i] = value;
byteLength += value.length;
}
const bytes = new Uint8Array(byteLength);
for (let i = 0, offset = 0; i < normalizedValues.length; i++) {
// While we could simply spread the values into an array and use
// `Uint8Array.from`, that is a lot slower than using `Uint8Array.set`.
bytes.set(normalizedValues[i], offset);
offset += normalizedValues[i].length;
}
return bytes;
}
/**
* Create a {@link DataView} from a {@link Uint8Array}. This is a convenience
* function that avoids having to create a {@link DataView} manually, which
* requires passing the `byteOffset` and `byteLength` parameters every time.
*
* Not passing the `byteOffset` and `byteLength` parameters can result in
* unexpected behavior when the {@link Uint8Array} is a view of a larger
* {@link ArrayBuffer}, e.g., when using {@link Uint8Array.subarray}.
*
* This function also supports Node.js {@link Buffer}s.
*
* @example
* ```typescript
* const bytes = new Uint8Array([1, 2, 3]);
*
* // This is equivalent to:
* // const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
* const dataView = createDataView(bytes);
* ```
* @param bytes - The bytes to create the {@link DataView} from.
* @returns The {@link DataView}.
*/
export function createDataView(bytes: Uint8Array): DataView {
if (typeof Buffer !== 'undefined' && bytes instanceof Buffer) {
const buffer = bytes.buffer.slice(
bytes.byteOffset,
bytes.byteOffset + bytes.byteLength,
);
return new DataView(buffer);
}
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
}