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

streetsidesoftware / cspell / 20635026836

01 Jan 2026 07:57AM UTC coverage: 93.043% (+0.1%) from 92.94%
20635026836

Pull #8265

github

web-flow
Merge 01699e25d into 90ae3e28f
Pull Request #8265: fix: Make endian required when encoding a StringTable

8749 of 11464 branches covered (76.32%)

24 of 24 new or added lines in 3 files covered. (100.0%)

10 existing lines in 2 files now uncovered.

17199 of 18485 relevant lines covered (93.04%)

116648.04 hits per line

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

94.36
/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 field indicates the size of each element in bytes.
34
     * - 1 = 1 byte (8 bits) - for bytes or UTF-8 strings
35
     * - 2 = 2 bytes (16 bits)
36
     * - 4 = 4 bytes (32 bits)
37
     * - 8 = 8 bytes (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[] = [];
25✔
73
    #elementsByName: Map<string, FormatElement> = new Map();
25✔
74
    #offset = 0;
25✔
75
    #textEncoder = new TextEncoder();
25✔
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);
8!
80
        return this.addData(name, description, 'value', uValue);
8✔
81
    }
82

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

88
    addUint32(name: string, description: string, value?: number): BinaryFormatBuilder {
89
        const uValue = value !== undefined ? rawNumberToUint32Array(value) : rawNumberToUint32Array(0);
11!
90
        return this.addData(name, description, 'value', uValue);
11✔
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);
18✔
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);
6✔
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);
20✔
124
    }
125

126
    /**
127
     * A pointer to a string of UTF-8 bytes, 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 this
144
     */
145
    addPointer(byteSize: ByteSize, name: string, description: string, overload?: string): BinaryFormatBuilder {
146
        const alignment: ByteAlignment = 4;
48✔
147
        let offset = byteAlign(this.#offset, alignment);
48✔
148
        if (overload) {
48✔
149
            const existing = this.#elementsByName.get(overload);
9✔
150
            assert(existing, `Overload target not found: ${overload}`);
9✔
151
            offset = byteAlign(existing.offset, alignment);
9✔
152
            assert(existing.offset === offset, `Overload target offset mismatch: ${overload}`);
9✔
153
        }
154
        const element: FormatElement = {
48✔
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);
48✔
166
        return this;
48✔
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);
66!
171
        this.addData(name, description, 'value', value);
66✔
172
        return this;
66✔
173
    }
174

175
    addUint8Array(name: string, description: string, length: number | Uint8Array | number[]): BinaryFormatBuilder {
176
        const value = typeof length === 'number' ? new Uint8Array(length) : new Uint8Array(length);
1!
177
        this.addData(name, description, 'value', value);
1✔
178
        return this;
1✔
179
    }
180

181
    addData(name: string, description: string, formatType: FormatType, data: DataArrayView): BinaryFormatBuilder {
182
        const byteSize = data.byteLength / data.length;
92✔
183
        assert(isByteAlignment(byteSize), `Invalid byte size: ${byteSize} for field: ${name}`);
92✔
184
        const alignment = byteSize;
92✔
185
        const offset = byteAlign(this.#offset, byteSize);
92✔
186
        const value = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
92✔
187
        const size = value.byteLength;
92✔
188
        this.#addElement({ name, description, type: formatType, alignment, offset, size, value, byteSize });
92✔
189
        return this;
92✔
190
    }
191

192
    #addElement(element: FormatElement): void {
193
        assert(!this.#elementsByName.has(element.name), `Duplicate element name: ${element.name}`);
140✔
194
        const expectedOffset = byteAlign(element.offset, element.alignment);
140✔
195
        assert(
140✔
196
            element.offset === expectedOffset,
197
            `Element alignment mismatch for ${element.name} with alignment ${element.alignment}. Expected: ${expectedOffset}, Found: ${element.offset}`,
198
        );
199
        this.#elementsByName.set(element.name, element);
140✔
200
        this.#elements.push(element);
140✔
201
        if (!element.overload) {
140!
202
            this.#offset = element.offset + element.size;
131✔
203
        }
204
    }
205

206
    build(): BinaryFormat {
207
        return new BinaryFormat([...this.#elements]);
24✔
208
    }
209
}
210

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

225
    constructor(elements: FormatElement[]) {
226
        this.elements = elements;
24✔
227
        this.#fieldsByName = new Map(elements.map((el) => [el.name, el] as const));
140✔
228
        this.#offset = Math.max(...elements.map((el) => el.offset + el.size), 0);
140✔
229
    }
230

231
    get size(): number {
232
        return this.#offset;
17✔
233
    }
234

235
    getField(name: string): FormatElement | undefined {
236
        return this.#fieldsByName.get(name);
70✔
237
    }
238

239
    toJSON(): unknown {
240
        return this.elements.map(formatElementToJSON);
1✔
241
    }
242

243
    toString(): string {
244
        const nameWidth = Math.max('name'.length, ...this.elements.map((el) => el.name.length), 'name'.length);
4✔
245
        const offsetWidth = 8;
1✔
246
        const sizeWidth = 6;
1✔
247
        const typeWidth = Math.max('type'.length, ...this.elements.map((el) => el.type.length), 'type'.length);
4✔
248
        const lines: string[] = [];
1✔
249

250
        addHeaderLines();
1✔
251
        this.elements.forEach(addElement);
1✔
252

253
        return lines.join('\n');
1✔
254

255
        function addHeaderLines(): void {
256
            const line = formatLine(['name', 'offset', 'size', 'type', 'mask', 'description', 'value']);
1✔
257
            lines.push('Binary Format:');
1✔
258
            lines.push(line);
1✔
259
            lines.push('-'.repeat(line.length));
1✔
260
        }
261

262
        function addElement(e: FormatElement): void {
263
            lines.push(
4✔
264
                formatLine([
265
                    e.name,
266
                    e.offset.toString(),
267
                    e.size.toString(),
268
                    e.type,
269
                    e.byteSize.toString(2).padStart(4, '0'),
270
                    e.description,
271
                    e.value ? `${e.value}` : '',
4!
272
                ]),
273
            );
274
        }
275

276
        type LineData = [
277
            name: string,
278
            offset: string,
279
            size: string,
280
            type: string,
281
            mask: string,
282
            description: string,
283
            value: string,
284
        ];
285

286
        function formatLine([name, offset, size, type, mask, description, value]: LineData): string {
287
            name = name.padEnd(nameWidth, ' ');
5✔
288
            offset = offset.padStart(offsetWidth, ' ');
5✔
289
            size = size.padStart(sizeWidth, ' ');
5✔
290
            type = type.padEnd(typeWidth, ' ');
5✔
291
            value = value ? `(${value})` : '';
5!
292
            return `${name} ${offset} ${size} ${type} ${mask} ${description} ${value}`.trim();
5✔
293
        }
294
    }
295
}
296

297
export interface DataElement {
298
    name: string;
299
    offset: number;
300
    size: number;
301
    data: U8Array;
302
    ref?: FormatElement | undefined;
303
}
304

305
export interface DataElementWithRef extends DataElement {
306
    ref: FormatElement;
307
}
308

309
export type ByteAlignment = 1 | 2 | 4 | 8;
310

311
export class BinaryDataBuilder {
312
    #dataElementMap: Map<string, DataElement> = new Map();
17✔
313
    #offset = 0;
17✔
314
    #endian: 'LE' | 'BE';
315
    #useLE: boolean;
316
    #encoder = new TextEncoder();
17✔
317
    #dataByOffset: Map<number, U8Array> = new Map();
17✔
318
    readonly format: BinaryFormat;
319

320
    constructor(format: BinaryFormat, endian: 'LE' | 'BE' = endianness()) {
13!
321
        this.format = format;
17✔
322
        this.#offset = format.size;
17✔
323
        this.#endian = endian;
17✔
324
        this.#useLE = endian === 'LE';
17✔
325
        this.#dataElementMap = new Map();
17✔
326
        this.#populateDataElementMap();
17✔
327
    }
328

329
    #populateDataElementMap() {
330
        for (const ref of this.format.elements) {
17✔
331
            const { name, offset, size } = ref;
94✔
332
            let data = this.#dataByOffset.get(offset);
94✔
333
            if (!data || data.byteLength < size) {
94!
334
                data = new Uint8Array(size);
89✔
335
                this.#dataByOffset.set(offset, data);
89✔
336
            }
337
            if (ref.value) {
94!
338
                data.set(ref.value);
56✔
339
            }
340
            const de: DataElement = { name, offset, size, data, ref };
94✔
341
            this.#dataElementMap.set(de.name, de);
94✔
342
            this.#offset = Math.max(this.#offset, offset + size);
94✔
343
        }
344
    }
345

346
    setString(name: string, value: string): BinaryDataBuilder {
347
        const element = this.getDataElement(name);
10✔
348
        assert(element, `Field not found: ${name}`);
10✔
349
        const formatElement = element.ref;
10✔
350
        assert(formatElement, `Field Format not found: ${name}`);
10✔
351
        assert(formatElement.byteSize === BytesSize.string, `Field is not a string: ${name}`);
10✔
352

353
        const r = this.#encoder.encodeInto(value, element.data);
10✔
354
        assert(r.read === value.length, `String too long for field ${name}: ${value}`);
10✔
355
        return this;
10✔
356
    }
357

358
    setUint32(name: string, value: number): BinaryDataBuilder {
359
        const element = this.getDataElement(name);
2✔
360
        assert(element, `Field not found: ${name}`);
2✔
361
        const formatElement = element.ref;
2✔
362
        assert(formatElement, `Field Format not found: ${name}`);
2✔
363
        assert(formatElement.byteSize === BytesSize.uint32, `Field is not a uint32: ${name}`);
2✔
364

365
        const view = new DataView(element.data.buffer, element.data.byteOffset, element.data.byteLength);
2✔
366
        const useLittle = this.#endian === 'LE';
2✔
367
        view.setUint32(0, value, useLittle);
2✔
368

369
        return this;
2✔
370
    }
371

372
    setUint16(name: string, value: number): BinaryDataBuilder {
373
        const element = this.getDataElement(name);
2✔
374
        assert(element, `Field not found: ${name}`);
2✔
375
        const formatElement = element.ref;
2✔
376
        assert(formatElement, `Field Format not found: ${name}`);
2✔
377
        assert(formatElement.byteSize === BytesSize.uint16, `Field is not a uint16: ${name}`);
2✔
378

379
        const view = new DataView(element.data.buffer, element.data.byteOffset, element.data.byteLength);
2✔
380
        const useLittle = this.#endian === 'LE';
2✔
381
        view.setUint16(0, value, useLittle);
2✔
382

383
        return this;
2✔
384
    }
385

386
    setUint8(name: string, value: number): BinaryDataBuilder {
387
        const element = this.getDataElement(name);
4✔
388
        assert(element, `Field not found: ${name}`);
4✔
389
        const formatElement = element.ref;
4✔
390
        assert(formatElement, `Field Format not found: ${name}`);
4✔
391
        assert(formatElement.byteSize === BytesSize.uint8, `Field is not a uint8: ${name}`);
4✔
392
        element.data[0] = value;
4✔
393

394
        return this;
4✔
395
    }
396

397
    /**
398
     * Adjust the offset so it lands on the alignment boundary.
399
     * 1 = byte align
400
     * 2 = 16bit align
401
     * 4 = 32bit align
402
     * 8 = 64bit align
403
     * @param alignment - the byte alignment
404
     */
405
    alignTo(alignment: ByteAlignment): void {
406
        const aMask = alignment - 1;
25✔
407
        this.#offset = (this.#offset + aMask) & ~aMask; // align to alignment bytes
25✔
408
    }
409

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

426
    /**
427
     * Append the data and set the pointer to it.
428
     * The Uint32Array  will be converted to the proper endianness if necessary.
429
     * @param name - name of the pointer field
430
     * @param data - the data to add
431
     * @param alignment - the alignment for the data, default 4
432
     * @returns this
433
     */
434
    setPtrUint32Array(name: string, data: U32Array, alignment: ByteAlignment = 4): BinaryDataBuilder {
11!
435
        return this.#setPtrData(name, convertUint32ArrayToUint8Array(data, this.#useLE), alignment);
11✔
436
    }
437

438
    /**
439
     * Append the data and set the pointer to it.
440
     * The Uint16Array  will be converted to the proper endianness if necessary.
441
     * @param name - name of the pointer field
442
     * @param data - the data to add
443
     * @param alignment - the alignment for the data, default 2
444
     * @returns this
445
     */
446
    setPtrUint16Array(name: string, data: U16Array, alignment: ByteAlignment = 2): BinaryDataBuilder {
4!
447
        return this.#setPtrData(name, convertUint16ArrayToUint8Array(data, this.#useLE), alignment);
4✔
448
    }
449

450
    /**
451
     * Append the data and set the pointer to it.
452
     * @param name - name of the pointer field
453
     * @param data - the data to add
454
     * @param alignment - the alignment for the data, default 1
455
     * @returns this
456
     */
457
    setPtrUint8Array(name: string, data: U8Array, alignment: ByteAlignment = 1): BinaryDataBuilder {
6!
458
        return this.#setPtrData(name, data, alignment);
6✔
459
    }
460

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

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

485
    #setPtr(element: DataElement, dataOffset: number, dataLength: number): void {
486
        assert(element.data.byteLength >= 8, `Pointer data too small: ${element.name}`);
24✔
487
        const view = new DataView(element.data.buffer, element.data.byteOffset, element.data.byteLength);
24✔
488
        view.setUint32(0, dataOffset, this.#useLE);
24✔
489
        view.setUint32(4, dataLength, this.#useLE);
24✔
490
    }
491

492
    get offset(): number {
UNCOV
493
        return this.#offset;
×
494
    }
495

496
    get endian(): 'LE' | 'BE' {
497
        return this.#endian;
1✔
498
    }
499

500
    getDataElement(name: string): DataElement | undefined {
501
        return this.#dataElementMap.get(name);
42✔
502
    }
503

504
    build(): U8Array {
505
        const buffer = new Uint8Array(this.#offset);
17✔
506
        for (const element of this.#dataElementMap.values()) {
17✔
507
            buffer.set(element.data, element.offset);
119✔
508
        }
509
        return buffer;
17✔
510
    }
511
}
512

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

526
export function convertUint16ArrayEndiannessInPlace<T extends ArrayBufferView<ArrayBuffer>>(data: T): T;
527
export function convertUint16ArrayEndiannessInPlace<T extends ArrayBufferView>(data: T): T;
528
export function convertUint16ArrayEndiannessInPlace(data: Uint16Array): Uint16Array;
529
export function convertUint16ArrayEndiannessInPlace(data: Uint16Array): Uint16Array {
UNCOV
530
    const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
×
531
    const byteLength = data.length * 2;
×
532
    for (let i = 0; i < byteLength; i += 2) {
×
533
        const v = view.getUint16(i, true);
×
534
        view.setUint16(i, v, false);
×
535
    }
UNCOV
536
    return data;
×
537
}
538

539
export function convertUint32ArrayToUint8Array(
540
    data: U32Array,
541
    useLittle: boolean,
542
    isLE: boolean = isLittleEndian,
11!
543
): U8Array {
544
    if (isLE === useLittle) {
11!
545
        return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
9✔
546
    }
547

548
    const target = new Uint32Array(data.length);
2✔
549
    target.set(data);
2✔
550
    convertUint32ArrayEndiannessInPlace(target);
2✔
551
    return new Uint8Array(target.buffer, target.byteOffset, target.byteLength);
2✔
552
}
553

554
export function convertUint16ArrayToUint8Array(
555
    data: U16Array,
556
    useLittle: boolean,
557
    isLE: boolean = isLittleEndian,
4!
558
): U8Array {
559
    if (isLE === useLittle) {
4!
560
        return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
4✔
561
    }
562

UNCOV
563
    const target = new Uint16Array(data.length);
×
564
    target.set(data);
×
565
    convertUint16ArrayEndiannessInPlace(target);
×
566
    return new Uint8Array(target.buffer, target.byteOffset, target.byteLength);
×
567
}
568

569
function rawNumberToUint32Array(value: number): Uint32Array<ArrayBuffer> {
570
    return new Uint32Array([value]);
11✔
571
}
572

573
function rawNumberToUint16Array(value: number): U16Array {
574
    return new Uint16Array([value]);
6✔
575
}
576

577
export class BinaryDataReader {
578
    readonly data: U8Array;
579
    readonly format: BinaryFormat;
580
    #decoder = new TextDecoder();
16✔
581
    #useLE: boolean;
582

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

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

613
    /**
614
     * Get a Uint32 from the data.
615
     * @param name - name of the Uint32 field
616
     * @returns number value
617
     */
618
    getUint32(name: string): number {
619
        const element = this.getDataElement(name);
6✔
620
        const formatElement = element.ref;
6✔
621
        assert(
6✔
622
            formatElement.type === 'value' && formatElement.byteSize === BytesSize.uint32,
12!
623
            `Field is not a uint32: ${name}`,
624
        );
625
        const view = new DataView(element.data.buffer, element.data.byteOffset, element.data.byteLength);
6✔
626
        return view.getUint32(0, this.#useLE);
6✔
627
    }
628

629
    /**
630
     * Get a Uint16 from the data.
631
     * @param name - name of the Uint16 field
632
     * @returns number value
633
     */
634
    getUint16(name: string): number {
635
        const element = this.getDataElement(name);
3✔
636
        const formatElement = element.ref;
3✔
637
        assert(
3✔
638
            formatElement.type === 'value' && formatElement.byteSize === BytesSize.uint16,
6!
639
            `Field is not a uint16: ${name}`,
640
        );
641
        const view = new DataView(element.data.buffer, element.data.byteOffset, element.data.byteLength);
3✔
642
        return view.getUint16(0, this.#useLE);
3✔
643
    }
644

645
    /**
646
     * Read a field as Uint16 starting at the given byte offset.
647
     * @param name - name of field
648
     * @param byteOffset - offset of in bytes from the beginning of the field
649
     * @returns the value read.
650
     */
651
    getAsUint16(name: string, byteOffset: number = 0): number {
1!
652
        const element = this.getDataElement(name);
1✔
653
        const view = new DataView(element.data.buffer, element.data.byteOffset, element.data.byteLength);
1✔
654
        return view.getUint16(byteOffset, this.#useLE);
1✔
655
    }
656

657
    /**
658
     * Get a Uint8 from the data.
659
     * @param name - name of the Uint8 field
660
     * @returns number value
661
     */
662
    getUint8(name: string): number {
663
        const element = this.getDataElement(name);
4✔
664
        const formatElement = element.ref;
4✔
665
        assert(
4✔
666
            formatElement.type === 'value' && formatElement.byteSize === BytesSize.uint8,
8!
667
            `Field is not a uint8: ${name}`,
668
        );
669
        return element.data[0];
4✔
670
    }
671

672
    /**
673
     * Gets Uint32Array data from a pointer field.
674
     * Note: The returned Uint32Array may be a view of the underlying data.
675
     * If the endianness does not match, a copy will be made.
676
     * @param name - name of the field
677
     * @returns Uint32Array value
678
     */
679
    getPtrUint32Array(name: string): U32Array {
680
        const element = this.getDataElement(name);
9✔
681
        const ref = element.ref;
9✔
682
        assert(ref.type === 'ptr+size' && ref.byteSize === BytesSize.uint32, `Field is not a ptrUint32Array: ${name}`);
9!
683
        const arrData = this.#getPtrData(element);
9✔
684
        const rawData32 = new Uint32Array<ArrayBuffer>(
9✔
685
            arrData.buffer,
686
            arrData.byteOffset,
687
            arrData.byteLength / ref.byteSize,
688
        );
689
        if (isLittleEndian === this.#useLE) {
9!
690
            return rawData32;
7✔
691
        }
692
        const data = new Uint32Array(rawData32);
2✔
693
        return convertUint32ArrayEndiannessInPlace(data);
2✔
694
    }
695

696
    /**
697
     * Gets Uint16Array data from a pointer field.
698
     * Note: The returned Uint16Array may be a view of the underlying data.
699
     * If the endianness does not match, a copy will be made.
700
     * @param name - name of the field
701
     * @returns Uint16Array value
702
     */
703
    getPtrUint16Array(name: string): U16Array {
704
        const element = this.getDataElement(name);
4✔
705
        const ref = element.ref;
4✔
706
        assert(ref.type === 'ptr+size' && ref.byteSize === BytesSize.uint16, `Field is not a ptrUint16Array: ${name}`);
4!
707
        const arrData = this.#getPtrData(element);
4✔
708
        const rawData16 = new Uint16Array<ArrayBuffer>(
4✔
709
            arrData.buffer,
710
            arrData.byteOffset,
711
            arrData.byteLength / ref.byteSize,
712
        );
713
        if (isLittleEndian === this.#useLE) {
4!
714
            return rawData16;
4✔
715
        }
UNCOV
716
        const data = new Uint16Array(rawData16);
×
717
        return convertUint16ArrayEndiannessInPlace(data);
×
718
    }
719

720
    /**
721
     * Gets Uint8Array data from a pointer field.
722
     * Note: The returned Uint8Array is a view of the underlying data.
723
     * @param name - name of the field
724
     * @returns Uint8Array value
725
     */
726
    getPtrUint8Array(name: string): U8Array {
727
        const element = this.getDataElement(name);
8✔
728
        assert(element.ref.type === 'ptr+size', `Field is not a ptr+size: ${name}`);
8✔
729
        return this.#getPtrData(element);
8✔
730
    }
731

732
    /**
733
     * Gets string data from a pointer field.
734
     * @param name - name of the field
735
     * @returns string value
736
     */
737
    getPtrString(name: string): string {
738
        const element = this.getDataElement(name);
3✔
739
        assert(element.ref.type === 'ptr+size', `Field is not a ptr+size: ${name}`);
3✔
740
        const strData = this.#getPtrData(element);
3✔
741
        return this.#decoder.decode(strData);
3✔
742
    }
743

744
    #getPtrData(element: DataElementWithRef): U8Array {
745
        const formatElement = element.ref;
27✔
746
        assert(formatElement.type === 'ptr+size', `Field is not a ptr+size: ${element.name} (${formatElement.type})`);
27✔
747
        const view = new DataView<ArrayBuffer>(element.data.buffer, element.data.byteOffset, element.data.byteLength);
27✔
748
        const offset = view.getUint32(0, this.#useLE);
27✔
749
        const length = view.getUint32(4, this.#useLE);
27✔
750
        return this.data.subarray(offset, offset + length);
27✔
751
    }
752

753
    /**
754
     * Get the Element information by name
755
     * @param name - name of the field
756
     * @returns DataElementWithRef
757
     */
758
    getDataElement(name: string): DataElementWithRef {
759
        const element = this.format.getField(name);
64✔
760
        assert(element, `Field not found: ${name}`);
64✔
761
        const data = this.data.subarray(element.offset, element.offset + element.size);
64✔
762
        return {
64✔
763
            name: element.name,
764
            offset: element.offset,
765
            size: element.size,
766
            data,
767
            ref: element,
768
        };
769
    }
770

771
    set endian(endian: 'LE' | 'BE') {
UNCOV
772
        this.#useLE = endian === 'LE';
×
773
    }
774

775
    get endian(): 'LE' | 'BE' {
776
        return this.#useLE ? 'LE' : 'BE';
6!
777
    }
778

779
    reverseEndian(): void {
780
        this.#useLE = !this.#useLE;
×
781
    }
782

783
    /**
784
     * Get the raw bytes for a field.
785
     * @param name - name of the field
786
     * @returns the bytes or undefined
787
     */
788
    getUnit8Array(name: string): U8Array | undefined {
789
        const element = this.getDataElement(name);
1✔
790
        if (!element) return undefined;
1!
791
        return element.data;
1✔
792
    }
793

794
    /**
795
     * Get the FormatElement for a field.
796
     * @param name - name of the field
797
     * @returns the element or undefined
798
     */
799
    getField(name: string): FormatElement | undefined {
800
        return this.format.getField(name);
1✔
801
    }
802
}
803

804
function formatElementToJSON(fe: FormatElement): unknown {
805
    const { value } = fe;
4✔
806
    const v = value ? [...value] : undefined;
4!
807
    return { ...fe, value: v };
4✔
808
}
809

810
function byteAlign(offset: number, alignment: ByteAlignment): number {
811
    const aMask = alignment - 1;
289✔
812
    return (offset + aMask) & ~aMask; // align to alignment bytes
289✔
813
}
814

815
function isByteAlignment(value: number): value is ByteAlignment {
816
    return value === 1 || value === 2 || value === 4 || value === 8;
92!
817
}
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

© 2026 Coveralls, Inc