-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathbytes.ts
403 lines (370 loc) · 10.3 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
/**
* Functions for dealing with byte arrays (`Uint8Array`).
* @module
*/
import {
equals as _equals,
includeSlice as _includesSlice,
startsWith as _startsWith,
endsWith as _endsWith,
split as _split,
chunk as _chunk,
} from "./array/base.ts";
import { decodeBase64, decodeHex, encodeBase64, encodeHex } from "./encoding.ts";
import { sum } from "./math.ts";
import { Constructor } from "./types.ts";
const defaultEncoder = new TextEncoder();
const defaultDecoder = new TextDecoder();
/**
* A byte array is a `Uint8Array` that can be coerced to a string with `utf8`
* encoding.
*/
export class ByteArray extends Uint8Array {
override toString(): string {
return text(this);
}
toJSON(): { type: "ByteArray"; data: number[]; } {
return {
type: "ByteArray",
data: Array.from(this),
};
}
}
/**
* Converts the given data to a byte array.
*
* @example
* ```ts
* import bytes from "@ayonli/jsext/bytes";
*
* const arr = bytes("Hello, World!");
*
* console.log(arr);
* // ByteArray(13) [Uint8Array] [ 72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33 ]
*
* console.log(String(arr)); // "Hello, World!"
*
* // from hex
* const arr2 = bytes("48656c6c6f2c20576f726c6421", "hex");
*
* // from base64
* const arr3 = bytes("SGVsbG8sIFdvcmxkIQ==", "base64");
* ```
*/
export default function bytes(str: string, encoding?: "utf8" | "hex" | "base64"): ByteArray;
export default function bytes(arr: string | ArrayBufferLike | ArrayBufferView | ArrayLike<number>): ByteArray;
/**
* Creates a byte array with the specified length.
*
* @example
* ```ts
* import bytes from "@ayonli/jsext/bytes";
*
* const arr = bytes(10);
*
* console.log(arr);
* // ByteArray(10) [Uint8Array] [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
* ```
*/
export default function bytes(length: number): ByteArray;
export default function bytes(
data: number | string | ArrayBufferLike | ArrayBufferView | ArrayLike<number>,
encoding: "utf8" | "hex" | "base64" = "utf8"
): ByteArray {
if (typeof data === "number") {
return new ByteArray(data);
} else if (typeof data === "string") {
let _data: Uint8Array;
if (encoding === "hex") {
_data = decodeHex(data);
} else if (encoding === "base64") {
_data = decodeBase64(data);
} else {
_data = defaultEncoder.encode(data);
}
return new ByteArray(_data.buffer, _data.byteOffset, _data.byteLength);
} else if (ArrayBuffer.isView(data)) {
return new ByteArray(data.buffer, data.byteOffset, data.byteLength);
} else {
return new ByteArray(data);
}
}
/**
* Converts the byte array (or `Uint8Array`) to a string.
* @param encoding Default value: `utf8`.
*
* @example
* ```ts
* import { text } from "@ayonli/jsext/bytes";
*
* const arr = new Uint8Array([72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]);
*
* console.log(text(arr)); // "Hello, World!"
* console.log(text(arr, "hex")); // "48656c6c6f2c20576f726c6421"
* console.log(text(arr, "base64")); // "SGVsbG8sIFdvcmxkIQ=="
* ```
*/
export function text(
bytes: Uint8Array,
encoding: "utf8" | "hex" | "base64" = "utf8"
): string {
if (encoding === "hex") {
return encodeHex(bytes);
} else if (encoding === "base64") {
return encodeBase64(bytes);
} else if (typeof Buffer === "function" && bytes instanceof Buffer) {
return bytes.toString("utf8");
} else {
return defaultDecoder.decode(bytes);
}
}
/**
* Copies bytes from `src` array to `dest` and returns the number of bytes copied.
*
* @example
* ```ts
* import { copy } from "@ayonli/jsext/bytes";
*
* const src = new Uint8Array([1, 2, 3, 4, 5]);
* const dest = new Uint8Array(3);
*
* const n = copy(src, dest);
*
* console.log(n); // 3
* console.log(dest); // Uint8Array(3) [ 1, 2, 3 ]
* ```
*/
export function copy(src: Uint8Array, dest: Uint8Array): number {
if (src.length > dest.length) {
src = src.subarray(0, dest.length);
}
dest.set(src);
return src.length;
}
/**
* Like `Buffer.concat` but for native `Uint8Array`.
*
* @example
* ```ts
* import { concat } from "@ayonli/jsext/bytes";
*
* const arr1 = new Uint8Array([1, 2, 3]);
* const arr2 = new Uint8Array([4, 5, 6]);
*
* const result = concat(arr1, arr2);
*
* console.log(result); // Uint8Array(6) [ 1, 2, 3, 4, 5, 6 ]
* ```
*/
export function concat<T extends Uint8Array>(...arrays: T[]): T {
const length = sum(...arrays.map(arr => arr.length));
const ctor = ((arrays[0] as T)?.constructor || Uint8Array) as Constructor<T>;
const result = typeof Buffer === "function" && Object.is(ctor, Buffer)
? Buffer.alloc(length) as unknown as T
: new ctor(length);
let offset = 0;
for (const arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
/**
* Like `Buffer.compare` but for native `Uint8Array`.
*
* @example
* ```ts
* import { compare } from "@ayonli/jsext/bytes";
*
* const arr1 = new Uint8Array([1, 2, 3]);
* const arr2 = new Uint8Array([1, 2, 4]);
* const arr3 = new Uint8Array([1, 2, 3, 4]);
* const arr4 = new Uint8Array([1, 2, 3]);
* const arr5 = new Uint8Array([1, 2]);
*
* console.log(compare(arr1, arr2)); // -1
* console.log(compare(arr1, arr3)); // -1
* console.log(compare(arr1, arr4)); // 0
* console.log(compare(arr1, arr5)); // 1
* ```
*/
export function compare(arr1: Uint8Array, arr2: Uint8Array): -1 | 0 | 1 {
if (arr1 === arr2) {
return 0;
}
for (let i = 0; i < arr1.length; i++) {
const ele1 = arr1[i] as number;
const ele2 = arr2[i];
if (ele2 === undefined) {
return 1;
} else if (ele1 < ele2) {
return -1;
} else if (ele1 > ele2) {
return 1;
}
}
return arr1.length < arr2.length ? -1 : 0;
}
/**
* Checks if the two byte arrays are equal to each other.
*
* @example
* ```ts
* import { equals } from "@ayonli/jsext/bytes";
*
* const arr1 = new Uint8Array([1, 2, 3]);
* const arr2 = new Uint8Array([1, 2, 3]);
* const arr3 = new Uint8Array([1, 2, 4]);
*
* console.log(equals(arr1, arr2)); // true
* console.log(equals(arr1, arr3)); // false
* ```
*/
export function equals(arr1: Uint8Array, arr2: Uint8Array): boolean {
if (arr1 === arr2) {
return true;
} else if (arr1.length !== arr2.length) {
return false;
} else if (arr1.length < 1000) {
return _equals(arr1, arr2);
}
const len = arr1.length;
const compressible = Math.floor(len / 4);
const _arr1 = new Uint32Array(arr1.buffer, 0, compressible);
const _arr2 = new Uint32Array(arr2.buffer, 0, compressible);
for (let i = compressible * 4; i < len; i++) {
if (arr1[i] !== arr2[i]) {
return false;
}
}
for (let i = 0; i < _arr1.length; i++) {
if (_arr1[i] !== _arr2[i]) {
return false;
}
}
return true;
}
/**
* Checks if the byte array contains another array as a slice of its contents.
*
* @example
* ```ts
* import { includesSlice } from "@ayonli/jsext/bytes";
*
* const arr = new Uint8Array([1, 2, 3, 4, 5]);
*
* console.log(includesSlice(arr, new Uint8Array([3, 4]))); // true
* console.log(includesSlice(arr, new Uint8Array([4, 3]))); // false
* ```
*/
export function includesSlice(arr: Uint8Array, slice: Uint8Array): boolean {
return _includesSlice(arr, slice);
}
/**
* Returns the index of the first occurrence of the slice in the byte array, or
* -1 if it is not present. Optionally, we can specify where to start the search
* by providing the `start` parameter.
*
* @example
* ```ts
* import { indexOfSlice } from "@ayonli/jsext/bytes";
*
* const arr = new Uint8Array([1, 2, 3, 4, 5]);
* const slice = new Uint8Array([3, 4]);
*
* console.log(indexOfSlice(arr, slice)); // 2
* console.log(indexOfSlice(arr, slice, 3)); // -1, starts searching from index 3
* ```
*/
export function indexOfSlice(arr: Uint8Array, slice: Uint8Array, start = 0): number {
if (start < 0) {
start = Math.max(0, arr.length + start);
}
const limit = arr.length - slice.length;
const s = slice[0];
for (let i = start; i <= limit; i++) {
if (arr[i] !== s) {
continue;
}
let j = 1;
while (j < slice.length && arr[i + j] === slice[j]) {
j++;
}
if (j === slice.length) {
return i;
}
}
return -1;
}
/**
* Checks if the byte array starts with the given prefix.
*
* @example
* ```ts
* import { startsWith } from "@ayonli/jsext/bytes";
*
* const arr = new Uint8Array([1, 2, 3, 4, 5]);
*
* console.log(startsWith(arr, new Uint8Array([1, 2]))); // true
* console.log(startsWith(arr, new Uint8Array([2, 1]))); // false
* ```
*/
export function startsWith(arr: Uint8Array, prefix: Uint8Array): boolean {
return _startsWith(arr, prefix);
}
/**
* Checks if the byte array ends with the given suffix.
*
* @example
* ```ts
* import { endsWith } from "@ayonli/jsext/bytes";
*
* const arr = new Uint8Array([1, 2, 3, 4, 5]);
*
* console.log(endsWith(arr, new Uint8Array([4, 5]))); // true
* console.log(endsWith(arr, new Uint8Array([5, 4]))); // false
* ```
*/
export function endsWith(arr: Uint8Array, suffix: Uint8Array): boolean {
return _endsWith(arr, suffix);
}
/**
* Breaks the byte array into smaller chunks according to the given delimiter.
*
* @example
* ```ts
* import { split } from "@ayonli/jsext/bytes";
*
* const arr = new Uint8Array([1, 2, 3, 0, 4, 5, 0, 6, 7]);
*
* console.log(split(arr, 0));
* // [
* // Uint8Array(3) [ 1, 2, 3 ],
* // Uint8Array(2) [ 4, 5 ],
* // Uint8Array(2) [ 6, 7 ]
* // ]
* ```
*/
export function split<T extends Uint8Array>(arr: T, delimiter: number): T[] {
return _split(arr, delimiter) as T[];
}
/**
* Breaks the byte array into smaller chunks according to the given length.
*
* @example
* ```ts
* import { chunk } from "@ayonli/jsext/bytes";
*
* const arr = new Uint8Array([1, 2, 3, 4, 5, 6, 7]);
*
* console.log(chunk(arr, 3));
* // [
* // Uint8Array(3) [ 1, 2, 3 ],
* // Uint8Array(3) [ 4, 5, 6 ],
* // Uint8Array(1) [ 7 ]
* // ]
* ```
*/
export function chunk<T extends Uint8Array>(arr: T, length: number): T[] {
return _chunk(arr, length) as T[];
}