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

streetsidesoftware / cspell / 20568885960

29 Dec 2025 08:57AM UTC coverage: 92.813%. First build
20568885960

Pull #8243

github

web-flow
Merge 6838753d6 into 0cd2b7dfc
Pull Request #8243: fix: Add StringTable and refactor BinaryFormat

8648 of 11263 branches covered (76.78%)

305 of 344 new or added lines in 2 files covered. (88.66%)

16957 of 18270 relevant lines covered (92.81%)

29980.39 hits per line

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

87.06
/packages/cspell-trie-lib/src/lib/binary/binaryFormat.ts
1
import { assert } from '../utils/assert.ts';
2
import { endianness } from '../utils/endian.ts';
3

4
const isLittleEndian: boolean = endianness() === 'LE';
39✔
5

6
export type FormatType =
7
    | 'value' // A raw value
8
    // | 'ptr' // A 32bit pointer without length
9
    | 'ptr+size'; // A 32bit pointer followed by a 32bit size
10

11
type U8Array = Uint8Array<ArrayBuffer>;
12
type U32Array = Uint32Array<ArrayBuffer>;
13
type U16Array = Uint16Array<ArrayBuffer>;
14

15
const BytesSize = {
39✔
16
    uint8: 1, // Uint8Array.BYTES_PER_ELEMENT
17
    uint16: 2, // Uint16Array.BYTES_PER_ELEMENT
18
    uint32: 4, // Uint32Array.BYTES_PER_ELEMENT
19
    uint64: 8, // Uint64Array.BYTES_PER_ELEMENT
20
    string: 1,
21
} as const;
22

23
export type ByteSize = 1 | 2 | 4 | 8;
24

25
export interface FormatElement {
26
    /** name of the element */
27
    name: string;
28
    /** the description of the element */
29
    description: string;
30
    /** the type of element */
31
    type: FormatType;
32
    /**
33
     * This is a bit field indicating the size of each element in bits.
34
     * - 1 = 8 bits - for bytes or utf8 strings
35
     * - 2 = 16 bits
36
     * - 4 = 32 bits
37
     * - 8 = 64 bits
38
     *
39
     * Used for pointers to indicate the size of each element pointed to.
40
     */
41
    byteSize: ByteSize;
42

43
    /** the byte alignment of this element, not what it might be pointed to */
44
    alignment: ByteAlignment;
45

46
    /** offset in bytes */
47
    offset: number;
48
    /** size in bytes */
49
    size: number;
50

51
    /** An expected value */
52
    value?: Uint8Array<ArrayBuffer> | undefined;
53

54
    /**
55
     * This is to allow two different elements to share the same data.
56
     */
57
    overload?: string | undefined;
58
}
59

60
interface DataArrayView extends ArrayBufferView<ArrayBuffer> {
61
    length: number;
62
}
63

64
/**
65
 * BinaryFormatBuilder is used to define the structure and layout of binary data.
66
 * It provides methods to add various data types (uint8, uint16, uint32, strings, arrays)
67
 * and pointers to the format definition. Each element is automatically aligned and positioned
68
 * based on its type and size. Once all elements are added, call build() to create an
69
 * immutable BinaryFormat that can be used with BinaryDataBuilder and BinaryDataReader.
70
 */
71
export class BinaryFormatBuilder {
72
    #elements: FormatElement[] = [];
19✔
73
    #elementsByName: Map<string, FormatElement> = new Map();
19✔
74
    #offset = 0;
19✔
75
    #textEncoder = new TextEncoder();
19✔
76

77
    addUint8(name: string, description: string, value?: number | Uint8Array | number[]): BinaryFormatBuilder {
78
        const uValue =
79
            value === undefined || typeof value === 'number' ? new Uint8Array([value || 0]) : new Uint8Array(value);
4!
80
        return this.addData(name, description, 'value', uValue);
4✔
81
    }
82

83
    addUnit16(name: string, description: string, value?: number): BinaryFormatBuilder {
NEW
84
        const uValue = value !== undefined ? rawNumberToUint16Array(value) : rawNumberToUint16Array(0);
×
NEW
85
        return this.addData(name, description, 'value', uValue);
×
86
    }
87

88
    addUint32(name: string, description: string, value?: number): BinaryFormatBuilder {
89
        const uValue = value !== undefined ? rawNumberToUint32Array(value) : rawNumberToUint32Array(0);
9!
90
        return this.addData(name, description, 'value', uValue);
9✔
91
    }
92

93
    /**
94
     * A pointer to a uint32 array, it has two parts, the offset and the length.
95
     * @param name - name of pointer
96
     * @param description - the description of the field
97
     * @param overload - optional name of element to overload
98
     * @returns this
99
     */
100
    addUint32ArrayPtr(name: string, description: string, overload?: string): BinaryFormatBuilder {
101
        return this.addPointer(BytesSize.uint32, name, description, overload);
13✔
102
    }
103

104
    /**
105
     * A pointer to a uint16 array, it has two parts, the offset and the length.
106
     * @param name - name of pointer
107
     * @param description - the description of the field
108
     * @param overload - optional name of element to overload
109
     * @returns this
110
     */
111
    addUint16ArrayPtr(name: string, description: string, overload?: string): BinaryFormatBuilder {
112
        return this.addPointer(BytesSize.uint16, name, description, overload);
4✔
113
    }
114

115
    /**
116
     * A pointer to a uint8 array, it has two parts, the offset and the length.
117
     * @param name - name of pointer
118
     * @param description - the description of the field
119
     * @param overload - optional name of element to overload
120
     * @returns this
121
     */
122
    addUint8ArrayPtr(name: string, description: string, overload?: string): BinaryFormatBuilder {
123
        return this.addPointer(BytesSize.uint8, name, description, overload);
7✔
124
    }
125

126
    /**
127
     * A pointer to a uint8 array, it has two parts, the offset and the length.
128
     * @param name - name of pointer
129
     * @param description - the description of the field
130
     * @param overload - optional name of element to overload
131
     * @returns this
132
     */
133
    addStringPtr(name: string, description: string, overload?: string): BinaryFormatBuilder {
134
        return this.addPointer(BytesSize.string, name, description, overload);
4✔
135
    }
136

137
    /**
138
     * Add a pointer element.
139
     * @param byteSize - size of each element pointed to
140
     * @param name - name of the pointer
141
     * @param description - description of the pointer
142
     * @param overload - optional name of element to overload
143
     * @returns
144
     */
145
    addPointer(byteSize: ByteSize, name: string, description: string, overload?: string): BinaryFormatBuilder {
146
        const alignment: ByteAlignment = 4;
28✔
147
        let offset = byteAlign(this.#offset, alignment);
28✔
148
        if (overload) {
28✔
149
            const existing = this.#elementsByName.get(overload);
5✔
150
            assert(existing, `Overload target not found: ${overload}`);
5✔
151
            offset = byteAlign(existing.offset, alignment);
5✔
152
            assert(existing.offset === offset, `Overload target offset mismatch: ${overload}`);
5✔
153
        }
154
        const element: FormatElement = {
28✔
155
            name,
156
            description,
157
            type: 'ptr+size',
158
            alignment,
159
            offset,
160
            size: 8,
161
            value: undefined,
162
            byteSize,
163
            overload,
164
        };
165
        this.#addElement(element);
28✔
166
        return this;
28✔
167
    }
168

169
    addString(name: string, description: string, length: number | string): BinaryFormatBuilder {
170
        const value = typeof length === 'string' ? this.#textEncoder.encode(length) : new Uint8Array(length);
50!
171
        this.addData(name, description, 'value', value);
50✔
172
        return this;
50✔
173
    }
174

175
    addData(name: string, description: string, formatType: FormatType, data: DataArrayView): BinaryFormatBuilder {
176
        const byteSize = data.byteLength / data.length;
63✔
177
        assert(isByteAlignment(byteSize), `Invalid byte size: ${byteSize} for field: ${name}`);
63✔
178
        const alignment = byteSize;
63✔
179
        const offset = byteAlign(this.#offset, byteSize);
63✔
180
        const value = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
63✔
181
        const size = value.byteLength;
63✔
182
        this.#addElement({ name, description, type: formatType, alignment, offset, size, value, byteSize });
63✔
183
        return this;
63✔
184
    }
185

186
    #addElement(element: FormatElement): void {
187
        assert(!this.#elementsByName.has(element.name), `Duplicate element name: ${element.name}`);
91✔
188
        const expectedOffset = byteAlign(element.offset, element.alignment);
91✔
189
        assert(
91✔
190
            element.offset === expectedOffset,
191
            `Element alignment mismatch for ${element.name} with alignment ${element.alignment}. Expected: ${expectedOffset}, Found: ${element.offset}`,
192
        );
193
        this.#elementsByName.set(element.name, element);
91✔
194
        this.#elements.push(element);
91✔
195
        if (!element.overload) {
91!
196
            this.#offset = element.offset + element.size;
86✔
197
        }
198
    }
199

200
    build(): BinaryFormat {
201
        return new BinaryFormat([...this.#elements]);
18✔
202
    }
203
}
204

205
/**
206
 * BinaryFormat represents the structure and layout of binary data.
207
 * It contains a collection of format elements that describe the fields,
208
 * their types, offsets, sizes, and byte alignment within the binary data.
209
 *
210
 * This class is typically created using BinaryFormatBuilder and is used
211
 * by BinaryDataBuilder and BinaryDataReader to write and read binary data
212
 * according to the defined format.
213
 */
214
export class BinaryFormat {
215
    readonly elements: FormatElement[];
216
    #fieldsByName: Map<string, FormatElement> = new Map();
18✔
217
    #offset: number;
218

219
    constructor(elements: FormatElement[]) {
220
        this.elements = elements;
18✔
221
        this.#fieldsByName = new Map(elements.map((el) => [el.name, el] as const));
91✔
222
        this.#offset = Math.max(...elements.map((el) => el.offset + el.size), 0);
91✔
223
    }
224

225
    get size(): number {
226
        return this.#offset;
13✔
227
    }
228

229
    getField(name: string): FormatElement | undefined {
230
        return this.#fieldsByName.get(name);
49✔
231
    }
232

233
    toJSON(): unknown {
234
        return this.elements.map(formatElementToJSON);
1✔
235
    }
236

237
    toString(): string {
238
        const nameWidth = Math.max('name'.length, ...this.elements.map((el) => el.name.length), 'name'.length);
4✔
239
        const offsetWidth = 8;
1✔
240
        const sizeWidth = 6;
1✔
241
        const typeWidth = Math.max('type'.length, ...this.elements.map((el) => el.type.length), 'type'.length);
4✔
242
        const lines: string[] = [];
1✔
243

244
        addHeaderLines();
1✔
245
        this.elements.forEach(addElement);
1✔
246

247
        return lines.join('\n');
1✔
248

249
        function addHeaderLines(): void {
250
            const line = formatLine(['name', 'offset', 'size', 'type', 'mask', 'description', 'value']);
1✔
251
            lines.push('Binary Format:');
1✔
252
            lines.push(line);
1✔
253
            lines.push('-'.repeat(line.length));
1✔
254
        }
255

256
        function addElement(e: FormatElement): void {
257
            lines.push(
4✔
258
                formatLine([
259
                    e.name,
260
                    e.offset.toString(),
261
                    e.size.toString(),
262
                    e.type,
263
                    e.byteSize.toString(2).padStart(4, '0'),
264
                    e.description,
265
                    e.value ? `${e.value}` : '',
4!
266
                ]),
267
            );
268
        }
269

270
        type LineData = [
271
            name: string,
272
            offset: string,
273
            size: string,
274
            type: string,
275
            mask: string,
276
            description: string,
277
            value: string,
278
        ];
279

280
        function formatLine([name, offset, size, type, mask, description, value]: LineData): string {
281
            name = name.padEnd(nameWidth, ' ');
5✔
282
            offset = offset.padStart(offsetWidth, ' ');
5✔
283
            size = size.padStart(sizeWidth, ' ');
5✔
284
            type = type.padEnd(typeWidth, ' ');
5✔
285
            value = value ? `(${value})` : '';
5!
286
            return `${name} ${offset} ${size} ${type} ${mask} ${description} ${value}`.trim();
5✔
287
        }
288
    }
289
}
290

291
export interface DataElement {
292
    name: string;
293
    offset: number;
294
    size: number;
295
    data: U8Array;
296
    ref?: FormatElement | undefined;
297
}
298

299
export interface DataElementWithRef extends DataElement {
300
    ref: FormatElement;
301
}
302

303
export type ByteAlignment = 1 | 2 | 4 | 8;
304

305
export class BinaryDataBuilder {
306
    #dataElementMap: Map<string, DataElement> = new Map();
13✔
307
    #offset = 0;
13✔
308
    #endian: 'LE' | 'BE';
309
    #useLE: boolean;
310
    #encoder = new TextEncoder();
13✔
311
    #dataByOffset: Map<number, U8Array> = new Map();
13✔
312
    readonly format: BinaryFormat;
313

314
    constructor(format: BinaryFormat, endian: 'LE' | 'BE' = endianness()) {
10!
315
        this.format = format;
13✔
316
        this.#offset = format.size;
13✔
317
        this.#endian = endian;
13✔
318
        this.#useLE = endian === 'LE';
13✔
319
        this.#dataElementMap = new Map();
13✔
320
        this.#populateDataElementMap();
13✔
321
    }
322

323
    #populateDataElementMap() {
324
        for (const ref of this.format.elements) {
13✔
325
            const { name, offset, size } = ref;
66✔
326
            let data = this.#dataByOffset.get(offset);
66✔
327
            if (!data || data.byteLength < size) {
66!
328
                data = new Uint8Array(size);
63✔
329
                this.#dataByOffset.set(offset, data);
63✔
330
            }
331
            if (ref.value) {
66!
332
                data.set(ref.value);
39✔
333
            }
334
            const de: DataElement = { name, offset, size, data, ref };
66✔
335
            this.#dataElementMap.set(de.name, de);
66✔
336
            this.#offset = Math.max(this.#offset, offset + size);
66✔
337
        }
338
    }
339

340
    setString(name: string, value: string): BinaryDataBuilder {
341
        const element = this.getDataElement(name);
8✔
342
        assert(element, `Field not found: ${name}`);
8✔
343
        const formatElement = element.ref;
8✔
344
        assert(formatElement, `Field Format not found: ${name}`);
8✔
345
        assert(formatElement.byteSize == BytesSize.string, `Field is not a string: ${name}`);
8✔
346

347
        const r = this.#encoder.encodeInto(value, element.data);
8✔
348
        assert(r.read === value.length, `String too long for field ${name}: ${value}`);
8✔
349
        return this;
8✔
350
    }
351

352
    setUint32(name: string, value: number): BinaryDataBuilder {
353
        const element = this.getDataElement(name);
2✔
354
        assert(element, `Field not found: ${name}`);
2✔
355
        const formatElement = element.ref;
2✔
356
        assert(formatElement, `Field Format not found: ${name}`);
2✔
357
        assert(formatElement.byteSize == BytesSize.uint32, `Field is not a uint32: ${name}`);
2✔
358

359
        const view = new DataView(element.data.buffer, element.data.byteOffset, element.data.byteLength);
2✔
360
        const useLittle = this.#endian === 'LE';
2✔
361
        view.setUint32(0, value, useLittle);
2✔
362

363
        return this;
2✔
364
    }
365

366
    setUint16(name: string, value: number): BinaryDataBuilder {
NEW
367
        const element = this.getDataElement(name);
×
NEW
368
        assert(element, `Field not found: ${name}`);
×
NEW
369
        const formatElement = element.ref;
×
NEW
370
        assert(formatElement, `Field Format not found: ${name}`);
×
NEW
371
        assert(formatElement.byteSize == BytesSize.uint16, `Field is not a uint16: ${name}`);
×
372

NEW
373
        const view = new DataView(element.data.buffer, element.data.byteOffset, element.data.byteLength);
×
NEW
374
        const useLittle = this.#endian === 'LE';
×
NEW
375
        view.setUint16(0, value, useLittle);
×
376

NEW
377
        return this;
×
378
    }
379

380
    setUint8(name: string, value: number): BinaryDataBuilder {
381
        const element = this.getDataElement(name);
2✔
382
        assert(element, `Field not found: ${name}`);
2✔
383
        const formatElement = element.ref;
2✔
384
        assert(formatElement, `Field Format not found: ${name}`);
2✔
385
        assert(formatElement.byteSize == BytesSize.uint8, `Field is not a uint8: ${name}`);
2✔
386
        element.data[0] = value;
2✔
387

388
        return this;
2✔
389
    }
390

391
    /**
392
     * Adjust the offset so it lands on the alignment boundary.
393
     * 1 = byte align
394
     * 2 = 16bit align
395
     * 4 = 32bit align
396
     * 8 = 64bit align
397
     * @param alignment - the byte alignment
398
     */
399
    alignTo(alignment: ByteAlignment): void {
400
        const aMask = alignment - 1;
20✔
401
        this.#offset = (this.#offset + aMask) & ~aMask; // align to alignment bytes
20✔
402
    }
403

404
    /**
405
     * Append a data element to the binary data.
406
     * @param data - the data to add
407
     * @returns the DataElement added
408
     */
409
    addDataElement(data: Uint8Array<ArrayBuffer>, alignment: ByteAlignment): DataElement {
410
        this.alignTo(alignment);
20✔
411
        const offset = this.#offset;
20✔
412
        const name = `data_${offset}`;
20✔
413
        const size = data.byteLength;
20✔
414
        const de: DataElement = { name, offset, size, data };
20✔
415
        this.#dataElementMap.set(de.name, de);
20✔
416
        this.#offset = offset + size;
20✔
417
        return de;
20✔
418
    }
419

420
    /**
421
     * Append the data and set the pointer to it.
422
     * The Uint32Array  will be converted to the proper endianness if necessary.
423
     * @param name - name of the pointer field
424
     * @param data - the data to add
425
     * @param alignment - the alignment for the data, default 4
426
     * @returns this
427
     */
428
    setPtrUint32Array(name: string, data: U32Array, alignment: ByteAlignment = 4): BinaryDataBuilder {
9!
429
        return this.#setPtrData(name, covertUint32ArrayToUint8Array(data, this.#useLE), alignment);
9✔
430
    }
431

432
    /**
433
     * Append the data and set the pointer to it.
434
     * The Uint16Array  will be converted to the proper endianness if necessary.
435
     * @param name - name of the pointer field
436
     * @param data - the data to add
437
     * @param alignment - the alignment for the data, default 2
438
     * @returns this
439
     */
440
    setPtrUint16Array(name: string, data: U16Array, alignment: ByteAlignment = 2): BinaryDataBuilder {
3!
441
        return this.#setPtrData(name, covertUint16ArrayToUint8Array(data, this.#useLE), alignment);
3✔
442
    }
443

444
    /**
445
     * Append the data and set the pointer to it.
446
     * @param name - name of the pointer field
447
     * @param data - the data to add
448
     * @param alignment - the alignment for the data, default 1
449
     * @returns this
450
     */
451
    setPtrUint8Array(name: string, data: U8Array, alignment: ByteAlignment = 1): BinaryDataBuilder {
4!
452
        return this.#setPtrData(name, data, alignment);
4✔
453
    }
454

455
    /**
456
     * Append the string and set the pointer to it. It will be encoded as UTF-8.
457
     * Note: the alignment is 1. Use alignTo() if you need a different alignment.
458
     * @param name - name of the pointer field
459
     * @param str - the data to add
460
     * @returns this
461
     */
462
    setPtrString(name: string, str: string): BinaryDataBuilder {
463
        return this.#setPtrData(name, this.#encoder.encode(str), 1);
3✔
464
    }
465

466
    #setPtrData(name: string, dataView: DataArrayView, alignment: ByteAlignment): this {
467
        const element = this.getDataElement(name);
19✔
468
        assert(element, `Field not found: ${name}`);
19✔
469
        const formatElement = element.ref;
19✔
470
        assert(formatElement, `Field Format not found: ${name}`);
19✔
471
        assert(formatElement.type === 'ptr+size', `Field is not a pointer: ${name}`);
19✔
472
        assert(formatElement.byteSize === alignment, `Pointer byte size mismatch: ${name}`);
19✔
473
        const data = new Uint8Array(dataView.buffer, dataView.byteOffset, dataView.byteLength);
19✔
474
        const de = this.addDataElement(data, alignment);
19✔
475
        this.#setPtr(element, de.offset, de.size);
19✔
476
        return this;
19✔
477
    }
478

479
    #setPtr(element: DataElement, dataOffset: number, dataLength: number): void {
480
        assert(element.data.byteLength >= 8, `Pointer data too small: ${element.name}`);
19✔
481
        const view = new DataView(element.data.buffer, element.data.byteOffset, element.data.byteLength);
19✔
482
        view.setUint32(0, dataOffset, this.#useLE);
19✔
483
        view.setUint32(4, dataLength, this.#useLE);
19✔
484
    }
485

486
    get offset(): number {
NEW
487
        return this.#offset;
×
488
    }
489

490
    get endian(): 'LE' | 'BE' {
NEW
491
        return this.#endian;
×
492
    }
493

494
    getDataElement(name: string): DataElement | undefined {
495
        return this.#dataElementMap.get(name);
31✔
496
    }
497

498
    build(): U8Array {
499
        const buffer = new Uint8Array(this.#offset);
13✔
500
        for (const element of this.#dataElementMap.values()) {
13✔
501
            buffer.set(element.data, element.offset);
86✔
502
        }
503
        return buffer;
13✔
504
    }
505
}
506

507
export function convertUint32ArrayEndiannessInPlace<T extends ArrayBufferView<ArrayBuffer>>(data: T): T;
508
export function convertUint32ArrayEndiannessInPlace<T extends ArrayBufferView>(data: T): T;
509
export function convertUint32ArrayEndiannessInPlace(data: Uint32Array): Uint32Array;
510
export function convertUint32ArrayEndiannessInPlace(data: Uint32Array): Uint32Array {
511
    const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
4✔
512
    const byteLength = data.length * 4;
4✔
513
    for (let i = 0; i < byteLength; i += 4) {
4✔
514
        const v = view.getUint32(i, true);
22✔
515
        view.setUint32(i, v, false);
22✔
516
    }
517
    return data;
4✔
518
}
519

520
export function convertUint16ArrayEndiannessInPlace<T extends ArrayBufferView<ArrayBuffer>>(data: T): T;
521
export function convertUint16ArrayEndiannessInPlace<T extends ArrayBufferView>(data: T): T;
522
export function convertUint16ArrayEndiannessInPlace(data: Uint16Array): Uint16Array;
523
export function convertUint16ArrayEndiannessInPlace(data: Uint16Array): Uint16Array {
NEW
524
    const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
×
NEW
525
    const byteLength = data.length * 2;
×
NEW
526
    for (let i = 0; i < byteLength; i += 2) {
×
NEW
527
        const v = view.getUint16(i, true);
×
NEW
528
        view.setUint16(i, v, false);
×
529
    }
NEW
530
    return data;
×
531
}
532

533
export function covertUint32ArrayToUint8Array(
534
    data: U32Array,
535
    useLittle: boolean,
536
    isLE: boolean = isLittleEndian,
9!
537
): U8Array {
538
    if (isLE === useLittle) {
9!
539
        return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
7✔
540
    }
541

542
    const target = new Uint32Array(data.length);
2✔
543
    target.set(data);
2✔
544
    convertUint32ArrayEndiannessInPlace(target);
2✔
545
    return new Uint8Array(target.buffer, target.byteOffset, target.byteLength);
2✔
546
}
547

548
export function covertUint16ArrayToUint8Array(
549
    data: U16Array,
550
    useLittle: boolean,
551
    isLE: boolean = isLittleEndian,
3!
552
): U8Array {
553
    if (isLE === useLittle) {
3!
554
        return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
3✔
555
    }
556

NEW
557
    const target = new Uint16Array(data.length);
×
NEW
558
    target.set(data);
×
NEW
559
    convertUint16ArrayEndiannessInPlace(target);
×
NEW
560
    return new Uint8Array(target.buffer, target.byteOffset, target.byteLength);
×
561
}
562

563
function rawNumberToUint32Array(value: number): Uint32Array<ArrayBuffer> {
564
    return new Uint32Array([value]);
9✔
565
}
566

567
function rawNumberToUint16Array(value: number): U16Array {
NEW
568
    return new Uint16Array([value]);
×
569
}
570

571
export class BinaryDataReader {
572
    readonly data: U8Array;
573
    readonly format: BinaryFormat;
574
    #decoder = new TextDecoder();
12✔
575
    #useLE: boolean;
576

577
    /**
578
     * Binary Data Reader
579
     * @param data - the raw binary data
580
     * @param format - the expected format
581
     * @param endian - the endian of the data (can be changed later)
582
     */
583
    constructor(data: ArrayBufferView<ArrayBuffer>, format: BinaryFormat, endian: 'LE' | 'BE' = endianness()) {
9!
584
        this.data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
12✔
585
        this.format = format;
12✔
586
        this.#useLE = endian === 'LE';
12✔
587
    }
588

589
    /**
590
     * Get a string from the data.
591
     * It will decode the string as UTF-8 from the following field types: 'string', 'ptrString', 'ptrUint8Array'.
592
     * @param name - name of the string field
593
     * @returns string value
594
     */
595
    getString(name: string): string {
596
        const element = this.getDataElement(name);
21✔
597
        const formatElement = element.ref;
21✔
598
        assert(formatElement.byteSize == BytesSize.string, `Field is not a string: ${name}`);
21✔
599
        if (formatElement.type === 'value') {
21!
600
            return this.#decoder.decode(element.data);
18✔
601
        }
602
        assert(formatElement.type === 'ptr+size', `Field is not a string: ${name}`);
3✔
603
        const strData = this.#getPtrData(element);
3✔
604
        return this.#decoder.decode(strData);
3✔
605
    }
606

607
    /**
608
     * Get a uint32 from the data.
609
     * @param name - name of the uint32 field
610
     * @returns number value
611
     */
612
    getUint32(name: string): number {
613
        const element = this.getDataElement(name);
5✔
614
        const formatElement = element.ref;
5✔
615
        assert(
5✔
616
            formatElement.type === 'value' && formatElement.byteSize == BytesSize.uint32,
10!
617
            `Field is not a uint32: ${name}`,
618
        );
619
        const view = new DataView(element.data.buffer, element.data.byteOffset, element.data.byteLength);
5✔
620
        return view.getUint32(0, this.#useLE);
5✔
621
    }
622

623
    /**
624
     * Get a uint32 from the data.
625
     * @param name - name of the uint32 field
626
     * @returns number value
627
     */
628
    getUint16(name: string): number {
NEW
629
        const element = this.getDataElement(name);
×
NEW
630
        const formatElement = element.ref;
×
NEW
631
        assert(
×
632
            formatElement.type === 'value' && formatElement.byteSize == BytesSize.uint16,
×
633
            `Field is not a uint16: ${name}`,
634
        );
NEW
635
        const view = new DataView(element.data.buffer, element.data.byteOffset, element.data.byteLength);
×
NEW
636
        return view.getUint16(0, this.#useLE);
×
637
    }
638

639
    /**
640
     * Get a uint32 from the data.
641
     * @param name - name of the uint32 field
642
     * @returns number value
643
     */
644
    getUint8(name: string): number {
645
        const element = this.getDataElement(name);
2✔
646
        const formatElement = element.ref;
2✔
647
        assert(
2✔
648
            formatElement.type === 'value' && formatElement.byteSize == BytesSize.uint8,
4!
649
            `Field is not a uint8: ${name}`,
650
        );
651
        return element.data[0];
2✔
652
    }
653

654
    /**
655
     * Gets Uint32Array data from a pointer field.
656
     * Note: The returned Uint32Array may be a view of the underlying data.
657
     * If the endianness does not match, a copy will be made.
658
     * @param name - name of the field
659
     * @returns Uint32Array value
660
     */
661
    getPtrUint32Array(name: string): U32Array {
662
        const element = this.getDataElement(name);
8✔
663
        const ref = element.ref;
8✔
664
        assert(ref.type === 'ptr+size' && ref.byteSize == BytesSize.uint32, `Field is not a ptrUint32Array: ${name}`);
8!
665
        const arrData = this.#getPtrData(element);
8✔
666
        const rawData32 = new Uint32Array<ArrayBuffer>(
8✔
667
            arrData.buffer,
668
            arrData.byteOffset,
669
            arrData.byteLength / ref.byteSize,
670
        );
671
        if (isLittleEndian === this.#useLE) {
8!
672
            return rawData32;
6✔
673
        }
674
        const data = new Uint32Array(rawData32);
2✔
675
        return convertUint32ArrayEndiannessInPlace(data);
2✔
676
    }
677

678
    /**
679
     * Gets Uint16Array data from a pointer field.
680
     * Note: The returned Uint16Array may be a view of the underlying data.
681
     * If the endianness does not match, a copy will be made.
682
     * @param name - name of the field
683
     * @returns Uint16Array value
684
     */
685
    getPtrUint16Array(name: string): U16Array {
686
        const element = this.getDataElement(name);
3✔
687
        const ref = element.ref;
3✔
688
        assert(ref.type === 'ptr+size' && ref.byteSize == BytesSize.uint16, `Field is not a ptrUint16Array: ${name}`);
3!
689
        const arrData = this.#getPtrData(element);
3✔
690
        const rawData16 = new Uint16Array<ArrayBuffer>(
3✔
691
            arrData.buffer,
692
            arrData.byteOffset,
693
            arrData.byteLength / ref.byteSize,
694
        );
695
        if (isLittleEndian === this.#useLE) {
3!
696
            return rawData16;
3✔
697
        }
NEW
698
        const data = new Uint16Array(rawData16);
×
NEW
699
        return convertUint16ArrayEndiannessInPlace(data);
×
700
    }
701

702
    /**
703
     * Gets Uint8Array data from a pointer field.
704
     * Note: The returned Uint8Array is a view of the underlying data.
705
     * @param name - name of the field
706
     * @returns Uint8Array value
707
     */
708
    getPtrUint8Array(name: string): U8Array {
709
        const element = this.getDataElement(name);
3✔
710
        assert(element.ref.type === 'ptr+size', `Field is not a ptr_and_length: ${name}`);
3✔
711
        return this.#getPtrData(element);
3✔
712
    }
713

714
    /**
715
     * Gets string data from a pointer field.
716
     * @param name - name of the field
717
     * @returns string value
718
     */
719
    getPtrString(name: string): string {
720
        const element = this.getDataElement(name);
3✔
721
        assert(element.ref.type === 'ptr+size', `Field is not a ptr_and_length: ${name}`);
3✔
722
        const strData = this.#getPtrData(element);
3✔
723
        return this.#decoder.decode(strData);
3✔
724
    }
725

726
    #getPtrData(element: DataElementWithRef): U8Array {
727
        const formatElement = element.ref;
20✔
728
        assert(formatElement.type === 'ptr+size', `Field is not a ptr: ${element.name} (${formatElement.type})`);
20✔
729
        const view = new DataView<ArrayBuffer>(element.data.buffer, element.data.byteOffset, element.data.byteLength);
20✔
730
        const offset = view.getUint32(0, this.#useLE);
20✔
731
        const length = view.getUint32(4, this.#useLE);
20✔
732
        return this.data.subarray(offset, offset + length);
20✔
733
    }
734

735
    /**
736
     * Get the Element information by name
737
     * @param name - name of the field
738
     * @returns DataElementWithRef
739
     */
740
    getDataElement(name: string): DataElementWithRef {
741
        const element = this.format.getField(name);
45✔
742
        assert(element, `Field not found: ${name}`);
45✔
743
        const data = this.data.subarray(element.offset, element.offset + element.size);
45✔
744
        return {
45✔
745
            name: element.name,
746
            offset: element.offset,
747
            size: element.size,
748
            data,
749
            ref: element,
750
        };
751
    }
752

753
    set endian(endian: 'LE' | 'BE') {
NEW
754
        this.#useLE = endian === 'LE';
×
755
    }
756

757
    reverseEndian(): void {
NEW
758
        this.#useLE = !this.#useLE;
×
759
    }
760
}
761

762
function formatElementToJSON(fe: FormatElement): unknown {
763
    const { value } = fe;
4✔
764
    const v = value ? [...value] : undefined;
4!
765
    return { ...fe, value: v };
4✔
766
}
767

768
function byteAlign(offset: number, alignment: ByteAlignment): number {
769
    const aMask = alignment - 1;
187✔
770
    return (offset + aMask) & ~aMask; // align to alignment bytes
187✔
771
}
772

773
function isByteAlignment(value: number): value is ByteAlignment {
774
    return value === 1 || value === 2 || value === 4 || value === 8;
63!
775
}
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