• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

benrr101 / node-taglib-sharp / 48391054

29 Oct 2023 04:39AM UTC coverage: 92.535% (-1.4%) from 93.934%
48391054

push

appveyor

benrr101
Merge branch 'release/v5.2.0'

3244 of 4129 branches covered (0.0%)

Branch coverage included in aggregate %.

2177 of 2177 new or added lines in 61 files covered. (100.0%)

26728 of 28261 relevant lines covered (94.58%)

423.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.68
/src/byteVector.ts
1
import * as IConv from "iconv-lite";
1✔
2
import * as fs from "fs";
1✔
3
import {IFileAbstraction} from "./fileAbstraction";
4
import {IStream} from "./stream";
5
import {Guards, NumberUtils} from "./utils";
1✔
6

7
/**
8
 * @summary Specifies the text encoding used when converting betweenInclusive a string and a
9
 *     {@link ByteVector}.
10
 * @remarks
11
 *     This enumeration is used by {@link ByteVector.fromString} and
12
 *     {@link ByteVector.toString}
13
 */
14
export enum StringType {
1✔
15
    /**
16
     * @summary The string is to be Latin-1 encoded.
17
     */
18
    Latin1 = 0,
1✔
19

20
    /**
21
     * @summary The string is to be UTF-16 encoded.
22
     */
23
    UTF16 = 1,
1✔
24

25
    /**
26
     * @summary The string is to be UTF-16BE encoded.
27
     */
28
    UTF16BE = 2,
1✔
29

30
    /**
31
     * @summary The string is to be UTF-8 encoded.
32
     */
33
    UTF8 = 3,
1✔
34

35
    /**
36
     * @summary The string is to be UTF-16LE encoded.
37
     */
38
    UTF16LE = 4,
1✔
39

40
    /**
41
     * @summary The string is to be encoded as a hex string for each byte (eg, 0x00, 0x12, 0xAF).
42
     *     Intended to be used for debugging purposes, only.
43
     */
44
    Hex = 5
1✔
45
}
46

47
/**
48
 * Wrapper around the `iconv-lite` library to provide string encoding and decoding functionality.
49
 */
50
export class Encoding {
1✔
51
    private static readonly HEX_ENCODING_KEY = "hex";
1✔
52
    private static readonly ENCODINGS = new Map<StringType, Encoding>([
1✔
53
        [StringType.Latin1, new Encoding("latin1")],
54
        [StringType.UTF8, new Encoding("utf8")],
55
        [StringType.UTF16BE, new Encoding("utf16-be")],
56
        [StringType.UTF16LE, new Encoding("utf16-le")],
57
        [StringType.Hex, new Encoding(Encoding.HEX_ENCODING_KEY)]
58
    ]);
59

60
    /**
61
     * Contains the last generic UTF16 encoding read. Defaults to UTF16-LE
62
     */
63
    private static _lastUtf16Encoding: StringType = StringType.UTF16LE;
1✔
64

65
    private readonly _encoding: string;
66

67
    private constructor(encoding: string) {
68
        this._encoding = encoding;
5✔
69
    }
70

71
    /**
72
     * Gets the appropriate encoding instance for encoding and decoding strings, based on the
73
     * provided `type`.
74
     * @param type Type of string to get an {@link Encoding} class instance for
75
     * @param bom Optional, the byte order marker for the string. Used to determine UTF16 endianess
76
     */
77
    public static getEncoding(type: StringType, bom?: ByteVector): Encoding {
78
        // When reading a collection of UTF16 strings, sometimes only the first one will contain
79
        // the BOM. In that case, this field will inform the file what encoding to use for the
80
        // second string.
81
        if (type === StringType.UTF16) {
7,728✔
82
            // If we have a BOM, return the appropriate encoding. Otherwise, assume we're
83
            // reading from a string that was already identified. In that case, we'll use
84
            // the last used encoding.
85
            if (bom && bom.length >= 2) {
45✔
86
                if (bom.get(0) === 0xFF && bom.get(1) === 0xFE) {
44!
87
                    this._lastUtf16Encoding = StringType.UTF16LE;
44✔
88
                } else {
89
                    this._lastUtf16Encoding = StringType.UTF16BE;
×
90
                }
91
            }
92

93
            type = this._lastUtf16Encoding;
45✔
94
        }
95

96
        return this.ENCODINGS.get(type);
7,728✔
97

98
        // NOTE: The original .NET implementation has the notion of "broken" latin behavior that
99
        //       uses Encoding.Default. I have removed it in this port because 1) this behavior is
100
        //       not used anywhere in the library, 2) Encoding.Default could be anything depending
101
        //       on the machine's region, 3) in .NET Core this is always UTF8.
102
        //       If it turns out we need support for other non unicode encodings, we'll want to
103
        //       revisit this.
104
    }
105

106
    public decode(data: Uint8Array): string {
107
        if (this._encoding === Encoding.HEX_ENCODING_KEY) {
3,553!
108
            // Special case for HEX string
109
            return data.reduce<string>((accum: string, currentValue) => {
×
110
                return accum + `0x${currentValue.toString(16).padStart(2, "0")} `;
×
111
            }, "");
112
        }
113

114
        // @TODO: The next version of iconv-lite will add Uint8Array to the types for decode. Until
115
        //    then, I have word it should work w/an 'unsafe' cast. See
116
        //    https://github.com/ashtuchkin/iconv-lite/issues/293
117
        return IConv.decode(<Buffer> data, this._encoding);
3,553✔
118
    }
119

120
    public encode(text: string): Uint8Array {
121
        return IConv.encode(text, this._encoding);
4,172✔
122
    }
123
}
124

125
/**
126
 * Wrapper around a `Uint8Array` that provides functionality for reading and writing byte arrays.
127
 * @remarks
128
 *     Implementation of this class uses a single `Uint8Array` to store bytes. Due to
129
 *     `Uint8Array`s being fixed length, any operation that inserts or removes values into the
130
 *     instance will result in a copy of the internal array being made. If multiple additions will
131
 *     be made, rather than using multiple inserts/adds, the {@link ByteVector.concatenate} method
132
 *     is provided to group additions/inserts and therefore improve performance.
133
 *
134
 *     The original .NET implementation had an ubiquitous `mid` method that would return a subset
135
 *     of the bytes in the current instance. In versions <5 of the node port, `mid` would make a
136
 *     copy of the subset of the bytes. Since this was frequently done right before reading a
137
 *     number or string, this copy was extremely wasteful. In version 5, the `mid` method was
138
 *     replaced with `subarray` which behaves identically to `Uint8Array.subarray` and returns
139
 *     an instance that is a 'view' of an existing instance - no copying involved. However, all
140
 *     write operations make copies, instances that are backed by 'views' may waste memory by
141
 *     referencing a `Uint8Array` that is much larger than the view.
142
 *
143
 *     With this in mind, best practices for using `ByteVectors`:
144
 *     * Calling {@link ByteVector.subarray} is cheap, use it when possible
145
 *     * If storing a subset of a `ByteVector`, store a copy with {@link ByteVector.toByteVector}
146
 *     * If building a `ByteVector`, use {@link ByteVector.concatenate} when possible
147
 *     * If the instance should be immutable, use {@link ByteVector.makeReadOnly}
148
 */
149
export class ByteVector {
1✔
150

151
    // #region Members
152

153
    private static readonly CRC_TABLE: Uint32Array = new Uint32Array([
1✔
154
        0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9,
155
        0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005,
156
        0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61,
157
        0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd,
158
        0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9,
159
        0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75,
160
        0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011,
161
        0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd,
162
        0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039,
163
        0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5,
164
        0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81,
165
        0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d,
166
        0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49,
167
        0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95,
168
        0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1,
169
        0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d,
170
        0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae,
171
        0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072,
172
        0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16,
173
        0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca,
174
        0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde,
175
        0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02,
176
        0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066,
177
        0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba,
178
        0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e,
179
        0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692,
180
        0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6,
181
        0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a,
182
        0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e,
183
        0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2,
184
        0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686,
185
        0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a,
186
        0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637,
187
        0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb,
188
        0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f,
189
        0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53,
190
        0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47,
191
        0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b,
192
        0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff,
193
        0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623,
194
        0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7,
195
        0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b,
196
        0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f,
197
        0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3,
198
        0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7,
199
        0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b,
200
        0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f,
201
        0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3,
202
        0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640,
203
        0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c,
204
        0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8,
205
        0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24,
206
        0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30,
207
        0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec,
208
        0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088,
209
        0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654,
210
        0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0,
211
        0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c,
212
        0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18,
213
        0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4,
214
        0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0,
215
        0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c,
216
        0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668,
217
        0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4
218
    ]);
219

220
    /**
221
     * Contains a one byte text delimiter
222
     */
223
    private static readonly TD1: ByteVector = ByteVector.fromSize(1).makeReadOnly();
1✔
224

225
    /**
226
     * Contains a two byte text delimiter
227
     */
228
    private static readonly TD2: ByteVector = ByteVector.fromSize(2).makeReadOnly();
1✔
229

230
    private _bytes: Uint8Array;
231
    private _isReadOnly: boolean = false;
34,237✔
232

233
    // #endregion
234

235
    // #region Constructors
236

237
    private constructor(bytes: Uint8Array) {
238
        this._bytes = bytes;
34,237✔
239
    }
240

241
    /**
242
     * Creates a {@link ByteVector} from a collection of bytes, byte arrays, and byte vectors. This
243
     * method is better to use when a known quantity of byte vectors will be concatenated together,
244
     * since doing multiple calls to {@link ByteVector.addByteVector} results in the entire byte
245
     * vector being copied for each call.
246
     * @param vectors ByteVectors, byte arrays, or straight bytes to concatenate together into a
247
     *     new {@link ByteVector}
248
     * @returns
249
     *     Single byte vector with the contents of the byte vectors in `vectors` concatenated
250
     *     together
251
     */
252
    // @TODO Remove usages of .addX when this can be substituted
253
    public static concatenate(... vectors: Array<Uint8Array|ByteVector|number|undefined>): ByteVector {
254
        // Get the length of the vector we need to create
255
        const totalLength = vectors.reduce<number>((accum, vector) => {
5,043✔
256
            if (vector === undefined || vector === null) {
20,217✔
257
                // Ignore falsy values
258
                return accum;
755✔
259
            }
260
            if (typeof(vector) === "number") {
19,462✔
261
                // Add 1 for a single byte
262
                return accum + 1;
2,711✔
263
            }
264

265
            // Add length of vector to length
266
            return accum + vector.length;
16,751✔
267
        }, 0);
268

269
        // Create a single big vector and copy the contents into it
270
        const result = ByteVector.fromSize(totalLength);
5,043✔
271
        let currentPosition = 0;
5,043✔
272
        for (const v of vectors) {
5,043✔
273
            if (v === undefined || v === null) {
20,217✔
274
                // Ignore falsy values
275
                continue;
755✔
276
            }
277

278
            if (typeof(v) === "number") {
19,462✔
279
                // We were given a single byte
280
                Guards.byte(v, "Byte values");
2,711✔
281
                result._bytes[currentPosition] = v;
2,711✔
282
                currentPosition += 1;
2,711✔
283
            } else {
284
                // We were given an array of bytes
285
                const getter = v instanceof ByteVector
16,751✔
286
                    ? (i: number) => v.get(i)
657,153✔
287
                    : (i: number) => v[i];
38✔
288

289
                for (let i = 0; i < v.length; i++) {
16,751✔
290
                    result._bytes[currentPosition + i] = getter(i);
657,191✔
291
                }
292
                currentPosition += v.length;
16,751✔
293
            }
294
        }
295

296
        return result;
5,043✔
297
    }
298

299
    /**
300
     * Creates an empty {@link ByteVector}
301
     */
302
    public static empty(): ByteVector {
303
        return new ByteVector(new Uint8Array(0));
4,915✔
304
    }
305

306
    /**
307
     * Creates a {@link ByteVector} from a base64 string.
308
     * @param str Base64 string to convert into a byte vector
309
     */
310
    public static fromBase64String(str: string): ByteVector {
311
        Guards.notNullOrUndefined(str, "str");
50✔
312

313
        const bytes = Buffer.from(str, "base64");
50✔
314
        return new ByteVector(bytes);
50✔
315
    }
316

317
    /**
318
     * Creates a {@link ByteVector} from a `Uint8Array` or `Buffer`
319
     * @param bytes Uint8Array of the bytes to put in the ByteVector
320
     * @param length Optionally, number of bytes to read. If this is not provided, it will default
321
     *     to the full length of `bytes`. If it is less than the length of `bytes`, `bytes` will be
322
     *     copied into the {@link ByteVector}.
323
     */
324
    public static fromByteArray(
325
        bytes: Uint8Array | Buffer | number[],
326
        length: number = bytes.length
4,299✔
327
    ): ByteVector {
328
        Guards.truthy(bytes, "bytes");
5,497✔
329
        Guards.safeUint(length, "length");
5,495✔
330
        if (length > bytes.length) {
5,491✔
331
            throw new Error("Argument out of range: length must be less than or equal to the length of the byte array");
1✔
332
        }
333

334
        if (!(bytes instanceof Uint8Array || bytes instanceof Buffer)) {
5,490✔
335
            bytes = new Uint8Array(bytes);
214✔
336
        }
337

338
        if (length < bytes.length) {
5,490✔
339
            bytes = new Uint8Array(bytes.subarray(0, length));
69✔
340
        }
341
        return new ByteVector(bytes);
5,490✔
342
    }
343

344
    /**
345
     * Creates a new instance by reading in the contents of a specified file abstraction.
346
     * @param abstraction File abstraction to read
347
     */
348
    public static fromFileAbstraction(abstraction: IFileAbstraction): ByteVector {
349
        Guards.truthy(abstraction, "abstraction");
3✔
350

351
        const stream = abstraction.readStream;
1✔
352
        const output = this.fromInternalStream(stream);
1✔
353
        abstraction.closeStream(stream);
1✔
354
        return output;
1✔
355
    }
356

357
    /**
358
     * Creates a 4 byte {@link ByteVector} with a signed 32-bit integer as the data
359
     * @param value Signed 32-bit integer to use as the data.
360
     * @param isBigEndian If `true`, `value` will be stored in big endian format. If `false`,
361
     *     `value` will be stored in little endian format
362
     */
363
    public static fromInt(value: number, isBigEndian: boolean = true): ByteVector {
27✔
364
        Guards.int(value, "value");
35✔
365

366
        const bytes = new Uint8Array(4);
30✔
367
        const dv = new DataView(bytes.buffer);
30✔
368
        dv.setInt32(0, value, !isBigEndian);
30✔
369

370
        return new ByteVector(bytes);
30✔
371
    }
372

373
    /**
374
     * Creates a ByteVector using the contents of an TagLibSharp-node stream as the contents. This
375
     * method reads from the current offset of the stream, not the beginning of the stream
376
     * @param stream TagLibSharp-node internal stream object
377
     */
378
    public static fromInternalStream(stream: IStream): ByteVector {
379
        // @TODO: Validate how much memory is used vs doing a concatenate
380
        Guards.truthy(stream, "stream");
11✔
381

382
        const vector = ByteVector.empty();
9✔
383
        const bytes = new Uint8Array(4096);
9✔
384
        while (true) {
9✔
385
            const bytesRead = stream.read(bytes, 0, bytes.length);
10✔
386
            if (bytesRead < bytes.length) {
10✔
387
                vector.addByteArray(bytes, bytesRead);
9✔
388
                break;
9✔
389
            } else {
390
                vector.addByteArray(bytes);
1✔
391
            }
392
        }
393

394
        return vector;
9✔
395
    }
396

397
    /**
398
     * Creates an 8 byte {@link ByteVector} with a signed 64-bit integer as the data
399
     * @param value Signed 64-bit integer to use as the data. If using a `bigint`, it must fit
400
     *     within 8 bytes. If using a `number`, it must be a safe integer.
401
     * @param isBigEndian If `true`, `value` will be stored in big endian format. If `false`,
402
     *     `value` will be stored in little endian format
403
     */
404
    public static fromLong(value: bigint | number, isBigEndian: boolean = true): ByteVector {
24✔
405
        let bigIntValue: bigint;
406
        if (typeof(value) === "number") {
44✔
407
            Guards.safeInt(value, "value");
6✔
408
            bigIntValue = BigInt(value);
6✔
409
        } else {
410
            Guards.long(value, "value");
38✔
411
            bigIntValue = value;
36✔
412
        }
413

414
        const bytes = new Uint8Array(8);
42✔
415
        const dv = new DataView(bytes.buffer);
42✔
416
        dv.setBigInt64(0, bigIntValue, !isBigEndian);
42✔
417

418
        return new ByteVector(bytes);
40✔
419
    }
420

421
    /**
422
     * Creates a 1 byte {@link ByteVector} with an unsigned 8-bit integer as the data
423
     * @param value Unsigned 8-bit integer to use as the data.
424
     */
425
    public static fromByte(value: number): ByteVector {
426
        Guards.byte(value, "value");
3✔
427

428
        const bytes = new Uint8Array(1);
3✔
429
        const dv = new DataView(bytes.buffer);
3✔
430
        dv.setUint8(0, value);
3✔
431

432
        return new ByteVector(bytes);
3✔
433
    }
434

435
    /**
436
     * Creates a {@link ByteVector} using the contents of a file as the data
437
     * @param path Path to the file to store in the ByteVector
438
     */
439
    public static fromPath(path: string): ByteVector {
440
        Guards.truthy(path, "path");
4✔
441

442
        // NOTE: We are doing this with read file b/c it removes the headache of working with streams
443
        // @TODO: Add support for async file reading
444
        const fileBuffer = fs.readFileSync(path);
1✔
445
        return ByteVector.fromByteArray(fileBuffer);
1✔
446
    }
447

448
    /**
449
     * Creates a 2 byte {@link ByteVector} with a signed 16-bit integer as the data
450
     * @param value Signed 16-bit integer to use as the data.
451
     * @param isBigEndian If `true`, `value` will be stored in big endian format. If `false`,
452
     *     `value` will be stored in little endian format
453
     */
454
    public static fromShort(value: number, isBigEndian: boolean = true): ByteVector {
30✔
455
        Guards.short(value, "value");
40✔
456

457
        const bytes = new Uint8Array(2);
33✔
458
        const dv = new DataView(bytes.buffer);
33✔
459
        dv.setInt16(0, value, !isBigEndian);
33✔
460

461
        return new ByteVector(bytes);
33✔
462
    }
463

464
    /**
465
     * Creates a {@link ByteVector} of a given length with a given value for all the elements
466
     * @param size Length of the ByteVector. Must be a positive safe integer
467
     * @param fill Byte value to initialize all elements to. Must be a positive 8-bit integer
468
     */
469
    public static fromSize(size: number, fill: number = 0x0): ByteVector {
6,188✔
470
        Guards.safeUint(size, "size");
6,957✔
471
        Guards.byte(fill, "fill");
6,952✔
472

473
        const bytes = new Uint8Array(size);
6,949✔
474
        bytes.fill(fill);
6,949✔
475
        return new ByteVector(bytes);
6,949✔
476
    }
477

478
    /**
479
     * Creates {@link ByteVector} with the contents of a stream as the data. The stream will be read
480
     * to the end before the ByteVector is returned.
481
     * @param readStream Readable stream that will be read in entirety.
482
     */
483
    public static fromStream(readStream: NodeJS.ReadableStream): Promise<ByteVector> {
484
        return new Promise<ByteVector>((complete, fail) => {
4✔
485
            if (!readStream) {
4✔
486
                fail(new Error("Null argument exception: Stream was not provided"));
2✔
487
            }
488

489
            // Setup the output
490
            const output = ByteVector.empty();
4✔
491

492
            // Setup the events to read the stream
493
            readStream.on("readable", () => {
4✔
494
                const bytes = <Buffer> readStream.read();
3✔
495
                if (bytes) {
3✔
496
                    output.addByteArray(bytes);
1✔
497
                }
498
            });
499
            readStream.on("end", () => {
2✔
500
                complete(output);
2✔
501
            });
502
            readStream.on("error", (error: Error) => {
2✔
503
                fail(error);
×
504
            });
505
        });
506
    }
507

508
    /**
509
     * Creates {@link ByteVector} with the byte representation of a string as the data.
510
     * @param text String to store in the ByteVector
511
     * @param type StringType to use to encode the string. If {@link StringType.UTF16} is used, the
512
     *        string will be encoded as UTF16-LE.
513
     * @param length Number of characters from the string to store in the ByteVector. Must be a
514
     *        positive 32-bit integer.
515
     */
516
    public static fromString(
517
        text: string,
518
        type: StringType,
519
        length: number = Number.MAX_SAFE_INTEGER
4,564✔
520
    ): ByteVector {
521
        // @TODO: Allow adding delimiters and find usages that immediately add a delimiter
522
        Guards.notNullOrUndefined(text, "text");
4,573✔
523
        if (!Number.isInteger(length) || !Number.isSafeInteger(length) || length < 0) {
4,573✔
524
            throw new Error("Argument out of range exception: length is invalid");
4✔
525
        }
526

527
        // If we're doing UTF16 w/o specifying an endian-ness, inject a BOM which also coerces
528
        // the converter to use UTF16LE
529
        const vector = type === StringType.UTF16
4,569✔
530
            ? new ByteVector(new Uint8Array([0xff, 0xfe]))
4,569✔
531
            : ByteVector.empty();
532

533
        // NOTE: This mirrors the behavior from the original .NET implementation where empty
534
        //       strings return an empty ByteVector (possibly with UTF16LE BOM)
535
        if (!text) {
4,569✔
536
            return vector;
397✔
537
        }
538

539
        // Shorten text if only part of it was requested
540
        if (text.length > length) {
4,172✔
541
            text = text.substring(0, length);
5✔
542
        }
543

544
        // Encode the string into bytes
545
        const textBytes = Encoding.getEncoding(type, vector).encode(text);
4,172✔
546
        vector.addByteArray(textBytes);
4,172✔
547
        return vector;
4,172✔
548
    }
549

550
    /**
551
     * Creates a 4 byte {@link ByteVector} with a positive 32-bit integer as the data
552
     * @param value Positive 32-bit integer to use as the data
553
     * @param isBigEndian If `true`, `value` will be stored in big endian format. If `false`,
554
     *     `value` will be stored in little endian format
555
     */
556
    public static fromUint(value: number, isBigEndian: boolean = true): ByteVector {
917✔
557
        Guards.uint(value, "value");
4,224✔
558

559
        const bytes = new Uint8Array(4);
4,219✔
560
        const dv = new DataView(bytes.buffer);
4,219✔
561
        dv.setUint32(0, value, !isBigEndian);
4,219✔
562
        return new ByteVector(bytes);
4,219✔
563
    }
564

565
    /**
566
     * Creates an 8 byte {@link ByteVector} with a positive 64-bit integer as the data
567
     * @param value Positive 64-bit integer to use as the data. If using a `bigint` it must fit
568
     *     within 8 bytes.
569
     * @param isBigEndian If `true`, `value` will be stored in big endian format. If `false`,
570
     *     `value` will be stored in little endian format
571
     */
572
    public static fromUlong(value: bigint | number, isBigEndian: boolean = true): ByteVector {
102✔
573
        let bigIntValue: bigint;
574
        if (typeof(value) === "number") {
375✔
575
            Guards.safeUint(value, "value");
178✔
576
            bigIntValue = BigInt(value);
175✔
577
        } else {
578
            Guards.ulong(value, "value");
197✔
579
            bigIntValue = value;
195✔
580
        }
581

582
        const bytes = new Uint8Array(8);
370✔
583
        const dv = new DataView(bytes.buffer);
370✔
584
        dv.setBigUint64(0, bigIntValue, !isBigEndian);
370✔
585
        return new ByteVector(bytes);
366✔
586
    }
587

588
    /**
589
     * Creates a 2 byte {@link ByteVector} with a positive 16-bit integer as the data
590
     * @param value Positive 16-bit integer to use as the data.
591
     * @param isBigEndian If `true`, `value` will be stored in big endian format. If `false`,
592
     *     `value` will be stored in little endian format
593
     */
594
    public static fromUshort(value: number, isBigEndian: boolean = true): ByteVector {
552✔
595
        Guards.ushort(value, "value");
2,038✔
596

597
        const bytes = new Uint8Array(2);
2,027✔
598
        const dv = new DataView(bytes.buffer);
2,027✔
599
        dv.setUint16(0, value, !isBigEndian);
2,027✔
600
        return new ByteVector(bytes);
2,027✔
601
    }
602

603
    // #endregion
604

605
    // #region Properties
606

607
    /**
608
     * Calculates the CRC32 of the current instance.
609
     */
610
    public get checksum(): number {
611
        let sum = 0;
152✔
612
        for (const b of this._bytes) {
152✔
613
            const index = NumberUtils.uintXor(NumberUtils.uintAnd(NumberUtils.uintRShift(sum, 24), 0xFF), b);
959✔
614
            sum = NumberUtils.uintXor(NumberUtils.uintLShift(sum, 8), ByteVector.CRC_TABLE[index]);
959✔
615
        }
616
        return sum;
152✔
617
    }
618

619
    /**
620
     * Whether the current instance has 0 bytes stored.
621
     */
622
    public get isEmpty(): boolean { return this._bytes?.length === 0; }
170!
623

624
    /**
625
     * Whether the current instance is read-only. If `true`, any call that will modify the instance
626
     * will throw.
627
     */
628
    public get isReadOnly(): boolean { return this._isReadOnly; }
159✔
629

630
    /**
631
     * Whether the current instance is a 'view' of another byte vector.
632
     */
633
    public get isView(): boolean {
634
        return this._bytes.byteOffset !== 0 && this._bytes.byteLength !== this._bytes.buffer.byteLength;
1,247✔
635
    }
636

637
    /**
638
     * Number of bytes currently in this instance.
639
     */
640
    public get length(): number { return this._bytes.length; }
2,459,127✔
641

642
    // #endregion
643

644
    // #region Static Methods
645

646
    /**
647
     * Gets the appropriate length null-byte text delimiter for the specified `type`.
648
     * @param type String type to get delimiter for
649
     */
650
    public static getTextDelimiter(type: StringType): ByteVector {
651
        return type === StringType.UTF16 || type === StringType.UTF16BE || type === StringType.UTF16LE
718✔
652
            ? ByteVector.TD2
718✔
653
            : ByteVector.TD1;
654
    }
655

656
    /**
657
     * Compares two byte vectors. Returns a numeric value
658
     * @param a Byte vector to compare against `b`
659
     * @param b Byte vector to compare against `a`
660
     * @returns
661
     *     `0` if the two vectors are the same. Any other value indicates the two are
662
     *     different. If the two vectors differ by length, this will be the length of `a` minus the
663
     *     length of `b`. If the lengths are the same it will be the difference between the first
664
     *     element that differs.
665
     */
666
    public static compare(a: ByteVector, b: ByteVector): number {
667
        Guards.truthy(a, "a");
4,969✔
668
        Guards.truthy(b, "b");
4,969✔
669

670
        let diff = a.length - b.length;
4,965✔
671
        for (let i = 0; diff === 0 && i < a.length; i++) {
4,965✔
672
            diff = a.get(i) - b.get(i);
12,881✔
673
        }
674

675
        return diff;
4,965✔
676
    }
677

678
    /**
679
     * Returns `true` if the contents of the two {@link ByteVector}s are identical, returns `false`
680
     * otherwise
681
     * @param first ByteVector to compare with `second`
682
     * @param second ByteVector to compare with `first`
683
     */
684
    public static equals(first: ByteVector, second: ByteVector): boolean {
685
        const fNull = !first;
4,963✔
686
        const sNull = !second;
4,963✔
687
        if (fNull && sNull) {
4,963✔
688
            // Since (f|s)null could be true with `undefined` OR `null`, we'll just let === decide it for us
689
            return first === second;
4✔
690
        }
691

692
        if (fNull || sNull) {
4,959✔
693
            // If only one is null/undefined, then they are not equal
694
            return false;
8✔
695
        }
696

697
        return ByteVector.compare(first, second) === 0;
4,951✔
698
    }
699

700
    // #endregion
701

702
    // #region Public Methods
703

704
    /**
705
     * Gets iterator for iterating over bytes in the current instance.
706
     */
707
    public *[Symbol.iterator](): Iterator<number> {
708
        for (const b of this._bytes) {
2✔
709
            yield b;
8✔
710
        }
711
    }
712

713
    /**
714
     * Adds a single byte to the end of the current instance
715
     * @param byte Value to add to the end of the ByteVector. Must be positive 8-bit integer.
716
     */
717
    public addByte(byte: number): void {
718
        Guards.byte(byte, "byte");
229✔
719
        this.throwIfReadOnly();
226✔
720

721
        this.addByteArray(new Uint8Array([byte]));
225✔
722
    }
723

724
    /**
725
     * Adds an array of bytes to the end of the current instance
726
     * @param data Array of bytes to add to the end of the ByteVector
727
     * @param length Number of elements from `data` to copy into the current instance
728
     */
729
    public addByteArray(data: Uint8Array, length?: number): void {
730
        Guards.truthy(data, "data");
4,979✔
731
        this.throwIfReadOnly();
4,977✔
732

733
        if (data.length === 0 || length === 0) {
4,976✔
734
            return;
10✔
735
        }
736

737
        // Create a copy of the existing byte array with additional space at the end for the new
738
        // byte array. Copy the new array into it.
739
        length = length || data.length;
4,966✔
740
        const oldData = this._bytes;
4,966✔
741
        const newData = length !== data.length ? data.subarray(0, length) : data;
4,966✔
742

743
        this._bytes = new Uint8Array(oldData.length + length);
4,966✔
744
        this._bytes.set(oldData);
4,966✔
745
        this._bytes.set(newData, oldData.length);
4,966✔
746
    }
747

748
    /**
749
     * Adds a {@link ByteVector} to the end of this ByteVector
750
     * @param data ByteVector to add to the end of this ByteVector
751
     */
752
    public addByteVector(data: ByteVector): void {
753
        Guards.truthy(data, "data");
559✔
754
        this.throwIfReadOnly();
557✔
755

756
        this.addByteArray(data._bytes);
556✔
757
    }
758

759
    /**
760
     * Removes all elements from this {@link ByteVector}
761
     * @remarks This method replaces the internal byte array with a new one.
762
     */
763
    public clear(): void {
764
        this.throwIfReadOnly();
5✔
765
        this._bytes = new Uint8Array(0);
3✔
766
    }
767

768
    /**
769
     * Determines if `pattern` exists at a certain `offset` in this byte vector.
770
     * @param pattern ByteVector to search for at in this byte vector
771
     * @param offset Position in this byte vector to search for the pattern. If omitted, defaults
772
     *     to `0`
773
     */
774
    public containsAt(pattern: ByteVector, offset: number = 0): boolean {
5✔
775
        Guards.truthy(pattern, "pattern");
915✔
776
        Guards.safeInt(offset, "offset");
911✔
777

778
        // Sanity check - make sure we're within the range of the comprehension
779
        if (offset < 0 || offset >= this.length || pattern.length === 0) {
907✔
780
            return false;
4✔
781
        }
782

783
        // Look through looking for a mismatch
784
        for (let i = 0; i < pattern.length; i++) {
903✔
785
            if (this._bytes[offset + i] !== pattern.get(i)) {
2,513✔
786
                return false;
476✔
787
            }
788
        }
789

790
        return true;
427✔
791
    }
792

793
    /**
794
     * Compares the current instance to another byte vector. Returns a numeric result.
795
     * @param other Other byte vector to compare against the current instance.
796
     */
797
    public compareTo(other: ByteVector): number {
798
        return ByteVector.compare(this, other);
9✔
799
    }
800

801
    /**
802
     * Determines whether this byte vector ends with the provided `pattern`.
803
     * @param pattern ByteVector to look for at the end of this byte vector
804
     */
805
    public endsWith(pattern: ByteVector): boolean {
806
        return this.containsAt(pattern, this.length - pattern.length);
35✔
807
    }
808

809
    /**
810
     * Determines whether this byte vector ends with a part of the `pattern`.
811
     * NOTE: if this instance ends with `pattern` perfectly, it must end with n-1 or
812
     * fewer bytes.
813
     * @param pattern ByteVector to look for at the end of this byte vector
814
     */
815
    public endsWithPartialMatch(pattern: ByteVector): number {
816
        Guards.truthy(pattern, "pattern");
9✔
817

818
        // Short circuit impossible calls
819
        if (pattern.length > this.length) {
7✔
820
            return -1;
1✔
821
        }
822

823
        // Try to match the last n-1 bytes of the source (where n is the pattern length), if that
824
        // fails, try to match n-2, n-3... until there are no more to try.
825
        const startIndex = this.length - pattern.length;
6✔
826
        for (let i = 0; i < pattern.length; i++) {
6✔
827
            const patternSubset = pattern.subarray(0, pattern.length - i);
20✔
828
            if (this.containsAt(patternSubset, startIndex + i)) {
20✔
829
                return startIndex + i;
5✔
830
            }
831
        }
832

833
        return -1;
1✔
834
    }
835

836
    /**
837
     * Determines if this instance has identical contents to the `other` instance.
838
     * @param other Other instance to compare against the current instance.
839
     */
840
    public equals(other: ByteVector): boolean {
841
        return ByteVector.equals(this, other);
42✔
842
    }
843

844
    /**
845
     * Searches this instance for the `pattern`. Returns the index of the first instance
846
     * of the pattern, or `-1` if it was not found. Providing a `byteAlign` requires the
847
     * pattern to appear at an index that is a multiple of the byteAlign parameter.
848
     * Example: searching "abcd" for "ab" with byteAlign 1 will return 0. Searching "abcd" for
849
     * "ab" with byteAlign 2 will return 1. Searching "00ab" for "ab" with byteAlign 2 will return
850
     * 2. Searching "0abc" with byteAlign 2 will return -1.
851
     * @param pattern Pattern of bytes to search this instance for
852
     * @param byteAlign Optional, byte alignment the pattern much align to
853
     */
854
    public find(pattern: ByteVector, byteAlign: number = 1): number {
121✔
855
        Guards.truthy(pattern, "pattern");
487✔
856
        Guards.uint(byteAlign, "byteAlign");
485✔
857
        Guards.greaterThanInclusive(byteAlign, 1, "byteAlign");
477✔
858

859
        // Sanity check impossible matches
860
        if (this.length === 0 || pattern.length === 0 || pattern.length > this.length) {
475✔
861
            return -1;
13✔
862
        }
863

864
        for (let i = 0; i < this.length; i += byteAlign) {
462✔
865
            let j = 0;
23,368✔
866
            while (j < pattern.length) {
23,368✔
867
                if (this._bytes[i + j] !== pattern.get(j)) {
23,746✔
868
                    break;
23,081✔
869
                }
870

871
                j++;
665✔
872
            }
873

874
            if (j === pattern.length ) {
23,368✔
875
                return i;
287✔
876
            }
877
        }
878

879
        return -1;
175✔
880
    }
881

882
    /**
883
     * Gets the byte at the given `index`.
884
     * @param index Element index to return
885
     */
886
    public get(index: number): number {
887
        Guards.uint(index, "index");
1,139,975✔
888
        Guards.lessThanInclusive(index, this.length - 1, "index");
1,139,970✔
889
        return this._bytes[index];
1,139,967✔
890
    }
891

892
    /**
893
     * Gets the index of the first occurrence of the specified value.
894
     * @param item A byte to find within the current instance.
895
     * @returns
896
     *     An integer containing the first index at which the value was found, or -1 if it was not
897
     *     found
898
     */
899
    public indexOf(item: number): number {
900
        return this._bytes.indexOf(item);
42✔
901
    }
902

903
    /**
904
     * Makes the current instance read-only, causing any call that would modify it or allow it to
905
     * be modified to throw.
906
     */
907
    public makeReadOnly(): this {
908
        this._isReadOnly = true;
263✔
909
        return this;
263✔
910
    }
911

912
    /**
913
     * Searches this instance for the `pattern` occurring after a given offset. Returns the index
914
     * of the first instance of the pattern, relative to the start of the array, or `-1` if it was
915
     * not found. Providing a `byteAlign` requires the pattern to appear at an index that is a
916
     * multiple of the byteAlign parameter. Example: searching "abcd" for "ab" with byteAlign 1
917
     * will return 0. Searching "abcd" for "ab" with byteAlign 2 will return 1. Searching "00ab"
918
     * for "ab" with byteAlign 2 will return 2. Searching "0abc" with byteAlign 2 will return -1.
919
     * @param pattern Pattern of bytes to search this instance for
920
     * @param offset Index into the instance to begin searching for `pattern`
921
     * @param byteAlign Optional, byte alignment the pattern much align to
922
     */
923
    public offsetFind(pattern: ByteVector, offset: number, byteAlign?: number): number {
924
        const findIndex = this.subarray(offset).find(pattern, byteAlign);
403✔
925
        return findIndex >= 0 ? findIndex + offset : findIndex;
391✔
926
    }
927

928
    /**
929
     * Resizes this instance to the length specified in `size`. If the desired size is
930
     * longer than the current length, it will be filled with the byte value in
931
     * `padding`. If the desired size is shorter than the current length, bytes will be
932
     * removed.
933
     * @param size Length of the byte vector after resizing. Must be unsigned 32-bit integer
934
     * @param padding Byte to fill any excess space created after resizing
935
     */
936
    public resize(size: number, padding: number = 0x0): void {
51✔
937
        Guards.uint(size, "size");
56✔
938
        Guards.byte(padding, "padding");
51✔
939

940
        const oldData = this._bytes;
48✔
941
        if (size < this.length) {
48✔
942
            // Shorten it
943
            this._bytes = new Uint8Array(size);
20✔
944
            this._bytes.set(oldData.subarray(0, size));
20✔
945
        } else if (this.length < size) {
28✔
946
            // Lengthen it
947
            this._bytes = new Uint8Array(size);
26✔
948
            this._bytes.set(oldData);
26✔
949
            this._bytes.fill(padding, oldData.length);
26✔
950
        }
951
        // Do nothing on same size
952
    }
953

954
    /**
955
     * Finds a byte vector by searching from the end of this instance and working towards the
956
     * beginning of this instance. Returns the index of the first instance of the pattern, or `-1`
957
     * if it was not found. Providing a `byteAlign` requires the pattern to appear at an
958
     * index that is a multiple of the byteAlign parameter.
959
     * Example: searching "abcd" for "ab" with byteAlign 1 will return 0. Searching "abcd" for
960
     * "ab" with byteAlign 2 will return 1. Searching "00ab" for "ab" with byteAlign 2 will return
961
     * 2. Searching "0abc" with byteAlign 2 will return -1.
962
     * @param pattern Pattern of bytes to search this instance for
963
     * @param byteAlign Optional, byte alignment the pattern must align to
964
     */
965
    public rFind(pattern: ByteVector, byteAlign: number = 1): number {
44✔
966
        Guards.truthy(pattern, "pattern");
53✔
967
        Guards.uint(byteAlign, "byteAlign");
51✔
968
        Guards.greaterThanInclusive(byteAlign, 1, "byteAlign");
47✔
969

970
        // Sanity check impossible matches
971
        if (pattern.length === 0 || pattern.length > this.length) {
46✔
972
            return -1;
3✔
973
        }
974

975
        const alignOffset = this.length % byteAlign;
43✔
976
        for (let i = this.length - alignOffset - pattern.length; i >= 0; i -= byteAlign) {
43✔
977
            let j = 0;
22,481✔
978
            while (j < pattern.length) {
22,481✔
979
                if (this._bytes[i + j] !== pattern.get(j)) {
22,527✔
980
                    break;
22,462✔
981
                }
982

983
                j++;
65✔
984
            }
985

986
            if (j === pattern.length) {
22,481✔
987
                return i;
19✔
988
            }
989
        }
990

991
        return -1;
24✔
992
    }
993

994
    /**
995
     * Sets the value at a specified index
996
     * @param index Index to set the value of
997
     * @param value Value to set at the index. Must be a valid integer betweenInclusive 0x0 and 0xff
998
     */
999
    public set(index: number, value: number): void {
1000
        Guards.uint(index, "index");
15,815✔
1001
        Guards.lessThanInclusive(index, this.length - 1, "index");
15,810✔
1002
        Guards.byte(value, "value");
15,809✔
1003

1004
        this.splice(index, 1, [value]);
15,806✔
1005
    }
1006

1007
    /**
1008
     * Changes the contents of the current instance by removing or replacing existing elements
1009
     * and/or adding new elements.
1010
     * @param start Index at which to start changing the array. Must be less than the length of
1011
     *     the instance
1012
     * @param deleteCount Number of elements in the array to remove from start. If greater than
1013
     *     the remaining length of the element, it will be capped at the remaining length
1014
     * @param items Elements to add to the array beginning from start. If omitted, the method will
1015
     *     only remove elements from the current instance.
1016
     */
1017
    public splice(start: number, deleteCount: number, items?: ByteVector|Uint8Array|number[]): void {
1018
        Guards.safeUint(start, "start");
15,987✔
1019
        Guards.lessThanInclusive(start, this.length, "start");
15,982✔
1020
        Guards.safeUint(deleteCount, "deleteCount");
15,981✔
1021
        this.throwIfReadOnly();
15,976✔
1022

1023
        // Determine how many elements we're *actually* deleting
1024
        deleteCount = Math.min(deleteCount, this.length - start);
15,974✔
1025

1026
        const addCount = items ? items.length : 0;
15,974✔
1027
        const newBytes = new Uint8Array(this._bytes.length - deleteCount + addCount);
15,974✔
1028
        newBytes.set(this._bytes.subarray(0, start));
15,974✔
1029
        if (items) {
15,974✔
1030
            items = items instanceof ByteVector ? items._bytes : items;
15,966✔
1031
            newBytes.set(items, start);
15,966✔
1032
        }
1033
        newBytes.set(this._bytes.subarray(start + deleteCount), start + addCount);
15,974✔
1034

1035
        this._bytes = newBytes;
15,974✔
1036
    }
1037

1038
    /**
1039
     * Splits this byte vector into a list of byte vectors using a separator
1040
     * @param separator Object to use to split this byte vector
1041
     * @param byteAlign Byte align to use when splitting. in order to split when a pattern is
1042
     *     encountered, the index at which it is found must be divisible by this value.
1043
     * @param max Maximum number of objects to return or 0 to not limit the number. If that number
1044
     *     is reached, the last value will contain the remainder of the file even if it contains
1045
     *     more instances of `separator`.
1046
     * @returns ByteVector[] The split contents of the current instance
1047
     */
1048
    public split(separator: ByteVector, byteAlign: number = 1, max: number = 0): ByteVector[] {
56✔
1049
        Guards.truthy(separator, "pattern");
45✔
1050
        Guards.uint(byteAlign, "byteAlign");
43✔
1051
        Guards.greaterThanInclusive(byteAlign, 1, "byteAlign");
39✔
1052
        Guards.uint(max, "max");
38✔
1053

1054
        const vectors = [];
34✔
1055
        const condition = (o: number): boolean => o !== -1 && (max < 1 || max > vectors.length + 1);
65✔
1056
        const increment = (o: number): number => this.offsetFind(separator, o + separator.length, byteAlign);
34✔
1057

1058
        let previousOffset = 0;
34✔
1059
        let offset = this.offsetFind(separator, 0, byteAlign);
34✔
1060
        for (offset; condition(offset); offset = increment(offset)) {
34✔
1061
            vectors.push(this.subarray(previousOffset, offset - previousOffset));
31✔
1062
            previousOffset = offset + separator.length;
31✔
1063
        }
1064

1065
        if (previousOffset < this.length) {
34✔
1066
            vectors.push(this.subarray(previousOffset));
29✔
1067
        }
1068

1069
        return vectors;
34✔
1070
    }
1071

1072
    /**
1073
     * Returns a window over the current instance.
1074
     * @param startIndex Offset into this instance where the comprehension begins
1075
     * @param length Number of elements from the instance to include. If omitted, defaults to the
1076
     *     remainder of the instance
1077
     */
1078
    public subarray(startIndex: number, length: number = this._bytes.length - startIndex): ByteVector {
792✔
1079
        Guards.safeUint(startIndex, "startIndex");
9,018✔
1080
        Guards.safeUint(length, "length");
9,008✔
1081
        return new ByteVector(this._bytes.subarray(startIndex, startIndex + length));
9,002✔
1082
    }
1083

1084
    /**
1085
     * Checks whether or not a pattern appears at the beginning of the current instance.
1086
     * @param pattern ByteVector containing the pattern to check for in the current instance.
1087
     * @returns
1088
     *     `true` if the pattern was found at the beginning of the current instance, `false`
1089
     *     otherwise.
1090
     */
1091
    public startsWith(pattern: ByteVector): boolean {
1092
        return this.containsAt(pattern, 0);
523✔
1093
    }
1094

1095
    /**
1096
     * Returns the current instance as a base64 encoded string.
1097
     */
1098
    public toBase64String(): string {
1099
        return Buffer.from(this._bytes.buffer, this._bytes.byteOffset, this._bytes.byteLength)
10✔
1100
            .toString("base64");
1101
    }
1102

1103
    /**
1104
     * Returns the bytes for the instance. Don't use it unless you need to.
1105
     * @internal
1106
     * @deprecated DON'T USE IT UNLESS YOU HAVE NO CHOICE.
1107
     */
1108
    public toByteArray(): Uint8Array {
1109
        return this._bytes;
1✔
1110
    }
1111

1112
    /**
1113
     * Returns a writable copy of the bytes represented by this instance.
1114
     * @remarks This is a **copy** of the data. Use sparingly.
1115
     */
1116
    public toByteVector(): ByteVector {
1117
        if (this.isView) {
1,079✔
1118
            const bytes = new Uint8Array(this._bytes);
254✔
1119
            return new ByteVector(bytes);
254✔
1120
        }
1121

1122
        // This is a concrete instance, return a new instance pointing to the existing bytes
1123
        // This is safe because the existing bytes will be copied on write
1124
        return new ByteVector(this._bytes);
825✔
1125
    }
1126

1127
    /**
1128
     * Converts the first eight bytes of the current instance to a double-precision floating-point
1129
     * value.
1130
     * @param mostSignificantByteFirst If `true` the most significant byte appears first (big
1131
     *        endian format).
1132
     * @throws Error If there are less than eight bytes in the current instance.
1133
     * @returns A double value containing the value read from the current instance.
1134
     */
1135
    public toDouble(mostSignificantByteFirst: boolean = true): number {
17✔
1136
        // NOTE: This is the behavior from the .NET implementation, due to BitConverter behavior
1137
        if (this.length < 8) {
22✔
1138
            throw new Error("Invalid operation: Cannot convert a byte vector of <8 bytes to double");
8✔
1139
        }
1140
        const dv = new DataView(this._bytes.buffer, this._bytes.byteOffset, 8);
14✔
1141
        return dv.getFloat64(0, !mostSignificantByteFirst);
14✔
1142
    }
1143

1144
    /**
1145
     * Converts the first four bytes of the current instance to a single-precision floating-point
1146
     * value.
1147
     * @param mostSignificantByteFirst If `true` the most significant byte appears first (big
1148
     *        endian format).
1149
     * @throws Error If there are less than four bytes in the current instance
1150
     * @returns A float value containing the value read from the current instance.
1151
     */
1152
    public toFloat(mostSignificantByteFirst: boolean = true): number {
11✔
1153
        const dv = new DataView(this._bytes.buffer, this._bytes.byteOffset, 4);
15✔
1154
        return dv.getFloat32(0, !mostSignificantByteFirst);
11✔
1155
    }
1156

1157
    /**
1158
     * Converts the first four bytes of the current instance to a signed integer. If the current
1159
     * instance is less than four bytes, the most significant bytes will be filled with 0x00.
1160
     * @param mostSignificantByteFirst If `true` the most significant byte appears first (big
1161
     *        endian format)
1162
     * @returns A signed integer value containing the value read from the current instance
1163
     */
1164
    public toInt(mostSignificantByteFirst: boolean = true): number {
18✔
1165
        const dv = this.getSizedDataView(4, mostSignificantByteFirst);
64✔
1166
        return dv.getInt32(0, !mostSignificantByteFirst);
64✔
1167
    }
1168

1169
    /**
1170
     * Converts the first eight bytes of the current instance to a signed long. If the current
1171
     * instance is less than eight bytes, the most significant bytes will be filled with 0x00.
1172
     * @param mostSignificantByteFirst If `true` the most significant byte appears first (big
1173
     *     endian format)
1174
     * @returns
1175
     *     A signed long value containing the value read from the current instance,
1176
     *     represented as a BigInt due to JavaScript's 52-bit integer limitation.
1177
     */
1178
    public toLong(mostSignificantByteFirst: boolean = true): bigint {
14✔
1179
        const dv = this.getSizedDataView(8, mostSignificantByteFirst);
248✔
1180
        return dv.getBigInt64(0, !mostSignificantByteFirst);
248✔
1181
    }
1182

1183
    /**
1184
     * Converts the first two bytes of the current instance to a signed short. If the current
1185
     * instance is less than two bytes, the most significant bytes will be filled with 0x00.
1186
     * @param mostSignificantByteFirst If `true` the most significant byte appears first (big
1187
     *    endian format)
1188
     * @returns A signed short value containing the value read from the current instance
1189
     */
1190
    public toShort(mostSignificantByteFirst: boolean = true): number {
35✔
1191
        const dv = this.getSizedDataView(2, mostSignificantByteFirst);
43✔
1192
        return dv.getInt16(0, !mostSignificantByteFirst);
43✔
1193
    }
1194

1195
    /**
1196
     * Converts a portion of the current instance to a string using a specified encoding
1197
     * @param type Value indicating the encoding to use when converting to a string.
1198
     * @returns String containing the converted bytes
1199
     */
1200
    public toString(type: StringType): string {
1201
        // @TODO: Would it be useful to have a null terminated version that finds first null
1202
        //      terminator and returns that subset of bytes?
1203
        const bom = type === StringType.UTF16 && this.length > 1
3,556✔
1204
            ? this.subarray(0, 2)
3,556✔
1205
            : undefined;
1206
        return Encoding.getEncoding(type, bom).decode(this._bytes);
3,556✔
1207
    }
1208

1209
    /**
1210
     * Converts the current instance into an array of strings starting at the specified offset and
1211
     * using the specified encoding, assuming the values are `null` separated and limiting it to a
1212
     * specified number of items.
1213
     * @param type A {@link StringType} value indicating the encoding to use when converting
1214
     * @param count Value specifying a limit to the number of strings to create. Once the limit has
1215
     *        been reached, the last string will be filled by the remainder of the data
1216
     * @returns Array of strings containing the converted text.
1217
     */
1218
    public toStrings(type: StringType, count: number = Number.MAX_SAFE_INTEGER): string[] {
73✔
1219
        Guards.safeUint(count, "count");
136✔
1220

1221
        const delimiter = ByteVector.getTextDelimiter(type);
134✔
1222
        let ptr = 0;
134✔
1223
        const strings = [];
134✔
1224
        while (ptr < this.length && strings.length < count) {
134✔
1225
            // Find the next delimiter
1226
            const delimiterPosition = this.offsetFind(delimiter, ptr, delimiter.length);
217✔
1227
            if (delimiterPosition < 0) {
217✔
1228
                // We didn't find another delimiter, so break out of the loop
1229
                break;
115✔
1230
            }
1231

1232
            const str = this.subarray(ptr, delimiterPosition - ptr).toString(type);
102✔
1233
            strings.push(str);
102✔
1234

1235
            ptr = delimiterPosition + delimiter.length;
102✔
1236
        }
1237

1238
        // If there's any remaining bytes, convert them to string
1239
        if (ptr < this.length && strings.length < count) {
134✔
1240
            const str = this.subarray(ptr).toString(type);
115✔
1241
            strings.push(str);
115✔
1242
        }
1243

1244
        return strings;
134✔
1245
    }
1246

1247
    /**
1248
     * Converts the first four bytes of the current instance to an unsigned integer. If the current
1249
     * instance is less than four bytes, the most significant bytes will be filled with 0x00.
1250
     * @param mostSignificantByteFirst If `true` the most significant byte appears first (big
1251
     *     endian format)
1252
     * @returns An unsigned integer value containing the value read from the current instance
1253
     */
1254
    public toUint(mostSignificantByteFirst: boolean = true): number {
618✔
1255
        const dv = this.getSizedDataView(4, mostSignificantByteFirst);
2,446✔
1256
        return dv.getUint32(0, !mostSignificantByteFirst);
2,446✔
1257
    }
1258

1259
    /**
1260
     * Converts the first eight bytes of the current instance to an unsigned long. If the current
1261
     * instance is less than eight bytes, the most significant bytes will be filled with 0x00.
1262
     * @param mostSignificantByteFirst If `true` the most significant byte appears first (big
1263
     *     endian format)
1264
     * @returns
1265
     *     An unsigned short value containing the value read from the current instance,
1266
     *     represented as a BigInt due to JavaScript's 32-bit integer limitation
1267
     */
1268
    public toUlong(mostSignificantByteFirst: boolean = true): bigint {
115✔
1269
        const dv = this.getSizedDataView(8, mostSignificantByteFirst);
127✔
1270
        return dv.getBigUint64(0, !mostSignificantByteFirst);
127✔
1271
    }
1272

1273
    /**
1274
     * Converts the first two bytes of the current instance to an unsigned short. If the current
1275
     * instance is less than two bytes, the most significant bytes will be filled with 0x00.
1276
     * @param mostSignificantByteFirst If `true` the most significant byte appears first (big
1277
     *     endian format)
1278
     * @returns An unsigned short value containing the value read from the current instance
1279
     */
1280
    public toUshort(mostSignificantByteFirst: boolean = true): number {
431✔
1281
        const dv = this.getSizedDataView(2, mostSignificantByteFirst);
1,323✔
1282
        return dv.getUint16(0, !mostSignificantByteFirst);
1,323✔
1283
    }
1284

1285
    // #endregion
1286

1287
    private getSizedDataView(size: number, mostSignificantByteFirst: boolean): DataView {
1288
        const difference = size - this._bytes.length;
4,251✔
1289
        if (difference <= 0) {
4,251✔
1290
            // Comprehension is at least the required size
1291
            return new DataView(this._bytes.buffer, this._bytes.byteOffset, size);
3,983✔
1292
        }
1293

1294
        // Comprehension is too short. Pad it out.
1295
        const fullSizeArray = new Uint8Array(size);
268✔
1296
        fullSizeArray.set(this._bytes, mostSignificantByteFirst ? difference : 0);
268✔
1297
        return new DataView(fullSizeArray.buffer);
268✔
1298
    }
1299

1300
    private throwIfReadOnly(): void {
1301
        if (this._isReadOnly) {
21,741✔
1302
            throw new Error("Invalid operation: Cannot modify a read-only ByteVector");
7✔
1303
        }
1304
    }
1305
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc