@k67/kaitai-struct-ts 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1362 @@
1
+ // src/utils/errors.ts
2
+ var KaitaiError = class _KaitaiError extends Error {
3
+ constructor(message, position) {
4
+ super(message);
5
+ this.position = position;
6
+ this.name = "KaitaiError";
7
+ Object.setPrototypeOf(this, _KaitaiError.prototype);
8
+ }
9
+ };
10
+ var ValidationError = class _ValidationError extends KaitaiError {
11
+ constructor(message, position) {
12
+ super(message, position);
13
+ this.name = "ValidationError";
14
+ Object.setPrototypeOf(this, _ValidationError.prototype);
15
+ }
16
+ };
17
+ var ParseError = class _ParseError extends KaitaiError {
18
+ constructor(message, position) {
19
+ super(message, position);
20
+ this.name = "ParseError";
21
+ Object.setPrototypeOf(this, _ParseError.prototype);
22
+ }
23
+ };
24
+ var EOFError = class _EOFError extends KaitaiError {
25
+ constructor(message = "Unexpected end of stream", position) {
26
+ super(message, position);
27
+ this.name = "EOFError";
28
+ Object.setPrototypeOf(this, _EOFError.prototype);
29
+ }
30
+ };
31
+ var NotImplementedError = class _NotImplementedError extends KaitaiError {
32
+ constructor(feature) {
33
+ super(`Feature not yet implemented: ${feature}`);
34
+ this.name = "NotImplementedError";
35
+ Object.setPrototypeOf(this, _NotImplementedError.prototype);
36
+ }
37
+ };
38
+
39
+ // src/utils/encoding.ts
40
+ function decodeString(bytes, encoding) {
41
+ const normalizedEncoding = encoding.toLowerCase().replace(/[-_]/g, "");
42
+ switch (normalizedEncoding) {
43
+ case "utf8":
44
+ case "utf-8":
45
+ return decodeUtf8(bytes);
46
+ case "ascii":
47
+ case "usascii":
48
+ return decodeAscii(bytes);
49
+ case "utf16":
50
+ case "utf16le":
51
+ case "utf-16le":
52
+ return decodeUtf16Le(bytes);
53
+ case "utf16be":
54
+ case "utf-16be":
55
+ return decodeUtf16Be(bytes);
56
+ case "latin1":
57
+ case "iso88591":
58
+ case "iso-8859-1":
59
+ return decodeLatin1(bytes);
60
+ default:
61
+ if (typeof TextDecoder !== "undefined") {
62
+ try {
63
+ return new TextDecoder(encoding).decode(bytes);
64
+ } catch {
65
+ throw new Error(`Unsupported encoding: ${encoding}`);
66
+ }
67
+ }
68
+ throw new Error(`Unsupported encoding: ${encoding}`);
69
+ }
70
+ }
71
+ function decodeUtf8(bytes) {
72
+ if (typeof TextDecoder !== "undefined") {
73
+ return new TextDecoder("utf-8").decode(bytes);
74
+ }
75
+ let result = "";
76
+ let i = 0;
77
+ while (i < bytes.length) {
78
+ const byte1 = bytes[i++];
79
+ if (byte1 < 128) {
80
+ result += String.fromCharCode(byte1);
81
+ } else if (byte1 < 224) {
82
+ const byte2 = bytes[i++];
83
+ result += String.fromCharCode((byte1 & 31) << 6 | byte2 & 63);
84
+ } else if (byte1 < 240) {
85
+ const byte2 = bytes[i++];
86
+ const byte3 = bytes[i++];
87
+ result += String.fromCharCode(
88
+ (byte1 & 15) << 12 | (byte2 & 63) << 6 | byte3 & 63
89
+ );
90
+ } else {
91
+ const byte2 = bytes[i++];
92
+ const byte3 = bytes[i++];
93
+ const byte4 = bytes[i++];
94
+ let codePoint = (byte1 & 7) << 18 | (byte2 & 63) << 12 | (byte3 & 63) << 6 | byte4 & 63;
95
+ codePoint -= 65536;
96
+ result += String.fromCharCode(
97
+ 55296 + (codePoint >> 10),
98
+ 56320 + (codePoint & 1023)
99
+ );
100
+ }
101
+ }
102
+ return result;
103
+ }
104
+ function decodeAscii(bytes) {
105
+ let result = "";
106
+ for (let i = 0; i < bytes.length; i++) {
107
+ result += String.fromCharCode(bytes[i] & 127);
108
+ }
109
+ return result;
110
+ }
111
+ function decodeLatin1(bytes) {
112
+ let result = "";
113
+ for (let i = 0; i < bytes.length; i++) {
114
+ result += String.fromCharCode(bytes[i]);
115
+ }
116
+ return result;
117
+ }
118
+ function decodeUtf16Le(bytes) {
119
+ if (typeof TextDecoder !== "undefined") {
120
+ return new TextDecoder("utf-16le").decode(bytes);
121
+ }
122
+ let result = "";
123
+ for (let i = 0; i < bytes.length; i += 2) {
124
+ const charCode = bytes[i] | bytes[i + 1] << 8;
125
+ result += String.fromCharCode(charCode);
126
+ }
127
+ return result;
128
+ }
129
+ function decodeUtf16Be(bytes) {
130
+ if (typeof TextDecoder !== "undefined") {
131
+ return new TextDecoder("utf-16be").decode(bytes);
132
+ }
133
+ let result = "";
134
+ for (let i = 0; i < bytes.length; i += 2) {
135
+ const charCode = bytes[i] << 8 | bytes[i + 1];
136
+ result += String.fromCharCode(charCode);
137
+ }
138
+ return result;
139
+ }
140
+
141
+ // src/stream/KaitaiStream.ts
142
+ var KaitaiStream = class _KaitaiStream {
143
+ /**
144
+ * Create a new KaitaiStream from a buffer
145
+ * @param buffer - ArrayBuffer or Uint8Array containing the binary data
146
+ */
147
+ constructor(buffer) {
148
+ this._pos = 0;
149
+ this._bits = 0;
150
+ this._bitsLeft = 0;
151
+ if (buffer instanceof ArrayBuffer) {
152
+ this.buffer = new Uint8Array(buffer);
153
+ this.view = new DataView(buffer);
154
+ } else {
155
+ this.buffer = buffer;
156
+ this.view = new DataView(
157
+ buffer.buffer,
158
+ buffer.byteOffset,
159
+ buffer.byteLength
160
+ );
161
+ }
162
+ }
163
+ /**
164
+ * Current position in the stream
165
+ */
166
+ get pos() {
167
+ return this._pos;
168
+ }
169
+ set pos(value) {
170
+ this._pos = value;
171
+ this._bitsLeft = 0;
172
+ }
173
+ /**
174
+ * Total size of the stream in bytes
175
+ */
176
+ get size() {
177
+ return this.buffer.length;
178
+ }
179
+ /**
180
+ * Check if we've reached the end of the stream
181
+ */
182
+ isEof() {
183
+ return this._pos >= this.buffer.length;
184
+ }
185
+ /**
186
+ * Seek to a specific position in the stream
187
+ * @param pos - Position to seek to
188
+ */
189
+ seek(pos) {
190
+ if (pos < 0 || pos > this.buffer.length) {
191
+ throw new Error(`Invalid seek position: ${pos}`);
192
+ }
193
+ this.pos = pos;
194
+ }
195
+ /**
196
+ * Ensure we have enough bytes available
197
+ * @param count - Number of bytes needed
198
+ */
199
+ ensureBytes(count) {
200
+ if (this._pos + count > this.buffer.length) {
201
+ throw new EOFError(
202
+ `Requested ${count} bytes at position ${this._pos}, but only ${this.buffer.length - this._pos} bytes available`,
203
+ this._pos
204
+ );
205
+ }
206
+ }
207
+ // ==================== Unsigned Integers ====================
208
+ /**
209
+ * Read 1-byte unsigned integer (0 to 255)
210
+ */
211
+ readU1() {
212
+ this.ensureBytes(1);
213
+ return this.buffer[this._pos++];
214
+ }
215
+ /**
216
+ * Read 2-byte unsigned integer, little-endian
217
+ */
218
+ readU2le() {
219
+ this.ensureBytes(2);
220
+ const value = this.view.getUint16(this._pos, true);
221
+ this._pos += 2;
222
+ return value;
223
+ }
224
+ /**
225
+ * Read 2-byte unsigned integer, big-endian
226
+ */
227
+ readU2be() {
228
+ this.ensureBytes(2);
229
+ const value = this.view.getUint16(this._pos, false);
230
+ this._pos += 2;
231
+ return value;
232
+ }
233
+ /**
234
+ * Read 4-byte unsigned integer, little-endian
235
+ */
236
+ readU4le() {
237
+ this.ensureBytes(4);
238
+ const value = this.view.getUint32(this._pos, true);
239
+ this._pos += 4;
240
+ return value;
241
+ }
242
+ /**
243
+ * Read 4-byte unsigned integer, big-endian
244
+ */
245
+ readU4be() {
246
+ this.ensureBytes(4);
247
+ const value = this.view.getUint32(this._pos, false);
248
+ this._pos += 4;
249
+ return value;
250
+ }
251
+ /**
252
+ * Read 8-byte unsigned integer, little-endian
253
+ * Returns BigInt for values > Number.MAX_SAFE_INTEGER
254
+ */
255
+ readU8le() {
256
+ this.ensureBytes(8);
257
+ const value = this.view.getBigUint64(this._pos, true);
258
+ this._pos += 8;
259
+ return value;
260
+ }
261
+ /**
262
+ * Read 8-byte unsigned integer, big-endian
263
+ * Returns BigInt for values > Number.MAX_SAFE_INTEGER
264
+ */
265
+ readU8be() {
266
+ this.ensureBytes(8);
267
+ const value = this.view.getBigUint64(this._pos, false);
268
+ this._pos += 8;
269
+ return value;
270
+ }
271
+ // ==================== Signed Integers ====================
272
+ /**
273
+ * Read 1-byte signed integer (-128 to 127)
274
+ */
275
+ readS1() {
276
+ this.ensureBytes(1);
277
+ return this.view.getInt8(this._pos++);
278
+ }
279
+ /**
280
+ * Read 2-byte signed integer, little-endian
281
+ */
282
+ readS2le() {
283
+ this.ensureBytes(2);
284
+ const value = this.view.getInt16(this._pos, true);
285
+ this._pos += 2;
286
+ return value;
287
+ }
288
+ /**
289
+ * Read 2-byte signed integer, big-endian
290
+ */
291
+ readS2be() {
292
+ this.ensureBytes(2);
293
+ const value = this.view.getInt16(this._pos, false);
294
+ this._pos += 2;
295
+ return value;
296
+ }
297
+ /**
298
+ * Read 4-byte signed integer, little-endian
299
+ */
300
+ readS4le() {
301
+ this.ensureBytes(4);
302
+ const value = this.view.getInt32(this._pos, true);
303
+ this._pos += 4;
304
+ return value;
305
+ }
306
+ /**
307
+ * Read 4-byte signed integer, big-endian
308
+ */
309
+ readS4be() {
310
+ this.ensureBytes(4);
311
+ const value = this.view.getInt32(this._pos, false);
312
+ this._pos += 4;
313
+ return value;
314
+ }
315
+ /**
316
+ * Read 8-byte signed integer, little-endian
317
+ * Returns BigInt for values outside Number.MAX_SAFE_INTEGER range
318
+ */
319
+ readS8le() {
320
+ this.ensureBytes(8);
321
+ const value = this.view.getBigInt64(this._pos, true);
322
+ this._pos += 8;
323
+ return value;
324
+ }
325
+ /**
326
+ * Read 8-byte signed integer, big-endian
327
+ * Returns BigInt for values outside Number.MAX_SAFE_INTEGER range
328
+ */
329
+ readS8be() {
330
+ this.ensureBytes(8);
331
+ const value = this.view.getBigInt64(this._pos, false);
332
+ this._pos += 8;
333
+ return value;
334
+ }
335
+ // ==================== Floating Point ====================
336
+ /**
337
+ * Read 4-byte IEEE 754 single-precision float, little-endian
338
+ */
339
+ readF4le() {
340
+ this.ensureBytes(4);
341
+ const value = this.view.getFloat32(this._pos, true);
342
+ this._pos += 4;
343
+ return value;
344
+ }
345
+ /**
346
+ * Read 4-byte IEEE 754 single-precision float, big-endian
347
+ */
348
+ readF4be() {
349
+ this.ensureBytes(4);
350
+ const value = this.view.getFloat32(this._pos, false);
351
+ this._pos += 4;
352
+ return value;
353
+ }
354
+ /**
355
+ * Read 8-byte IEEE 754 double-precision float, little-endian
356
+ */
357
+ readF8le() {
358
+ this.ensureBytes(8);
359
+ const value = this.view.getFloat64(this._pos, true);
360
+ this._pos += 8;
361
+ return value;
362
+ }
363
+ /**
364
+ * Read 8-byte IEEE 754 double-precision float, big-endian
365
+ */
366
+ readF8be() {
367
+ this.ensureBytes(8);
368
+ const value = this.view.getFloat64(this._pos, false);
369
+ this._pos += 8;
370
+ return value;
371
+ }
372
+ // ==================== Byte Arrays ====================
373
+ /**
374
+ * Read a fixed number of bytes
375
+ * @param length - Number of bytes to read
376
+ */
377
+ readBytes(length) {
378
+ this.ensureBytes(length);
379
+ const bytes = this.buffer.slice(this._pos, this._pos + length);
380
+ this._pos += length;
381
+ return bytes;
382
+ }
383
+ /**
384
+ * Read all remaining bytes until end of stream
385
+ */
386
+ readBytesFull() {
387
+ const bytes = this.buffer.slice(this._pos);
388
+ this._pos = this.buffer.length;
389
+ return bytes;
390
+ }
391
+ /**
392
+ * Read bytes until a terminator byte is found
393
+ * @param term - Terminator byte value
394
+ * @param include - Include terminator in result
395
+ * @param consume - Consume terminator from stream
396
+ * @param eosError - Throw error if EOS reached before terminator
397
+ */
398
+ readBytesterm(term, include = false, consume = true, eosError = true) {
399
+ const start = this._pos;
400
+ let end = start;
401
+ while (end < this.buffer.length && this.buffer[end] !== term) {
402
+ end++;
403
+ }
404
+ const foundTerm = end < this.buffer.length;
405
+ if (!foundTerm && eosError) {
406
+ throw new EOFError(
407
+ `Terminator byte ${term} not found before end of stream`,
408
+ this._pos
409
+ );
410
+ }
411
+ const includeEnd = include && foundTerm ? end + 1 : end;
412
+ const bytes = this.buffer.slice(start, includeEnd);
413
+ if (foundTerm && consume) {
414
+ this._pos = end + 1;
415
+ } else {
416
+ this._pos = end;
417
+ }
418
+ return bytes;
419
+ }
420
+ // ==================== Strings ====================
421
+ /**
422
+ * Read a fixed-length string
423
+ * @param length - Number of bytes to read
424
+ * @param encoding - Character encoding (default: UTF-8)
425
+ */
426
+ readStr(length, encoding = "UTF-8") {
427
+ const bytes = this.readBytes(length);
428
+ return decodeString(bytes, encoding);
429
+ }
430
+ /**
431
+ * Read a null-terminated string
432
+ * @param encoding - Character encoding (default: UTF-8)
433
+ * @param term - Terminator byte (default: 0)
434
+ * @param include - Include terminator in result
435
+ * @param consume - Consume terminator from stream
436
+ * @param eosError - Throw error if EOS reached before terminator
437
+ */
438
+ readStrz(encoding = "UTF-8", term = 0, include = false, consume = true, eosError = true) {
439
+ const bytes = this.readBytesterm(term, include, consume, eosError);
440
+ return decodeString(bytes, encoding);
441
+ }
442
+ // ==================== Bit-level Reading ====================
443
+ /**
444
+ * Align bit reading to byte boundary
445
+ */
446
+ alignToByte() {
447
+ this._bitsLeft = 0;
448
+ }
449
+ /**
450
+ * Read specified number of bits as unsigned integer (big-endian)
451
+ * @param n - Number of bits to read (1-64)
452
+ */
453
+ readBitsIntBe(n) {
454
+ if (n < 1 || n > 64) {
455
+ throw new Error(`Invalid bit count: ${n}. Must be between 1 and 64`);
456
+ }
457
+ let result = 0n;
458
+ for (let bitsNeeded = n; bitsNeeded > 0; ) {
459
+ if (this._bitsLeft === 0) {
460
+ this._bits = this.readU1();
461
+ this._bitsLeft = 8;
462
+ }
463
+ const bitsToRead = Math.min(bitsNeeded, this._bitsLeft);
464
+ const mask = (1 << bitsToRead) - 1;
465
+ const shift = this._bitsLeft - bitsToRead;
466
+ result = result << BigInt(bitsToRead) | BigInt(this._bits >> shift & mask);
467
+ this._bitsLeft -= bitsToRead;
468
+ bitsNeeded -= bitsToRead;
469
+ }
470
+ return result;
471
+ }
472
+ /**
473
+ * Read specified number of bits as unsigned integer (little-endian)
474
+ * @param n - Number of bits to read (1-64)
475
+ */
476
+ readBitsIntLe(n) {
477
+ if (n < 1 || n > 64) {
478
+ throw new Error(`Invalid bit count: ${n}. Must be between 1 and 64`);
479
+ }
480
+ let result = 0n;
481
+ let bitPos = 0;
482
+ for (let bitsNeeded = n; bitsNeeded > 0; ) {
483
+ if (this._bitsLeft === 0) {
484
+ this._bits = this.readU1();
485
+ this._bitsLeft = 8;
486
+ }
487
+ const bitsToRead = Math.min(bitsNeeded, this._bitsLeft);
488
+ const mask = (1 << bitsToRead) - 1;
489
+ result |= BigInt(this._bits & mask) << BigInt(bitPos);
490
+ this._bits >>= bitsToRead;
491
+ this._bitsLeft -= bitsToRead;
492
+ bitsNeeded -= bitsToRead;
493
+ bitPos += bitsToRead;
494
+ }
495
+ return result;
496
+ }
497
+ // ==================== Utility Methods ====================
498
+ /**
499
+ * Get the underlying buffer
500
+ */
501
+ getBuffer() {
502
+ return this.buffer;
503
+ }
504
+ /**
505
+ * Create a substream from current position with specified size
506
+ * @param size - Size of the substream in bytes
507
+ */
508
+ substream(size) {
509
+ this.ensureBytes(size);
510
+ const subBuffer = this.buffer.slice(this._pos, this._pos + size);
511
+ this._pos += size;
512
+ return new _KaitaiStream(subBuffer);
513
+ }
514
+ };
515
+
516
+ // src/parser/schema.ts
517
+ var BUILTIN_TYPES = [
518
+ // Unsigned integers
519
+ "u1",
520
+ "u2",
521
+ "u2le",
522
+ "u2be",
523
+ "u4",
524
+ "u4le",
525
+ "u4be",
526
+ "u8",
527
+ "u8le",
528
+ "u8be",
529
+ // Signed integers
530
+ "s1",
531
+ "s2",
532
+ "s2le",
533
+ "s2be",
534
+ "s4",
535
+ "s4le",
536
+ "s4be",
537
+ "s8",
538
+ "s8le",
539
+ "s8be",
540
+ // Floating point
541
+ "f4",
542
+ "f4le",
543
+ "f4be",
544
+ "f8",
545
+ "f8le",
546
+ "f8be",
547
+ // String
548
+ "str",
549
+ "strz"
550
+ ];
551
+ function isBuiltinType(type) {
552
+ return BUILTIN_TYPES.includes(type);
553
+ }
554
+ function getTypeEndianness(type) {
555
+ if (type.endsWith("le")) return "le";
556
+ if (type.endsWith("be")) return "be";
557
+ return void 0;
558
+ }
559
+ function getBaseType(type) {
560
+ if (type.endsWith("le") || type.endsWith("be")) {
561
+ return type.slice(0, -2);
562
+ }
563
+ return type;
564
+ }
565
+ function isIntegerType(type) {
566
+ const base = getBaseType(type);
567
+ return /^[us][1248]$/.test(base);
568
+ }
569
+ function isFloatType(type) {
570
+ const base = getBaseType(type);
571
+ return /^f[48]$/.test(base);
572
+ }
573
+ function isStringType(type) {
574
+ return type === "str" || type === "strz";
575
+ }
576
+
577
+ // src/parser/KsyParser.ts
578
+ import { parse as parseYaml } from "yaml";
579
+ var KsyParser = class {
580
+ /**
581
+ * Parse a .ksy YAML string into a typed schema object.
582
+ *
583
+ * @param yaml - YAML string containing the .ksy definition
584
+ * @param options - Parsing options
585
+ * @returns Parsed and validated schema
586
+ * @throws {ParseError} If YAML parsing fails
587
+ * @throws {ValidationError} If schema validation fails
588
+ */
589
+ parse(yaml, options = {}) {
590
+ const { validate = true, strict = false } = options;
591
+ let parsed;
592
+ try {
593
+ parsed = parseYaml(yaml);
594
+ } catch (error) {
595
+ throw new ParseError(
596
+ `Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`
597
+ );
598
+ }
599
+ if (typeof parsed !== "object" || parsed === null) {
600
+ throw new ParseError("KSY file must contain an object");
601
+ }
602
+ const schema = parsed;
603
+ if (validate) {
604
+ const result = this.validate(schema, { strict });
605
+ if (!result.valid) {
606
+ const errorMessages = result.errors.map((e) => e.message).join("; ");
607
+ throw new ValidationError(
608
+ `Schema validation failed: ${errorMessages}`
609
+ );
610
+ }
611
+ if (result.warnings.length > 0 && !strict) {
612
+ console.warn(
613
+ "Schema validation warnings:",
614
+ result.warnings.map((w) => w.message)
615
+ );
616
+ }
617
+ }
618
+ return schema;
619
+ }
620
+ /**
621
+ * Validate a schema object.
622
+ *
623
+ * @param schema - Schema to validate
624
+ * @param options - Validation options
625
+ * @returns Validation result with errors and warnings
626
+ */
627
+ validate(schema, options = {}) {
628
+ const { strict = false, isNested = false } = options;
629
+ const errors = [];
630
+ const warnings = [];
631
+ if (!schema.meta && !isNested) {
632
+ errors.push({
633
+ message: 'Missing required "meta" section',
634
+ path: [],
635
+ code: "MISSING_META"
636
+ });
637
+ } else if (schema.meta) {
638
+ if (!schema.meta.id) {
639
+ errors.push({
640
+ message: 'Missing required "meta.id" field',
641
+ path: ["meta"],
642
+ code: "MISSING_META_ID"
643
+ });
644
+ } else if (typeof schema.meta.id !== "string") {
645
+ errors.push({
646
+ message: '"meta.id" must be a string',
647
+ path: ["meta", "id"],
648
+ code: "INVALID_META_ID_TYPE"
649
+ });
650
+ } else if (!/^[a-z][a-z0-9_]*$/.test(schema.meta.id)) {
651
+ warnings.push({
652
+ message: '"meta.id" should follow snake_case naming convention',
653
+ path: ["meta", "id"],
654
+ code: "META_ID_NAMING"
655
+ });
656
+ }
657
+ if (schema.meta.endian) {
658
+ if (typeof schema.meta.endian === "string" && schema.meta.endian !== "le" && schema.meta.endian !== "be") {
659
+ errors.push({
660
+ message: '"meta.endian" must be "le" or "be"',
661
+ path: ["meta", "endian"],
662
+ code: "INVALID_ENDIAN"
663
+ });
664
+ }
665
+ }
666
+ }
667
+ if (schema.seq) {
668
+ if (!Array.isArray(schema.seq)) {
669
+ errors.push({
670
+ message: '"seq" must be an array',
671
+ path: ["seq"],
672
+ code: "INVALID_SEQ_TYPE"
673
+ });
674
+ } else {
675
+ schema.seq.forEach((attr, index) => {
676
+ this.validateAttribute(
677
+ attr,
678
+ ["seq", String(index)],
679
+ errors,
680
+ warnings,
681
+ strict
682
+ );
683
+ });
684
+ }
685
+ }
686
+ if (schema.instances) {
687
+ if (typeof schema.instances !== "object") {
688
+ errors.push({
689
+ message: '"instances" must be an object',
690
+ path: ["instances"],
691
+ code: "INVALID_INSTANCES_TYPE"
692
+ });
693
+ } else {
694
+ Object.entries(schema.instances).forEach(([key, instance]) => {
695
+ this.validateAttribute(
696
+ instance,
697
+ ["instances", key],
698
+ errors,
699
+ warnings,
700
+ strict
701
+ );
702
+ });
703
+ }
704
+ }
705
+ if (schema.types) {
706
+ if (typeof schema.types !== "object") {
707
+ errors.push({
708
+ message: '"types" must be an object',
709
+ path: ["types"],
710
+ code: "INVALID_TYPES_TYPE"
711
+ });
712
+ } else {
713
+ Object.entries(schema.types).forEach(([key, type]) => {
714
+ const typeResult = this.validate(type, { ...options, isNested: true });
715
+ errors.push(
716
+ ...typeResult.errors.map((e) => ({
717
+ ...e,
718
+ path: ["types", key, ...e.path]
719
+ }))
720
+ );
721
+ warnings.push(
722
+ ...typeResult.warnings.map((w) => ({
723
+ ...w,
724
+ path: ["types", key, ...w.path]
725
+ }))
726
+ );
727
+ });
728
+ }
729
+ }
730
+ if (schema.enums) {
731
+ if (typeof schema.enums !== "object") {
732
+ errors.push({
733
+ message: '"enums" must be an object',
734
+ path: ["enums"],
735
+ code: "INVALID_ENUMS_TYPE"
736
+ });
737
+ }
738
+ }
739
+ return {
740
+ valid: errors.length === 0 && (strict ? warnings.length === 0 : true),
741
+ errors,
742
+ warnings
743
+ };
744
+ }
745
+ /**
746
+ * Validate an attribute specification.
747
+ *
748
+ * @param attr - Attribute to validate
749
+ * @param path - Path to this attribute in the schema
750
+ * @param errors - Array to collect errors
751
+ * @param warnings - Array to collect warnings
752
+ * @param strict - Whether to be strict about warnings
753
+ * @private
754
+ */
755
+ validateAttribute(attr, path, errors, warnings, _strict) {
756
+ if (attr.id && typeof attr.id === "string") {
757
+ if (!/^[a-z][a-z0-9_]*$/.test(attr.id)) {
758
+ warnings.push({
759
+ message: `Field "${attr.id}" should follow snake_case naming convention`,
760
+ path: [...path, "id"],
761
+ code: "FIELD_ID_NAMING"
762
+ });
763
+ }
764
+ }
765
+ if (attr.repeat) {
766
+ if (attr.repeat !== "expr" && attr.repeat !== "eos" && attr.repeat !== "until") {
767
+ errors.push({
768
+ message: '"repeat" must be "expr", "eos", or "until"',
769
+ path: [...path, "repeat"],
770
+ code: "INVALID_REPEAT"
771
+ });
772
+ }
773
+ if (attr.repeat === "expr" && !attr["repeat-expr"]) {
774
+ errors.push({
775
+ message: '"repeat-expr" is required when repeat is "expr"',
776
+ path: [...path, "repeat-expr"],
777
+ code: "MISSING_REPEAT_EXPR"
778
+ });
779
+ }
780
+ if (attr.repeat === "until" && !attr["repeat-until"]) {
781
+ errors.push({
782
+ message: '"repeat-until" is required when repeat is "until"',
783
+ path: [...path, "repeat-until"],
784
+ code: "MISSING_REPEAT_UNTIL"
785
+ });
786
+ }
787
+ }
788
+ if (attr["size-eos"] && attr.size) {
789
+ warnings.push({
790
+ message: '"size-eos" and "size" are mutually exclusive',
791
+ path: [...path],
792
+ code: "SIZE_EOS_WITH_SIZE"
793
+ });
794
+ }
795
+ if (attr.contents) {
796
+ if (!Array.isArray(attr.contents) && typeof attr.contents !== "string") {
797
+ errors.push({
798
+ message: '"contents" must be an array or string',
799
+ path: [...path, "contents"],
800
+ code: "INVALID_CONTENTS_TYPE"
801
+ });
802
+ }
803
+ }
804
+ }
805
+ /**
806
+ * Parse multiple .ksy files and resolve imports.
807
+ *
808
+ * @param mainYaml - Main .ksy file content
809
+ * @param imports - Map of import names to their YAML content
810
+ * @param options - Parsing options
811
+ * @returns Parsed schema with resolved imports
812
+ */
813
+ parseWithImports(mainYaml, _imports, options = {}) {
814
+ const mainSchema = this.parse(mainYaml, options);
815
+ return mainSchema;
816
+ }
817
+ };
818
+
819
+ // src/interpreter/Context.ts
820
+ var Context = class _Context {
821
+ /**
822
+ * Create a new execution context.
823
+ *
824
+ * @param _io - Binary stream being read
825
+ * @param _root - Root object of the parse tree
826
+ * @param _parent - Parent object (optional)
827
+ */
828
+ constructor(_io, _root = null, _parent = null) {
829
+ this._io = _io;
830
+ this._root = _root;
831
+ /** Stack of parent objects */
832
+ this.parentStack = [];
833
+ /** Current object being parsed */
834
+ this._current = {};
835
+ if (_parent !== null) {
836
+ this.parentStack.push(_parent);
837
+ }
838
+ }
839
+ /**
840
+ * Get the current I/O stream.
841
+ * Accessible in expressions as `_io`.
842
+ *
843
+ * @returns Current stream
844
+ */
845
+ get io() {
846
+ return this._io;
847
+ }
848
+ /**
849
+ * Get the root object.
850
+ * Accessible in expressions as `_root`.
851
+ *
852
+ * @returns Root object
853
+ */
854
+ get root() {
855
+ return this._root;
856
+ }
857
+ /**
858
+ * Get the parent object.
859
+ * Accessible in expressions as `_parent`.
860
+ *
861
+ * @returns Parent object or null if at root
862
+ */
863
+ get parent() {
864
+ return this.parentStack.length > 0 ? this.parentStack[this.parentStack.length - 1] : null;
865
+ }
866
+ /**
867
+ * Get the current object being parsed.
868
+ * Used to access fields defined earlier in the sequence.
869
+ *
870
+ * @returns Current object
871
+ */
872
+ get current() {
873
+ return this._current;
874
+ }
875
+ /**
876
+ * Set the current object.
877
+ *
878
+ * @param obj - Object to set as current
879
+ */
880
+ set current(obj) {
881
+ this._current = obj;
882
+ }
883
+ /**
884
+ * Push a new parent onto the stack.
885
+ * Used when entering a nested type.
886
+ *
887
+ * @param parent - Parent object to push
888
+ */
889
+ pushParent(parent) {
890
+ this.parentStack.push(parent);
891
+ }
892
+ /**
893
+ * Pop the current parent from the stack.
894
+ * Used when exiting a nested type.
895
+ *
896
+ * @returns The popped parent object
897
+ */
898
+ popParent() {
899
+ return this.parentStack.pop();
900
+ }
901
+ /**
902
+ * Get a value from the context by path.
903
+ * Supports special names: _io, _root, _parent, _index.
904
+ *
905
+ * @param name - Name or path to resolve
906
+ * @returns Resolved value
907
+ */
908
+ resolve(name) {
909
+ switch (name) {
910
+ case "_io":
911
+ return this._io;
912
+ case "_root":
913
+ return this._root;
914
+ case "_parent":
915
+ return this.parent;
916
+ case "_index":
917
+ return this._current["_index"];
918
+ default:
919
+ if (name in this._current) {
920
+ return this._current[name];
921
+ }
922
+ return void 0;
923
+ }
924
+ }
925
+ /**
926
+ * Set a value in the current object.
927
+ *
928
+ * @param name - Field name
929
+ * @param value - Value to set
930
+ */
931
+ set(name, value) {
932
+ this._current[name] = value;
933
+ }
934
+ /**
935
+ * Create a child context for nested parsing.
936
+ * The current object becomes the parent in the child context.
937
+ *
938
+ * @param stream - Stream for the child context (defaults to current stream)
939
+ * @returns New child context
940
+ */
941
+ createChild(stream) {
942
+ const childContext = new _Context(
943
+ stream || this._io,
944
+ this._root || this._current,
945
+ this._current
946
+ );
947
+ return childContext;
948
+ }
949
+ /**
950
+ * Clone this context.
951
+ * Creates a shallow copy with the same stream, root, and parent.
952
+ *
953
+ * @returns Cloned context
954
+ */
955
+ clone() {
956
+ const cloned = new _Context(this._io, this._root, this.parent);
957
+ cloned._current = { ...this._current };
958
+ cloned.parentStack = [...this.parentStack];
959
+ return cloned;
960
+ }
961
+ };
962
+
963
+ // src/interpreter/TypeInterpreter.ts
964
+ var TypeInterpreter = class _TypeInterpreter {
965
+ /**
966
+ * Create a new type interpreter.
967
+ *
968
+ * @param schema - Kaitai Struct schema to interpret
969
+ * @param parentMeta - Parent schema's meta (for nested types)
970
+ */
971
+ constructor(schema, parentMeta) {
972
+ this.schema = schema;
973
+ this.parentMeta = parentMeta;
974
+ if (!schema.meta && !parentMeta) {
975
+ throw new ParseError("Schema must have meta section");
976
+ }
977
+ if (schema.meta && !schema.meta.id && !parentMeta) {
978
+ throw new ParseError("Root schema must have meta.id");
979
+ }
980
+ }
981
+ /**
982
+ * Parse binary data according to the schema.
983
+ *
984
+ * @param stream - Binary stream to parse
985
+ * @param parent - Parent object (for nested types)
986
+ * @returns Parsed object
987
+ */
988
+ parse(stream, parent) {
989
+ const result = {};
990
+ const context = new Context(stream, result, parent);
991
+ context.current = result;
992
+ if (this.schema.seq) {
993
+ for (const attr of this.schema.seq) {
994
+ const value = this.parseAttribute(attr, context);
995
+ if (attr.id) {
996
+ result[attr.id] = value;
997
+ }
998
+ }
999
+ }
1000
+ return result;
1001
+ }
1002
+ /**
1003
+ * Parse a single attribute according to its specification.
1004
+ *
1005
+ * @param attr - Attribute specification
1006
+ * @param context - Execution context
1007
+ * @returns Parsed value
1008
+ * @private
1009
+ */
1010
+ parseAttribute(attr, context) {
1011
+ const stream = context.io;
1012
+ if (attr.if) {
1013
+ throw new NotImplementedError("Conditional parsing (if)");
1014
+ }
1015
+ if (attr.pos !== void 0) {
1016
+ const pos = typeof attr.pos === "number" ? attr.pos : 0;
1017
+ stream.seek(pos);
1018
+ }
1019
+ if (attr.io) {
1020
+ throw new NotImplementedError("Custom I/O streams");
1021
+ }
1022
+ if (attr.repeat) {
1023
+ return this.parseRepeated(attr, context);
1024
+ }
1025
+ if (attr.contents) {
1026
+ return this.parseContents(attr, context);
1027
+ }
1028
+ return this.parseValue(attr, context);
1029
+ }
1030
+ /**
1031
+ * Parse a repeated attribute.
1032
+ *
1033
+ * @param attr - Attribute specification with repeat
1034
+ * @param context - Execution context
1035
+ * @returns Array of parsed values
1036
+ * @private
1037
+ */
1038
+ parseRepeated(attr, context) {
1039
+ const result = [];
1040
+ const stream = context.io;
1041
+ switch (attr.repeat) {
1042
+ case "expr": {
1043
+ const count = typeof attr["repeat-expr"] === "number" ? attr["repeat-expr"] : 0;
1044
+ for (let i = 0; i < count; i++) {
1045
+ context.set("_index", i);
1046
+ result.push(this.parseValue(attr, context));
1047
+ }
1048
+ break;
1049
+ }
1050
+ case "eos": {
1051
+ while (!stream.isEof()) {
1052
+ context.set("_index", result.length);
1053
+ result.push(this.parseValue(attr, context));
1054
+ }
1055
+ break;
1056
+ }
1057
+ case "until": {
1058
+ if (!attr["repeat-until"]) {
1059
+ throw new ParseError("repeat-until expression is required");
1060
+ }
1061
+ throw new NotImplementedError("repeat-until");
1062
+ }
1063
+ default:
1064
+ throw new ParseError(`Unknown repeat type: ${attr.repeat}`);
1065
+ }
1066
+ return result;
1067
+ }
1068
+ /**
1069
+ * Parse and validate contents.
1070
+ *
1071
+ * @param attr - Attribute specification with contents
1072
+ * @param context - Execution context
1073
+ * @returns The validated contents
1074
+ * @private
1075
+ */
1076
+ parseContents(attr, context) {
1077
+ const stream = context.io;
1078
+ const expected = attr.contents;
1079
+ if (Array.isArray(expected)) {
1080
+ const bytes = stream.readBytes(expected.length);
1081
+ for (let i = 0; i < expected.length; i++) {
1082
+ if (bytes[i] !== expected[i]) {
1083
+ throw new ValidationError(
1084
+ `Contents mismatch at byte ${i}: expected ${expected[i]}, got ${bytes[i]}`,
1085
+ stream.pos - expected.length + i
1086
+ );
1087
+ }
1088
+ }
1089
+ return bytes;
1090
+ } else {
1091
+ const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
1092
+ const str = stream.readStr(expected.length, encoding);
1093
+ if (str !== expected) {
1094
+ throw new ValidationError(
1095
+ `Contents mismatch: expected "${expected}", got "${str}"`,
1096
+ stream.pos - expected.length
1097
+ );
1098
+ }
1099
+ return str;
1100
+ }
1101
+ }
1102
+ /**
1103
+ * Parse a single value according to its type.
1104
+ *
1105
+ * @param attr - Attribute specification
1106
+ * @param context - Execution context
1107
+ * @returns Parsed value
1108
+ * @private
1109
+ */
1110
+ parseValue(attr, context) {
1111
+ const stream = context.io;
1112
+ const type = attr.type;
1113
+ if (attr.size !== void 0) {
1114
+ const size = typeof attr.size === "number" ? attr.size : 0;
1115
+ if (type === "str" || !type) {
1116
+ const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
1117
+ if (type === "str") {
1118
+ return stream.readStr(size, encoding);
1119
+ } else {
1120
+ return stream.readBytes(size);
1121
+ }
1122
+ } else {
1123
+ const substream = stream.substream(size);
1124
+ return this.parseType(type, substream, context);
1125
+ }
1126
+ }
1127
+ if (attr["size-eos"]) {
1128
+ if (type === "str") {
1129
+ const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
1130
+ const bytes = stream.readBytesFull();
1131
+ return new TextDecoder(encoding).decode(bytes);
1132
+ } else {
1133
+ return stream.readBytesFull();
1134
+ }
1135
+ }
1136
+ if (!type) {
1137
+ throw new ParseError("Attribute must have either type, size, or contents");
1138
+ }
1139
+ return this.parseType(type, stream, context);
1140
+ }
1141
+ /**
1142
+ * Parse a value of a specific type.
1143
+ *
1144
+ * @param type - Type name or switch specification
1145
+ * @param stream - Stream to read from
1146
+ * @param context - Execution context
1147
+ * @returns Parsed value
1148
+ * @private
1149
+ */
1150
+ parseType(type, stream, context) {
1151
+ if (typeof type === "object") {
1152
+ throw new NotImplementedError("Switch types");
1153
+ }
1154
+ if (isBuiltinType(type)) {
1155
+ return this.parseBuiltinType(type, stream, context);
1156
+ }
1157
+ if (this.schema.types && type in this.schema.types) {
1158
+ const typeSchema = this.schema.types[type];
1159
+ const meta = this.schema.meta || this.parentMeta;
1160
+ const interpreter = new _TypeInterpreter(typeSchema, meta);
1161
+ return interpreter.parse(stream, context.current);
1162
+ }
1163
+ throw new ParseError(`Unknown type: ${type}`);
1164
+ }
1165
+ /**
1166
+ * Parse a built-in type.
1167
+ *
1168
+ * @param type - Built-in type name
1169
+ * @param stream - Stream to read from
1170
+ * @param context - Execution context
1171
+ * @returns Parsed value
1172
+ * @private
1173
+ */
1174
+ parseBuiltinType(type, stream, _context) {
1175
+ const base = getBaseType(type);
1176
+ const typeEndian = getTypeEndianness(type);
1177
+ const meta = this.schema.meta || this.parentMeta;
1178
+ const metaEndian = meta?.endian;
1179
+ const endian = typeEndian || (typeof metaEndian === "string" ? metaEndian : "le");
1180
+ if (isIntegerType(type)) {
1181
+ return this.readInteger(base, endian, stream);
1182
+ }
1183
+ if (isFloatType(type)) {
1184
+ return this.readFloat(base, endian, stream);
1185
+ }
1186
+ if (isStringType(type)) {
1187
+ throw new ParseError("String types require size, size-eos, or terminator");
1188
+ }
1189
+ throw new ParseError(`Unknown built-in type: ${type}`);
1190
+ }
1191
+ /**
1192
+ * Read an integer value.
1193
+ *
1194
+ * @param type - Integer type (u1, u2, u4, u8, s1, s2, s4, s8)
1195
+ * @param endian - Endianness
1196
+ * @param stream - Stream to read from
1197
+ * @returns Integer value
1198
+ * @private
1199
+ */
1200
+ readInteger(type, endian, stream) {
1201
+ switch (type) {
1202
+ case "u1":
1203
+ return stream.readU1();
1204
+ case "u2":
1205
+ return endian === "le" ? stream.readU2le() : stream.readU2be();
1206
+ case "u4":
1207
+ return endian === "le" ? stream.readU4le() : stream.readU4be();
1208
+ case "u8":
1209
+ return endian === "le" ? stream.readU8le() : stream.readU8be();
1210
+ case "s1":
1211
+ return stream.readS1();
1212
+ case "s2":
1213
+ return endian === "le" ? stream.readS2le() : stream.readS2be();
1214
+ case "s4":
1215
+ return endian === "le" ? stream.readS4le() : stream.readS4be();
1216
+ case "s8":
1217
+ return endian === "le" ? stream.readS8le() : stream.readS8be();
1218
+ default:
1219
+ throw new ParseError(`Unknown integer type: ${type}`);
1220
+ }
1221
+ }
1222
+ /**
1223
+ * Read a floating point value.
1224
+ *
1225
+ * @param type - Float type (f4, f8)
1226
+ * @param endian - Endianness
1227
+ * @param stream - Stream to read from
1228
+ * @returns Float value
1229
+ * @private
1230
+ */
1231
+ readFloat(type, endian, stream) {
1232
+ switch (type) {
1233
+ case "f4":
1234
+ return endian === "le" ? stream.readF4le() : stream.readF4be();
1235
+ case "f8":
1236
+ return endian === "le" ? stream.readF8le() : stream.readF8be();
1237
+ default:
1238
+ throw new ParseError(`Unknown float type: ${type}`);
1239
+ }
1240
+ }
1241
+ };
1242
+
1243
+ // src/index.ts
1244
+ function parse(ksyYaml, buffer, options = {}) {
1245
+ const { validate = true, strict = false } = options;
1246
+ const parser = new KsyParser();
1247
+ const schema = parser.parse(ksyYaml, { validate, strict });
1248
+ const stream = new KaitaiStream(buffer);
1249
+ const interpreter = new TypeInterpreter(schema);
1250
+ return interpreter.parse(stream);
1251
+ }
1252
+ export {
1253
+ BUILTIN_TYPES,
1254
+ Context,
1255
+ EOFError,
1256
+ KaitaiError,
1257
+ KaitaiStream,
1258
+ KsyParser,
1259
+ NotImplementedError,
1260
+ ParseError,
1261
+ TypeInterpreter,
1262
+ ValidationError,
1263
+ getBaseType,
1264
+ getTypeEndianness,
1265
+ isBuiltinType,
1266
+ isFloatType,
1267
+ isIntegerType,
1268
+ isStringType,
1269
+ parse
1270
+ };
1271
+ /**
1272
+ * @fileoverview Custom error classes for Kaitai Struct parsing and validation
1273
+ * @module utils/errors
1274
+ * @author Fabiano Pinto
1275
+ * @license MIT
1276
+ */
1277
+ /**
1278
+ * @fileoverview String encoding and decoding utilities for binary data
1279
+ * @module utils/encoding
1280
+ * @author Fabiano Pinto
1281
+ * @license MIT
1282
+ */
1283
+ /**
1284
+ * @fileoverview Binary stream reader for Kaitai Struct
1285
+ * @module stream/KaitaiStream
1286
+ * @author Fabiano Pinto
1287
+ * @license MIT
1288
+ */
1289
+ /**
1290
+ * @fileoverview Binary stream reading functionality
1291
+ * @module stream
1292
+ * @author Fabiano Pinto
1293
+ * @license MIT
1294
+ */
1295
+ /**
1296
+ * @fileoverview Type definitions for Kaitai Struct YAML schema (.ksy files)
1297
+ * @module parser/schema
1298
+ * @author Fabiano Pinto
1299
+ * @license MIT
1300
+ */
1301
+ /**
1302
+ * @fileoverview Parser for Kaitai Struct YAML (.ksy) files
1303
+ * @module parser/KsyParser
1304
+ * @author Fabiano Pinto
1305
+ * @license MIT
1306
+ */
1307
+ /**
1308
+ * @fileoverview Parser module for Kaitai Struct YAML files
1309
+ * @module parser
1310
+ * @author Fabiano Pinto
1311
+ * @license MIT
1312
+ */
1313
+ /**
1314
+ * @fileoverview Execution context for Kaitai Struct parsing
1315
+ * @module interpreter/Context
1316
+ * @author Fabiano Pinto
1317
+ * @license MIT
1318
+ */
1319
+ /**
1320
+ * @fileoverview Type interpreter for executing Kaitai Struct schemas
1321
+ * @module interpreter/TypeInterpreter
1322
+ * @author Fabiano Pinto
1323
+ * @license MIT
1324
+ */
1325
+ /**
1326
+ * @fileoverview Interpreter module for executing Kaitai Struct schemas
1327
+ * @module interpreter
1328
+ * @author Fabiano Pinto
1329
+ * @license MIT
1330
+ */
1331
+ /**
1332
+ * @fileoverview Main entry point for kaitai-struct-ts library
1333
+ * @module kaitai-struct-ts
1334
+ * @author Fabiano Pinto
1335
+ * @license MIT
1336
+ * @version 0.2.0
1337
+ *
1338
+ * @description
1339
+ * A runtime interpreter for Kaitai Struct binary format definitions in TypeScript.
1340
+ * Parse any binary data format by providing a .ksy (Kaitai Struct YAML) definition file.
1341
+ *
1342
+ * @example
1343
+ * ```typescript
1344
+ * import { parse } from 'kaitai-struct-ts'
1345
+ *
1346
+ * const ksyDefinition = `
1347
+ * meta:
1348
+ * id: my_format
1349
+ * endian: le
1350
+ * seq:
1351
+ * - id: magic
1352
+ * contents: [0x4D, 0x5A]
1353
+ * - id: version
1354
+ * type: u2
1355
+ * `
1356
+ *
1357
+ * const buffer = new Uint8Array([0x4D, 0x5A, 0x01, 0x00])
1358
+ * const result = parse(ksyDefinition, buffer)
1359
+ * console.log(result.version) // 1
1360
+ * ```
1361
+ */
1362
+ //# sourceMappingURL=index.mjs.map