@k67/kaitai-struct-ts 0.7.3 → 0.9.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/cli.js CHANGED
@@ -5,6 +5,73 @@
5
5
  var import_fs = require("fs");
6
6
  var import_path = require("path");
7
7
  var import_util = require("util");
8
+ var import_yaml2 = require("yaml");
9
+
10
+ // src/parser/schema.ts
11
+ var BUILTIN_TYPES = [
12
+ // Unsigned integers
13
+ "u1",
14
+ "u2",
15
+ "u2le",
16
+ "u2be",
17
+ "u4",
18
+ "u4le",
19
+ "u4be",
20
+ "u8",
21
+ "u8le",
22
+ "u8be",
23
+ // Signed integers
24
+ "s1",
25
+ "s2",
26
+ "s2le",
27
+ "s2be",
28
+ "s4",
29
+ "s4le",
30
+ "s4be",
31
+ "s8",
32
+ "s8le",
33
+ "s8be",
34
+ // Floating point
35
+ "f4",
36
+ "f4le",
37
+ "f4be",
38
+ "f8",
39
+ "f8le",
40
+ "f8be",
41
+ // String
42
+ "str",
43
+ "strz"
44
+ ];
45
+ function isBuiltinType(type) {
46
+ if (BUILTIN_TYPES.includes(type)) return true;
47
+ if (/^b\d+$/.test(type)) return true;
48
+ return false;
49
+ }
50
+ function getTypeEndianness(type) {
51
+ if (type.endsWith("le")) return "le";
52
+ if (type.endsWith("be")) return "be";
53
+ return void 0;
54
+ }
55
+ function getBaseType(type) {
56
+ if (type.endsWith("le") || type.endsWith("be")) {
57
+ return type.slice(0, -2);
58
+ }
59
+ return type;
60
+ }
61
+ function isIntegerType(type) {
62
+ const base = getBaseType(type);
63
+ return /^[us][1248]$/.test(base);
64
+ }
65
+ function isFloatType(type) {
66
+ const base = getBaseType(type);
67
+ return /^f[48]$/.test(base);
68
+ }
69
+ function isStringType(type) {
70
+ return type === "str" || type === "strz";
71
+ }
72
+
73
+ // src/parser/KsyParser.ts
74
+ var import_yaml = require("yaml");
8
75
 
9
76
  // src/utils/errors.ts
10
77
  var KaitaiError = class _KaitaiError extends Error {
@@ -44,964 +111,974 @@ var NotImplementedError = class _NotImplementedError extends KaitaiError {
44
111
  }
45
112
  };
46
113
 
47
- // src/utils/encoding.ts
48
- function decodeString(bytes, encoding) {
49
- const normalizedEncoding = encoding.toLowerCase().replace(/[-_]/g, "");
50
- switch (normalizedEncoding) {
51
- case "utf8":
52
- case "utf-8":
53
- return decodeUtf8(bytes);
54
- case "ascii":
55
- case "usascii":
56
- return decodeAscii(bytes);
57
- case "utf16":
58
- case "utf16le":
59
- case "utf-16le":
60
- return decodeUtf16Le(bytes);
61
- case "utf16be":
62
- case "utf-16be":
63
- return decodeUtf16Be(bytes);
64
- case "latin1":
65
- case "iso88591":
66
- case "iso-8859-1":
67
- return decodeLatin1(bytes);
68
- default:
69
- if (typeof TextDecoder !== "undefined") {
70
- try {
71
- return new TextDecoder(encoding).decode(bytes);
72
- } catch {
73
- throw new Error(`Unsupported encoding: ${encoding}`);
74
- }
75
- }
76
- throw new Error(`Unsupported encoding: ${encoding}`);
77
- }
78
- }
79
- function decodeUtf8(bytes) {
80
- if (typeof TextDecoder !== "undefined") {
81
- return new TextDecoder("utf-8").decode(bytes);
82
- }
83
- let result = "";
84
- let i = 0;
85
- while (i < bytes.length) {
86
- const byte1 = bytes[i++];
87
- if (byte1 < 128) {
88
- result += String.fromCharCode(byte1);
89
- } else if (byte1 < 224) {
90
- const byte2 = bytes[i++];
91
- result += String.fromCharCode((byte1 & 31) << 6 | byte2 & 63);
92
- } else if (byte1 < 240) {
93
- const byte2 = bytes[i++];
94
- const byte3 = bytes[i++];
95
- result += String.fromCharCode(
96
- (byte1 & 15) << 12 | (byte2 & 63) << 6 | byte3 & 63
97
- );
98
- } else {
99
- const byte2 = bytes[i++];
100
- const byte3 = bytes[i++];
101
- const byte4 = bytes[i++];
102
- let codePoint = (byte1 & 7) << 18 | (byte2 & 63) << 12 | (byte3 & 63) << 6 | byte4 & 63;
103
- codePoint -= 65536;
104
- result += String.fromCharCode(
105
- 55296 + (codePoint >> 10),
106
- 56320 + (codePoint & 1023)
107
- );
108
- }
109
- }
110
- return result;
111
- }
112
- function decodeAscii(bytes) {
113
- let result = "";
114
- for (let i = 0; i < bytes.length; i++) {
115
- result += String.fromCharCode(bytes[i] & 127);
116
- }
117
- return result;
118
- }
119
- function decodeLatin1(bytes) {
120
- let result = "";
121
- for (let i = 0; i < bytes.length; i++) {
122
- result += String.fromCharCode(bytes[i]);
123
- }
124
- return result;
125
- }
126
- function decodeUtf16Le(bytes) {
127
- if (typeof TextDecoder !== "undefined") {
128
- return new TextDecoder("utf-16le").decode(bytes);
129
- }
130
- let result = "";
131
- for (let i = 0; i < bytes.length; i += 2) {
132
- const charCode = bytes[i] | bytes[i + 1] << 8;
133
- result += String.fromCharCode(charCode);
134
- }
135
- return result;
136
- }
137
- function decodeUtf16Be(bytes) {
138
- if (typeof TextDecoder !== "undefined") {
139
- return new TextDecoder("utf-16be").decode(bytes);
140
- }
141
- let result = "";
142
- for (let i = 0; i < bytes.length; i += 2) {
143
- const charCode = bytes[i] << 8 | bytes[i + 1];
144
- result += String.fromCharCode(charCode);
145
- }
146
- return result;
147
- }
148
-
149
- // src/stream/KaitaiStream.ts
150
- var KaitaiStream = class _KaitaiStream {
114
+ // src/parser/KsyParser.ts
115
+ var KsyParser = class {
151
116
  /**
152
- * Create a new KaitaiStream from a buffer
153
- * @param buffer - ArrayBuffer or Uint8Array containing the binary data
117
+ * Parse a .ksy YAML string into a typed schema object.
118
+ *
119
+ * @param yaml - YAML string containing the .ksy definition
120
+ * @param options - Parsing options
121
+ * @returns Parsed and validated schema
122
+ * @throws {ParseError} If YAML parsing fails
123
+ * @throws {ValidationError} If schema validation fails
154
124
  */
155
- constructor(buffer) {
156
- this._pos = 0;
157
- this._bits = 0;
158
- this._bitsLeft = 0;
159
- if (buffer instanceof ArrayBuffer) {
160
- this.buffer = new Uint8Array(buffer);
161
- this.view = new DataView(buffer);
162
- } else {
163
- this.buffer = buffer;
164
- this.view = new DataView(
165
- buffer.buffer,
166
- buffer.byteOffset,
167
- buffer.byteLength
125
+ parse(yaml, options = {}) {
126
+ const { validate = true, strict = false } = options;
127
+ let parsed;
128
+ try {
129
+ parsed = (0, import_yaml.parse)(yaml);
130
+ } catch (error) {
131
+ throw new ParseError(
132
+ `Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`
168
133
  );
169
134
  }
170
- }
171
- /**
172
- * Current position in the stream
173
- */
174
- get pos() {
175
- return this._pos;
176
- }
177
- set pos(value) {
178
- this._pos = value;
179
- this._bitsLeft = 0;
180
- }
181
- /**
182
- * Total size of the stream in bytes
183
- */
184
- get size() {
185
- return this.buffer.length;
186
- }
187
- /**
188
- * Check if we've reached the end of the stream
189
- */
190
- isEof() {
191
- return this._pos >= this.buffer.length;
192
- }
193
- /**
194
- * Seek to a specific position in the stream
195
- * @param pos - Position to seek to
196
- */
197
- seek(pos) {
198
- if (pos < 0 || pos > this.buffer.length) {
199
- throw new Error(`Invalid seek position: ${pos}`);
135
+ if (typeof parsed !== "object" || parsed === null) {
136
+ throw new ParseError("KSY file must contain an object");
200
137
  }
201
- this.pos = pos;
138
+ const schema = parsed;
139
+ if (validate) {
140
+ const result = this.validate(schema, { strict });
141
+ if (!result.valid) {
142
+ const errorMessages = result.errors.map((e) => e.message).join("; ");
143
+ throw new ValidationError(
144
+ `Schema validation failed: ${errorMessages}`
145
+ );
146
+ }
147
+ if (result.warnings.length > 0 && !strict) {
148
+ console.warn(
149
+ "Schema validation warnings:",
150
+ result.warnings.map((w) => w.message)
151
+ );
152
+ }
153
+ }
154
+ return schema;
202
155
  }
203
156
  /**
204
- * Ensure we have enough bytes available
205
- * @param count - Number of bytes needed
157
+ * Validate a schema object.
158
+ *
159
+ * @param schema - Schema to validate
160
+ * @param options - Validation options
161
+ * @returns Validation result with errors and warnings
206
162
  */
207
- ensureBytes(count) {
208
- if (this._pos + count > this.buffer.length) {
209
- throw new EOFError(
210
- `Requested ${count} bytes at position ${this._pos}, but only ${this.buffer.length - this._pos} bytes available`,
211
- this._pos
212
- );
163
+ validate(schema, options = {}) {
164
+ const { strict = false, isNested = false } = options;
165
+ const errors = [];
166
+ const warnings = [];
167
+ if (!schema.meta && !isNested) {
168
+ errors.push({
169
+ message: 'Missing required "meta" section',
170
+ path: [],
171
+ code: "MISSING_META"
172
+ });
173
+ } else if (schema.meta) {
174
+ if (!schema.meta.id) {
175
+ errors.push({
176
+ message: 'Missing required "meta.id" field',
177
+ path: ["meta"],
178
+ code: "MISSING_META_ID"
179
+ });
180
+ } else if (typeof schema.meta.id !== "string") {
181
+ errors.push({
182
+ message: '"meta.id" must be a string',
183
+ path: ["meta", "id"],
184
+ code: "INVALID_META_ID_TYPE"
185
+ });
186
+ } else if (!/^[a-z][a-z0-9_]*$/.test(schema.meta.id)) {
187
+ warnings.push({
188
+ message: '"meta.id" should follow snake_case naming convention',
189
+ path: ["meta", "id"],
190
+ code: "META_ID_NAMING"
191
+ });
192
+ }
193
+ if (schema.meta.endian) {
194
+ if (typeof schema.meta.endian === "string" && schema.meta.endian !== "le" && schema.meta.endian !== "be") {
195
+ errors.push({
196
+ message: '"meta.endian" must be "le" or "be"',
197
+ path: ["meta", "endian"],
198
+ code: "INVALID_ENDIAN"
199
+ });
200
+ }
201
+ }
202
+ }
203
+ if (schema.seq) {
204
+ if (!Array.isArray(schema.seq)) {
205
+ errors.push({
206
+ message: '"seq" must be an array',
207
+ path: ["seq"],
208
+ code: "INVALID_SEQ_TYPE"
209
+ });
210
+ } else {
211
+ schema.seq.forEach((attr, index) => {
212
+ this.validateAttribute(
213
+ attr,
214
+ ["seq", String(index)],
215
+ errors,
216
+ warnings,
217
+ strict
218
+ );
219
+ });
220
+ }
213
221
  }
222
+ if (schema.instances) {
223
+ if (typeof schema.instances !== "object") {
224
+ errors.push({
225
+ message: '"instances" must be an object',
226
+ path: ["instances"],
227
+ code: "INVALID_INSTANCES_TYPE"
228
+ });
229
+ } else {
230
+ Object.entries(schema.instances).forEach(([key, instance]) => {
231
+ this.validateAttribute(
232
+ instance,
233
+ ["instances", key],
234
+ errors,
235
+ warnings,
236
+ strict
237
+ );
238
+ });
239
+ }
240
+ }
241
+ if (schema.types) {
242
+ if (typeof schema.types !== "object") {
243
+ errors.push({
244
+ message: '"types" must be an object',
245
+ path: ["types"],
246
+ code: "INVALID_TYPES_TYPE"
247
+ });
248
+ } else {
249
+ Object.entries(schema.types).forEach(([key, type]) => {
250
+ const typeResult = this.validate(type, { ...options, isNested: true });
251
+ errors.push(
252
+ ...typeResult.errors.map((e) => ({
253
+ ...e,
254
+ path: ["types", key, ...e.path]
255
+ }))
256
+ );
257
+ warnings.push(
258
+ ...typeResult.warnings.map((w) => ({
259
+ ...w,
260
+ path: ["types", key, ...w.path]
261
+ }))
262
+ );
263
+ });
264
+ }
265
+ }
266
+ if (schema.enums) {
267
+ if (typeof schema.enums !== "object") {
268
+ errors.push({
269
+ message: '"enums" must be an object',
270
+ path: ["enums"],
271
+ code: "INVALID_ENUMS_TYPE"
272
+ });
273
+ }
274
+ }
275
+ return {
276
+ valid: errors.length === 0 && (strict ? warnings.length === 0 : true),
277
+ errors,
278
+ warnings
279
+ };
214
280
  }
215
- // ==================== Unsigned Integers ====================
216
281
  /**
217
- * Read 1-byte unsigned integer (0 to 255)
282
+ * Validate an attribute specification.
283
+ *
284
+ * @param attr - Attribute to validate
285
+ * @param path - Path to this attribute in the schema
286
+ * @param errors - Array to collect errors
287
+ * @param warnings - Array to collect warnings
288
+ * @param strict - Whether to be strict about warnings
289
+ * @private
218
290
  */
219
- readU1() {
220
- this.ensureBytes(1);
221
- return this.buffer[this._pos++];
291
+ validateAttribute(attr, path, errors, warnings, _strict) {
292
+ if (attr.id && typeof attr.id === "string") {
293
+ if (!/^[a-z][a-z0-9_]*$/.test(attr.id)) {
294
+ warnings.push({
295
+ message: `Field "${attr.id}" should follow snake_case naming convention`,
296
+ path: [...path, "id"],
297
+ code: "FIELD_ID_NAMING"
298
+ });
299
+ }
300
+ }
301
+ if (attr.repeat) {
302
+ if (attr.repeat !== "expr" && attr.repeat !== "eos" && attr.repeat !== "until") {
303
+ errors.push({
304
+ message: '"repeat" must be "expr", "eos", or "until"',
305
+ path: [...path, "repeat"],
306
+ code: "INVALID_REPEAT"
307
+ });
308
+ }
309
+ if (attr.repeat === "expr" && !attr["repeat-expr"]) {
310
+ errors.push({
311
+ message: '"repeat-expr" is required when repeat is "expr"',
312
+ path: [...path, "repeat-expr"],
313
+ code: "MISSING_REPEAT_EXPR"
314
+ });
315
+ }
316
+ if (attr.repeat === "until" && !attr["repeat-until"]) {
317
+ errors.push({
318
+ message: '"repeat-until" is required when repeat is "until"',
319
+ path: [...path, "repeat-until"],
320
+ code: "MISSING_REPEAT_UNTIL"
321
+ });
322
+ }
323
+ }
324
+ if (attr["size-eos"] && attr.size) {
325
+ warnings.push({
326
+ message: '"size-eos" and "size" are mutually exclusive',
327
+ path: [...path],
328
+ code: "SIZE_EOS_WITH_SIZE"
329
+ });
330
+ }
331
+ if (attr.contents) {
332
+ if (!Array.isArray(attr.contents) && typeof attr.contents !== "string") {
333
+ errors.push({
334
+ message: '"contents" must be an array or string',
335
+ path: [...path, "contents"],
336
+ code: "INVALID_CONTENTS_TYPE"
337
+ });
338
+ }
339
+ }
222
340
  }
223
341
  /**
224
- * Read 2-byte unsigned integer, little-endian
225
- */
226
- readU2le() {
227
- this.ensureBytes(2);
228
- const value = this.view.getUint16(this._pos, true);
229
- this._pos += 2;
230
- return value;
342
+ * Parse multiple .ksy files and resolve imports.
343
+ *
344
+ * @param mainYaml - Main .ksy file content
345
+ * @param imports - Map of import names to their YAML content
346
+ * @param options - Parsing options
347
+ * @returns Parsed schema with resolved imports
348
+ * @throws {ParseError} If import resolution fails
349
+ * @example
350
+ * ```typescript
351
+ * const parser = new KsyParser()
352
+ * const imports = new Map([
353
+ * ['/common/riff', riffYamlContent]
354
+ * ])
355
+ * const schema = parser.parseWithImports(wavYaml, imports)
356
+ * ```
357
+ */
358
+ parseWithImports(mainYaml, imports, options = {}) {
359
+ const mainSchema = this.parse(mainYaml, options);
360
+ if (!mainSchema.meta.imports || mainSchema.meta.imports.length === 0) {
361
+ return mainSchema;
362
+ }
363
+ const resolvedTypes = {};
364
+ for (const importPath of mainSchema.meta.imports) {
365
+ if (!imports.has(importPath)) {
366
+ throw new ParseError(
367
+ `Import not found: ${importPath}. Available imports: ${Array.from(imports.keys()).join(", ")}`
368
+ );
369
+ }
370
+ const importYaml = imports.get(importPath);
371
+ const importedSchema = this.parse(importYaml, {
372
+ ...options,
373
+ validate: false
374
+ // Skip validation for imported schemas
375
+ });
376
+ const namespace = this.extractNamespace(importPath);
377
+ if (importedSchema.types) {
378
+ for (const [typeName, typeSchema] of Object.entries(
379
+ importedSchema.types
380
+ )) {
381
+ const qualifiedName = `${namespace}::${typeName}`;
382
+ resolvedTypes[qualifiedName] = typeSchema;
383
+ }
384
+ }
385
+ resolvedTypes[namespace] = {
386
+ meta: importedSchema.meta,
387
+ seq: importedSchema.seq,
388
+ instances: importedSchema.instances,
389
+ types: importedSchema.types,
390
+ enums: importedSchema.enums
391
+ };
392
+ if (importedSchema.enums) {
393
+ if (!mainSchema.enums) {
394
+ mainSchema.enums = {};
395
+ }
396
+ for (const [enumName, enumSpec] of Object.entries(
397
+ importedSchema.enums
398
+ )) {
399
+ const qualifiedEnumName = `${namespace}::${enumName}`;
400
+ mainSchema.enums[qualifiedEnumName] = enumSpec;
401
+ }
402
+ }
403
+ }
404
+ if (Object.keys(resolvedTypes).length > 0) {
405
+ mainSchema.types = {
406
+ ...resolvedTypes,
407
+ ...mainSchema.types
408
+ };
409
+ }
410
+ return mainSchema;
231
411
  }
232
412
  /**
233
- * Read 2-byte unsigned integer, big-endian
413
+ * Extract namespace from import path.
414
+ * Converts paths like '/common/riff' or 'common/riff' to 'riff'.
415
+ *
416
+ * @param importPath - Import path from meta.imports
417
+ * @returns Namespace identifier
418
+ * @private
234
419
  */
235
- readU2be() {
236
- this.ensureBytes(2);
237
- const value = this.view.getUint16(this._pos, false);
238
- this._pos += 2;
239
- return value;
420
+ extractNamespace(importPath) {
421
+ const normalized = importPath.startsWith("/") ? importPath.slice(1) : importPath;
422
+ const segments = normalized.split("/");
423
+ return segments[segments.length - 1];
240
424
  }
425
+ };
426
+
427
+ // src/interpreter/Context.ts
428
+ var Context = class _Context {
241
429
  /**
242
- * Read 4-byte unsigned integer, little-endian
430
+ * Create a new execution context.
431
+ *
432
+ * @param _io - Binary stream being read
433
+ * @param _root - Root object of the parse tree
434
+ * @param _parent - Parent object (optional)
435
+ * @param enums - Enum definitions from schema (optional)
243
436
  */
244
- readU4le() {
245
- this.ensureBytes(4);
246
- const value = this.view.getUint32(this._pos, true);
247
- this._pos += 4;
248
- return value;
437
+ constructor(_io, _root = null, _parent = null, enums) {
438
+ this._io = _io;
439
+ this._root = _root;
440
+ /** Stack of parent objects */
441
+ this.parentStack = [];
442
+ /** Current object being parsed */
443
+ this._current = {};
444
+ /** Enum definitions from schema */
445
+ this._enums = {};
446
+ if (_parent !== null) {
447
+ this.parentStack.push(_parent);
448
+ }
449
+ if (enums) {
450
+ this._enums = enums;
451
+ }
249
452
  }
250
453
  /**
251
- * Read 4-byte unsigned integer, big-endian
454
+ * Get the current I/O stream.
455
+ * Accessible in expressions as `_io`.
456
+ *
457
+ * @returns Current stream
252
458
  */
253
- readU4be() {
254
- this.ensureBytes(4);
255
- const value = this.view.getUint32(this._pos, false);
256
- this._pos += 4;
257
- return value;
258
- }
259
- /**
260
- * Read 8-byte unsigned integer, little-endian
261
- * Returns BigInt for values > Number.MAX_SAFE_INTEGER
262
- */
263
- readU8le() {
264
- this.ensureBytes(8);
265
- const value = this.view.getBigUint64(this._pos, true);
266
- this._pos += 8;
267
- return value;
459
+ get io() {
460
+ return this._io;
268
461
  }
269
462
  /**
270
- * Read 8-byte unsigned integer, big-endian
271
- * Returns BigInt for values > Number.MAX_SAFE_INTEGER
463
+ * Get the root object.
464
+ * Accessible in expressions as `_root`.
465
+ *
466
+ * @returns Root object
272
467
  */
273
- readU8be() {
274
- this.ensureBytes(8);
275
- const value = this.view.getBigUint64(this._pos, false);
276
- this._pos += 8;
277
- return value;
468
+ get root() {
469
+ return this._root;
278
470
  }
279
- // ==================== Signed Integers ====================
280
471
  /**
281
- * Read 1-byte signed integer (-128 to 127)
472
+ * Get the parent object.
473
+ * Accessible in expressions as `_parent`.
474
+ *
475
+ * @returns Parent object or null if at root
282
476
  */
283
- readS1() {
284
- this.ensureBytes(1);
285
- return this.view.getInt8(this._pos++);
477
+ get parent() {
478
+ return this.parentStack.length > 0 ? this.parentStack[this.parentStack.length - 1] : null;
286
479
  }
287
480
  /**
288
- * Read 2-byte signed integer, little-endian
481
+ * Get the current object being parsed.
482
+ * Used to access fields defined earlier in the sequence.
483
+ *
484
+ * @returns Current object
289
485
  */
290
- readS2le() {
291
- this.ensureBytes(2);
292
- const value = this.view.getInt16(this._pos, true);
293
- this._pos += 2;
294
- return value;
486
+ get current() {
487
+ return this._current;
295
488
  }
296
489
  /**
297
- * Read 2-byte signed integer, big-endian
490
+ * Set the current object.
491
+ *
492
+ * @param obj - Object to set as current
298
493
  */
299
- readS2be() {
300
- this.ensureBytes(2);
301
- const value = this.view.getInt16(this._pos, false);
302
- this._pos += 2;
303
- return value;
494
+ set current(obj) {
495
+ this._current = obj;
304
496
  }
305
497
  /**
306
- * Read 4-byte signed integer, little-endian
498
+ * Push a new parent onto the stack.
499
+ * Used when entering a nested type.
500
+ *
501
+ * @param parent - Parent object to push
307
502
  */
308
- readS4le() {
309
- this.ensureBytes(4);
310
- const value = this.view.getInt32(this._pos, true);
311
- this._pos += 4;
312
- return value;
503
+ pushParent(parent) {
504
+ this.parentStack.push(parent);
313
505
  }
314
506
  /**
315
- * Read 4-byte signed integer, big-endian
507
+ * Pop the current parent from the stack.
508
+ * Used when exiting a nested type.
509
+ *
510
+ * @returns The popped parent object
316
511
  */
317
- readS4be() {
318
- this.ensureBytes(4);
319
- const value = this.view.getInt32(this._pos, false);
320
- this._pos += 4;
321
- return value;
512
+ popParent() {
513
+ return this.parentStack.pop();
322
514
  }
323
515
  /**
324
- * Read 8-byte signed integer, little-endian
325
- * Returns BigInt for values outside Number.MAX_SAFE_INTEGER range
516
+ * Get a value from the context by path.
517
+ * Supports special names: _io, _root, _parent, _index.
518
+ *
519
+ * @param name - Name or path to resolve
520
+ * @returns Resolved value
326
521
  */
327
- readS8le() {
328
- this.ensureBytes(8);
329
- const value = this.view.getBigInt64(this._pos, true);
330
- this._pos += 8;
331
- return value;
522
+ resolve(name) {
523
+ switch (name) {
524
+ case "_io":
525
+ return this._io;
526
+ case "_root":
527
+ return this._root;
528
+ case "_parent":
529
+ return this.parent;
530
+ case "_index":
531
+ return this._current["_index"];
532
+ default:
533
+ if (name in this._current) {
534
+ return this._current[name];
535
+ }
536
+ return void 0;
537
+ }
332
538
  }
333
539
  /**
334
- * Read 8-byte signed integer, big-endian
335
- * Returns BigInt for values outside Number.MAX_SAFE_INTEGER range
540
+ * Set a value in the current object.
541
+ *
542
+ * @param name - Field name
543
+ * @param value - Value to set
336
544
  */
337
- readS8be() {
338
- this.ensureBytes(8);
339
- const value = this.view.getBigInt64(this._pos, false);
340
- this._pos += 8;
341
- return value;
545
+ set(name, value) {
546
+ this._current[name] = value;
342
547
  }
343
- // ==================== Floating Point ====================
344
548
  /**
345
- * Read 4-byte IEEE 754 single-precision float, little-endian
549
+ * Get enum value by name.
550
+ * Used for enum access in expressions (EnumName::value).
551
+ *
552
+ * @param enumName - Name of the enum
553
+ * @param valueName - Name of the enum value
554
+ * @returns Enum value (number) or undefined
346
555
  */
347
- readF4le() {
348
- this.ensureBytes(4);
349
- const value = this.view.getFloat32(this._pos, true);
350
- this._pos += 4;
351
- return value;
556
+ getEnumValue(enumName, valueName) {
557
+ const enumDef = this._enums[enumName];
558
+ if (!enumDef) {
559
+ return void 0;
560
+ }
561
+ for (const [key, value] of Object.entries(enumDef)) {
562
+ if (value === valueName) {
563
+ const numKey = Number(key);
564
+ return isNaN(numKey) ? key : numKey;
565
+ }
566
+ }
567
+ return void 0;
352
568
  }
353
569
  /**
354
- * Read 4-byte IEEE 754 single-precision float, big-endian
570
+ * Check if an enum exists.
571
+ *
572
+ * @param enumName - Name of the enum
573
+ * @returns True if enum exists
355
574
  */
356
- readF4be() {
357
- this.ensureBytes(4);
358
- const value = this.view.getFloat32(this._pos, false);
359
- this._pos += 4;
360
- return value;
575
+ hasEnum(enumName) {
576
+ return enumName in this._enums;
361
577
  }
362
578
  /**
363
- * Read 8-byte IEEE 754 double-precision float, little-endian
579
+ * Create a child context for nested parsing.
580
+ * The current object becomes the parent in the child context.
581
+ *
582
+ * @param stream - Stream for the child context (defaults to current stream)
583
+ * @returns New child context
364
584
  */
365
- readF8le() {
366
- this.ensureBytes(8);
367
- const value = this.view.getFloat64(this._pos, true);
368
- this._pos += 8;
369
- return value;
585
+ createChild(stream) {
586
+ const childContext = new _Context(
587
+ stream || this._io,
588
+ this._root || this._current,
589
+ this._current,
590
+ this._enums
591
+ );
592
+ return childContext;
370
593
  }
371
594
  /**
372
- * Read 8-byte IEEE 754 double-precision float, big-endian
595
+ * Clone this context.
596
+ * Creates a shallow copy with the same stream, root, and parent.
597
+ *
598
+ * @returns Cloned context
373
599
  */
374
- readF8be() {
375
- this.ensureBytes(8);
376
- const value = this.view.getFloat64(this._pos, false);
377
- this._pos += 8;
378
- return value;
600
+ clone() {
601
+ const cloned = new _Context(this._io, this._root, this.parent, this._enums);
602
+ cloned._current = { ...this._current };
603
+ cloned.parentStack = [...this.parentStack];
604
+ return cloned;
379
605
  }
380
- // ==================== Byte Arrays ====================
381
- /**
382
- * Read a fixed number of bytes
383
- * @param length - Number of bytes to read
384
- */
385
- readBytes(length) {
386
- this.ensureBytes(length);
387
- const bytes = this.buffer.slice(this._pos, this._pos + length);
388
- this._pos += length;
389
- return bytes;
606
+ };
607
+
608
+ // src/utils/encoding.ts
609
+ function decodeString(bytes, encoding) {
610
+ const normalizedEncoding = encoding.toLowerCase().replace(/[-_]/g, "");
611
+ switch (normalizedEncoding) {
612
+ case "utf8":
613
+ case "utf-8":
614
+ return decodeUtf8(bytes);
615
+ case "ascii":
616
+ case "usascii":
617
+ return decodeAscii(bytes);
618
+ case "utf16":
619
+ case "utf16le":
620
+ case "utf-16le":
621
+ return decodeUtf16Le(bytes);
622
+ case "utf16be":
623
+ case "utf-16be":
624
+ return decodeUtf16Be(bytes);
625
+ case "latin1":
626
+ case "iso88591":
627
+ case "iso-8859-1":
628
+ return decodeLatin1(bytes);
629
+ default:
630
+ if (typeof TextDecoder !== "undefined") {
631
+ try {
632
+ return new TextDecoder(encoding).decode(bytes);
633
+ } catch {
634
+ throw new Error(`Unsupported encoding: ${encoding}`);
635
+ }
636
+ }
637
+ throw new Error(`Unsupported encoding: ${encoding}`);
390
638
  }
391
- /**
392
- * Read all remaining bytes until end of stream
393
- */
394
- readBytesFull() {
395
- const bytes = this.buffer.slice(this._pos);
396
- this._pos = this.buffer.length;
397
- return bytes;
639
+ }
640
+ function decodeUtf8(bytes) {
641
+ if (typeof TextDecoder !== "undefined") {
642
+ return new TextDecoder("utf-8").decode(bytes);
398
643
  }
399
- /**
400
- * Read bytes until a terminator byte is found
401
- * @param term - Terminator byte value
402
- * @param include - Include terminator in result
403
- * @param consume - Consume terminator from stream
404
- * @param eosError - Throw error if EOS reached before terminator
405
- */
406
- readBytesterm(term, include = false, consume = true, eosError = true) {
407
- const start = this._pos;
408
- let end = start;
409
- while (end < this.buffer.length && this.buffer[end] !== term) {
410
- end++;
411
- }
412
- const foundTerm = end < this.buffer.length;
413
- if (!foundTerm && eosError) {
414
- throw new EOFError(
415
- `Terminator byte ${term} not found before end of stream`,
416
- this._pos
644
+ let result = "";
645
+ let i = 0;
646
+ while (i < bytes.length) {
647
+ const byte1 = bytes[i++];
648
+ if (byte1 < 128) {
649
+ result += String.fromCharCode(byte1);
650
+ } else if (byte1 < 224) {
651
+ const byte2 = bytes[i++];
652
+ result += String.fromCharCode((byte1 & 31) << 6 | byte2 & 63);
653
+ } else if (byte1 < 240) {
654
+ const byte2 = bytes[i++];
655
+ const byte3 = bytes[i++];
656
+ result += String.fromCharCode(
657
+ (byte1 & 15) << 12 | (byte2 & 63) << 6 | byte3 & 63
658
+ );
659
+ } else {
660
+ const byte2 = bytes[i++];
661
+ const byte3 = bytes[i++];
662
+ const byte4 = bytes[i++];
663
+ let codePoint = (byte1 & 7) << 18 | (byte2 & 63) << 12 | (byte3 & 63) << 6 | byte4 & 63;
664
+ codePoint -= 65536;
665
+ result += String.fromCharCode(
666
+ 55296 + (codePoint >> 10),
667
+ 56320 + (codePoint & 1023)
417
668
  );
418
669
  }
419
- const includeEnd = include && foundTerm ? end + 1 : end;
420
- const bytes = this.buffer.slice(start, includeEnd);
421
- if (foundTerm && consume) {
422
- this._pos = end + 1;
670
+ }
671
+ return result;
672
+ }
673
+ function decodeAscii(bytes) {
674
+ let result = "";
675
+ for (let i = 0; i < bytes.length; i++) {
676
+ result += String.fromCharCode(bytes[i] & 127);
677
+ }
678
+ return result;
679
+ }
680
+ function decodeLatin1(bytes) {
681
+ let result = "";
682
+ for (let i = 0; i < bytes.length; i++) {
683
+ result += String.fromCharCode(bytes[i]);
684
+ }
685
+ return result;
686
+ }
687
+ function decodeUtf16Le(bytes) {
688
+ if (typeof TextDecoder !== "undefined") {
689
+ return new TextDecoder("utf-16le").decode(bytes);
690
+ }
691
+ let result = "";
692
+ for (let i = 0; i < bytes.length; i += 2) {
693
+ const charCode = bytes[i] | bytes[i + 1] << 8;
694
+ result += String.fromCharCode(charCode);
695
+ }
696
+ return result;
697
+ }
698
+ function decodeUtf16Be(bytes) {
699
+ if (typeof TextDecoder !== "undefined") {
700
+ return new TextDecoder("utf-16be").decode(bytes);
701
+ }
702
+ let result = "";
703
+ for (let i = 0; i < bytes.length; i += 2) {
704
+ const charCode = bytes[i] << 8 | bytes[i + 1];
705
+ result += String.fromCharCode(charCode);
706
+ }
707
+ return result;
708
+ }
709
+
710
+ // src/stream/KaitaiStream.ts
711
+ var KaitaiStream = class _KaitaiStream {
712
+ /**
713
+ * Create a new KaitaiStream from a buffer
714
+ * @param buffer - ArrayBuffer or Uint8Array containing the binary data
715
+ */
716
+ constructor(buffer) {
717
+ this._pos = 0;
718
+ this._bits = 0;
719
+ this._bitsLeft = 0;
720
+ if (buffer instanceof ArrayBuffer) {
721
+ this.buffer = new Uint8Array(buffer);
722
+ this.view = new DataView(buffer);
423
723
  } else {
424
- this._pos = end;
724
+ this.buffer = buffer;
725
+ this.view = new DataView(
726
+ buffer.buffer,
727
+ buffer.byteOffset,
728
+ buffer.byteLength
729
+ );
425
730
  }
426
- return bytes;
427
731
  }
428
- // ==================== Strings ====================
429
732
  /**
430
- * Read a fixed-length string
431
- * @param length - Number of bytes to read
432
- * @param encoding - Character encoding (default: UTF-8)
733
+ * Current position in the stream
433
734
  */
434
- readStr(length, encoding = "UTF-8") {
435
- const bytes = this.readBytes(length);
436
- return decodeString(bytes, encoding);
735
+ get pos() {
736
+ return this._pos;
737
+ }
738
+ set pos(value) {
739
+ this._pos = value;
740
+ this._bitsLeft = 0;
437
741
  }
438
742
  /**
439
- * Read a null-terminated string
440
- * @param encoding - Character encoding (default: UTF-8)
441
- * @param term - Terminator byte (default: 0)
442
- * @param include - Include terminator in result
443
- * @param consume - Consume terminator from stream
444
- * @param eosError - Throw error if EOS reached before terminator
743
+ * Total size of the stream in bytes
445
744
  */
446
- readStrz(encoding = "UTF-8", term = 0, include = false, consume = true, eosError = true) {
447
- const bytes = this.readBytesterm(term, include, consume, eosError);
448
- return decodeString(bytes, encoding);
745
+ get size() {
746
+ return this.buffer.length;
449
747
  }
450
- // ==================== Bit-level Reading ====================
451
748
  /**
452
- * Align bit reading to byte boundary
749
+ * Check if we've reached the end of the stream
453
750
  */
454
- alignToByte() {
455
- this._bitsLeft = 0;
751
+ isEof() {
752
+ return this._pos >= this.buffer.length;
456
753
  }
457
754
  /**
458
- * Read specified number of bits as unsigned integer (big-endian)
459
- * @param n - Number of bits to read (1-64)
755
+ * Seek to a specific position in the stream
756
+ * @param pos - Position to seek to
460
757
  */
461
- readBitsIntBe(n) {
462
- if (n < 1 || n > 64) {
463
- throw new Error(`Invalid bit count: ${n}. Must be between 1 and 64`);
464
- }
465
- let result = 0n;
466
- for (let bitsNeeded = n; bitsNeeded > 0; ) {
467
- if (this._bitsLeft === 0) {
468
- this._bits = this.readU1();
469
- this._bitsLeft = 8;
470
- }
471
- const bitsToRead = Math.min(bitsNeeded, this._bitsLeft);
472
- const mask = (1 << bitsToRead) - 1;
473
- const shift = this._bitsLeft - bitsToRead;
474
- result = result << BigInt(bitsToRead) | BigInt(this._bits >> shift & mask);
475
- this._bitsLeft -= bitsToRead;
476
- bitsNeeded -= bitsToRead;
758
+ seek(pos) {
759
+ if (pos < 0 || pos > this.buffer.length) {
760
+ throw new Error(`Invalid seek position: ${pos}`);
477
761
  }
478
- return result;
762
+ this.pos = pos;
479
763
  }
480
764
  /**
481
- * Read specified number of bits as unsigned integer (little-endian)
482
- * @param n - Number of bits to read (1-64)
765
+ * Ensure we have enough bytes available
766
+ * @param count - Number of bytes needed
483
767
  */
484
- readBitsIntLe(n) {
485
- if (n < 1 || n > 64) {
486
- throw new Error(`Invalid bit count: ${n}. Must be between 1 and 64`);
487
- }
488
- let result = 0n;
489
- let bitPos = 0;
490
- for (let bitsNeeded = n; bitsNeeded > 0; ) {
491
- if (this._bitsLeft === 0) {
492
- this._bits = this.readU1();
493
- this._bitsLeft = 8;
494
- }
495
- const bitsToRead = Math.min(bitsNeeded, this._bitsLeft);
496
- const mask = (1 << bitsToRead) - 1;
497
- result |= BigInt(this._bits & mask) << BigInt(bitPos);
498
- this._bits >>= bitsToRead;
499
- this._bitsLeft -= bitsToRead;
500
- bitsNeeded -= bitsToRead;
501
- bitPos += bitsToRead;
768
+ ensureBytes(count) {
769
+ if (this._pos + count > this.buffer.length) {
770
+ throw new EOFError(
771
+ `Requested ${count} bytes at position ${this._pos}, but only ${this.buffer.length - this._pos} bytes available`,
772
+ this._pos
773
+ );
502
774
  }
503
- return result;
504
775
  }
505
- // ==================== Utility Methods ====================
776
+ // ==================== Unsigned Integers ====================
506
777
  /**
507
- * Get the underlying buffer
778
+ * Read 1-byte unsigned integer (0 to 255)
508
779
  */
509
- getBuffer() {
510
- return this.buffer;
780
+ readU1() {
781
+ this.ensureBytes(1);
782
+ return this.buffer[this._pos++];
511
783
  }
512
784
  /**
513
- * Create a substream from current position with specified size
514
- * @param size - Size of the substream in bytes
785
+ * Read 2-byte unsigned integer, little-endian
515
786
  */
516
- substream(size) {
517
- this.ensureBytes(size);
518
- const subBuffer = this.buffer.slice(this._pos, this._pos + size);
519
- this._pos += size;
520
- return new _KaitaiStream(subBuffer);
787
+ readU2le() {
788
+ this.ensureBytes(2);
789
+ const value = this.view.getUint16(this._pos, true);
790
+ this._pos += 2;
791
+ return value;
521
792
  }
522
- };
523
-
524
- // src/parser/schema.ts
525
- var BUILTIN_TYPES = [
526
- // Unsigned integers
527
- "u1",
528
- "u2",
529
- "u2le",
530
- "u2be",
531
- "u4",
532
- "u4le",
533
- "u4be",
534
- "u8",
535
- "u8le",
536
- "u8be",
537
- // Signed integers
538
- "s1",
539
- "s2",
540
- "s2le",
541
- "s2be",
542
- "s4",
543
- "s4le",
544
- "s4be",
545
- "s8",
546
- "s8le",
547
- "s8be",
548
- // Floating point
549
- "f4",
550
- "f4le",
551
- "f4be",
552
- "f8",
553
- "f8le",
554
- "f8be",
555
- // String
556
- "str",
557
- "strz"
558
- ];
559
- function isBuiltinType(type) {
560
- return BUILTIN_TYPES.includes(type);
561
- }
562
- function getTypeEndianness(type) {
563
- if (type.endsWith("le")) return "le";
564
- if (type.endsWith("be")) return "be";
565
- return void 0;
566
- }
567
- function getBaseType(type) {
568
- if (type.endsWith("le") || type.endsWith("be")) {
569
- return type.slice(0, -2);
793
+ /**
794
+ * Read 2-byte unsigned integer, big-endian
795
+ */
796
+ readU2be() {
797
+ this.ensureBytes(2);
798
+ const value = this.view.getUint16(this._pos, false);
799
+ this._pos += 2;
800
+ return value;
570
801
  }
571
- return type;
572
- }
573
- function isIntegerType(type) {
574
- const base = getBaseType(type);
575
- return /^[us][1248]$/.test(base);
576
- }
577
- function isFloatType(type) {
578
- const base = getBaseType(type);
579
- return /^f[48]$/.test(base);
580
- }
581
- function isStringType(type) {
582
- return type === "str" || type === "strz";
583
- }
584
-
585
- // src/parser/KsyParser.ts
586
- var import_yaml = require("yaml");
587
- var KsyParser = class {
588
802
  /**
589
- * Parse a .ksy YAML string into a typed schema object.
590
- *
591
- * @param yaml - YAML string containing the .ksy definition
592
- * @param options - Parsing options
593
- * @returns Parsed and validated schema
594
- * @throws {ParseError} If YAML parsing fails
595
- * @throws {ValidationError} If schema validation fails
803
+ * Read 4-byte unsigned integer, little-endian
596
804
  */
597
- parse(yaml, options = {}) {
598
- const { validate = true, strict = false } = options;
599
- let parsed;
600
- try {
601
- parsed = (0, import_yaml.parse)(yaml);
602
- } catch (error) {
603
- throw new ParseError(
604
- `Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`
605
- );
606
- }
607
- if (typeof parsed !== "object" || parsed === null) {
608
- throw new ParseError("KSY file must contain an object");
609
- }
610
- const schema = parsed;
611
- if (validate) {
612
- const result = this.validate(schema, { strict });
613
- if (!result.valid) {
614
- const errorMessages = result.errors.map((e) => e.message).join("; ");
615
- throw new ValidationError(
616
- `Schema validation failed: ${errorMessages}`
617
- );
618
- }
619
- if (result.warnings.length > 0 && !strict) {
620
- console.warn(
621
- "Schema validation warnings:",
622
- result.warnings.map((w) => w.message)
623
- );
624
- }
625
- }
626
- return schema;
805
+ readU4le() {
806
+ this.ensureBytes(4);
807
+ const value = this.view.getUint32(this._pos, true);
808
+ this._pos += 4;
809
+ return value;
627
810
  }
628
811
  /**
629
- * Validate a schema object.
630
- *
631
- * @param schema - Schema to validate
632
- * @param options - Validation options
633
- * @returns Validation result with errors and warnings
812
+ * Read 4-byte unsigned integer, big-endian
634
813
  */
635
- validate(schema, options = {}) {
636
- const { strict = false, isNested = false } = options;
637
- const errors = [];
638
- const warnings = [];
639
- if (!schema.meta && !isNested) {
640
- errors.push({
641
- message: 'Missing required "meta" section',
642
- path: [],
643
- code: "MISSING_META"
644
- });
645
- } else if (schema.meta) {
646
- if (!schema.meta.id) {
647
- errors.push({
648
- message: 'Missing required "meta.id" field',
649
- path: ["meta"],
650
- code: "MISSING_META_ID"
651
- });
652
- } else if (typeof schema.meta.id !== "string") {
653
- errors.push({
654
- message: '"meta.id" must be a string',
655
- path: ["meta", "id"],
656
- code: "INVALID_META_ID_TYPE"
657
- });
658
- } else if (!/^[a-z][a-z0-9_]*$/.test(schema.meta.id)) {
659
- warnings.push({
660
- message: '"meta.id" should follow snake_case naming convention',
661
- path: ["meta", "id"],
662
- code: "META_ID_NAMING"
663
- });
664
- }
665
- if (schema.meta.endian) {
666
- if (typeof schema.meta.endian === "string" && schema.meta.endian !== "le" && schema.meta.endian !== "be") {
667
- errors.push({
668
- message: '"meta.endian" must be "le" or "be"',
669
- path: ["meta", "endian"],
670
- code: "INVALID_ENDIAN"
671
- });
672
- }
673
- }
674
- }
675
- if (schema.seq) {
676
- if (!Array.isArray(schema.seq)) {
677
- errors.push({
678
- message: '"seq" must be an array',
679
- path: ["seq"],
680
- code: "INVALID_SEQ_TYPE"
681
- });
682
- } else {
683
- schema.seq.forEach((attr, index) => {
684
- this.validateAttribute(
685
- attr,
686
- ["seq", String(index)],
687
- errors,
688
- warnings,
689
- strict
690
- );
691
- });
692
- }
693
- }
694
- if (schema.instances) {
695
- if (typeof schema.instances !== "object") {
696
- errors.push({
697
- message: '"instances" must be an object',
698
- path: ["instances"],
699
- code: "INVALID_INSTANCES_TYPE"
700
- });
701
- } else {
702
- Object.entries(schema.instances).forEach(([key, instance]) => {
703
- this.validateAttribute(
704
- instance,
705
- ["instances", key],
706
- errors,
707
- warnings,
708
- strict
709
- );
710
- });
711
- }
712
- }
713
- if (schema.types) {
714
- if (typeof schema.types !== "object") {
715
- errors.push({
716
- message: '"types" must be an object',
717
- path: ["types"],
718
- code: "INVALID_TYPES_TYPE"
719
- });
720
- } else {
721
- Object.entries(schema.types).forEach(([key, type]) => {
722
- const typeResult = this.validate(type, { ...options, isNested: true });
723
- errors.push(
724
- ...typeResult.errors.map((e) => ({
725
- ...e,
726
- path: ["types", key, ...e.path]
727
- }))
728
- );
729
- warnings.push(
730
- ...typeResult.warnings.map((w) => ({
731
- ...w,
732
- path: ["types", key, ...w.path]
733
- }))
734
- );
735
- });
736
- }
737
- }
738
- if (schema.enums) {
739
- if (typeof schema.enums !== "object") {
740
- errors.push({
741
- message: '"enums" must be an object',
742
- path: ["enums"],
743
- code: "INVALID_ENUMS_TYPE"
744
- });
745
- }
746
- }
747
- return {
748
- valid: errors.length === 0 && (strict ? warnings.length === 0 : true),
749
- errors,
750
- warnings
751
- };
814
+ readU4be() {
815
+ this.ensureBytes(4);
816
+ const value = this.view.getUint32(this._pos, false);
817
+ this._pos += 4;
818
+ return value;
819
+ }
820
+ /**
821
+ * Read 8-byte unsigned integer, little-endian
822
+ * Returns BigInt for values > Number.MAX_SAFE_INTEGER
823
+ */
824
+ readU8le() {
825
+ this.ensureBytes(8);
826
+ const value = this.view.getBigUint64(this._pos, true);
827
+ this._pos += 8;
828
+ return value;
752
829
  }
753
830
  /**
754
- * Validate an attribute specification.
755
- *
756
- * @param attr - Attribute to validate
757
- * @param path - Path to this attribute in the schema
758
- * @param errors - Array to collect errors
759
- * @param warnings - Array to collect warnings
760
- * @param strict - Whether to be strict about warnings
761
- * @private
831
+ * Read 8-byte unsigned integer, big-endian
832
+ * Returns BigInt for values > Number.MAX_SAFE_INTEGER
762
833
  */
763
- validateAttribute(attr, path, errors, warnings, _strict) {
764
- if (attr.id && typeof attr.id === "string") {
765
- if (!/^[a-z][a-z0-9_]*$/.test(attr.id)) {
766
- warnings.push({
767
- message: `Field "${attr.id}" should follow snake_case naming convention`,
768
- path: [...path, "id"],
769
- code: "FIELD_ID_NAMING"
770
- });
771
- }
772
- }
773
- if (attr.repeat) {
774
- if (attr.repeat !== "expr" && attr.repeat !== "eos" && attr.repeat !== "until") {
775
- errors.push({
776
- message: '"repeat" must be "expr", "eos", or "until"',
777
- path: [...path, "repeat"],
778
- code: "INVALID_REPEAT"
779
- });
780
- }
781
- if (attr.repeat === "expr" && !attr["repeat-expr"]) {
782
- errors.push({
783
- message: '"repeat-expr" is required when repeat is "expr"',
784
- path: [...path, "repeat-expr"],
785
- code: "MISSING_REPEAT_EXPR"
786
- });
787
- }
788
- if (attr.repeat === "until" && !attr["repeat-until"]) {
789
- errors.push({
790
- message: '"repeat-until" is required when repeat is "until"',
791
- path: [...path, "repeat-until"],
792
- code: "MISSING_REPEAT_UNTIL"
793
- });
794
- }
795
- }
796
- if (attr["size-eos"] && attr.size) {
797
- warnings.push({
798
- message: '"size-eos" and "size" are mutually exclusive',
799
- path: [...path],
800
- code: "SIZE_EOS_WITH_SIZE"
801
- });
802
- }
803
- if (attr.contents) {
804
- if (!Array.isArray(attr.contents) && typeof attr.contents !== "string") {
805
- errors.push({
806
- message: '"contents" must be an array or string',
807
- path: [...path, "contents"],
808
- code: "INVALID_CONTENTS_TYPE"
809
- });
810
- }
811
- }
834
+ readU8be() {
835
+ this.ensureBytes(8);
836
+ const value = this.view.getBigUint64(this._pos, false);
837
+ this._pos += 8;
838
+ return value;
812
839
  }
840
+ // ==================== Signed Integers ====================
813
841
  /**
814
- * Parse multiple .ksy files and resolve imports.
815
- *
816
- * @param mainYaml - Main .ksy file content
817
- * @param imports - Map of import names to their YAML content
818
- * @param options - Parsing options
819
- * @returns Parsed schema with resolved imports
842
+ * Read 1-byte signed integer (-128 to 127)
820
843
  */
821
- parseWithImports(mainYaml, _imports, options = {}) {
822
- const mainSchema = this.parse(mainYaml, options);
823
- return mainSchema;
844
+ readS1() {
845
+ this.ensureBytes(1);
846
+ return this.view.getInt8(this._pos++);
824
847
  }
825
- };
826
-
827
- // src/interpreter/Context.ts
828
- var Context = class _Context {
829
848
  /**
830
- * Create a new execution context.
831
- *
832
- * @param _io - Binary stream being read
833
- * @param _root - Root object of the parse tree
834
- * @param _parent - Parent object (optional)
835
- * @param enums - Enum definitions from schema (optional)
849
+ * Read 2-byte signed integer, little-endian
836
850
  */
837
- constructor(_io, _root = null, _parent = null, enums) {
838
- this._io = _io;
839
- this._root = _root;
840
- /** Stack of parent objects */
841
- this.parentStack = [];
842
- /** Current object being parsed */
843
- this._current = {};
844
- /** Enum definitions from schema */
845
- this._enums = {};
846
- if (_parent !== null) {
847
- this.parentStack.push(_parent);
848
- }
849
- if (enums) {
850
- this._enums = enums;
851
- }
851
+ readS2le() {
852
+ this.ensureBytes(2);
853
+ const value = this.view.getInt16(this._pos, true);
854
+ this._pos += 2;
855
+ return value;
852
856
  }
853
857
  /**
854
- * Get the current I/O stream.
855
- * Accessible in expressions as `_io`.
856
- *
857
- * @returns Current stream
858
+ * Read 2-byte signed integer, big-endian
858
859
  */
859
- get io() {
860
- return this._io;
860
+ readS2be() {
861
+ this.ensureBytes(2);
862
+ const value = this.view.getInt16(this._pos, false);
863
+ this._pos += 2;
864
+ return value;
861
865
  }
862
866
  /**
863
- * Get the root object.
864
- * Accessible in expressions as `_root`.
865
- *
866
- * @returns Root object
867
+ * Read 4-byte signed integer, little-endian
867
868
  */
868
- get root() {
869
- return this._root;
869
+ readS4le() {
870
+ this.ensureBytes(4);
871
+ const value = this.view.getInt32(this._pos, true);
872
+ this._pos += 4;
873
+ return value;
870
874
  }
871
875
  /**
872
- * Get the parent object.
873
- * Accessible in expressions as `_parent`.
874
- *
875
- * @returns Parent object or null if at root
876
+ * Read 4-byte signed integer, big-endian
876
877
  */
877
- get parent() {
878
- return this.parentStack.length > 0 ? this.parentStack[this.parentStack.length - 1] : null;
878
+ readS4be() {
879
+ this.ensureBytes(4);
880
+ const value = this.view.getInt32(this._pos, false);
881
+ this._pos += 4;
882
+ return value;
879
883
  }
880
884
  /**
881
- * Get the current object being parsed.
882
- * Used to access fields defined earlier in the sequence.
883
- *
884
- * @returns Current object
885
+ * Read 8-byte signed integer, little-endian
886
+ * Returns BigInt for values outside Number.MAX_SAFE_INTEGER range
885
887
  */
886
- get current() {
887
- return this._current;
888
+ readS8le() {
889
+ this.ensureBytes(8);
890
+ const value = this.view.getBigInt64(this._pos, true);
891
+ this._pos += 8;
892
+ return value;
893
+ }
894
+ /**
895
+ * Read 8-byte signed integer, big-endian
896
+ * Returns BigInt for values outside Number.MAX_SAFE_INTEGER range
897
+ */
898
+ readS8be() {
899
+ this.ensureBytes(8);
900
+ const value = this.view.getBigInt64(this._pos, false);
901
+ this._pos += 8;
902
+ return value;
903
+ }
904
+ // ==================== Floating Point ====================
905
+ /**
906
+ * Read 4-byte IEEE 754 single-precision float, little-endian
907
+ */
908
+ readF4le() {
909
+ this.ensureBytes(4);
910
+ const value = this.view.getFloat32(this._pos, true);
911
+ this._pos += 4;
912
+ return value;
913
+ }
914
+ /**
915
+ * Read 4-byte IEEE 754 single-precision float, big-endian
916
+ */
917
+ readF4be() {
918
+ this.ensureBytes(4);
919
+ const value = this.view.getFloat32(this._pos, false);
920
+ this._pos += 4;
921
+ return value;
922
+ }
923
+ /**
924
+ * Read 8-byte IEEE 754 double-precision float, little-endian
925
+ */
926
+ readF8le() {
927
+ this.ensureBytes(8);
928
+ const value = this.view.getFloat64(this._pos, true);
929
+ this._pos += 8;
930
+ return value;
931
+ }
932
+ /**
933
+ * Read 8-byte IEEE 754 double-precision float, big-endian
934
+ */
935
+ readF8be() {
936
+ this.ensureBytes(8);
937
+ const value = this.view.getFloat64(this._pos, false);
938
+ this._pos += 8;
939
+ return value;
940
+ }
941
+ // ==================== Byte Arrays ====================
942
+ /**
943
+ * Read a fixed number of bytes
944
+ * @param length - Number of bytes to read
945
+ */
946
+ readBytes(length) {
947
+ this.ensureBytes(length);
948
+ const bytes = this.buffer.slice(this._pos, this._pos + length);
949
+ this._pos += length;
950
+ return bytes;
951
+ }
952
+ /**
953
+ * Read all remaining bytes until end of stream
954
+ */
955
+ readBytesFull() {
956
+ const bytes = this.buffer.slice(this._pos);
957
+ this._pos = this.buffer.length;
958
+ return bytes;
959
+ }
960
+ /**
961
+ * Read bytes until a terminator byte is found
962
+ * @param term - Terminator byte value
963
+ * @param include - Include terminator in result
964
+ * @param consume - Consume terminator from stream
965
+ * @param eosError - Throw error if EOS reached before terminator
966
+ */
967
+ readBytesterm(term, include = false, consume = true, eosError = true) {
968
+ const start = this._pos;
969
+ let end = start;
970
+ while (end < this.buffer.length && this.buffer[end] !== term) {
971
+ end++;
972
+ }
973
+ const foundTerm = end < this.buffer.length;
974
+ if (!foundTerm && eosError) {
975
+ throw new EOFError(
976
+ `Terminator byte ${term} not found before end of stream`,
977
+ this._pos
978
+ );
979
+ }
980
+ const includeEnd = include && foundTerm ? end + 1 : end;
981
+ const bytes = this.buffer.slice(start, includeEnd);
982
+ if (foundTerm && consume) {
983
+ this._pos = end + 1;
984
+ } else {
985
+ this._pos = end;
986
+ }
987
+ return bytes;
888
988
  }
989
+ // ==================== Strings ====================
889
990
  /**
890
- * Set the current object.
891
- *
892
- * @param obj - Object to set as current
991
+ * Read a fixed-length string
992
+ * @param length - Number of bytes to read
993
+ * @param encoding - Character encoding (default: UTF-8)
893
994
  */
894
- set current(obj) {
895
- this._current = obj;
995
+ readStr(length, encoding = "UTF-8") {
996
+ const bytes = this.readBytes(length);
997
+ return decodeString(bytes, encoding);
896
998
  }
897
999
  /**
898
- * Push a new parent onto the stack.
899
- * Used when entering a nested type.
900
- *
901
- * @param parent - Parent object to push
1000
+ * Read a null-terminated string
1001
+ * @param encoding - Character encoding (default: UTF-8)
1002
+ * @param term - Terminator byte (default: 0)
1003
+ * @param include - Include terminator in result
1004
+ * @param consume - Consume terminator from stream
1005
+ * @param eosError - Throw error if EOS reached before terminator
902
1006
  */
903
- pushParent(parent) {
904
- this.parentStack.push(parent);
1007
+ readStrz(encoding = "UTF-8", term = 0, include = false, consume = true, eosError = true) {
1008
+ const bytes = this.readBytesterm(term, include, consume, eosError);
1009
+ return decodeString(bytes, encoding);
905
1010
  }
1011
+ // ==================== Bit-level Reading ====================
906
1012
  /**
907
- * Pop the current parent from the stack.
908
- * Used when exiting a nested type.
909
- *
910
- * @returns The popped parent object
1013
+ * Align bit reading to byte boundary
911
1014
  */
912
- popParent() {
913
- return this.parentStack.pop();
1015
+ alignToByte() {
1016
+ this._bitsLeft = 0;
914
1017
  }
915
1018
  /**
916
- * Get a value from the context by path.
917
- * Supports special names: _io, _root, _parent, _index.
918
- *
919
- * @param name - Name or path to resolve
920
- * @returns Resolved value
1019
+ * Read specified number of bits as unsigned integer (big-endian)
1020
+ * @param n - Number of bits to read (1-64)
921
1021
  */
922
- resolve(name) {
923
- switch (name) {
924
- case "_io":
925
- return this._io;
926
- case "_root":
927
- return this._root;
928
- case "_parent":
929
- return this.parent;
930
- case "_index":
931
- return this._current["_index"];
932
- default:
933
- if (name in this._current) {
934
- return this._current[name];
935
- }
936
- return void 0;
1022
+ readBitsIntBe(n) {
1023
+ if (n < 1 || n > 64) {
1024
+ throw new Error(`Invalid bit count: ${n}. Must be between 1 and 64`);
937
1025
  }
1026
+ let result = 0n;
1027
+ for (let bitsNeeded = n; bitsNeeded > 0; ) {
1028
+ if (this._bitsLeft === 0) {
1029
+ this._bits = this.readU1();
1030
+ this._bitsLeft = 8;
1031
+ }
1032
+ const bitsToRead = Math.min(bitsNeeded, this._bitsLeft);
1033
+ const mask = (1 << bitsToRead) - 1;
1034
+ const shift = this._bitsLeft - bitsToRead;
1035
+ result = result << BigInt(bitsToRead) | BigInt(this._bits >> shift & mask);
1036
+ this._bitsLeft -= bitsToRead;
1037
+ bitsNeeded -= bitsToRead;
1038
+ }
1039
+ return result;
938
1040
  }
939
1041
  /**
940
- * Set a value in the current object.
941
- *
942
- * @param name - Field name
943
- * @param value - Value to set
944
- */
945
- set(name, value) {
946
- this._current[name] = value;
947
- }
948
- /**
949
- * Get enum value by name.
950
- * Used for enum access in expressions (EnumName::value).
951
- *
952
- * @param enumName - Name of the enum
953
- * @param valueName - Name of the enum value
954
- * @returns Enum value (number) or undefined
1042
+ * Read specified number of bits as unsigned integer (little-endian)
1043
+ * @param n - Number of bits to read (1-64)
955
1044
  */
956
- getEnumValue(enumName, valueName) {
957
- const enumDef = this._enums[enumName];
958
- if (!enumDef) {
959
- return void 0;
1045
+ readBitsIntLe(n) {
1046
+ if (n < 1 || n > 64) {
1047
+ throw new Error(`Invalid bit count: ${n}. Must be between 1 and 64`);
960
1048
  }
961
- for (const [key, value] of Object.entries(enumDef)) {
962
- if (value === valueName) {
963
- const numKey = Number(key);
964
- return isNaN(numKey) ? key : numKey;
1049
+ let result = 0n;
1050
+ let bitPos = 0;
1051
+ for (let bitsNeeded = n; bitsNeeded > 0; ) {
1052
+ if (this._bitsLeft === 0) {
1053
+ this._bits = this.readU1();
1054
+ this._bitsLeft = 8;
965
1055
  }
1056
+ const bitsToRead = Math.min(bitsNeeded, this._bitsLeft);
1057
+ const mask = (1 << bitsToRead) - 1;
1058
+ result |= BigInt(this._bits & mask) << BigInt(bitPos);
1059
+ this._bits >>= bitsToRead;
1060
+ this._bitsLeft -= bitsToRead;
1061
+ bitsNeeded -= bitsToRead;
1062
+ bitPos += bitsToRead;
966
1063
  }
967
- return void 0;
968
- }
969
- /**
970
- * Check if an enum exists.
971
- *
972
- * @param enumName - Name of the enum
973
- * @returns True if enum exists
974
- */
975
- hasEnum(enumName) {
976
- return enumName in this._enums;
1064
+ return result;
977
1065
  }
1066
+ // ==================== Utility Methods ====================
978
1067
  /**
979
- * Create a child context for nested parsing.
980
- * The current object becomes the parent in the child context.
981
- *
982
- * @param stream - Stream for the child context (defaults to current stream)
983
- * @returns New child context
1068
+ * Get the underlying buffer
984
1069
  */
985
- createChild(stream) {
986
- const childContext = new _Context(
987
- stream || this._io,
988
- this._root || this._current,
989
- this._current,
990
- this._enums
991
- );
992
- return childContext;
1070
+ getBuffer() {
1071
+ return this.buffer;
993
1072
  }
994
1073
  /**
995
- * Clone this context.
996
- * Creates a shallow copy with the same stream, root, and parent.
997
- *
998
- * @returns Cloned context
1074
+ * Create a substream from current position with specified size
1075
+ * @param size - Size of the substream in bytes
999
1076
  */
1000
- clone() {
1001
- const cloned = new _Context(this._io, this._root, this.parent, this._enums);
1002
- cloned._current = { ...this._current };
1003
- cloned.parentStack = [...this.parentStack];
1004
- return cloned;
1077
+ substream(size) {
1078
+ this.ensureBytes(size);
1079
+ const subBuffer = this.buffer.slice(this._pos, this._pos + size);
1080
+ this._pos += size;
1081
+ return new _KaitaiStream(subBuffer);
1005
1082
  }
1006
1083
  };
1007
1084
 
@@ -1132,6 +1209,17 @@ var Lexer = class {
1132
1209
  }
1133
1210
  return createToken("NUMBER" /* NUMBER */, parseInt(value, 16), start);
1134
1211
  }
1212
+ if (this.current === "0" && this.peek() === "b") {
1213
+ value += this.current;
1214
+ this.advance();
1215
+ value += this.current;
1216
+ this.advance();
1217
+ while (this.current !== null && /[01]/.test(this.current)) {
1218
+ value += this.current;
1219
+ this.advance();
1220
+ }
1221
+ return createToken("NUMBER" /* NUMBER */, parseInt(value, 2), start);
1222
+ }
1135
1223
  while (this.current !== null && this.isDigit(this.current)) {
1136
1224
  value += this.current;
1137
1225
  this.advance();
@@ -1343,6 +1431,9 @@ function createMethodCall(object, method, args) {
1343
1431
  function createEnumAccess(enumName, value) {
1344
1432
  return { kind: "EnumAccess", enumName, value };
1345
1433
  }
1434
+ function createArrayLiteral(elements) {
1435
+ return { kind: "ArrayLiteral", elements };
1436
+ }
1346
1437
 
1347
1438
  // src/expression/Parser.ts
1348
1439
  var ExpressionParser = class {
@@ -1639,6 +1730,17 @@ var ExpressionParser = class {
1639
1730
  this.expect("RPAREN" /* RPAREN */, "Expected ) after expression");
1640
1731
  return expr;
1641
1732
  }
1733
+ if (this.match("LBRACKET" /* LBRACKET */)) {
1734
+ const elements = [];
1735
+ if (this.current().type !== "RBRACKET" /* RBRACKET */) {
1736
+ elements.push(this.parseTernary());
1737
+ while (this.match("COMMA" /* COMMA */)) {
1738
+ elements.push(this.parseTernary());
1739
+ }
1740
+ }
1741
+ this.expect("RBRACKET" /* RBRACKET */, "Expected ] after array literal");
1742
+ return createArrayLiteral(elements);
1743
+ }
1642
1744
  throw new ParseError(
1643
1745
  `Unexpected token: ${this.current().type}`,
1644
1746
  this.current().position
@@ -1677,6 +1779,8 @@ var Evaluator = class {
1677
1779
  return this.evaluateMethodCall(n.object, n.method, n.args, context);
1678
1780
  case "EnumAccess":
1679
1781
  return this.evaluateEnumAccess(n.enumName, n.value, context);
1782
+ case "ArrayLiteral":
1783
+ return this.evaluateArrayLiteral(n.elements, context);
1680
1784
  default:
1681
1785
  throw new ParseError(`Unknown AST node kind: ${node.kind}`);
1682
1786
  }
@@ -1865,8 +1969,30 @@ var Evaluator = class {
1865
1969
  if (typeof left === "bigint" || typeof right === "bigint") {
1866
1970
  return BigInt(left) === BigInt(right);
1867
1971
  }
1972
+ const toArray = (v) => {
1973
+ if (Array.isArray(v))
1974
+ return v.map((x) => typeof x === "bigint" ? Number(x) : x);
1975
+ if (v instanceof Uint8Array) return Array.from(v);
1976
+ return null;
1977
+ };
1978
+ const leftArr = toArray(left);
1979
+ const rightArr = toArray(right);
1980
+ if (leftArr && rightArr) {
1981
+ if (leftArr.length !== rightArr.length) return false;
1982
+ for (let i = 0; i < leftArr.length; i++) {
1983
+ if (leftArr[i] !== rightArr[i]) return false;
1984
+ }
1985
+ return true;
1986
+ }
1868
1987
  return left === right;
1869
1988
  }
1989
+ /**
1990
+ * Evaluate an array literal.
1991
+ * @private
1992
+ */
1993
+ evaluateArrayLiteral(elements, context) {
1994
+ return elements.map((e) => this.evaluate(e, context));
1995
+ }
1870
1996
  /**
1871
1997
  * Helper: Convert to number.
1872
1998
  * @private
@@ -1901,7 +2027,8 @@ var Evaluator = class {
1901
2027
 
1902
2028
  // src/expression/index.ts
1903
2029
  function evaluateExpression(expression, context) {
1904
- const lexer = new Lexer(expression);
2030
+ const preprocessed = expression.replace(/\.as<[^>]+>/g, "");
2031
+ const lexer = new Lexer(preprocessed);
1905
2032
  const tokens = lexer.tokenize();
1906
2033
  const parser = new ExpressionParser(tokens);
1907
2034
  const ast = parser.parse();
@@ -1927,6 +2054,18 @@ var TypeInterpreter = class _TypeInterpreter {
1927
2054
  throw new ParseError("Root schema must have meta.id");
1928
2055
  }
1929
2056
  }
2057
+ /**
2058
+ * Safely extract a KaitaiStream from an object that may expose `_io`.
2059
+ * Avoids using `any` casts to satisfy linting.
2060
+ */
2061
+ static getKaitaiIO(val) {
2062
+ if (val && typeof val === "object") {
2063
+ const rec = val;
2064
+ const maybe = rec["_io"];
2065
+ if (maybe instanceof KaitaiStream) return maybe;
2066
+ }
2067
+ return null;
2068
+ }
1930
2069
  /**
1931
2070
  * Parse binary data according to the schema.
1932
2071
  *
@@ -1939,6 +2078,7 @@ var TypeInterpreter = class _TypeInterpreter {
1939
2078
  const result = {};
1940
2079
  const context = new Context(stream, result, parent, this.schema.enums);
1941
2080
  context.current = result;
2081
+ result["_io"] = stream;
1942
2082
  if (typeArgs && this.schema.params) {
1943
2083
  for (let i = 0; i < this.schema.params.length && i < typeArgs.length; i++) {
1944
2084
  const param = this.schema.params[i];
@@ -2041,7 +2181,22 @@ var TypeInterpreter = class _TypeInterpreter {
2041
2181
  * @private
2042
2182
  */
2043
2183
  parseAttribute(attr, context) {
2044
- const stream = context.io;
2184
+ let stream = context.io;
2185
+ if (attr.io !== void 0) {
2186
+ const ioVal = this.evaluateValue(attr.io, context);
2187
+ if (ioVal instanceof KaitaiStream) {
2188
+ stream = ioVal;
2189
+ } else {
2190
+ const kio = _TypeInterpreter.getKaitaiIO(ioVal);
2191
+ if (kio) {
2192
+ stream = kio;
2193
+ } else {
2194
+ throw new ParseError(
2195
+ "io must evaluate to a KaitaiStream or an object with _io"
2196
+ );
2197
+ }
2198
+ }
2199
+ }
2045
2200
  if (attr.if) {
2046
2201
  const condition = this.evaluateValue(attr.if, context);
2047
2202
  if (!condition) {
@@ -2058,9 +2213,6 @@ var TypeInterpreter = class _TypeInterpreter {
2058
2213
  throw new ParseError(`pos must evaluate to a number, got ${typeof pos}`);
2059
2214
  }
2060
2215
  }
2061
- if (attr.io) {
2062
- throw new NotImplementedError("Custom I/O streams");
2063
- }
2064
2216
  if (attr.repeat) {
2065
2217
  return this.parseRepeated(attr, context);
2066
2218
  }
@@ -2157,7 +2309,7 @@ var TypeInterpreter = class _TypeInterpreter {
2157
2309
  }
2158
2310
  return bytes;
2159
2311
  } else {
2160
- const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
2312
+ const encoding = attr.encoding || this.schema.meta?.encoding || this.parentMeta?.encoding || "UTF-8";
2161
2313
  const str = stream.readStr(expected.length, encoding);
2162
2314
  if (str !== expected) {
2163
2315
  throw new ValidationError(
@@ -2186,7 +2338,7 @@ var TypeInterpreter = class _TypeInterpreter {
2186
2338
  throw new ParseError(`size must be non-negative, got ${size}`);
2187
2339
  }
2188
2340
  if (type === "str" || !type) {
2189
- const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
2341
+ const encoding = attr.encoding || this.schema.meta?.encoding || this.parentMeta?.encoding || "UTF-8";
2190
2342
  let data;
2191
2343
  if (type === "str") {
2192
2344
  data = stream.readBytes(size);
@@ -2212,11 +2364,19 @@ var TypeInterpreter = class _TypeInterpreter {
2212
2364
  }
2213
2365
  if (attr["size-eos"]) {
2214
2366
  if (type === "str") {
2215
- const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
2367
+ const encoding = attr.encoding || this.schema.meta?.encoding || this.parentMeta?.encoding || "UTF-8";
2216
2368
  const bytes = stream.readBytesFull();
2217
2369
  return new TextDecoder(encoding).decode(bytes);
2218
2370
  } else {
2219
- return stream.readBytesFull();
2371
+ let bytes = stream.readBytesFull();
2372
+ if (attr.process) {
2373
+ bytes = this.applyProcessing(bytes, attr.process);
2374
+ }
2375
+ if (type) {
2376
+ const sub = new KaitaiStream(bytes);
2377
+ return this.parseType(type, sub, context, attr["type-args"]);
2378
+ }
2379
+ return bytes;
2220
2380
  }
2221
2381
  }
2222
2382
  if (!type) {
@@ -2312,8 +2472,21 @@ var TypeInterpreter = class _TypeInterpreter {
2312
2472
  if (isFloatType(type)) {
2313
2473
  return this.readFloat(base, endian, stream);
2314
2474
  }
2475
+ if (/^b\d+$/.test(type)) {
2476
+ const n = parseInt(type.slice(1), 10);
2477
+ const val = stream.readBitsIntBe(n);
2478
+ const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
2479
+ return val <= maxSafe ? Number(val) : val;
2480
+ }
2315
2481
  if (isStringType(type)) {
2316
- throw new ParseError("String types require size, size-eos, or terminator");
2482
+ const encoding = this.schema.meta?.encoding || "UTF-8";
2483
+ if (type === "strz") {
2484
+ return stream.readStrz(encoding, 0, false, true, true);
2485
+ } else if (type === "str") {
2486
+ throw new ParseError(
2487
+ "str type requires size, size-eos, or terminator attribute"
2488
+ );
2489
+ }
2317
2490
  }
2318
2491
  throw new ParseError(`Unknown built-in type: ${type}`);
2319
2492
  }
@@ -2413,16 +2586,6 @@ var TypeInterpreter = class _TypeInterpreter {
2413
2586
  }
2414
2587
  };
2415
2588
 
2416
- // src/index.ts
2417
- function parse(ksyYaml, buffer, options = {}) {
2418
- const { validate = true, strict = false } = options;
2419
- const parser = new KsyParser();
2420
- const schema = parser.parse(ksyYaml, { validate, strict });
2421
- const stream = new KaitaiStream(buffer);
2422
- const interpreter = new TypeInterpreter(schema);
2423
- return interpreter.parse(stream);
2424
- }
2425
-
2426
2589
  // src/cli.ts
2427
2590
  function getVersion() {
2428
2591
  try {
@@ -2544,6 +2707,43 @@ function readFile(filePath, description) {
2544
2707
  process.exit(1);
2545
2708
  }
2546
2709
  }
2710
+ function loadImports(ksyPath, ksyContent, quiet) {
2711
+ const imports = /* @__PURE__ */ new Map();
2712
+ try {
2713
+ const schema = (0, import_yaml2.parse)(ksyContent);
2714
+ if (!schema.meta?.imports || schema.meta.imports.length === 0) {
2715
+ return imports;
2716
+ }
2717
+ const ksyDir = (0, import_path.dirname)((0, import_path.resolve)(ksyPath));
2718
+ for (const importPath of schema.meta.imports) {
2719
+ const normalizedPath = importPath.startsWith("/") ? importPath.slice(1) : importPath;
2720
+ const importFilePath = (0, import_path.resolve)(ksyDir, "..", normalizedPath + ".ksy");
2721
+ if (!(0, import_fs.existsSync)(importFilePath)) {
2722
+ console.error(
2723
+ `Error: Import file not found: ${importFilePath} (from import: ${importPath})`
2724
+ );
2725
+ process.exit(1);
2726
+ }
2727
+ if (!quiet) {
2728
+ console.error(` Loading import: ${importPath} -> ${importFilePath}`);
2729
+ }
2730
+ const importContent = (0, import_fs.readFileSync)(importFilePath, "utf-8");
2731
+ imports.set(importPath, importContent);
2732
+ const nestedImports = loadImports(importFilePath, importContent, quiet);
2733
+ for (const [nestedPath, nestedContent] of nestedImports) {
2734
+ if (!imports.has(nestedPath)) {
2735
+ imports.set(nestedPath, nestedContent);
2736
+ }
2737
+ }
2738
+ }
2739
+ } catch (error) {
2740
+ console.error(
2741
+ `Error loading imports: ${error instanceof Error ? error.message : String(error)}`
2742
+ );
2743
+ process.exit(1);
2744
+ }
2745
+ return imports;
2746
+ }
2547
2747
  function extractField(obj, path) {
2548
2748
  const parts = path.split(".");
2549
2749
  let current = obj;
@@ -2555,11 +2755,17 @@ function extractField(obj, path) {
2555
2755
  }
2556
2756
  return current;
2557
2757
  }
2758
+ function jsonReplacer(_key, value) {
2759
+ if (typeof value === "bigint") {
2760
+ return value.toString();
2761
+ }
2762
+ return value;
2763
+ }
2558
2764
  function formatOutput(data, format, pretty) {
2559
2765
  if (format === "yaml") {
2560
- return JSON.stringify(data, null, 2).replace(/^{$/gm, "").replace(/^}$/gm, "").replace(/^\s*"([^"]+)":\s*/gm, "$1: ").replace(/,$/gm, "");
2766
+ return JSON.stringify(data, jsonReplacer, 2).replace(/^{$/gm, "").replace(/^}$/gm, "").replace(/^\s*"([^"]+)":\s*/gm, "$1: ").replace(/,$/gm, "");
2561
2767
  }
2562
- return pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
2768
+ return pretty ? JSON.stringify(data, jsonReplacer, 2) : JSON.stringify(data, jsonReplacer);
2563
2769
  }
2564
2770
  function main() {
2565
2771
  const { options, positional } = parseCliArgs();
@@ -2595,16 +2801,30 @@ function main() {
2595
2801
  }
2596
2802
  const ksyContent = readFile(ksyFile, "KSY definition file").toString("utf-8");
2597
2803
  const binaryData = readFile(binaryFile, "Binary file");
2804
+ if (!options.quiet) {
2805
+ console.error("Detecting imports...");
2806
+ }
2807
+ const imports = loadImports(ksyFile, ksyContent, options.quiet || false);
2808
+ if (!options.quiet && imports.size > 0) {
2809
+ console.error(`Loaded ${imports.size} import(s)`);
2810
+ }
2598
2811
  const parseOptions = {
2599
2812
  validate: options.validate,
2600
2813
  strict: options.strict
2601
2814
  };
2602
2815
  if (!options.quiet) {
2603
- console.error("Parsing...");
2816
+ console.error("Parsing schema...");
2604
2817
  }
2605
2818
  let result;
2606
2819
  try {
2607
- result = parse(ksyContent, binaryData, parseOptions);
2820
+ const parser = new KsyParser();
2821
+ const schema = imports.size > 0 ? parser.parseWithImports(ksyContent, imports, parseOptions) : parser.parse(ksyContent, parseOptions);
2822
+ if (!options.quiet) {
2823
+ console.error("Parsing binary data...");
2824
+ }
2825
+ const stream = new KaitaiStream(binaryData);
2826
+ const interpreter = new TypeInterpreter(schema);
2827
+ result = interpreter.parse(stream);
2608
2828
  } catch (error) {
2609
2829
  console.error(
2610
2830
  `Parse error: ${error instanceof Error ? error.message : String(error)}`
@@ -2651,50 +2871,50 @@ function main() {
2651
2871
  }
2652
2872
  main();
2653
2873
  /**
2654
- * @fileoverview Custom error classes for Kaitai Struct parsing and validation
2655
- * @module utils/errors
2874
+ * @fileoverview Type definitions for Kaitai Struct YAML schema (.ksy files)
2875
+ * @module parser/schema
2656
2876
  * @author Fabiano Pinto
2657
2877
  * @license MIT
2658
2878
  */
2659
2879
  /**
2660
- * @fileoverview String encoding and decoding utilities for binary data
2661
- * @module utils/encoding
2880
+ * @fileoverview Custom error classes for Kaitai Struct parsing and validation
2881
+ * @module utils/errors
2662
2882
  * @author Fabiano Pinto
2663
2883
  * @license MIT
2664
2884
  */
2665
2885
  /**
2666
- * @fileoverview Binary stream reader for Kaitai Struct
2667
- * @module stream/KaitaiStream
2886
+ * @fileoverview Parser for Kaitai Struct YAML (.ksy) files
2887
+ * @module parser/KsyParser
2668
2888
  * @author Fabiano Pinto
2669
2889
  * @license MIT
2670
2890
  */
2671
2891
  /**
2672
- * @fileoverview Binary stream reading functionality
2673
- * @module stream
2892
+ * @fileoverview Parser module for Kaitai Struct YAML files
2893
+ * @module parser
2674
2894
  * @author Fabiano Pinto
2675
2895
  * @license MIT
2676
2896
  */
2677
2897
  /**
2678
- * @fileoverview Type definitions for Kaitai Struct YAML schema (.ksy files)
2679
- * @module parser/schema
2898
+ * @fileoverview Execution context for Kaitai Struct parsing
2899
+ * @module interpreter/Context
2680
2900
  * @author Fabiano Pinto
2681
2901
  * @license MIT
2682
2902
  */
2683
2903
  /**
2684
- * @fileoverview Parser for Kaitai Struct YAML (.ksy) files
2685
- * @module parser/KsyParser
2904
+ * @fileoverview String encoding and decoding utilities for binary data
2905
+ * @module utils/encoding
2686
2906
  * @author Fabiano Pinto
2687
2907
  * @license MIT
2688
2908
  */
2689
2909
  /**
2690
- * @fileoverview Parser module for Kaitai Struct YAML files
2691
- * @module parser
2910
+ * @fileoverview Binary stream reader for Kaitai Struct
2911
+ * @module stream/KaitaiStream
2692
2912
  * @author Fabiano Pinto
2693
2913
  * @license MIT
2694
2914
  */
2695
2915
  /**
2696
- * @fileoverview Execution context for Kaitai Struct parsing
2697
- * @module interpreter/Context
2916
+ * @fileoverview Binary stream reading functionality
2917
+ * @module stream
2698
2918
  * @author Fabiano Pinto
2699
2919
  * @license MIT
2700
2920
  */
@@ -2746,37 +2966,6 @@ main();
2746
2966
  * @author Fabiano Pinto
2747
2967
  * @license MIT
2748
2968
  */
2749
- /**
2750
- * @fileoverview Main entry point for kaitai-struct-ts library
2751
- * @module kaitai-struct-ts
2752
- * @author Fabiano Pinto
2753
- * @license MIT
2754
- * @version 0.2.0
2755
- *
2756
- * @description
2757
- * A runtime interpreter for Kaitai Struct binary format definitions in TypeScript.
2758
- * Parse any binary data format by providing a .ksy (Kaitai Struct YAML) definition file.
2759
- *
2760
- * @example
2761
- * ```typescript
2762
- * import { parse } from 'kaitai-struct-ts'
2763
- *
2764
- * const ksyDefinition = `
2765
- * meta:
2766
- * id: my_format
2767
- * endian: le
2768
- * seq:
2769
- * - id: magic
2770
- * contents: [0x4D, 0x5A]
2771
- * - id: version
2772
- * type: u2
2773
- * `
2774
- *
2775
- * const buffer = new Uint8Array([0x4D, 0x5A, 0x01, 0x00])
2776
- * const result = parse(ksyDefinition, buffer)
2777
- * console.log(result.version) // 1
2778
- * ```
2779
- */
2780
2969
  /**
2781
2970
  * @fileoverview CLI utility for parsing binary files with Kaitai Struct definitions
2782
2971
  * @module kaitai-struct-ts/cli