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

homebridge / HAP-NodeJS / 24967666677

26 Apr 2026 09:37PM UTC coverage: 64.646% (+1.2%) from 63.43%
24967666677

push

github

bwp91
chore: dependency updates, inc. `typescript`

1863 of 3360 branches covered (55.45%)

Branch coverage included in aggregate %.

6413 of 9442 relevant lines covered (67.92%)

308.79 hits per line

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

30.95
/src/lib/datastream/DataStreamParser.ts
1
import * as uuid from "../util/uuid";
21✔
2
import * as hapCrypto from "../util/hapCrypto";
21✔
3
import assert from "assert";
21✔
4
import createDebug from "debug";
21✔
5

6
// welcome to hell :)
7
// in this file lies madness and frustration. and It's not only about HDS. Also, JavaScript is hell
8

9
const debug = createDebug("HAP-NodeJS:DataStream:Parser");
21✔
10

11
class Magics {
12
  static readonly TERMINATOR = { type: "terminator" };
21✔
13
}
14

15
/**
16
 * @group HomeKit Data Streams (HDS)
17
 */
18
export class ValueWrapper<T> { // basically used to differentiate between different sized integers when encoding (to force certain encoding)
21✔
19

20
  value: T;
21

22
  constructor(value: T) {
23
    this.value = value;
18✔
24
  }
25

26
  public equals(obj: ValueWrapper<T>) : boolean {
27
    return this.constructor.name === obj.constructor.name && obj.value === this.value;
3✔
28
  }
29

30
}
31

32
/**
33
 * @group HomeKit Data Streams (HDS)
34
 */
35
export class Int8 extends ValueWrapper<number> {}
21✔
36

37
/**
38
 * @group HomeKit Data Streams (HDS)
39
 */
40
export class Int16 extends ValueWrapper<number> {}
21✔
41

42
/**
43
 * @group HomeKit Data Streams (HDS)
44
 */
45
export class Int32 extends ValueWrapper<number> {}
21✔
46

47
/**
48
 * @group HomeKit Data Streams (HDS)
49
 */
50
export class Int64 extends ValueWrapper<number> {}
21✔
51

52
/**
53
 * @group HomeKit Data Streams (HDS)
54
 */
55
export class Float32 extends ValueWrapper<number> {}
21✔
56
/**
57
 * @group HomeKit Data Streams (HDS)
58
 */
59
export class Float64 extends ValueWrapper<number> {}
21✔
60

61
/**
62
 * @group HomeKit Data Streams (HDS)
63
 */
64
export class SecondsSince2001 extends ValueWrapper<number> {}
21✔
65

66
/**
67
 * @group HomeKit Data Streams (HDS)
68
 */
69
export class UUID extends ValueWrapper<string> {
21✔
70

71
  constructor(value: string) {
72
    assert(uuid.isValid(value), "invalid uuid format");
×
73
    super(value);
×
74
  }
75

76
}
77

78
/**
79
 * @group HomeKit Data Streams (HDS)
80
 */
81
export const enum DataFormatTags {
21✔
82
  INVALID = 0x00,
21✔
83

84
  TRUE = 0x01,
21✔
85
  FALSE = 0x02,
21✔
86

87
  TERMINATOR = 0x03,
21✔
88
  NULL = 0x04,
21✔
89
  UUID = 0x05,
21✔
90
  DATE = 0x06,
21✔
91

92
  INTEGER_MINUS_ONE = 0x07,
21✔
93
  INTEGER_RANGE_START_0 = 0x08,
21✔
94
  INTEGER_RANGE_STOP_39 = 0x2E,
21✔
95
  INT8 = 0x30,
21✔
96
  INT16LE = 0x31,
21✔
97
  INT32LE = 0x32,
21✔
98
  INT64LE = 0x33,
21✔
99

100
  FLOAT32LE = 0x35,
21✔
101
  FLOAT64LE = 0x36,
21✔
102

103
  UTF8_LENGTH_START = 0x40,
21✔
104
  UTF8_LENGTH_STOP = 0x60,
21✔
105
  UTF8_LENGTH8 = 0x61,
21✔
106
  UTF8_LENGTH16LE = 0x62,
21✔
107
  UTF8_LENGTH32LE = 0x63,
21✔
108
  UTF8_LENGTH64LE = 0x64,
21✔
109
  UTF8_NULL_TERMINATED = 0x6F,
21✔
110

111
  DATA_LENGTH_START = 0x70,
21✔
112
  DATA_LENGTH_STOP = 0x90,
21✔
113
  DATA_LENGTH8 = 0x91,
21✔
114
  DATA_LENGTH16LE = 0x92,
21✔
115
  DATA_LENGTH32LE = 0x93,
21✔
116
  DATA_LENGTH64LE = 0x94,
21✔
117
  DATA_TERMINATED = 0x9F,
21✔
118

119
  COMPRESSION_START = 0xA0,
21✔
120
  COMPRESSION_STOP = 0xCF,
21✔
121

122
  ARRAY_LENGTH_START = 0xD0,
21✔
123
  ARRAY_LENGTH_STOP = 0xDE,
21✔
124
  ARRAY_TERMINATED = 0xDF,
21✔
125

126
  DICTIONARY_LENGTH_START = 0xE0,
21✔
127
  DICTIONARY_LENGTH_STOP = 0xEE,
21✔
128
  DICTIONARY_TERMINATED = 0xEF,
21✔
129
}
130

131
/**
132
 * @group HomeKit Data Streams (HDS)
133
 */
134
export class DataStreamParser {
21✔
135
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
  public static decode(buffer: DataStreamReader): any {
137
    const tag = buffer.readTag();
24✔
138

139
    if (tag === DataFormatTags.INVALID) {
24!
140
      throw new Error("HDSDecoder: zero tag detected on index " + buffer.readerIndex);
×
141
    } else if (tag === DataFormatTags.TRUE) {
24!
142
      return buffer.readTrue();
×
143
    } else if (tag === DataFormatTags.FALSE) {
24!
144
      return buffer.readFalse();
×
145
    } else if (tag === DataFormatTags.TERMINATOR) {
24!
146
      return Magics.TERMINATOR;
×
147
    } else if (tag === DataFormatTags.NULL) {
24!
148
      return null;
×
149
    } else if (tag === DataFormatTags.UUID) {
24!
150
      return buffer.readUUID();
×
151
    } else if (tag === DataFormatTags.DATE) {
24!
152
      return buffer.readSecondsSince2001_01_01();
×
153
    } else if (tag === DataFormatTags.INTEGER_MINUS_ONE) {
24!
154
      return buffer.readNegOne();
×
155
    } else if (tag >= DataFormatTags.INTEGER_RANGE_START_0 && tag <= DataFormatTags.INTEGER_RANGE_STOP_39) {
24!
156
      return buffer.readIntRange(tag); // integer values from 0-39
×
157
    } else if (tag === DataFormatTags.INT8) {
24!
158
      return buffer.readInt8();
×
159
    } else if (tag === DataFormatTags.INT16LE) {
24!
160
      return buffer.readInt16LE();
×
161
    } else if (tag === DataFormatTags.INT32LE) {
24✔
162
      return buffer.readInt32LE();
9✔
163
    } else if (tag === DataFormatTags.INT64LE) {
15✔
164
      return buffer.readInt64LE();
3✔
165
    } else if (tag === DataFormatTags.FLOAT32LE) {
12!
166
      return buffer.readFloat32LE();
×
167
    } else if (tag === DataFormatTags.FLOAT64LE) {
12✔
168
      return buffer.readFloat64LE();
6✔
169
    } else if (tag >= DataFormatTags.UTF8_LENGTH_START && tag <= DataFormatTags.UTF8_LENGTH_STOP) {
6!
170
      const length = tag - DataFormatTags.UTF8_LENGTH_START;
6✔
171
      return buffer.readUTF8(length);
6✔
172
    } else if (tag === DataFormatTags.UTF8_LENGTH8) {
×
173
      return buffer.readUTF8_Length8();
×
174
    } else if (tag === DataFormatTags.UTF8_LENGTH16LE) {
×
175
      return buffer.readUTF8_Length16LE();
×
176
    } else if (tag === DataFormatTags.UTF8_LENGTH32LE) {
×
177
      return buffer.readUTF8_Length32LE();
×
178
    } else if (tag === DataFormatTags.UTF8_LENGTH64LE) {
×
179
      return buffer.readUTF8_Length64LE();
×
180
    } else if (tag === DataFormatTags.UTF8_NULL_TERMINATED) {
×
181
      return buffer.readUTF8_NULL_terminated();
×
182
    } else if (tag >= DataFormatTags.DATA_LENGTH_START && tag <= DataFormatTags.DATA_LENGTH_STOP) {
×
183
      const length = tag - DataFormatTags.DATA_LENGTH_START;
×
184
      buffer.readData(length);
×
185
    } else if (tag === DataFormatTags.DATA_LENGTH8) {
×
186
      return buffer.readData_Length8();
×
187
    } else if (tag === DataFormatTags.DATA_LENGTH16LE) {
×
188
      return buffer.readData_Length16LE();
×
189
    } else if (tag === DataFormatTags.DATA_LENGTH32LE) {
×
190
      return buffer.readData_Length32LE();
×
191
    } else if (tag === DataFormatTags.DATA_LENGTH64LE) {
×
192
      return buffer.readData_Length64LE();
×
193
    } else if (tag === DataFormatTags.DATA_TERMINATED) {
×
194
      return buffer.readData_terminated();
×
195
    } else if (tag >= DataFormatTags.COMPRESSION_START && tag <= DataFormatTags.COMPRESSION_STOP) {
×
196
      const index = tag - DataFormatTags.COMPRESSION_START;
×
197
      return buffer.decompressData(index);
×
198
    } else if (tag >= DataFormatTags.ARRAY_LENGTH_START && tag <= DataFormatTags.ARRAY_LENGTH_STOP) {
×
199
      const length = tag - DataFormatTags.ARRAY_LENGTH_START;
×
200
      const array = [];
×
201

202
      for (let i = 0; i < length; i++) {
×
203
        array.push(this.decode(buffer));
×
204
      }
205

206
      return array;
×
207
    } else if (tag === DataFormatTags.ARRAY_TERMINATED) {
×
208
      const array = [];
×
209

210
      let element;
211
      while ((element = this.decode(buffer)) !== Magics.TERMINATOR) {
×
212
        array.push(element);
×
213
      }
214

215
      return array;
×
216
    } else if (tag >= DataFormatTags.DICTIONARY_LENGTH_START && tag <= DataFormatTags.DICTIONARY_LENGTH_STOP) {
×
217
      const length = tag - DataFormatTags.DICTIONARY_LENGTH_START;
×
218
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
219
      const dictionary: Record<any, any> = {};
×
220

221
      for (let i = 0; i < length; i++) {
×
222
        const key = this.decode(buffer);
×
223
        dictionary[key] = this.decode(buffer);
×
224
      }
225

226
      return dictionary;
×
227
    } else if (tag === DataFormatTags.DICTIONARY_TERMINATED) {
×
228
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
229
      const dictionary: Record<any, any> = {};
×
230

231
      let key;
232
      while ((key = this.decode(buffer)) !== Magics.TERMINATOR) {
×
233
        dictionary[key] = this.decode(buffer); // decode value
×
234
      }
235

236
      return dictionary;
×
237
    } else {
238
      throw new Error("HDSDecoder: encountered unknown tag on index " + buffer.readerIndex + ": " + tag.toString(16));
×
239
    }
240
  }
241

242
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any
243
  public static encode(data: any, buffer: DataStreamWriter): void {
244
    if (data === undefined) {
×
245
      throw new Error("HDSEncoder: cannot encode undefined");
×
246
    }
247

248
    if (data === null) {
×
249
      buffer.writeTag(DataFormatTags.NULL);
×
250
      return;
×
251
    }
252

253
    switch (typeof data) {
×
254
    case "boolean":
255
      if (data) {
×
256
        buffer.writeTrue();
×
257
      } else {
258
        buffer.writeFalse();
×
259
      }
260
      break;
×
261
    case "number":
262
      if (Number.isInteger(data)) {
×
263
        buffer.writeNumber(data);
×
264
      } else {
265
        buffer.writeFloat64LE(new Float64(data));
×
266
      }
267
      break;
×
268
    case "string":
269
      buffer.writeUTF8(data);
×
270
      break;
×
271
    case "object":
272
      if (Array.isArray(data)) {
×
273
        const length = data.length;
×
274

275
        if (length <= 12) {
×
276
          buffer.writeTag(DataFormatTags.ARRAY_LENGTH_START + length);
×
277
        } else {
278
          buffer.writeTag(DataFormatTags.ARRAY_TERMINATED);
×
279
        }
280

281
        data.forEach(element => {
×
282
          this.encode(element, buffer);
×
283
        });
284

285
        if (length > 12) {
×
286
          buffer.writeTag(DataFormatTags.TERMINATOR);
×
287
        }
288
      } else if (data instanceof ValueWrapper) {
×
289
        if (data instanceof Int8) {
×
290
          buffer.writeInt8(data);
×
291
        } else if (data instanceof Int16) {
×
292
          buffer.writeInt16LE(data);
×
293
        } else if (data instanceof Int32) {
×
294
          buffer.writeInt32LE(data);
×
295
        } else if (data instanceof Int64) {
×
296
          buffer.writeInt64LE(data);
×
297
        } else if (data instanceof Float32) {
×
298
          buffer.writeFloat32LE(data);
×
299
        } else if (data instanceof Float64) {
×
300
          buffer.writeFloat64LE(data);
×
301
        } else if (data instanceof SecondsSince2001) {
×
302
          buffer.writeSecondsSince2001_01_01(data);
×
303
        } else if (data instanceof UUID) {
×
304
          buffer.writeUUID(data.value);
×
305
        } else {
306
          throw new Error("Unknown wrapped object 'ValueWrapper' of class " + data.constructor.name);
×
307
        }
308
      } else if (data instanceof Buffer) {
×
309
        buffer.writeData(data);
×
310
      } else { // object is treated as dictionary
311
        const entries = Object.entries(data)
×
312
          .filter(entry => entry[1] !== undefined); // explicitly setting undefined will result in an entry here
×
313

314
        if (entries.length <= 14) {
×
315
          buffer.writeTag(DataFormatTags.DICTIONARY_LENGTH_START + entries.length);
×
316
        } else {
317
          buffer.writeTag(DataFormatTags.DICTIONARY_TERMINATED);
×
318
        }
319

320
        entries.forEach(entry => {
×
321
          this.encode(entry[0], buffer); // encode key
×
322
          this.encode(entry[1], buffer); // encode value
×
323
        });
324

325
        if (entries.length > 14) {
×
326
          buffer.writeTag(DataFormatTags.TERMINATOR);
×
327
        }
328
      }
329
      break;
×
330
    default:
331
      throw new Error("HDSEncoder: no idea how to encode value of type '" + (typeof data) +"': " + data);
×
332
    }
333
  }
334
}
335

336
/**
337
 * @group HomeKit Data Streams (HDS)
338
 */
339
export class DataStreamReader {
21✔
340

341
  private readonly data: Buffer;
342
  readerIndex: number;
343

344
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
345
  private trackedCompressedData: any[] = [];
21✔
346

347
  constructor(data: Buffer) {
348
    this.data = data;
21✔
349
    this.readerIndex = 0;
21✔
350
  }
351

352
  finished(): void {
353
    if (this.readerIndex < this.data.length) {
×
354
      const remainingHex = this.data.subarray(this.readerIndex, this.data.length).toString("hex");
×
355
      debug("WARNING Finished reading HDS stream, but there are still %d bytes remaining () %s", this.data.length - this.readerIndex, remainingHex);
×
356
    }
357
  }
358

359
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
360
  decompressData(index: number): any {
361
    if (index >= this.trackedCompressedData.length) {
×
362
      throw new Error("HDSDecoder: Tried decompression of data for an index out of range (index " + index +
×
363
        " and got " + this.trackedCompressedData.length + " elements)");
364
    }
365

366
    return this.trackedCompressedData[index];
×
367
  }
368

369
  private trackData<T>(data: T): T {
370
    this.trackedCompressedData.push(data);
24✔
371
    return data;
24✔
372
  }
373

374
  private ensureLength(bytes: number) {
375
    if (this.readerIndex + bytes > this.data.length) {
48!
376
      const remaining = this.data.length - this.readerIndex;
×
377
      throw new Error("HDSDecoder: End of data stream. Tried reading " + bytes + " bytes however got only " + remaining + " remaining!");
×
378
    }
379
  }
380

381
  readTag(): number {
382
    this.ensureLength(1);
24✔
383
    return this.data.readUInt8(this.readerIndex++);
24✔
384
  }
385

386
  readTrue(): true {
387
    return this.trackData(true); // do those tag encoded values get cached?
×
388
  }
389

390
  readFalse(): false {
391
    return this.trackData(false);
×
392
  }
393

394
  readNegOne(): -1 {
395
    return this.trackData(-1);
×
396
  }
397

398
  readIntRange(tag: number): number {
399
    return this.trackData(tag - DataFormatTags.INTEGER_RANGE_START_0); // integer values from 0-39
×
400
  }
401

402
  readInt8(): number {
403
    this.ensureLength(1);
×
404
    return this.trackData(this.data.readInt8(this.readerIndex++));
×
405
  }
406

407
  readInt16LE(): number {
408
    this.ensureLength(2);
×
409
    const value = this.data.readInt16LE(this.readerIndex);
×
410
    this.readerIndex += 2;
×
411
    return this.trackData(value);
×
412
  }
413

414
  readInt32LE(): number {
415
    this.ensureLength(4);
9✔
416
    const value = this.data.readInt32LE(this.readerIndex);
9✔
417
    this.readerIndex += 4;
9✔
418
    return this.trackData(value);
9✔
419
  }
420

421
  readInt64LE(): number {
422
    this.ensureLength(8);
3✔
423

424
    const low = this.data.readInt32LE(this.readerIndex);
3✔
425
    let value = this.data.readInt32LE(this.readerIndex + 4) * 0x100000000 + low;
3✔
426
    if (low < 0) {
3!
427
      value += 0x100000000;
3✔
428
    }
429

430
    this.readerIndex += 8;
3✔
431
    return this.trackData(value);
3✔
432
  }
433

434
  readFloat32LE(): number {
435
    this.ensureLength(4);
×
436
    const value = this.data.readFloatLE(this.readerIndex);
×
437
    this.readerIndex += 4;
×
438
    return this.trackData(value);
×
439
  }
440

441
  readFloat64LE(): number {
442
    this.ensureLength(8);
6✔
443
    const value = this.data.readDoubleLE(this.readerIndex);
6✔
444
    this.readerIndex += 8;
6✔
445
    return this.trackData(value);
6✔
446
  }
447

448
  private readLength8(): number {
449
    this.ensureLength(1);
×
450
    return this.data.readUInt8(this.readerIndex++);
×
451
  }
452

453
  private readLength16LE(): number {
454
    this.ensureLength(2);
×
455
    const value = this.data.readUInt16LE(this.readerIndex);
×
456
    this.readerIndex += 2;
×
457
    return value;
×
458
  }
459

460
  private readLength32LE(): number {
461
    this.ensureLength(4);
×
462
    const value = this.data.readUInt32LE(this.readerIndex);
×
463
    this.readerIndex += 4;
×
464
    return value;
×
465
  }
466

467
  private readLength64LE(): number {
468
    this.ensureLength(8);
×
469

470
    const low = this.data.readUInt32LE(this.readerIndex);
×
471
    const value = this.data.readUInt32LE(this.readerIndex + 4) * 0x100000000 + low;
×
472

473
    this.readerIndex += 8;
×
474
    return value;
×
475
  }
476

477
  readUTF8(length: number): string {
478
    this.ensureLength(length);
6✔
479
    const value = this.data.toString("utf8", this.readerIndex, this.readerIndex + length);
6✔
480
    this.readerIndex += length;
6✔
481
    return this.trackData(value);
6✔
482
  }
483

484
  readUTF8_Length8(): string {
485
    const length = this.readLength8();
×
486
    return this.readUTF8(length);
×
487
  }
488

489
  readUTF8_Length16LE(): string {
490
    const length = this.readLength16LE();
×
491
    return this.readUTF8(length);
×
492
  }
493

494
  readUTF8_Length32LE(): string {
495
    const length = this.readLength32LE();
×
496
    return this.readUTF8(length);
×
497
  }
498

499
  readUTF8_Length64LE(): string {
500
    const length = this.readLength64LE();
×
501
    return this.readUTF8(length);
×
502
  }
503

504
  readUTF8_NULL_terminated(): string {
505
    let offset = this.readerIndex;
×
506
    let nextByte;
507

508
    for (;;) {
×
509
      nextByte = this.data[offset];
×
510

511
      if (nextByte === undefined) {
×
512
        throw new Error("HDSDecoder: Reached end of data stream while reading NUL terminated string!");
×
513
      } else  if (nextByte === 0) {
×
514
        break;
×
515
      } else {
516
        offset++;
×
517
      }
518
    }
519

520
    const value = this.data.toString("utf8", this.readerIndex, offset);
×
521
    this.readerIndex = offset + 1;
×
522
    return this.trackData(value);
×
523
  }
524

525
  readData(length: number): Buffer {
526
    this.ensureLength(length);
×
527
    const value = this.data.subarray(this.readerIndex, this.readerIndex + length);
×
528
    this.readerIndex += length;
×
529

530
    return this.trackData(value);
×
531
  }
532

533
  readData_Length8(): Buffer {
534
    const length = this.readLength8();
×
535
    return this.readData(length);
×
536
  }
537

538
  readData_Length16LE(): Buffer {
539
    const length = this.readLength16LE();
×
540
    return this.readData(length);
×
541
  }
542

543
  readData_Length32LE(): Buffer {
544
    const length = this.readLength32LE();
×
545
    return this.readData(length);
×
546
  }
547

548
  readData_Length64LE(): Buffer {
549
    const length = this.readLength64LE();
×
550
    return this.readData(length);
×
551
  }
552

553
  readData_terminated(): Buffer {
554
    let offset = this.readerIndex;
×
555
    let nextByte;
556

557
    for (;;) {
×
558
      nextByte = this.data[offset];
×
559

560
      if (nextByte === undefined) {
×
561
        throw new Error("HDSDecoder: Reached end of data stream while reading terminated data!");
×
562
      } else  if (nextByte === DataFormatTags.TERMINATOR) {
×
563
        break;
×
564
      } else {
565
        offset++;
×
566
      }
567
    }
568

569
    const value = this.data.subarray(this.readerIndex, offset);
×
570
    this.readerIndex = offset + 1;
×
571
    return this.trackData(value);
×
572
  }
573

574
  readSecondsSince2001_01_01(): number {
575
    // second since 2001-01-01 00:00:00
576
    return this.readFloat64LE();
×
577
  }
578

579
  readUUID(): string { // big endian
580
    this.ensureLength(16);
×
581
    const value = uuid.unparse(this.data, this.readerIndex);
×
582
    this.readerIndex += 16;
×
583
    return this.trackData(value);
×
584
  }
585

586
}
587

588
class WrittenDataList { // wrapper class since javascript doesn't really have a way to override === operator
589
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
590
  private writtenData: any[] = [];
21✔
591

592
  push<T>(data: T) {
593
    this.writtenData.push(data);
24✔
594
  }
595

596
  indexOf<T>(data: T) {
597
    for (let i = 0; i < this.writtenData.length; i++) {
24✔
598
      const data0 = this.writtenData[i];
3✔
599

600
      if (data === data0) {
3!
601
        return i;
×
602
      }
603

604
      if (data instanceof ValueWrapper && data0 instanceof ValueWrapper) {
3!
605
        if (data.equals(data0)) {
3!
606
          return i;
×
607
        }
608
      }
609
    }
610

611
    return -1;
24✔
612
  }
613
}
614

615
/**
616
 * @group HomeKit Data Streams (HDS)
617
 */
618
export class DataStreamWriter {
21✔
619

620
  private static readonly chunkSize = 128; // seems to be a good default
21✔
621

622
  private data: Buffer;
623
  private writerIndex: number;
624

625
  private writtenData = new WrittenDataList();
21✔
626

627
  constructor() {
628
    this.data = Buffer.alloc(DataStreamWriter.chunkSize);
21✔
629
    this.writerIndex = 0;
21✔
630
  }
631

632
  length(): number {
633
    return this.writerIndex; // since writerIndex points to the next FREE index it also represents the length
×
634
  }
635

636
  getData(): Buffer {
637
    return this.data.subarray(0, this.writerIndex);
21✔
638
  }
639

640
  private ensureLength(bytes: number) {
641
    const neededBytes = (this.writerIndex + bytes) - this.data.length;
54✔
642
    if (neededBytes > 0) {
54!
643
      const chunks = Math.ceil(neededBytes / DataStreamWriter.chunkSize);
×
644

645
      // don't know if it's best for performance to immediately concatenate the buffers. That way it's
646
      // the easiest way to handle writing though.
647
      this.data = Buffer.concat([this.data, Buffer.alloc(chunks * DataStreamWriter.chunkSize)]);
×
648
    }
649
  }
650

651
  private compressDataIfPossible<T>(data: T): boolean {
652
    const index = this.writtenData.indexOf(data);
24✔
653
    if (index < 0) {
24!
654
      // data is not present yet
655
      this.writtenData.push(data);
24✔
656
      return false;
24✔
657
    } else if (index <= DataFormatTags.COMPRESSION_STOP - DataFormatTags.COMPRESSION_START) {
×
658
      // data was already written and the index is in the applicable range => shorten the payload
659
      this.writeTag(DataFormatTags.COMPRESSION_START + index);
×
660
      return true;
×
661
    }
662

663
    return false;
×
664
  }
665

666
  writeTag(tag: DataFormatTags): void {
667
    this.ensureLength(1);
24✔
668
    this.data.writeUInt8(tag, this.writerIndex++);
24✔
669
  }
670

671
  writeTrue(): void {
672
    this.writeTag(DataFormatTags.TRUE);
×
673
  }
674

675
  writeFalse(): void {
676
    this.writeTag(DataFormatTags.FALSE);
×
677
  }
678

679
  writeNumber(number: number): void {
680
    if (number === -1) {
12!
681
      this.writeTag(DataFormatTags.INTEGER_MINUS_ONE);
×
682
    } else if (number >= 0 && number <= 39) {
12!
683
      this.writeTag(DataFormatTags.INTEGER_RANGE_START_0 + number);
×
684
    } else if (number >= -128 && number <= 127) {
12!
685
      this.writeInt8(new Int8(number));
×
686
    } else if (number >= -32768 && number <= 32767) {
12!
687
      this.writeInt16LE(new Int16(number));
×
688
    } else if (number >= -2147483648 && number <= 2147483647) {
12✔
689
      this.writeInt32LE(new Int32(number));
9✔
690
    } else if (number >= Number.MIN_SAFE_INTEGER && number <= Number.MAX_SAFE_INTEGER) { // use correct uin64 restriction when we convert to bigint
3!
691
      this.writeInt64LE(new Int64(number));
3✔
692
    } else {
693
      throw new Error("Tried writing unrepresentable number (" + number + ")");
×
694
    }
695
  }
696

697
  writeInt8(int8: Int8): void {
698
    if (this.compressDataIfPossible(int8)) {
×
699
      return;
×
700
    }
701

702
    this.ensureLength(2);
×
703
    this.writeTag(DataFormatTags.INT8);
×
704
    this.data.writeInt8(int8.value, this.writerIndex++);
×
705
  }
706

707
  writeInt16LE(int16: Int16): void {
708
    if (this.compressDataIfPossible(int16)) {
×
709
      return;
×
710
    }
711

712
    this.ensureLength(3);
×
713
    this.writeTag(DataFormatTags.INT16LE);
×
714
    this.data.writeInt16LE(int16.value, this.writerIndex);
×
715
    this.writerIndex += 2;
×
716
  }
717

718
  writeInt32LE(int32: Int32): void {
719
    if (this.compressDataIfPossible(int32)) {
9!
720
      return;
×
721
    }
722

723
    this.ensureLength(5);
9✔
724
    this.writeTag(DataFormatTags.INT32LE);
9✔
725
    this.data.writeInt32LE(int32.value, this.writerIndex);
9✔
726
    this.writerIndex += 4;
9✔
727
  }
728

729
  writeInt64LE(int64: Int64): void {
730
    if (this.compressDataIfPossible(int64)) {
3!
731
      return;
×
732
    }
733

734
    this.ensureLength(9);
3✔
735
    this.writeTag(DataFormatTags.INT64LE);
3✔
736
    this.data.writeUInt32LE(int64.value, this.writerIndex);// TODO correctly implement int64; currently it's basically an int32
3✔
737
    this.data.writeUInt32LE(0, this.writerIndex + 4);
3✔
738
    this.writerIndex += 8;
3✔
739
  }
740

741
  writeFloat32LE(float32: Float32): void {
742
    if (this.compressDataIfPossible(float32)) {
×
743
      return;
×
744
    }
745

746
    this.ensureLength(5);
×
747
    this.writeTag(DataFormatTags.FLOAT32LE);
×
748
    this.data.writeFloatLE(float32.value, this.writerIndex);
×
749
    this.writerIndex += 4;
×
750
  }
751

752
  writeFloat64LE(float64: Float64): void {
753
    if (this.compressDataIfPossible(float64)) {
6!
754
      return;
×
755
    }
756

757
    this.ensureLength(9);
6✔
758
    this.writeTag(DataFormatTags.FLOAT64LE);
6✔
759
    this.data.writeDoubleLE(float64.value, this.writerIndex);
6✔
760
    this.writerIndex += 8;
6✔
761
  }
762

763
  private writeLength8(length: number): void {
764
    this.ensureLength(1);
×
765
    this.data.writeUInt8(length, this.writerIndex++);
×
766
  }
767

768
  private writeLength16LE(length: number): void {
769
    this.ensureLength(2);
×
770
    this.data.writeUInt16LE(length, this.writerIndex);
×
771
    this.writerIndex += 2;
×
772
  }
773

774
  private writeLength32LE(length: number): void {
775
    this.ensureLength(4);
×
776
    this.data.writeUInt32LE(length, this.writerIndex);
×
777
    this.writerIndex += 4;
×
778
  }
779

780
  private writeLength64LE(length: number): void {
781
    this.ensureLength(8);
×
782
    hapCrypto.writeUInt64LE(length, this.data, this.writerIndex);
×
783
    this.writerIndex += 8;
×
784
  }
785

786
  writeUTF8(utf8: string): void {
787
    if (this.compressDataIfPossible(utf8)) {
6!
788
      return;
×
789
    }
790

791
    const length = Buffer.byteLength(utf8);
6✔
792
    if (length <= 32) {
6!
793
      this.ensureLength(1 + length);
6✔
794
      this.writeTag(DataFormatTags.UTF8_LENGTH_START + length);
6✔
795
      this._writeUTF8(utf8);
6✔
796
    } else if (length <= 255) {
×
797
      this.writeUTF8_Length8(utf8);
×
798
    } else if (length <= 65535) {
×
799
      this.writeUTF8_Length16LE(utf8);
×
800
    } else if (length <= 4294967295) {
×
801
      this.writeUTF8_Length32LE(utf8);
×
802
    } else if (length <= Number.MAX_SAFE_INTEGER) { // use correct uin64 restriction when we convert to bigint
×
803
      this.writeUTF8_Length64LE(utf8);
×
804
    } else {
805
      this.writeUTF8_NULL_terminated(utf8);
×
806
    }
807
  }
808

809
  private _writeUTF8(utf8: string): void { // utility method
810
    const byteLength = Buffer.byteLength(utf8);
6✔
811
    this.ensureLength(byteLength);
6✔
812

813
    this.data.write(utf8, this.writerIndex, byteLength, "utf8");
6✔
814
    this.writerIndex += byteLength;
6✔
815
  }
816

817
  private writeUTF8_Length8(utf8: string): void {
818
    const length = Buffer.byteLength(utf8);
×
819
    this.ensureLength(2 + length);
×
820

821
    this.writeTag(DataFormatTags.UTF8_LENGTH8);
×
822
    this.writeLength8(length);
×
823
    this._writeUTF8(utf8);
×
824
  }
825

826
  private writeUTF8_Length16LE(utf8: string): void {
827
    const length = Buffer.byteLength(utf8);
×
828
    this.ensureLength(3 + length);
×
829

830
    this.writeTag(DataFormatTags.UTF8_LENGTH16LE);
×
831
    this.writeLength16LE(length);
×
832
    this._writeUTF8(utf8);
×
833
  }
834

835
  private writeUTF8_Length32LE(utf8: string): void {
836
    const length = Buffer.byteLength(utf8);
×
837
    this.ensureLength(5 + length);
×
838

839
    this.writeTag(DataFormatTags.UTF8_LENGTH32LE);
×
840
    this.writeLength32LE(length);
×
841
    this._writeUTF8(utf8);
×
842
  }
843

844
  private writeUTF8_Length64LE(utf8: string): void {
845
    const length = Buffer.byteLength(utf8);
×
846
    this.ensureLength(9 + length);
×
847

848
    this.writeTag(DataFormatTags.UTF8_LENGTH64LE);
×
849
    this.writeLength64LE(length);
×
850
    this._writeUTF8(utf8);
×
851
  }
852

853
  private writeUTF8_NULL_terminated(utf8: string): void {
854
    this.ensureLength(1 + Buffer.byteLength(utf8) + 1);
×
855

856
    this.writeTag(DataFormatTags.UTF8_NULL_TERMINATED);
×
857
    this._writeUTF8(utf8);
×
858
    this.data.writeUInt8(0, this.writerIndex++);
×
859
  }
860

861
  writeData(data: Buffer): void {
862
    if (this.compressDataIfPossible(data)) {
×
863
      return;
×
864
    }
865

866
    if (data.length <= 32) {
×
867
      this.writeTag(DataFormatTags.DATA_LENGTH_START + data.length);
×
868
      this._writeData(data);
×
869
    } else if (data.length <= 255) {
×
870
      this.writeData_Length8(data);
×
871
    } else if (data.length <= 65535) {
×
872
      this.writeData_Length16LE(data);
×
873
    } else if (data.length <= 4294967295) {
×
874
      this.writeData_Length32LE(data);
×
875
    } else if (data.length <= Number.MAX_SAFE_INTEGER) {
×
876
      this.writeData_Length64LE(data);
×
877
    } else {
878
      this.writeData_terminated(data);
×
879
    }
880
  }
881

882
  private _writeData(data: Buffer): void { // utility method
883
    this.ensureLength(data.length);
×
884
    for (let i = 0; i < data.length; i++) {
×
885
      this.data[this.writerIndex++] = data[i];
×
886
    }
887
  }
888

889
  private writeData_Length8(data: Buffer): void {
890
    this.ensureLength(2 + data.length);
×
891

892
    this.writeTag(DataFormatTags.DATA_LENGTH8);
×
893
    this.writeLength8(data.length);
×
894
    this._writeData(data);
×
895
  }
896

897
  private writeData_Length16LE(data: Buffer): void {
898
    this.ensureLength(3 + data.length);
×
899

900
    this.writeTag(DataFormatTags.DATA_LENGTH16LE);
×
901
    this.writeLength16LE(data.length);
×
902
    this._writeData(data);
×
903
  }
904

905
  private writeData_Length32LE(data: Buffer): void {
906
    this.ensureLength(5 + data.length);
×
907

908
    this.writeTag(DataFormatTags.DATA_LENGTH32LE);
×
909
    this.writeLength32LE(data.length);
×
910
    this._writeData(data);
×
911
  }
912

913
  private writeData_Length64LE(data: Buffer): void {
914
    this.ensureLength(9 + data.length);
×
915

916
    this.writeTag(DataFormatTags.DATA_LENGTH64LE);
×
917
    this.writeLength64LE(data.length);
×
918
    this._writeData(data);
×
919
  }
920

921
  private writeData_terminated(data: Buffer): void {
922
    this.ensureLength(1 + data.length + 1);
×
923

924
    this.writeTag(DataFormatTags.DATA_TERMINATED);
×
925
    this._writeData(data);
×
926
    this.writeTag(DataFormatTags.TERMINATOR);
×
927
  }
928

929
  writeSecondsSince2001_01_01(seconds: SecondsSince2001): void {
930
    if (this.compressDataIfPossible(seconds)) {
×
931
      return;
×
932
    }
933

934
    this.ensureLength(9);
×
935
    this.writeTag(DataFormatTags.DATE);
×
936
    this.data.writeDoubleLE(seconds.value, this.writerIndex);
×
937
    this.writerIndex += 8;
×
938
  }
939

940
  writeUUID(uuid_string: string): void {
941
    assert(uuid.isValid(uuid_string), "supplied uuid is invalid");
×
942
    if (this.compressDataIfPossible(new UUID(uuid_string))) {
×
943
      return;
×
944
    }
945

946
    this.ensureLength(17);
×
947
    this.writeTag(DataFormatTags.UUID);
×
948
    uuid.write(uuid_string, this.data, this.writerIndex);
×
949
    this.writerIndex += 16;
×
950
  }
951
}
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