@k67/kaitai-struct-ts 0.8.0 → 0.10.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,1003 +5,1111 @@
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 {
11
- constructor(message, position) {
12
- super(message);
78
+ constructor(message, position, context) {
79
+ super(_KaitaiError.formatMessage(message, position, context));
13
80
  this.position = position;
81
+ this.context = context;
14
82
  this.name = "KaitaiError";
15
83
  Object.setPrototypeOf(this, _KaitaiError.prototype);
16
84
  }
85
+ /**
86
+ * Format error message with position and context.
87
+ * @private
88
+ */
89
+ static formatMessage(message, position, context) {
90
+ let formatted = message;
91
+ if (position !== void 0) {
92
+ formatted += ` (at byte offset 0x${position.toString(16).toUpperCase()})`;
93
+ }
94
+ if (context && context.length > 0) {
95
+ const hexContext = _KaitaiError.formatHexContext(context, position);
96
+ formatted += `
97
+ ${hexContext}`;
98
+ }
99
+ return formatted;
100
+ }
101
+ /**
102
+ * Format hex dump context around error position.
103
+ * @private
104
+ */
105
+ static formatHexContext(data, position) {
106
+ const contextSize = 16;
107
+ const start = Math.max(0, (position ?? 0) - contextSize);
108
+ const end = Math.min(data.length, (position ?? 0) + contextSize);
109
+ const chunk = data.slice(start, end);
110
+ const lines = ["Context:"];
111
+ let offset = start;
112
+ for (let i = 0; i < chunk.length; i += 16) {
113
+ const lineBytes = chunk.slice(i, i + 16);
114
+ const hex = Array.from(lineBytes).map((b) => b.toString(16).padStart(2, "0")).join(" ");
115
+ const ascii = Array.from(lineBytes).map((b) => b >= 32 && b <= 126 ? String.fromCharCode(b) : ".").join("");
116
+ const offsetStr = ` ${(offset + i).toString(16).padStart(8, "0")}`;
117
+ const marker = position !== void 0 && position >= offset + i && position < offset + i + lineBytes.length ? " <--" : "";
118
+ lines.push(`${offsetStr}: ${hex.padEnd(48, " ")} | ${ascii}${marker}`);
119
+ }
120
+ return lines.join("\n");
121
+ }
17
122
  };
18
123
  var ValidationError = class _ValidationError extends KaitaiError {
19
- constructor(message, position) {
20
- super(message, position);
124
+ constructor(message, position, context) {
125
+ super(message, position, context);
21
126
  this.name = "ValidationError";
22
127
  Object.setPrototypeOf(this, _ValidationError.prototype);
23
128
  }
24
129
  };
25
130
  var ParseError = class _ParseError extends KaitaiError {
26
- constructor(message, position) {
27
- super(message, position);
131
+ constructor(message, position, context) {
132
+ super(message, position, context);
28
133
  this.name = "ParseError";
29
134
  Object.setPrototypeOf(this, _ParseError.prototype);
30
135
  }
31
136
  };
32
137
  var EOFError = class _EOFError extends KaitaiError {
33
- constructor(message = "Unexpected end of stream", position) {
34
- super(message, position);
138
+ constructor(message = "Unexpected end of stream", position, context) {
139
+ super(message, position, context);
35
140
  this.name = "EOFError";
36
141
  Object.setPrototypeOf(this, _EOFError.prototype);
37
142
  }
38
143
  };
39
- var NotImplementedError = class _NotImplementedError extends KaitaiError {
40
- constructor(feature) {
41
- super(`Feature not yet implemented: ${feature}`);
42
- this.name = "NotImplementedError";
43
- Object.setPrototypeOf(this, _NotImplementedError.prototype);
44
- }
45
- };
46
-
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
144
 
149
- // src/stream/KaitaiStream.ts
150
- var KaitaiStream = class _KaitaiStream {
145
+ // src/parser/KsyParser.ts
146
+ var KsyParser = class {
151
147
  /**
152
- * Create a new KaitaiStream from a buffer
153
- * @param buffer - ArrayBuffer or Uint8Array containing the binary data
148
+ * Parse a .ksy YAML string into a typed schema object.
149
+ *
150
+ * @param yaml - YAML string containing the .ksy definition
151
+ * @param options - Parsing options
152
+ * @returns Parsed and validated schema
153
+ * @throws {ParseError} If YAML parsing fails
154
+ * @throws {ValidationError} If schema validation fails
154
155
  */
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
156
+ parse(yaml, options = {}) {
157
+ const { validate = true, strict = false } = options;
158
+ let parsed;
159
+ try {
160
+ parsed = (0, import_yaml.parse)(yaml);
161
+ } catch (error) {
162
+ throw new ParseError(
163
+ `Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`
168
164
  );
169
165
  }
166
+ if (typeof parsed !== "object" || parsed === null) {
167
+ throw new ParseError("KSY file must contain an object");
168
+ }
169
+ const schema = parsed;
170
+ if (validate) {
171
+ const result = this.validate(schema, { strict });
172
+ if (!result.valid) {
173
+ const errorMessages = result.errors.map((e) => e.message).join("; ");
174
+ throw new ValidationError(
175
+ `Schema validation failed: ${errorMessages}`
176
+ );
177
+ }
178
+ if (result.warnings.length > 0 && !strict) {
179
+ console.warn(
180
+ "Schema validation warnings:",
181
+ result.warnings.map((w) => w.message)
182
+ );
183
+ }
184
+ }
185
+ return schema;
170
186
  }
171
187
  /**
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
188
+ * Validate a schema object.
189
+ *
190
+ * @param schema - Schema to validate
191
+ * @param options - Validation options
192
+ * @returns Validation result with errors and warnings
189
193
  */
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}`);
194
+ validate(schema, options = {}) {
195
+ const { strict = false, isNested = false } = options;
196
+ const errors = [];
197
+ const warnings = [];
198
+ if (!schema.meta && !isNested) {
199
+ errors.push({
200
+ message: 'Missing required "meta" section',
201
+ path: [],
202
+ code: "MISSING_META"
203
+ });
204
+ } else if (schema.meta) {
205
+ if (!schema.meta.id) {
206
+ errors.push({
207
+ message: 'Missing required "meta.id" field',
208
+ path: ["meta"],
209
+ code: "MISSING_META_ID"
210
+ });
211
+ } else if (typeof schema.meta.id !== "string") {
212
+ errors.push({
213
+ message: '"meta.id" must be a string',
214
+ path: ["meta", "id"],
215
+ code: "INVALID_META_ID_TYPE"
216
+ });
217
+ } else if (!/^[a-z][a-z0-9_]*$/.test(schema.meta.id)) {
218
+ warnings.push({
219
+ message: '"meta.id" should follow snake_case naming convention',
220
+ path: ["meta", "id"],
221
+ code: "META_ID_NAMING"
222
+ });
223
+ }
224
+ if (schema.meta.endian) {
225
+ if (typeof schema.meta.endian === "string" && schema.meta.endian !== "le" && schema.meta.endian !== "be") {
226
+ errors.push({
227
+ message: '"meta.endian" must be "le" or "be"',
228
+ path: ["meta", "endian"],
229
+ code: "INVALID_ENDIAN"
230
+ });
231
+ }
232
+ }
200
233
  }
201
- this.pos = pos;
234
+ if (schema.seq) {
235
+ if (!Array.isArray(schema.seq)) {
236
+ errors.push({
237
+ message: '"seq" must be an array',
238
+ path: ["seq"],
239
+ code: "INVALID_SEQ_TYPE"
240
+ });
241
+ } else {
242
+ schema.seq.forEach((attr, index) => {
243
+ this.validateAttribute(
244
+ attr,
245
+ ["seq", String(index)],
246
+ errors,
247
+ warnings,
248
+ strict
249
+ );
250
+ });
251
+ }
252
+ }
253
+ if (schema.instances) {
254
+ if (typeof schema.instances !== "object") {
255
+ errors.push({
256
+ message: '"instances" must be an object',
257
+ path: ["instances"],
258
+ code: "INVALID_INSTANCES_TYPE"
259
+ });
260
+ } else {
261
+ Object.entries(schema.instances).forEach(([key, instance]) => {
262
+ this.validateAttribute(
263
+ instance,
264
+ ["instances", key],
265
+ errors,
266
+ warnings,
267
+ strict
268
+ );
269
+ });
270
+ }
271
+ }
272
+ if (schema.types) {
273
+ if (typeof schema.types !== "object") {
274
+ errors.push({
275
+ message: '"types" must be an object',
276
+ path: ["types"],
277
+ code: "INVALID_TYPES_TYPE"
278
+ });
279
+ } else {
280
+ Object.entries(schema.types).forEach(([key, type]) => {
281
+ const typeResult = this.validate(type, { ...options, isNested: true });
282
+ errors.push(
283
+ ...typeResult.errors.map((e) => ({
284
+ ...e,
285
+ path: ["types", key, ...e.path]
286
+ }))
287
+ );
288
+ warnings.push(
289
+ ...typeResult.warnings.map((w) => ({
290
+ ...w,
291
+ path: ["types", key, ...w.path]
292
+ }))
293
+ );
294
+ });
295
+ }
296
+ }
297
+ if (schema.enums) {
298
+ if (typeof schema.enums !== "object") {
299
+ errors.push({
300
+ message: '"enums" must be an object',
301
+ path: ["enums"],
302
+ code: "INVALID_ENUMS_TYPE"
303
+ });
304
+ }
305
+ }
306
+ return {
307
+ valid: errors.length === 0 && (strict ? warnings.length === 0 : true),
308
+ errors,
309
+ warnings
310
+ };
202
311
  }
203
312
  /**
204
- * Ensure we have enough bytes available
205
- * @param count - Number of bytes needed
313
+ * Validate an attribute specification.
314
+ *
315
+ * @param attr - Attribute to validate
316
+ * @param path - Path to this attribute in the schema
317
+ * @param errors - Array to collect errors
318
+ * @param warnings - Array to collect warnings
319
+ * @param strict - Whether to be strict about warnings
320
+ * @private
206
321
  */
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
- );
322
+ validateAttribute(attr, path, errors, warnings, _strict) {
323
+ if (attr.id && typeof attr.id === "string") {
324
+ if (!/^[a-z][a-z0-9_]*$/.test(attr.id)) {
325
+ warnings.push({
326
+ message: `Field "${attr.id}" should follow snake_case naming convention`,
327
+ path: [...path, "id"],
328
+ code: "FIELD_ID_NAMING"
329
+ });
330
+ }
331
+ }
332
+ if (attr.repeat) {
333
+ if (attr.repeat !== "expr" && attr.repeat !== "eos" && attr.repeat !== "until") {
334
+ errors.push({
335
+ message: '"repeat" must be "expr", "eos", or "until"',
336
+ path: [...path, "repeat"],
337
+ code: "INVALID_REPEAT"
338
+ });
339
+ }
340
+ if (attr.repeat === "expr" && !attr["repeat-expr"]) {
341
+ errors.push({
342
+ message: '"repeat-expr" is required when repeat is "expr"',
343
+ path: [...path, "repeat-expr"],
344
+ code: "MISSING_REPEAT_EXPR"
345
+ });
346
+ }
347
+ if (attr.repeat === "until" && !attr["repeat-until"]) {
348
+ errors.push({
349
+ message: '"repeat-until" is required when repeat is "until"',
350
+ path: [...path, "repeat-until"],
351
+ code: "MISSING_REPEAT_UNTIL"
352
+ });
353
+ }
354
+ }
355
+ if (attr["size-eos"] && attr.size) {
356
+ warnings.push({
357
+ message: '"size-eos" and "size" are mutually exclusive',
358
+ path: [...path],
359
+ code: "SIZE_EOS_WITH_SIZE"
360
+ });
361
+ }
362
+ if (attr.contents) {
363
+ if (!Array.isArray(attr.contents) && typeof attr.contents !== "string") {
364
+ errors.push({
365
+ message: '"contents" must be an array or string',
366
+ path: [...path, "contents"],
367
+ code: "INVALID_CONTENTS_TYPE"
368
+ });
369
+ }
213
370
  }
214
371
  }
215
- // ==================== Unsigned Integers ====================
216
372
  /**
217
- * Read 1-byte unsigned integer (0 to 255)
218
- */
219
- readU1() {
220
- this.ensureBytes(1);
221
- return this.buffer[this._pos++];
373
+ * Parse multiple .ksy files and resolve imports.
374
+ *
375
+ * @param mainYaml - Main .ksy file content
376
+ * @param imports - Map of import names to their YAML content
377
+ * @param options - Parsing options
378
+ * @returns Parsed schema with resolved imports
379
+ * @throws {ParseError} If import resolution fails
380
+ * @example
381
+ * ```typescript
382
+ * const parser = new KsyParser()
383
+ * const imports = new Map([
384
+ * ['/common/riff', riffYamlContent]
385
+ * ])
386
+ * const schema = parser.parseWithImports(wavYaml, imports)
387
+ * ```
388
+ */
389
+ parseWithImports(mainYaml, imports, options = {}) {
390
+ const mainSchema = this.parse(mainYaml, options);
391
+ if (!mainSchema.meta.imports || mainSchema.meta.imports.length === 0) {
392
+ return mainSchema;
393
+ }
394
+ const resolvedTypes = {};
395
+ for (const importPath of mainSchema.meta.imports) {
396
+ if (!imports.has(importPath)) {
397
+ throw new ParseError(
398
+ `Import not found: ${importPath}. Available imports: ${Array.from(imports.keys()).join(", ")}`
399
+ );
400
+ }
401
+ const importYaml = imports.get(importPath);
402
+ const importedSchema = this.parse(importYaml, {
403
+ ...options,
404
+ validate: false
405
+ // Skip validation for imported schemas
406
+ });
407
+ const namespace = this.extractNamespace(importPath);
408
+ if (importedSchema.types) {
409
+ for (const [typeName, typeSchema] of Object.entries(
410
+ importedSchema.types
411
+ )) {
412
+ const qualifiedName = `${namespace}::${typeName}`;
413
+ resolvedTypes[qualifiedName] = typeSchema;
414
+ }
415
+ }
416
+ resolvedTypes[namespace] = {
417
+ meta: importedSchema.meta,
418
+ seq: importedSchema.seq,
419
+ instances: importedSchema.instances,
420
+ types: importedSchema.types,
421
+ enums: importedSchema.enums
422
+ };
423
+ if (importedSchema.enums) {
424
+ if (!mainSchema.enums) {
425
+ mainSchema.enums = {};
426
+ }
427
+ for (const [enumName, enumSpec] of Object.entries(
428
+ importedSchema.enums
429
+ )) {
430
+ const qualifiedEnumName = `${namespace}::${enumName}`;
431
+ mainSchema.enums[qualifiedEnumName] = enumSpec;
432
+ }
433
+ }
434
+ }
435
+ if (Object.keys(resolvedTypes).length > 0) {
436
+ mainSchema.types = {
437
+ ...resolvedTypes,
438
+ ...mainSchema.types
439
+ };
440
+ }
441
+ return mainSchema;
222
442
  }
223
443
  /**
224
- * Read 2-byte unsigned integer, little-endian
444
+ * Extract namespace from import path.
445
+ * Converts paths like '/common/riff' or 'common/riff' to 'riff'.
446
+ *
447
+ * @param importPath - Import path from meta.imports
448
+ * @returns Namespace identifier
449
+ * @private
225
450
  */
226
- readU2le() {
227
- this.ensureBytes(2);
228
- const value = this.view.getUint16(this._pos, true);
229
- this._pos += 2;
230
- return value;
451
+ extractNamespace(importPath) {
452
+ const normalized = importPath.startsWith("/") ? importPath.slice(1) : importPath;
453
+ const segments = normalized.split("/");
454
+ return segments[segments.length - 1];
231
455
  }
456
+ };
457
+
458
+ // src/interpreter/Context.ts
459
+ var Context = class _Context {
232
460
  /**
233
- * Read 2-byte unsigned integer, big-endian
461
+ * Create a new execution context.
462
+ *
463
+ * @param _io - Binary stream being read
464
+ * @param _root - Root object of the parse tree
465
+ * @param _parent - Parent object (optional)
466
+ * @param enums - Enum definitions from schema (optional)
234
467
  */
235
- readU2be() {
236
- this.ensureBytes(2);
237
- const value = this.view.getUint16(this._pos, false);
238
- this._pos += 2;
239
- return value;
468
+ constructor(_io, _root = null, _parent = null, enums) {
469
+ this._io = _io;
470
+ this._root = _root;
471
+ /** Stack of parent objects */
472
+ this.parentStack = [];
473
+ /** Current object being parsed */
474
+ this._current = {};
475
+ /** Enum definitions from schema */
476
+ this._enums = {};
477
+ if (_parent !== null) {
478
+ this.parentStack.push(_parent);
479
+ }
480
+ if (enums) {
481
+ this._enums = enums;
482
+ }
240
483
  }
241
484
  /**
242
- * Read 4-byte unsigned integer, little-endian
243
- */
244
- readU4le() {
245
- this.ensureBytes(4);
246
- const value = this.view.getUint32(this._pos, true);
247
- this._pos += 4;
248
- return value;
249
- }
250
- /**
251
- * Read 4-byte unsigned integer, big-endian
485
+ * Get the current I/O stream.
486
+ * Accessible in expressions as `_io`.
487
+ *
488
+ * @returns Current stream
252
489
  */
253
- readU4be() {
254
- this.ensureBytes(4);
255
- const value = this.view.getUint32(this._pos, false);
256
- this._pos += 4;
257
- return value;
490
+ get io() {
491
+ return this._io;
258
492
  }
259
493
  /**
260
- * Read 8-byte unsigned integer, little-endian
261
- * Returns BigInt for values > Number.MAX_SAFE_INTEGER
494
+ * Get the root object.
495
+ * Accessible in expressions as `_root`.
496
+ *
497
+ * @returns Root object
262
498
  */
263
- readU8le() {
264
- this.ensureBytes(8);
265
- const value = this.view.getBigUint64(this._pos, true);
266
- this._pos += 8;
267
- return value;
499
+ get root() {
500
+ return this._root;
268
501
  }
269
502
  /**
270
- * Read 8-byte unsigned integer, big-endian
271
- * Returns BigInt for values > Number.MAX_SAFE_INTEGER
503
+ * Get the parent object.
504
+ * Accessible in expressions as `_parent`.
505
+ *
506
+ * @returns Parent object or null if at root
272
507
  */
273
- readU8be() {
274
- this.ensureBytes(8);
275
- const value = this.view.getBigUint64(this._pos, false);
276
- this._pos += 8;
277
- return value;
508
+ get parent() {
509
+ return this.parentStack.length > 0 ? this.parentStack[this.parentStack.length - 1] : null;
278
510
  }
279
- // ==================== Signed Integers ====================
280
511
  /**
281
- * Read 1-byte signed integer (-128 to 127)
512
+ * Get the current object being parsed.
513
+ * Used to access fields defined earlier in the sequence.
514
+ *
515
+ * @returns Current object
282
516
  */
283
- readS1() {
284
- this.ensureBytes(1);
285
- return this.view.getInt8(this._pos++);
517
+ get current() {
518
+ return this._current;
286
519
  }
287
520
  /**
288
- * Read 2-byte signed integer, little-endian
521
+ * Set the current object.
522
+ *
523
+ * @param obj - Object to set as current
289
524
  */
290
- readS2le() {
291
- this.ensureBytes(2);
292
- const value = this.view.getInt16(this._pos, true);
293
- this._pos += 2;
294
- return value;
525
+ set current(obj) {
526
+ this._current = obj;
295
527
  }
296
528
  /**
297
- * Read 2-byte signed integer, big-endian
529
+ * Push a new parent onto the stack.
530
+ * Used when entering a nested type.
531
+ *
532
+ * @param parent - Parent object to push
298
533
  */
299
- readS2be() {
300
- this.ensureBytes(2);
301
- const value = this.view.getInt16(this._pos, false);
302
- this._pos += 2;
303
- return value;
534
+ pushParent(parent) {
535
+ this.parentStack.push(parent);
304
536
  }
305
537
  /**
306
- * Read 4-byte signed integer, little-endian
538
+ * Pop the current parent from the stack.
539
+ * Used when exiting a nested type.
540
+ *
541
+ * @returns The popped parent object
307
542
  */
308
- readS4le() {
309
- this.ensureBytes(4);
310
- const value = this.view.getInt32(this._pos, true);
311
- this._pos += 4;
312
- return value;
543
+ popParent() {
544
+ return this.parentStack.pop();
313
545
  }
314
546
  /**
315
- * Read 4-byte signed integer, big-endian
547
+ * Get a value from the context by path.
548
+ * Supports special names: _io, _root, _parent, _index.
549
+ *
550
+ * @param name - Name or path to resolve
551
+ * @returns Resolved value
316
552
  */
317
- readS4be() {
318
- this.ensureBytes(4);
319
- const value = this.view.getInt32(this._pos, false);
320
- this._pos += 4;
321
- return value;
553
+ resolve(name) {
554
+ switch (name) {
555
+ case "_io":
556
+ return this._io;
557
+ case "_root":
558
+ return this._root;
559
+ case "_parent":
560
+ return this.parent;
561
+ case "_index":
562
+ return this._current["_index"];
563
+ default:
564
+ if (name in this._current) {
565
+ return this._current[name];
566
+ }
567
+ return void 0;
568
+ }
322
569
  }
323
570
  /**
324
- * Read 8-byte signed integer, little-endian
325
- * Returns BigInt for values outside Number.MAX_SAFE_INTEGER range
571
+ * Set a value in the current object.
572
+ *
573
+ * @param name - Field name
574
+ * @param value - Value to set
326
575
  */
327
- readS8le() {
328
- this.ensureBytes(8);
329
- const value = this.view.getBigInt64(this._pos, true);
330
- this._pos += 8;
331
- return value;
576
+ set(name, value) {
577
+ this._current[name] = value;
332
578
  }
333
579
  /**
334
- * Read 8-byte signed integer, big-endian
335
- * Returns BigInt for values outside Number.MAX_SAFE_INTEGER range
580
+ * Get enum value by name.
581
+ * Used for enum access in expressions (EnumName::value).
582
+ *
583
+ * @param enumName - Name of the enum
584
+ * @param valueName - Name of the enum value
585
+ * @returns Enum value (number) or undefined
336
586
  */
337
- readS8be() {
338
- this.ensureBytes(8);
339
- const value = this.view.getBigInt64(this._pos, false);
340
- this._pos += 8;
341
- return value;
587
+ getEnumValue(enumName, valueName) {
588
+ const enumDef = this._enums[enumName];
589
+ if (!enumDef) {
590
+ return void 0;
591
+ }
592
+ for (const [key, value] of Object.entries(enumDef)) {
593
+ if (value === valueName) {
594
+ const numKey = Number(key);
595
+ return isNaN(numKey) ? key : numKey;
596
+ }
597
+ }
598
+ return void 0;
342
599
  }
343
- // ==================== Floating Point ====================
344
600
  /**
345
- * Read 4-byte IEEE 754 single-precision float, little-endian
601
+ * Check if an enum exists.
602
+ *
603
+ * @param enumName - Name of the enum
604
+ * @returns True if enum exists
346
605
  */
347
- readF4le() {
348
- this.ensureBytes(4);
349
- const value = this.view.getFloat32(this._pos, true);
350
- this._pos += 4;
351
- return value;
606
+ hasEnum(enumName) {
607
+ return enumName in this._enums;
352
608
  }
353
609
  /**
354
- * Read 4-byte IEEE 754 single-precision float, big-endian
610
+ * Create a child context for nested parsing.
611
+ * The current object becomes the parent in the child context.
612
+ *
613
+ * @param stream - Stream for the child context (defaults to current stream)
614
+ * @returns New child context
355
615
  */
356
- readF4be() {
357
- this.ensureBytes(4);
358
- const value = this.view.getFloat32(this._pos, false);
359
- this._pos += 4;
360
- return value;
616
+ createChild(stream) {
617
+ const childContext = new _Context(
618
+ stream || this._io,
619
+ this._root || this._current,
620
+ this._current,
621
+ this._enums
622
+ );
623
+ return childContext;
361
624
  }
362
625
  /**
363
- * Read 8-byte IEEE 754 double-precision float, little-endian
626
+ * Clone this context.
627
+ * Creates a shallow copy with the same stream, root, and parent.
628
+ *
629
+ * @returns Cloned context
364
630
  */
365
- readF8le() {
366
- this.ensureBytes(8);
367
- const value = this.view.getFloat64(this._pos, true);
368
- this._pos += 8;
369
- return value;
631
+ clone() {
632
+ const cloned = new _Context(this._io, this._root, this.parent, this._enums);
633
+ cloned._current = { ...this._current };
634
+ cloned.parentStack = [...this.parentStack];
635
+ return cloned;
370
636
  }
371
- /**
372
- * Read 8-byte IEEE 754 double-precision float, big-endian
373
- */
374
- readF8be() {
375
- this.ensureBytes(8);
376
- const value = this.view.getFloat64(this._pos, false);
377
- this._pos += 8;
378
- return value;
637
+ };
638
+
639
+ // src/utils/encoding.ts
640
+ function decodeString(bytes, encoding) {
641
+ const normalizedEncoding = encoding.toLowerCase().replace(/[-_]/g, "");
642
+ switch (normalizedEncoding) {
643
+ case "utf8":
644
+ case "utf-8":
645
+ return decodeUtf8(bytes);
646
+ case "ascii":
647
+ case "usascii":
648
+ return decodeAscii(bytes);
649
+ case "utf16":
650
+ case "utf16le":
651
+ case "utf-16le":
652
+ return decodeUtf16Le(bytes);
653
+ case "utf16be":
654
+ case "utf-16be":
655
+ return decodeUtf16Be(bytes);
656
+ case "latin1":
657
+ case "iso88591":
658
+ case "iso-8859-1":
659
+ return decodeLatin1(bytes);
660
+ default:
661
+ if (typeof TextDecoder !== "undefined") {
662
+ try {
663
+ return new TextDecoder(encoding).decode(bytes);
664
+ } catch {
665
+ throw new Error(`Unsupported encoding: ${encoding}`);
666
+ }
667
+ }
668
+ throw new Error(`Unsupported encoding: ${encoding}`);
379
669
  }
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;
670
+ }
671
+ function decodeUtf8(bytes) {
672
+ if (typeof TextDecoder !== "undefined") {
673
+ return new TextDecoder("utf-8").decode(bytes);
390
674
  }
675
+ let result = "";
676
+ let i = 0;
677
+ while (i < bytes.length) {
678
+ const byte1 = bytes[i++];
679
+ if (byte1 < 128) {
680
+ result += String.fromCharCode(byte1);
681
+ } else if (byte1 < 224) {
682
+ const byte2 = bytes[i++];
683
+ result += String.fromCharCode((byte1 & 31) << 6 | byte2 & 63);
684
+ } else if (byte1 < 240) {
685
+ const byte2 = bytes[i++];
686
+ const byte3 = bytes[i++];
687
+ result += String.fromCharCode(
688
+ (byte1 & 15) << 12 | (byte2 & 63) << 6 | byte3 & 63
689
+ );
690
+ } else {
691
+ const byte2 = bytes[i++];
692
+ const byte3 = bytes[i++];
693
+ const byte4 = bytes[i++];
694
+ let codePoint = (byte1 & 7) << 18 | (byte2 & 63) << 12 | (byte3 & 63) << 6 | byte4 & 63;
695
+ codePoint -= 65536;
696
+ result += String.fromCharCode(
697
+ 55296 + (codePoint >> 10),
698
+ 56320 + (codePoint & 1023)
699
+ );
700
+ }
701
+ }
702
+ return result;
703
+ }
704
+ function decodeAscii(bytes) {
705
+ let result = "";
706
+ for (let i = 0; i < bytes.length; i++) {
707
+ result += String.fromCharCode(bytes[i] & 127);
708
+ }
709
+ return result;
710
+ }
711
+ function decodeLatin1(bytes) {
712
+ let result = "";
713
+ for (let i = 0; i < bytes.length; i++) {
714
+ result += String.fromCharCode(bytes[i]);
715
+ }
716
+ return result;
717
+ }
718
+ function decodeUtf16Le(bytes) {
719
+ if (typeof TextDecoder !== "undefined") {
720
+ return new TextDecoder("utf-16le").decode(bytes);
721
+ }
722
+ let result = "";
723
+ for (let i = 0; i < bytes.length; i += 2) {
724
+ const charCode = bytes[i] | bytes[i + 1] << 8;
725
+ result += String.fromCharCode(charCode);
726
+ }
727
+ return result;
728
+ }
729
+ function decodeUtf16Be(bytes) {
730
+ if (typeof TextDecoder !== "undefined") {
731
+ return new TextDecoder("utf-16be").decode(bytes);
732
+ }
733
+ let result = "";
734
+ for (let i = 0; i < bytes.length; i += 2) {
735
+ const charCode = bytes[i] << 8 | bytes[i + 1];
736
+ result += String.fromCharCode(charCode);
737
+ }
738
+ return result;
739
+ }
740
+
741
+ // src/stream/KaitaiStream.ts
742
+ var KaitaiStream = class _KaitaiStream {
391
743
  /**
392
- * Read all remaining bytes until end of stream
744
+ * Create a new KaitaiStream from a buffer
745
+ * @param buffer - ArrayBuffer or Uint8Array containing the binary data
393
746
  */
394
- readBytesFull() {
395
- const bytes = this.buffer.slice(this._pos);
396
- this._pos = this.buffer.length;
397
- return bytes;
747
+ constructor(buffer) {
748
+ this._pos = 0;
749
+ this._bits = 0;
750
+ this._bitsLeft = 0;
751
+ if (buffer instanceof ArrayBuffer) {
752
+ this.buffer = new Uint8Array(buffer);
753
+ this.view = new DataView(buffer);
754
+ } else {
755
+ this.buffer = buffer;
756
+ this.view = new DataView(
757
+ buffer.buffer,
758
+ buffer.byteOffset,
759
+ buffer.byteLength
760
+ );
761
+ }
398
762
  }
399
763
  /**
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
764
+ * Current position in the stream
405
765
  */
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++;
766
+ get pos() {
767
+ return this._pos;
768
+ }
769
+ set pos(value) {
770
+ this._pos = value;
771
+ this._bitsLeft = 0;
772
+ }
773
+ /**
774
+ * Total size of the stream in bytes
775
+ */
776
+ get size() {
777
+ return this.buffer.length;
778
+ }
779
+ /**
780
+ * Check if we've reached the end of the stream
781
+ */
782
+ isEof() {
783
+ return this._pos >= this.buffer.length;
784
+ }
785
+ /**
786
+ * Seek to a specific position in the stream
787
+ * @param pos - Position to seek to
788
+ */
789
+ seek(pos) {
790
+ if (pos < 0 || pos > this.buffer.length) {
791
+ throw new Error(`Invalid seek position: ${pos}`);
411
792
  }
412
- const foundTerm = end < this.buffer.length;
413
- if (!foundTerm && eosError) {
793
+ this.pos = pos;
794
+ }
795
+ /**
796
+ * Ensure we have enough bytes available
797
+ * @param count - Number of bytes needed
798
+ */
799
+ ensureBytes(count) {
800
+ if (this._pos + count > this.buffer.length) {
414
801
  throw new EOFError(
415
- `Terminator byte ${term} not found before end of stream`,
802
+ `Requested ${count} bytes at position ${this._pos}, but only ${this.buffer.length - this._pos} bytes available`,
416
803
  this._pos
417
804
  );
418
805
  }
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;
423
- } else {
424
- this._pos = end;
425
- }
426
- return bytes;
427
806
  }
428
- // ==================== Strings ====================
807
+ // ==================== Unsigned Integers ====================
429
808
  /**
430
- * Read a fixed-length string
431
- * @param length - Number of bytes to read
432
- * @param encoding - Character encoding (default: UTF-8)
809
+ * Read 1-byte unsigned integer (0 to 255)
433
810
  */
434
- readStr(length, encoding = "UTF-8") {
435
- const bytes = this.readBytes(length);
436
- return decodeString(bytes, encoding);
811
+ readU1() {
812
+ this.ensureBytes(1);
813
+ return this.buffer[this._pos++];
437
814
  }
438
815
  /**
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
816
+ * Read 2-byte unsigned integer, little-endian
445
817
  */
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);
818
+ readU2le() {
819
+ this.ensureBytes(2);
820
+ const value = this.view.getUint16(this._pos, true);
821
+ this._pos += 2;
822
+ return value;
449
823
  }
450
- // ==================== Bit-level Reading ====================
451
824
  /**
452
- * Align bit reading to byte boundary
825
+ * Read 2-byte unsigned integer, big-endian
453
826
  */
454
- alignToByte() {
455
- this._bitsLeft = 0;
827
+ readU2be() {
828
+ this.ensureBytes(2);
829
+ const value = this.view.getUint16(this._pos, false);
830
+ this._pos += 2;
831
+ return value;
456
832
  }
457
833
  /**
458
- * Read specified number of bits as unsigned integer (big-endian)
459
- * @param n - Number of bits to read (1-64)
834
+ * Read 4-byte unsigned integer, little-endian
460
835
  */
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;
477
- }
478
- return result;
836
+ readU4le() {
837
+ this.ensureBytes(4);
838
+ const value = this.view.getUint32(this._pos, true);
839
+ this._pos += 4;
840
+ return value;
479
841
  }
480
842
  /**
481
- * Read specified number of bits as unsigned integer (little-endian)
482
- * @param n - Number of bits to read (1-64)
843
+ * Read 4-byte unsigned integer, big-endian
483
844
  */
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;
502
- }
503
- return result;
845
+ readU4be() {
846
+ this.ensureBytes(4);
847
+ const value = this.view.getUint32(this._pos, false);
848
+ this._pos += 4;
849
+ return value;
504
850
  }
505
- // ==================== Utility Methods ====================
506
851
  /**
507
- * Get the underlying buffer
852
+ * Read 8-byte unsigned integer, little-endian
853
+ * Returns BigInt for values > Number.MAX_SAFE_INTEGER
508
854
  */
509
- getBuffer() {
510
- return this.buffer;
855
+ readU8le() {
856
+ this.ensureBytes(8);
857
+ const value = this.view.getBigUint64(this._pos, true);
858
+ this._pos += 8;
859
+ return value;
511
860
  }
512
861
  /**
513
- * Create a substream from current position with specified size
514
- * @param size - Size of the substream in bytes
862
+ * Read 8-byte unsigned integer, big-endian
863
+ * Returns BigInt for values > Number.MAX_SAFE_INTEGER
515
864
  */
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);
865
+ readU8be() {
866
+ this.ensureBytes(8);
867
+ const value = this.view.getBigUint64(this._pos, false);
868
+ this._pos += 8;
869
+ return value;
521
870
  }
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);
871
+ // ==================== Signed Integers ====================
872
+ /**
873
+ * Read 1-byte signed integer (-128 to 127)
874
+ */
875
+ readS1() {
876
+ this.ensureBytes(1);
877
+ return this.view.getInt8(this._pos++);
570
878
  }
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
879
  /**
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
880
+ * Read 2-byte signed integer, little-endian
596
881
  */
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;
882
+ readS2le() {
883
+ this.ensureBytes(2);
884
+ const value = this.view.getInt16(this._pos, true);
885
+ this._pos += 2;
886
+ return value;
627
887
  }
628
888
  /**
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
889
+ * Read 2-byte signed integer, big-endian
634
890
  */
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
- };
891
+ readS2be() {
892
+ this.ensureBytes(2);
893
+ const value = this.view.getInt16(this._pos, false);
894
+ this._pos += 2;
895
+ return value;
752
896
  }
753
897
  /**
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
898
+ * Read 4-byte signed integer, little-endian
762
899
  */
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
- }
900
+ readS4le() {
901
+ this.ensureBytes(4);
902
+ const value = this.view.getInt32(this._pos, true);
903
+ this._pos += 4;
904
+ return value;
812
905
  }
813
906
  /**
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
907
+ * Read 4-byte signed integer, big-endian
820
908
  */
821
- parseWithImports(mainYaml, _imports, options = {}) {
822
- const mainSchema = this.parse(mainYaml, options);
823
- return mainSchema;
909
+ readS4be() {
910
+ this.ensureBytes(4);
911
+ const value = this.view.getInt32(this._pos, false);
912
+ this._pos += 4;
913
+ return value;
824
914
  }
825
- };
826
-
827
- // src/interpreter/Context.ts
828
- var Context = class _Context {
829
915
  /**
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)
916
+ * Read 8-byte signed integer, little-endian
917
+ * Returns BigInt for values outside Number.MAX_SAFE_INTEGER range
918
+ */
919
+ readS8le() {
920
+ this.ensureBytes(8);
921
+ const value = this.view.getBigInt64(this._pos, true);
922
+ this._pos += 8;
923
+ return value;
924
+ }
925
+ /**
926
+ * Read 8-byte signed integer, big-endian
927
+ * Returns BigInt for values outside Number.MAX_SAFE_INTEGER range
928
+ */
929
+ readS8be() {
930
+ this.ensureBytes(8);
931
+ const value = this.view.getBigInt64(this._pos, false);
932
+ this._pos += 8;
933
+ return value;
934
+ }
935
+ // ==================== Floating Point ====================
936
+ /**
937
+ * Read 4-byte IEEE 754 single-precision float, little-endian
836
938
  */
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
- }
939
+ readF4le() {
940
+ this.ensureBytes(4);
941
+ const value = this.view.getFloat32(this._pos, true);
942
+ this._pos += 4;
943
+ return value;
852
944
  }
853
945
  /**
854
- * Get the current I/O stream.
855
- * Accessible in expressions as `_io`.
856
- *
857
- * @returns Current stream
946
+ * Read 4-byte IEEE 754 single-precision float, big-endian
858
947
  */
859
- get io() {
860
- return this._io;
948
+ readF4be() {
949
+ this.ensureBytes(4);
950
+ const value = this.view.getFloat32(this._pos, false);
951
+ this._pos += 4;
952
+ return value;
861
953
  }
862
954
  /**
863
- * Get the root object.
864
- * Accessible in expressions as `_root`.
865
- *
866
- * @returns Root object
955
+ * Read 8-byte IEEE 754 double-precision float, little-endian
867
956
  */
868
- get root() {
869
- return this._root;
957
+ readF8le() {
958
+ this.ensureBytes(8);
959
+ const value = this.view.getFloat64(this._pos, true);
960
+ this._pos += 8;
961
+ return value;
870
962
  }
871
963
  /**
872
- * Get the parent object.
873
- * Accessible in expressions as `_parent`.
874
- *
875
- * @returns Parent object or null if at root
964
+ * Read 8-byte IEEE 754 double-precision float, big-endian
876
965
  */
877
- get parent() {
878
- return this.parentStack.length > 0 ? this.parentStack[this.parentStack.length - 1] : null;
966
+ readF8be() {
967
+ this.ensureBytes(8);
968
+ const value = this.view.getFloat64(this._pos, false);
969
+ this._pos += 8;
970
+ return value;
879
971
  }
972
+ // ==================== Byte Arrays ====================
880
973
  /**
881
- * Get the current object being parsed.
882
- * Used to access fields defined earlier in the sequence.
883
- *
884
- * @returns Current object
974
+ * Read a fixed number of bytes
975
+ * @param length - Number of bytes to read
885
976
  */
886
- get current() {
887
- return this._current;
977
+ readBytes(length) {
978
+ this.ensureBytes(length);
979
+ const bytes = this.buffer.slice(this._pos, this._pos + length);
980
+ this._pos += length;
981
+ return bytes;
888
982
  }
889
983
  /**
890
- * Set the current object.
891
- *
892
- * @param obj - Object to set as current
984
+ * Read all remaining bytes until end of stream
893
985
  */
894
- set current(obj) {
895
- this._current = obj;
986
+ readBytesFull() {
987
+ const bytes = this.buffer.slice(this._pos);
988
+ this._pos = this.buffer.length;
989
+ return bytes;
896
990
  }
897
991
  /**
898
- * Push a new parent onto the stack.
899
- * Used when entering a nested type.
900
- *
901
- * @param parent - Parent object to push
992
+ * Read bytes until a terminator byte is found
993
+ * @param term - Terminator byte value
994
+ * @param include - Include terminator in result
995
+ * @param consume - Consume terminator from stream
996
+ * @param eosError - Throw error if EOS reached before terminator
902
997
  */
903
- pushParent(parent) {
904
- this.parentStack.push(parent);
998
+ readBytesterm(term, include = false, consume = true, eosError = true) {
999
+ const start = this._pos;
1000
+ let end = start;
1001
+ while (end < this.buffer.length && this.buffer[end] !== term) {
1002
+ end++;
1003
+ }
1004
+ const foundTerm = end < this.buffer.length;
1005
+ if (!foundTerm && eosError) {
1006
+ throw new EOFError(
1007
+ `Terminator byte ${term} not found before end of stream`,
1008
+ this._pos
1009
+ );
1010
+ }
1011
+ const includeEnd = include && foundTerm ? end + 1 : end;
1012
+ const bytes = this.buffer.slice(start, includeEnd);
1013
+ if (foundTerm && consume) {
1014
+ this._pos = end + 1;
1015
+ } else {
1016
+ this._pos = end;
1017
+ }
1018
+ return bytes;
905
1019
  }
1020
+ // ==================== Strings ====================
906
1021
  /**
907
- * Pop the current parent from the stack.
908
- * Used when exiting a nested type.
909
- *
910
- * @returns The popped parent object
1022
+ * Read a fixed-length string
1023
+ * @param length - Number of bytes to read
1024
+ * @param encoding - Character encoding (default: UTF-8)
911
1025
  */
912
- popParent() {
913
- return this.parentStack.pop();
1026
+ readStr(length, encoding = "UTF-8") {
1027
+ const bytes = this.readBytes(length);
1028
+ return decodeString(bytes, encoding);
914
1029
  }
915
1030
  /**
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
1031
+ * Read a null-terminated string
1032
+ * @param encoding - Character encoding (default: UTF-8)
1033
+ * @param term - Terminator byte (default: 0)
1034
+ * @param include - Include terminator in result
1035
+ * @param consume - Consume terminator from stream
1036
+ * @param eosError - Throw error if EOS reached before terminator
921
1037
  */
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;
937
- }
1038
+ readStrz(encoding = "UTF-8", term = 0, include = false, consume = true, eosError = true) {
1039
+ const bytes = this.readBytesterm(term, include, consume, eosError);
1040
+ return decodeString(bytes, encoding);
938
1041
  }
1042
+ // ==================== Bit-level Reading ====================
939
1043
  /**
940
- * Set a value in the current object.
941
- *
942
- * @param name - Field name
943
- * @param value - Value to set
1044
+ * Align bit reading to byte boundary
944
1045
  */
945
- set(name, value) {
946
- this._current[name] = value;
1046
+ alignToByte() {
1047
+ this._bitsLeft = 0;
947
1048
  }
948
1049
  /**
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
1050
+ * Read specified number of bits as unsigned integer (big-endian)
1051
+ * @param n - Number of bits to read (1-64)
955
1052
  */
956
- getEnumValue(enumName, valueName) {
957
- const enumDef = this._enums[enumName];
958
- if (!enumDef) {
959
- return void 0;
1053
+ readBitsIntBe(n) {
1054
+ if (n < 1 || n > 64) {
1055
+ throw new Error(`Invalid bit count: ${n}. Must be between 1 and 64`);
960
1056
  }
961
- for (const [key, value] of Object.entries(enumDef)) {
962
- if (value === valueName) {
963
- const numKey = Number(key);
964
- return isNaN(numKey) ? key : numKey;
1057
+ let result = 0n;
1058
+ for (let bitsNeeded = n; bitsNeeded > 0; ) {
1059
+ if (this._bitsLeft === 0) {
1060
+ this._bits = this.readU1();
1061
+ this._bitsLeft = 8;
965
1062
  }
1063
+ const bitsToRead = Math.min(bitsNeeded, this._bitsLeft);
1064
+ const mask = (1 << bitsToRead) - 1;
1065
+ const shift = this._bitsLeft - bitsToRead;
1066
+ result = result << BigInt(bitsToRead) | BigInt(this._bits >> shift & mask);
1067
+ this._bitsLeft -= bitsToRead;
1068
+ bitsNeeded -= bitsToRead;
966
1069
  }
967
- return void 0;
1070
+ return result;
968
1071
  }
969
1072
  /**
970
- * Check if an enum exists.
971
- *
972
- * @param enumName - Name of the enum
973
- * @returns True if enum exists
1073
+ * Read specified number of bits as unsigned integer (little-endian)
1074
+ * @param n - Number of bits to read (1-64)
974
1075
  */
975
- hasEnum(enumName) {
976
- return enumName in this._enums;
1076
+ readBitsIntLe(n) {
1077
+ if (n < 1 || n > 64) {
1078
+ throw new Error(`Invalid bit count: ${n}. Must be between 1 and 64`);
1079
+ }
1080
+ let result = 0n;
1081
+ let bitPos = 0;
1082
+ for (let bitsNeeded = n; bitsNeeded > 0; ) {
1083
+ if (this._bitsLeft === 0) {
1084
+ this._bits = this.readU1();
1085
+ this._bitsLeft = 8;
1086
+ }
1087
+ const bitsToRead = Math.min(bitsNeeded, this._bitsLeft);
1088
+ const mask = (1 << bitsToRead) - 1;
1089
+ result |= BigInt(this._bits & mask) << BigInt(bitPos);
1090
+ this._bits >>= bitsToRead;
1091
+ this._bitsLeft -= bitsToRead;
1092
+ bitsNeeded -= bitsToRead;
1093
+ bitPos += bitsToRead;
1094
+ }
1095
+ return result;
977
1096
  }
1097
+ // ==================== Utility Methods ====================
978
1098
  /**
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
1099
+ * Get the underlying buffer
984
1100
  */
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;
1101
+ getBuffer() {
1102
+ return this.buffer;
993
1103
  }
994
- /**
995
- * Clone this context.
996
- * Creates a shallow copy with the same stream, root, and parent.
997
- *
998
- * @returns Cloned context
1104
+ /**
1105
+ * Create a substream from current position with specified size
1106
+ * @param size - Size of the substream in bytes
999
1107
  */
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;
1108
+ substream(size) {
1109
+ this.ensureBytes(size);
1110
+ const subBuffer = this.buffer.slice(this._pos, this._pos + size);
1111
+ this._pos += size;
1112
+ return new _KaitaiStream(subBuffer);
1005
1113
  }
1006
1114
  };
1007
1115
 
@@ -1132,6 +1240,17 @@ var Lexer = class {
1132
1240
  }
1133
1241
  return createToken("NUMBER" /* NUMBER */, parseInt(value, 16), start);
1134
1242
  }
1243
+ if (this.current === "0" && this.peek() === "b") {
1244
+ value += this.current;
1245
+ this.advance();
1246
+ value += this.current;
1247
+ this.advance();
1248
+ while (this.current !== null && /[01]/.test(this.current)) {
1249
+ value += this.current;
1250
+ this.advance();
1251
+ }
1252
+ return createToken("NUMBER" /* NUMBER */, parseInt(value, 2), start);
1253
+ }
1135
1254
  while (this.current !== null && this.isDigit(this.current)) {
1136
1255
  value += this.current;
1137
1256
  this.advance();
@@ -1343,6 +1462,9 @@ function createMethodCall(object, method, args) {
1343
1462
  function createEnumAccess(enumName, value) {
1344
1463
  return { kind: "EnumAccess", enumName, value };
1345
1464
  }
1465
+ function createArrayLiteral(elements) {
1466
+ return { kind: "ArrayLiteral", elements };
1467
+ }
1346
1468
 
1347
1469
  // src/expression/Parser.ts
1348
1470
  var ExpressionParser = class {
@@ -1639,6 +1761,17 @@ var ExpressionParser = class {
1639
1761
  this.expect("RPAREN" /* RPAREN */, "Expected ) after expression");
1640
1762
  return expr;
1641
1763
  }
1764
+ if (this.match("LBRACKET" /* LBRACKET */)) {
1765
+ const elements = [];
1766
+ if (this.current().type !== "RBRACKET" /* RBRACKET */) {
1767
+ elements.push(this.parseTernary());
1768
+ while (this.match("COMMA" /* COMMA */)) {
1769
+ elements.push(this.parseTernary());
1770
+ }
1771
+ }
1772
+ this.expect("RBRACKET" /* RBRACKET */, "Expected ] after array literal");
1773
+ return createArrayLiteral(elements);
1774
+ }
1642
1775
  throw new ParseError(
1643
1776
  `Unexpected token: ${this.current().type}`,
1644
1777
  this.current().position
@@ -1677,6 +1810,8 @@ var Evaluator = class {
1677
1810
  return this.evaluateMethodCall(n.object, n.method, n.args, context);
1678
1811
  case "EnumAccess":
1679
1812
  return this.evaluateEnumAccess(n.enumName, n.value, context);
1813
+ case "ArrayLiteral":
1814
+ return this.evaluateArrayLiteral(n.elements, context);
1680
1815
  default:
1681
1816
  throw new ParseError(`Unknown AST node kind: ${node.kind}`);
1682
1817
  }
@@ -1722,15 +1857,15 @@ var Evaluator = class {
1722
1857
  return !this.equals(leftVal, rightVal);
1723
1858
  // Bitwise
1724
1859
  case "<<":
1725
- return this.toInt(leftVal) << this.toInt(rightVal);
1860
+ return this.bitwiseOp(leftVal, rightVal, (a, b) => a << b);
1726
1861
  case ">>":
1727
- return this.toInt(leftVal) >> this.toInt(rightVal);
1862
+ return this.bitwiseOp(leftVal, rightVal, (a, b) => a >> b);
1728
1863
  case "&":
1729
- return this.toInt(leftVal) & this.toInt(rightVal);
1864
+ return this.bitwiseOp(leftVal, rightVal, (a, b) => a & b);
1730
1865
  case "|":
1731
- return this.toInt(leftVal) | this.toInt(rightVal);
1866
+ return this.bitwiseOp(leftVal, rightVal, (a, b) => a | b);
1732
1867
  case "^":
1733
- return this.toInt(leftVal) ^ this.toInt(rightVal);
1868
+ return this.bitwiseOp(leftVal, rightVal, (a, b) => a ^ b);
1734
1869
  // Logical
1735
1870
  case "and":
1736
1871
  return this.toBoolean(leftVal) && this.toBoolean(rightVal);
@@ -1774,6 +1909,16 @@ var Evaluator = class {
1774
1909
  `Cannot access property ${property} of null/undefined`
1775
1910
  );
1776
1911
  }
1912
+ if (property === "to_i") {
1913
+ if (typeof obj === "number") return Math.floor(obj);
1914
+ if (typeof obj === "bigint") return Number(obj);
1915
+ if (typeof obj === "string") return parseInt(obj, 10);
1916
+ if (typeof obj === "boolean") return obj ? 1 : 0;
1917
+ return this.toInt(obj);
1918
+ }
1919
+ if (property === "to_s") {
1920
+ return String(obj);
1921
+ }
1777
1922
  if (typeof obj === "object") {
1778
1923
  return obj[property];
1779
1924
  }
@@ -1800,8 +1945,9 @@ var Evaluator = class {
1800
1945
  * Evaluate method call (object.method()).
1801
1946
  * @private
1802
1947
  */
1803
- evaluateMethodCall(object, method, _args, context) {
1948
+ evaluateMethodCall(object, method, args, context) {
1804
1949
  const obj = this.evaluate(object, context);
1950
+ const evalArgs = args.map((arg) => this.evaluate(arg, context));
1805
1951
  if (method === "length" || method === "size") {
1806
1952
  if (Array.isArray(obj)) return obj.length;
1807
1953
  if (obj instanceof Uint8Array) return obj.length;
@@ -1809,13 +1955,182 @@ var Evaluator = class {
1809
1955
  throw new ParseError(`Object does not have a ${method} property`);
1810
1956
  }
1811
1957
  if (method === "to_i") {
1958
+ const base = evalArgs.length > 0 ? this.toInt(evalArgs[0]) : 10;
1959
+ if (typeof obj === "string") {
1960
+ return parseInt(obj, base);
1961
+ }
1812
1962
  return this.toInt(obj);
1813
1963
  }
1814
1964
  if (method === "to_s") {
1815
1965
  return String(obj);
1816
1966
  }
1967
+ if (typeof obj === "string") {
1968
+ return this.evaluateStringMethod(obj, method, evalArgs);
1969
+ }
1970
+ if (Array.isArray(obj) || obj instanceof Uint8Array) {
1971
+ return this.evaluateArrayMethod(obj, method, evalArgs);
1972
+ }
1817
1973
  throw new ParseError(`Unknown method: ${method}`);
1818
1974
  }
1975
+ /**
1976
+ * Evaluate string methods.
1977
+ * @private
1978
+ */
1979
+ evaluateStringMethod(str, method, args) {
1980
+ switch (method) {
1981
+ case "substring": {
1982
+ const start = args.length > 0 ? this.toInt(args[0]) : 0;
1983
+ const end = args.length > 1 ? this.toInt(args[1]) : void 0;
1984
+ return str.substring(start, end);
1985
+ }
1986
+ case "substr": {
1987
+ const start = args.length > 0 ? this.toInt(args[0]) : 0;
1988
+ const length = args.length > 1 ? this.toInt(args[1]) : void 0;
1989
+ return str.substr(start, length);
1990
+ }
1991
+ case "reverse":
1992
+ return str.split("").reverse().join("");
1993
+ case "to_i": {
1994
+ const base = args.length > 0 ? this.toInt(args[0]) : 10;
1995
+ return parseInt(str, base);
1996
+ }
1997
+ case "length":
1998
+ case "size":
1999
+ return str.length;
2000
+ // Ruby-style string methods used in Kaitai
2001
+ case "upcase":
2002
+ case "to_upper":
2003
+ return str.toUpperCase();
2004
+ case "downcase":
2005
+ case "to_lower":
2006
+ return str.toLowerCase();
2007
+ case "capitalize":
2008
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
2009
+ case "strip":
2010
+ case "trim":
2011
+ return str.trim();
2012
+ case "lstrip":
2013
+ case "trim_start":
2014
+ return str.trimStart();
2015
+ case "rstrip":
2016
+ case "trim_end":
2017
+ return str.trimEnd();
2018
+ case "starts_with":
2019
+ case "startsWith": {
2020
+ if (args.length === 0) {
2021
+ throw new ParseError("starts_with requires 1 argument");
2022
+ }
2023
+ return str.startsWith(String(args[0]));
2024
+ }
2025
+ case "ends_with":
2026
+ case "endsWith": {
2027
+ if (args.length === 0) {
2028
+ throw new ParseError("ends_with requires 1 argument");
2029
+ }
2030
+ return str.endsWith(String(args[0]));
2031
+ }
2032
+ case "includes":
2033
+ case "contains": {
2034
+ if (args.length === 0) {
2035
+ throw new ParseError("includes requires 1 argument");
2036
+ }
2037
+ return str.includes(String(args[0]));
2038
+ }
2039
+ case "index_of":
2040
+ case "indexOf": {
2041
+ if (args.length === 0) {
2042
+ throw new ParseError("index_of requires 1 argument");
2043
+ }
2044
+ return str.indexOf(String(args[0]));
2045
+ }
2046
+ case "split": {
2047
+ if (args.length === 0) {
2048
+ throw new ParseError("split requires 1 argument");
2049
+ }
2050
+ return str.split(String(args[0]));
2051
+ }
2052
+ case "replace": {
2053
+ if (args.length < 2) {
2054
+ throw new ParseError("replace requires 2 arguments");
2055
+ }
2056
+ return str.replace(String(args[0]), String(args[1]));
2057
+ }
2058
+ case "replace_all":
2059
+ case "replaceAll": {
2060
+ if (args.length < 2) {
2061
+ throw new ParseError("replace_all requires 2 arguments");
2062
+ }
2063
+ const search = String(args[0]);
2064
+ const replace = String(args[1]);
2065
+ return str.split(search).join(replace);
2066
+ }
2067
+ case "pad_left":
2068
+ case "padStart": {
2069
+ if (args.length === 0) {
2070
+ throw new ParseError("pad_left requires at least 1 argument");
2071
+ }
2072
+ const length = this.toInt(args[0]);
2073
+ const fillString = args.length > 1 ? String(args[1]) : " ";
2074
+ return str.padStart(length, fillString);
2075
+ }
2076
+ case "pad_right":
2077
+ case "padEnd": {
2078
+ if (args.length === 0) {
2079
+ throw new ParseError("pad_right requires at least 1 argument");
2080
+ }
2081
+ const length = this.toInt(args[0]);
2082
+ const fillString = args.length > 1 ? String(args[1]) : " ";
2083
+ return str.padEnd(length, fillString);
2084
+ }
2085
+ default:
2086
+ throw new ParseError(`Unknown string method: ${method}`);
2087
+ }
2088
+ }
2089
+ /**
2090
+ * Evaluate array methods.
2091
+ * @private
2092
+ */
2093
+ evaluateArrayMethod(arr, method, args) {
2094
+ const array = Array.isArray(arr) ? arr : Array.from(arr);
2095
+ switch (method) {
2096
+ case "length":
2097
+ case "size":
2098
+ return array.length;
2099
+ case "first":
2100
+ return array[0];
2101
+ case "last":
2102
+ return array[array.length - 1];
2103
+ case "min":
2104
+ return Math.min(...array.map((v) => this.toNumber(v)));
2105
+ case "max":
2106
+ return Math.max(...array.map((v) => this.toNumber(v)));
2107
+ case "reverse":
2108
+ return [...array].reverse();
2109
+ case "sort":
2110
+ return [...array].sort((a, b) => this.compare(a, b));
2111
+ case "includes":
2112
+ case "contains": {
2113
+ if (args.length === 0) {
2114
+ throw new ParseError("includes requires 1 argument");
2115
+ }
2116
+ return array.some((item) => this.equals(item, args[0]));
2117
+ }
2118
+ case "index_of":
2119
+ case "indexOf": {
2120
+ if (args.length === 0) {
2121
+ throw new ParseError("index_of requires 1 argument");
2122
+ }
2123
+ return array.findIndex((item) => this.equals(item, args[0]));
2124
+ }
2125
+ case "slice": {
2126
+ const start = args.length > 0 ? this.toInt(args[0]) : 0;
2127
+ const end = args.length > 1 ? this.toInt(args[1]) : void 0;
2128
+ return array.slice(start, end);
2129
+ }
2130
+ default:
2131
+ throw new ParseError(`Unknown array method: ${method}`);
2132
+ }
2133
+ }
1819
2134
  /**
1820
2135
  * Evaluate enum access (EnumName::value).
1821
2136
  * @private
@@ -1845,6 +2160,38 @@ var Evaluator = class {
1845
2160
  const result = a % b;
1846
2161
  return result < 0 ? result + b : result;
1847
2162
  }
2163
+ /**
2164
+ * Helper: Bitwise operation with BigInt support.
2165
+ * JavaScript bitwise operators work on 32-bit integers, but Kaitai
2166
+ * may use 64-bit values. For values that fit in 32 bits, use native ops.
2167
+ * For larger values, convert to BigInt (with limitations).
2168
+ * @private
2169
+ */
2170
+ bitwiseOp(left, right, op) {
2171
+ if (typeof left === "bigint" || typeof right === "bigint") {
2172
+ const leftBig = typeof left === "bigint" ? left : BigInt(left);
2173
+ const rightBig = typeof right === "bigint" ? right : BigInt(right);
2174
+ if (op.toString().includes("<<")) {
2175
+ return leftBig << BigInt(Number(rightBig));
2176
+ }
2177
+ if (op.toString().includes(">>")) {
2178
+ return leftBig >> BigInt(Number(rightBig));
2179
+ }
2180
+ if (op.toString().includes("&")) {
2181
+ return leftBig & rightBig;
2182
+ }
2183
+ if (op.toString().includes("|")) {
2184
+ return leftBig | rightBig;
2185
+ }
2186
+ if (op.toString().includes("^")) {
2187
+ return leftBig ^ rightBig;
2188
+ }
2189
+ }
2190
+ if (left === void 0 || left === null || right === void 0 || right === null) {
2191
+ throw new ParseError("Cannot perform bitwise operation on null/undefined");
2192
+ }
2193
+ return op(this.toInt(left), this.toInt(right));
2194
+ }
1848
2195
  /**
1849
2196
  * Helper: Compare two values.
1850
2197
  * @private
@@ -1865,8 +2212,30 @@ var Evaluator = class {
1865
2212
  if (typeof left === "bigint" || typeof right === "bigint") {
1866
2213
  return BigInt(left) === BigInt(right);
1867
2214
  }
2215
+ const toArray = (v) => {
2216
+ if (Array.isArray(v))
2217
+ return v.map((x) => typeof x === "bigint" ? Number(x) : x);
2218
+ if (v instanceof Uint8Array) return Array.from(v);
2219
+ return null;
2220
+ };
2221
+ const leftArr = toArray(left);
2222
+ const rightArr = toArray(right);
2223
+ if (leftArr && rightArr) {
2224
+ if (leftArr.length !== rightArr.length) return false;
2225
+ for (let i = 0; i < leftArr.length; i++) {
2226
+ if (leftArr[i] !== rightArr[i]) return false;
2227
+ }
2228
+ return true;
2229
+ }
1868
2230
  return left === right;
1869
2231
  }
2232
+ /**
2233
+ * Evaluate an array literal.
2234
+ * @private
2235
+ */
2236
+ evaluateArrayLiteral(elements, context) {
2237
+ return elements.map((e) => this.evaluate(e, context));
2238
+ }
1870
2239
  /**
1871
2240
  * Helper: Convert to number.
1872
2241
  * @private
@@ -1901,7 +2270,8 @@ var Evaluator = class {
1901
2270
 
1902
2271
  // src/expression/index.ts
1903
2272
  function evaluateExpression(expression, context) {
1904
- const lexer = new Lexer(expression);
2273
+ const preprocessed = expression.replace(/\.as<[^>]+>/g, "");
2274
+ const lexer = new Lexer(preprocessed);
1905
2275
  const tokens = lexer.tokenize();
1906
2276
  const parser = new ExpressionParser(tokens);
1907
2277
  const ast = parser.parse();
@@ -1909,6 +2279,112 @@ function evaluateExpression(expression, context) {
1909
2279
  return evaluator.evaluate(ast, context);
1910
2280
  }
1911
2281
 
2282
+ // src/utils/process.ts
2283
+ var import_pako = require("pako");
2284
+ function applyProcess(data, process2) {
2285
+ const spec = typeof process2 === "string" ? { algorithm: process2 } : process2;
2286
+ const algorithm = spec.algorithm;
2287
+ if (!algorithm) {
2288
+ throw new ParseError("Process specification missing algorithm");
2289
+ }
2290
+ switch (algorithm) {
2291
+ case "zlib":
2292
+ return processZlib(data);
2293
+ case "xor":
2294
+ return processXor(data, spec.key);
2295
+ case "rol":
2296
+ return processRol(data, spec.amount, spec.group);
2297
+ case "ror":
2298
+ return processRor(data, spec.amount, spec.group);
2299
+ case "bswap2":
2300
+ return processByteswap(data, 2);
2301
+ case "bswap4":
2302
+ return processByteswap(data, 4);
2303
+ case "bswap8":
2304
+ return processByteswap(data, 8);
2305
+ case "bswap16":
2306
+ return processByteswap(data, 16);
2307
+ default:
2308
+ throw new ParseError(
2309
+ `Unknown process algorithm: ${algorithm}. Supported: zlib, xor, rol, ror, bswap2, bswap4, bswap8, bswap16`
2310
+ );
2311
+ }
2312
+ }
2313
+ function processZlib(data) {
2314
+ try {
2315
+ return (0, import_pako.inflate)(data);
2316
+ } catch (error) {
2317
+ throw new ParseError(
2318
+ `Zlib decompression failed: ${error instanceof Error ? error.message : String(error)}`
2319
+ );
2320
+ }
2321
+ }
2322
+ function processXor(data, key) {
2323
+ if (key === void 0) {
2324
+ throw new ParseError("XOR process requires a key parameter");
2325
+ }
2326
+ const result = new Uint8Array(data.length);
2327
+ const keyBytes = Array.isArray(key) ? key : [key];
2328
+ if (keyBytes.length === 0) {
2329
+ throw new ParseError("XOR key cannot be empty");
2330
+ }
2331
+ for (let i = 0; i < data.length; i++) {
2332
+ result[i] = data[i] ^ keyBytes[i % keyBytes.length];
2333
+ }
2334
+ return result;
2335
+ }
2336
+ function processRol(data, amount, group) {
2337
+ const bits = amount ?? 1;
2338
+ const groupSize = group ?? 1;
2339
+ if (bits < 0 || bits > 7) {
2340
+ throw new ParseError("ROL amount must be between 0 and 7");
2341
+ }
2342
+ if (groupSize !== 1) {
2343
+ throw new ParseError("ROL with group size > 1 not yet supported");
2344
+ }
2345
+ const result = new Uint8Array(data.length);
2346
+ for (let i = 0; i < data.length; i++) {
2347
+ const byte = data[i];
2348
+ result[i] = (byte << bits | byte >> 8 - bits) & 255;
2349
+ }
2350
+ return result;
2351
+ }
2352
+ function processRor(data, amount, group) {
2353
+ const bits = amount ?? 1;
2354
+ const groupSize = group ?? 1;
2355
+ if (bits < 0 || bits > 7) {
2356
+ throw new ParseError("ROR amount must be between 0 and 7");
2357
+ }
2358
+ if (groupSize !== 1) {
2359
+ throw new ParseError("ROR with group size > 1 not yet supported");
2360
+ }
2361
+ const result = new Uint8Array(data.length);
2362
+ for (let i = 0; i < data.length; i++) {
2363
+ const byte = data[i];
2364
+ result[i] = (byte >> bits | byte << 8 - bits) & 255;
2365
+ }
2366
+ return result;
2367
+ }
2368
+ function processByteswap(data, groupSize) {
2369
+ if (![2, 4, 8, 16].includes(groupSize)) {
2370
+ throw new ParseError(
2371
+ `Invalid byteswap group size: ${groupSize}. Must be 2, 4, 8, or 16`
2372
+ );
2373
+ }
2374
+ if (data.length % groupSize !== 0) {
2375
+ throw new ParseError(
2376
+ `Data length ${data.length} is not aligned to group size ${groupSize}`
2377
+ );
2378
+ }
2379
+ const result = new Uint8Array(data.length);
2380
+ for (let i = 0; i < data.length; i += groupSize) {
2381
+ for (let j = 0; j < groupSize; j++) {
2382
+ result[i + j] = data[i + groupSize - 1 - j];
2383
+ }
2384
+ }
2385
+ return result;
2386
+ }
2387
+
1912
2388
  // src/interpreter/TypeInterpreter.ts
1913
2389
  var TypeInterpreter = class _TypeInterpreter {
1914
2390
  /**
@@ -1927,18 +2403,38 @@ var TypeInterpreter = class _TypeInterpreter {
1927
2403
  throw new ParseError("Root schema must have meta.id");
1928
2404
  }
1929
2405
  }
2406
+ /**
2407
+ * Safely extract a KaitaiStream from an object that may expose `_io`.
2408
+ * Avoids using `any` casts to satisfy linting.
2409
+ */
2410
+ static getKaitaiIO(val) {
2411
+ if (val && typeof val === "object") {
2412
+ const rec = val;
2413
+ const maybe = rec["_io"];
2414
+ if (maybe instanceof KaitaiStream) return maybe;
2415
+ }
2416
+ return null;
2417
+ }
1930
2418
  /**
1931
2419
  * Parse binary data according to the schema.
1932
2420
  *
1933
2421
  * @param stream - Binary stream to parse
1934
2422
  * @param parent - Parent object (for nested types)
1935
2423
  * @param typeArgs - Arguments for parametric types
2424
+ * @param root - Root object of the parse tree (for nested types)
1936
2425
  * @returns Parsed object
1937
2426
  */
1938
- parse(stream, parent, typeArgs) {
2427
+ parse(stream, parent, typeArgs, root) {
1939
2428
  const result = {};
1940
- const context = new Context(stream, result, parent, this.schema.enums);
2429
+ const actualRoot = root || result;
2430
+ const context = new Context(stream, actualRoot, parent, this.schema.enums);
1941
2431
  context.current = result;
2432
+ const startPos = stream.pos;
2433
+ result["_io"] = stream;
2434
+ if (root) {
2435
+ ;
2436
+ result["_root"] = root;
2437
+ }
1942
2438
  if (typeArgs && this.schema.params) {
1943
2439
  for (let i = 0; i < this.schema.params.length && i < typeArgs.length; i++) {
1944
2440
  const param = this.schema.params[i];
@@ -1947,6 +2443,9 @@ var TypeInterpreter = class _TypeInterpreter {
1947
2443
  context.set(param.id, evaluatedArg);
1948
2444
  }
1949
2445
  }
2446
+ if (this.schema.instances) {
2447
+ this.setupInstances(result, stream, context);
2448
+ }
1950
2449
  if (this.schema.seq) {
1951
2450
  for (const attr of this.schema.seq) {
1952
2451
  const value = this.parseAttribute(attr, context);
@@ -1955,9 +2454,8 @@ var TypeInterpreter = class _TypeInterpreter {
1955
2454
  }
1956
2455
  }
1957
2456
  }
1958
- if (this.schema.instances) {
1959
- this.setupInstances(result, stream, context);
1960
- }
2457
+ const endPos = stream.pos;
2458
+ result["_sizeof"] = endPos - startPos;
1961
2459
  return result;
1962
2460
  }
1963
2461
  /**
@@ -2041,7 +2539,22 @@ var TypeInterpreter = class _TypeInterpreter {
2041
2539
  * @private
2042
2540
  */
2043
2541
  parseAttribute(attr, context) {
2044
- const stream = context.io;
2542
+ let stream = context.io;
2543
+ if (attr.io !== void 0) {
2544
+ const ioVal = this.evaluateValue(attr.io, context);
2545
+ if (ioVal instanceof KaitaiStream) {
2546
+ stream = ioVal;
2547
+ } else {
2548
+ const kio = _TypeInterpreter.getKaitaiIO(ioVal);
2549
+ if (kio) {
2550
+ stream = kio;
2551
+ } else {
2552
+ throw new ParseError(
2553
+ "io must evaluate to a KaitaiStream or an object with _io"
2554
+ );
2555
+ }
2556
+ }
2557
+ }
2045
2558
  if (attr.if) {
2046
2559
  const condition = this.evaluateValue(attr.if, context);
2047
2560
  if (!condition) {
@@ -2058,9 +2571,6 @@ var TypeInterpreter = class _TypeInterpreter {
2058
2571
  throw new ParseError(`pos must evaluate to a number, got ${typeof pos}`);
2059
2572
  }
2060
2573
  }
2061
- if (attr.io) {
2062
- throw new NotImplementedError("Custom I/O streams");
2063
- }
2064
2574
  if (attr.repeat) {
2065
2575
  return this.parseRepeated(attr, context);
2066
2576
  }
@@ -2157,7 +2667,7 @@ var TypeInterpreter = class _TypeInterpreter {
2157
2667
  }
2158
2668
  return bytes;
2159
2669
  } else {
2160
- const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
2670
+ const encoding = attr.encoding || this.schema.meta?.encoding || this.parentMeta?.encoding || "UTF-8";
2161
2671
  const str = stream.readStr(expected.length, encoding);
2162
2672
  if (str !== expected) {
2163
2673
  throw new ValidationError(
@@ -2186,7 +2696,7 @@ var TypeInterpreter = class _TypeInterpreter {
2186
2696
  throw new ParseError(`size must be non-negative, got ${size}`);
2187
2697
  }
2188
2698
  if (type === "str" || !type) {
2189
- const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
2699
+ const encoding = attr.encoding || this.schema.meta?.encoding || this.parentMeta?.encoding || "UTF-8";
2190
2700
  let data;
2191
2701
  if (type === "str") {
2192
2702
  data = stream.readBytes(size);
@@ -2212,11 +2722,19 @@ var TypeInterpreter = class _TypeInterpreter {
2212
2722
  }
2213
2723
  if (attr["size-eos"]) {
2214
2724
  if (type === "str") {
2215
- const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
2725
+ const encoding = attr.encoding || this.schema.meta?.encoding || this.parentMeta?.encoding || "UTF-8";
2216
2726
  const bytes = stream.readBytesFull();
2217
2727
  return new TextDecoder(encoding).decode(bytes);
2218
2728
  } else {
2219
- return stream.readBytesFull();
2729
+ let bytes = stream.readBytesFull();
2730
+ if (attr.process) {
2731
+ bytes = this.applyProcessing(bytes, attr.process);
2732
+ }
2733
+ if (type) {
2734
+ const sub = new KaitaiStream(bytes);
2735
+ return this.parseType(type, sub, context, attr["type-args"]);
2736
+ }
2737
+ return bytes;
2220
2738
  }
2221
2739
  }
2222
2740
  if (!type) {
@@ -2242,11 +2760,13 @@ var TypeInterpreter = class _TypeInterpreter {
2242
2760
  context
2243
2761
  );
2244
2762
  }
2245
- if (isBuiltinType(type)) {
2246
- return this.parseBuiltinType(type, stream, context);
2763
+ const { typeName, args } = this.parseParameterizedType(type, context);
2764
+ const effectiveArgs = args.length > 0 ? args : typeArgs;
2765
+ if (isBuiltinType(typeName)) {
2766
+ return this.parseBuiltinType(typeName, stream, context);
2247
2767
  }
2248
- if (this.schema.types && type in this.schema.types) {
2249
- const typeSchema = this.schema.types[type];
2768
+ if (this.schema.types && typeName in this.schema.types) {
2769
+ const typeSchema = this.schema.types[typeName];
2250
2770
  const meta = this.schema.meta || this.parentMeta;
2251
2771
  if (this.schema.enums && !typeSchema.enums) {
2252
2772
  typeSchema.enums = this.schema.enums;
@@ -2255,9 +2775,100 @@ var TypeInterpreter = class _TypeInterpreter {
2255
2775
  typeSchema.types = this.schema.types;
2256
2776
  }
2257
2777
  const interpreter = new _TypeInterpreter(typeSchema, meta);
2258
- return interpreter.parse(stream, context.current, typeArgs);
2778
+ return interpreter.parse(
2779
+ stream,
2780
+ context.current,
2781
+ effectiveArgs,
2782
+ context.root
2783
+ );
2784
+ }
2785
+ throw new ParseError(`Unknown type: ${typeName}`);
2786
+ }
2787
+ /**
2788
+ * Parse parameterized type syntax and extract type name and arguments.
2789
+ * Supports: type_name(arg1, arg2, ...) or just type_name
2790
+ *
2791
+ * @param typeSpec - Type specification string
2792
+ * @param context - Execution context for evaluating argument expressions
2793
+ * @returns Object with typeName and evaluated args
2794
+ * @private
2795
+ */
2796
+ parseParameterizedType(typeSpec, context) {
2797
+ const match = typeSpec.match(/^([a-z_][a-z0-9_]*)\((.*)\)$/i);
2798
+ if (!match) {
2799
+ return { typeName: typeSpec, args: [] };
2800
+ }
2801
+ const typeName = match[1];
2802
+ const argsString = match[2].trim();
2803
+ if (!argsString) {
2804
+ return { typeName, args: [] };
2805
+ }
2806
+ const args = [];
2807
+ let current = "";
2808
+ let inString = false;
2809
+ let stringChar = "";
2810
+ let parenDepth = 0;
2811
+ for (let i = 0; i < argsString.length; i++) {
2812
+ const char = argsString[i];
2813
+ if (inString) {
2814
+ current += char;
2815
+ if (char === stringChar && argsString[i - 1] !== "\\") {
2816
+ inString = false;
2817
+ }
2818
+ } else if (char === '"' || char === "'") {
2819
+ inString = true;
2820
+ stringChar = char;
2821
+ current += char;
2822
+ } else if (char === "(") {
2823
+ parenDepth++;
2824
+ current += char;
2825
+ } else if (char === ")") {
2826
+ parenDepth--;
2827
+ current += char;
2828
+ } else if (char === "," && parenDepth === 0) {
2829
+ args.push(this.parseArgument(current.trim(), context));
2830
+ current = "";
2831
+ } else {
2832
+ current += char;
2833
+ }
2834
+ }
2835
+ if (current.trim()) {
2836
+ args.push(this.parseArgument(current.trim(), context));
2837
+ }
2838
+ return { typeName, args };
2839
+ }
2840
+ /**
2841
+ * Parse and evaluate a single type argument.
2842
+ *
2843
+ * @param arg - Argument string
2844
+ * @param context - Execution context
2845
+ * @returns Evaluated argument value
2846
+ * @private
2847
+ */
2848
+ parseArgument(arg, context) {
2849
+ if (arg === "true") return true;
2850
+ if (arg === "false") return false;
2851
+ if (arg.startsWith('"') && arg.endsWith('"') || arg.startsWith("'") && arg.endsWith("'")) {
2852
+ return arg.slice(1, -1);
2853
+ }
2854
+ if (/^-?\d+$/.test(arg)) {
2855
+ return parseInt(arg, 10);
2856
+ }
2857
+ if (/^-?\d+\.\d+$/.test(arg)) {
2858
+ return parseFloat(arg);
2859
+ }
2860
+ if (/^0x[0-9a-f]+$/i.test(arg)) {
2861
+ return parseInt(arg, 16);
2862
+ }
2863
+ try {
2864
+ const result = this.evaluateValue(arg, context);
2865
+ if (typeof result === "string" || typeof result === "number" || typeof result === "boolean") {
2866
+ return result;
2867
+ }
2868
+ return Number(result);
2869
+ } catch {
2870
+ return arg;
2259
2871
  }
2260
- throw new ParseError(`Unknown type: ${type}`);
2261
2872
  }
2262
2873
  /**
2263
2874
  * Parse a switch type (type selection based on expression).
@@ -2300,18 +2911,33 @@ var TypeInterpreter = class _TypeInterpreter {
2300
2911
  * @returns Parsed value
2301
2912
  * @private
2302
2913
  */
2303
- parseBuiltinType(type, stream, _context) {
2914
+ parseBuiltinType(type, stream, context) {
2304
2915
  const base = getBaseType(type);
2305
2916
  const typeEndian = getTypeEndianness(type);
2306
2917
  const meta = this.schema.meta || this.parentMeta;
2307
2918
  const metaEndian = meta?.endian;
2308
- const endian = typeEndian || (typeof metaEndian === "string" ? metaEndian : "le");
2919
+ let endian;
2920
+ if (typeEndian) {
2921
+ endian = typeEndian;
2922
+ } else if (typeof metaEndian === "string") {
2923
+ endian = metaEndian;
2924
+ } else if (metaEndian && typeof metaEndian === "object") {
2925
+ endian = this.evaluateEndianExpression(metaEndian, context);
2926
+ } else {
2927
+ endian = "le";
2928
+ }
2309
2929
  if (isIntegerType(type)) {
2310
2930
  return this.readInteger(base, endian, stream);
2311
2931
  }
2312
2932
  if (isFloatType(type)) {
2313
2933
  return this.readFloat(base, endian, stream);
2314
2934
  }
2935
+ if (/^b\d+$/.test(type)) {
2936
+ const n = parseInt(type.slice(1), 10);
2937
+ const val = stream.readBitsIntBe(n);
2938
+ const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
2939
+ return val <= maxSafe ? Number(val) : val;
2940
+ }
2315
2941
  if (isStringType(type)) {
2316
2942
  const encoding = this.schema.meta?.encoding || "UTF-8";
2317
2943
  if (type === "strz") {
@@ -2376,7 +3002,7 @@ var TypeInterpreter = class _TypeInterpreter {
2376
3002
  }
2377
3003
  /**
2378
3004
  * Apply processing transformation to data.
2379
- * Supports basic transformations like zlib decompression.
3005
+ * Delegates to the process utility module.
2380
3006
  *
2381
3007
  * @param data - Data to process
2382
3008
  * @param process - Processing specification
@@ -2384,13 +3010,35 @@ var TypeInterpreter = class _TypeInterpreter {
2384
3010
  * @private
2385
3011
  */
2386
3012
  applyProcessing(data, process2) {
2387
- const processType = typeof process2 === "string" ? process2 : process2.algorithm;
2388
- if (processType) {
2389
- throw new NotImplementedError(
2390
- `Processing type "${processType}" is not yet implemented. Supported in future versions with zlib, encryption, etc.`
2391
- );
3013
+ return applyProcess(data, process2);
3014
+ }
3015
+ /**
3016
+ * Evaluate expression-based endianness (switch-on).
3017
+ *
3018
+ * @param endianExpr - Endianness expression with switch-on and cases
3019
+ * @param context - Execution context
3020
+ * @returns Resolved endianness ('le' or 'be')
3021
+ * @private
3022
+ */
3023
+ evaluateEndianExpression(endianExpr, context) {
3024
+ const switchOn = endianExpr["switch-on"];
3025
+ const cases = endianExpr.cases;
3026
+ if (!switchOn || typeof switchOn !== "string") {
3027
+ throw new ParseError('Endian expression missing "switch-on" field');
3028
+ }
3029
+ if (!cases || typeof cases !== "object") {
3030
+ throw new ParseError('Endian expression missing "cases" field');
3031
+ }
3032
+ const switchValue = this.evaluateValue(switchOn, context);
3033
+ const key = String(switchValue);
3034
+ if (key in cases) {
3035
+ const endian = cases[key];
3036
+ if (endian !== "le" && endian !== "be") {
3037
+ throw new ParseError(`Invalid endianness value: ${endian}`);
3038
+ }
3039
+ return endian;
2392
3040
  }
2393
- return data;
3041
+ return "le";
2394
3042
  }
2395
3043
  /**
2396
3044
  * Evaluate a value that can be an expression or literal.
@@ -2420,16 +3068,6 @@ var TypeInterpreter = class _TypeInterpreter {
2420
3068
  }
2421
3069
  };
2422
3070
 
2423
- // src/index.ts
2424
- function parse(ksyYaml, buffer, options = {}) {
2425
- const { validate = true, strict = false } = options;
2426
- const parser = new KsyParser();
2427
- const schema = parser.parse(ksyYaml, { validate, strict });
2428
- const stream = new KaitaiStream(buffer);
2429
- const interpreter = new TypeInterpreter(schema);
2430
- return interpreter.parse(stream);
2431
- }
2432
-
2433
3071
  // src/cli.ts
2434
3072
  function getVersion() {
2435
3073
  try {
@@ -2551,6 +3189,43 @@ function readFile(filePath, description) {
2551
3189
  process.exit(1);
2552
3190
  }
2553
3191
  }
3192
+ function loadImports(ksyPath, ksyContent, quiet) {
3193
+ const imports = /* @__PURE__ */ new Map();
3194
+ try {
3195
+ const schema = (0, import_yaml2.parse)(ksyContent);
3196
+ if (!schema.meta?.imports || schema.meta.imports.length === 0) {
3197
+ return imports;
3198
+ }
3199
+ const ksyDir = (0, import_path.dirname)((0, import_path.resolve)(ksyPath));
3200
+ for (const importPath of schema.meta.imports) {
3201
+ const normalizedPath = importPath.startsWith("/") ? importPath.slice(1) : importPath;
3202
+ const importFilePath = (0, import_path.resolve)(ksyDir, "..", normalizedPath + ".ksy");
3203
+ if (!(0, import_fs.existsSync)(importFilePath)) {
3204
+ console.error(
3205
+ `Error: Import file not found: ${importFilePath} (from import: ${importPath})`
3206
+ );
3207
+ process.exit(1);
3208
+ }
3209
+ if (!quiet) {
3210
+ console.error(` Loading import: ${importPath} -> ${importFilePath}`);
3211
+ }
3212
+ const importContent = (0, import_fs.readFileSync)(importFilePath, "utf-8");
3213
+ imports.set(importPath, importContent);
3214
+ const nestedImports = loadImports(importFilePath, importContent, quiet);
3215
+ for (const [nestedPath, nestedContent] of nestedImports) {
3216
+ if (!imports.has(nestedPath)) {
3217
+ imports.set(nestedPath, nestedContent);
3218
+ }
3219
+ }
3220
+ }
3221
+ } catch (error) {
3222
+ console.error(
3223
+ `Error loading imports: ${error instanceof Error ? error.message : String(error)}`
3224
+ );
3225
+ process.exit(1);
3226
+ }
3227
+ return imports;
3228
+ }
2554
3229
  function extractField(obj, path) {
2555
3230
  const parts = path.split(".");
2556
3231
  let current = obj;
@@ -2562,17 +3237,44 @@ function extractField(obj, path) {
2562
3237
  }
2563
3238
  return current;
2564
3239
  }
2565
- function jsonReplacer(_key, value) {
2566
- if (typeof value === "bigint") {
2567
- return value.toString();
3240
+ function safeStringify(data, pretty) {
3241
+ function safeClone(obj, seen = /* @__PURE__ */ new WeakSet()) {
3242
+ if (obj === null || obj === void 0) return obj;
3243
+ if (typeof obj === "bigint") return String(obj);
3244
+ if (typeof obj === "string" || typeof obj === "number" || typeof obj === "boolean")
3245
+ return obj;
3246
+ if (obj instanceof Uint8Array) return Array.from(obj);
3247
+ if (typeof obj === "object") {
3248
+ if (seen.has(obj)) return "[Circular]";
3249
+ seen.add(obj);
3250
+ }
3251
+ if (Array.isArray(obj)) {
3252
+ return obj.map((item) => safeClone(item, seen));
3253
+ }
3254
+ if (typeof obj === "object") {
3255
+ const result = {};
3256
+ const objRecord = obj;
3257
+ for (const key in objRecord) {
3258
+ if (key === "_io" || key === "_root" || key === "_parent") continue;
3259
+ try {
3260
+ const value = objRecord[key];
3261
+ result[key] = safeClone(value, seen);
3262
+ } catch (error) {
3263
+ result[key] = `[Error: ${error instanceof Error ? error.message : "unavailable"}]`;
3264
+ }
3265
+ }
3266
+ return result;
3267
+ }
3268
+ return obj;
2568
3269
  }
2569
- return value;
3270
+ const safe = safeClone(data);
3271
+ return pretty ? JSON.stringify(safe, null, 2) : JSON.stringify(safe);
2570
3272
  }
2571
3273
  function formatOutput(data, format, pretty) {
2572
3274
  if (format === "yaml") {
2573
- return JSON.stringify(data, jsonReplacer, 2).replace(/^{$/gm, "").replace(/^}$/gm, "").replace(/^\s*"([^"]+)":\s*/gm, "$1: ").replace(/,$/gm, "");
3275
+ return safeStringify(data, true).replace(/^{$/gm, "").replace(/^}$/gm, "").replace(/^\s*"([^"]+)":\s*/gm, "$1: ").replace(/,$/gm, "");
2574
3276
  }
2575
- return pretty ? JSON.stringify(data, jsonReplacer, 2) : JSON.stringify(data, jsonReplacer);
3277
+ return safeStringify(data, pretty);
2576
3278
  }
2577
3279
  function main() {
2578
3280
  const { options, positional } = parseCliArgs();
@@ -2608,16 +3310,30 @@ function main() {
2608
3310
  }
2609
3311
  const ksyContent = readFile(ksyFile, "KSY definition file").toString("utf-8");
2610
3312
  const binaryData = readFile(binaryFile, "Binary file");
3313
+ if (!options.quiet) {
3314
+ console.error("Detecting imports...");
3315
+ }
3316
+ const imports = loadImports(ksyFile, ksyContent, options.quiet || false);
3317
+ if (!options.quiet && imports.size > 0) {
3318
+ console.error(`Loaded ${imports.size} import(s)`);
3319
+ }
2611
3320
  const parseOptions = {
2612
3321
  validate: options.validate,
2613
3322
  strict: options.strict
2614
3323
  };
2615
3324
  if (!options.quiet) {
2616
- console.error("Parsing...");
3325
+ console.error("Parsing schema...");
2617
3326
  }
2618
3327
  let result;
2619
3328
  try {
2620
- result = parse(ksyContent, binaryData, parseOptions);
3329
+ const parser = new KsyParser();
3330
+ const schema = imports.size > 0 ? parser.parseWithImports(ksyContent, imports, parseOptions) : parser.parse(ksyContent, parseOptions);
3331
+ if (!options.quiet) {
3332
+ console.error("Parsing binary data...");
3333
+ }
3334
+ const stream = new KaitaiStream(binaryData);
3335
+ const interpreter = new TypeInterpreter(schema);
3336
+ result = interpreter.parse(stream);
2621
3337
  } catch (error) {
2622
3338
  console.error(
2623
3339
  `Parse error: ${error instanceof Error ? error.message : String(error)}`
@@ -2664,50 +3380,44 @@ function main() {
2664
3380
  }
2665
3381
  main();
2666
3382
  /**
2667
- * @fileoverview Custom error classes for Kaitai Struct parsing and validation
2668
- * @module utils/errors
2669
- * @author Fabiano Pinto
2670
- * @license MIT
2671
- */
2672
- /**
2673
- * @fileoverview String encoding and decoding utilities for binary data
2674
- * @module utils/encoding
3383
+ * @fileoverview Type definitions for Kaitai Struct YAML schema (.ksy files)
3384
+ * @module parser/schema
2675
3385
  * @author Fabiano Pinto
2676
3386
  * @license MIT
2677
3387
  */
2678
3388
  /**
2679
- * @fileoverview Binary stream reader for Kaitai Struct
2680
- * @module stream/KaitaiStream
3389
+ * @fileoverview Custom error classes for Kaitai Struct parsing and validation
3390
+ * @module utils/errors
2681
3391
  * @author Fabiano Pinto
2682
3392
  * @license MIT
2683
3393
  */
2684
3394
  /**
2685
- * @fileoverview Binary stream reading functionality
2686
- * @module stream
3395
+ * @fileoverview Parser for Kaitai Struct YAML (.ksy) files
3396
+ * @module parser/KsyParser
2687
3397
  * @author Fabiano Pinto
2688
3398
  * @license MIT
2689
3399
  */
2690
3400
  /**
2691
- * @fileoverview Type definitions for Kaitai Struct YAML schema (.ksy files)
2692
- * @module parser/schema
3401
+ * @fileoverview Parser module for Kaitai Struct YAML files
3402
+ * @module parser
2693
3403
  * @author Fabiano Pinto
2694
3404
  * @license MIT
2695
3405
  */
2696
3406
  /**
2697
- * @fileoverview Parser for Kaitai Struct YAML (.ksy) files
2698
- * @module parser/KsyParser
3407
+ * @fileoverview Execution context for Kaitai Struct parsing
3408
+ * @module interpreter/Context
2699
3409
  * @author Fabiano Pinto
2700
3410
  * @license MIT
2701
3411
  */
2702
3412
  /**
2703
- * @fileoverview Parser module for Kaitai Struct YAML files
2704
- * @module parser
3413
+ * @fileoverview String encoding and decoding utilities for binary data
3414
+ * @module utils/encoding
2705
3415
  * @author Fabiano Pinto
2706
3416
  * @license MIT
2707
3417
  */
2708
3418
  /**
2709
- * @fileoverview Execution context for Kaitai Struct parsing
2710
- * @module interpreter/Context
3419
+ * @fileoverview Binary stream reader for Kaitai Struct
3420
+ * @module stream/KaitaiStream
2711
3421
  * @author Fabiano Pinto
2712
3422
  * @license MIT
2713
3423
  */
@@ -2747,6 +3457,12 @@ main();
2747
3457
  * @author Fabiano Pinto
2748
3458
  * @license MIT
2749
3459
  */
3460
+ /**
3461
+ * @fileoverview Data processing utilities for Kaitai Struct
3462
+ * @module utils/process
3463
+ * @author Fabiano Pinto
3464
+ * @license MIT
3465
+ */
2750
3466
  /**
2751
3467
  * @fileoverview Type interpreter for executing Kaitai Struct schemas
2752
3468
  * @module interpreter/TypeInterpreter
@@ -2760,35 +3476,10 @@ main();
2760
3476
  * @license MIT
2761
3477
  */
2762
3478
  /**
2763
- * @fileoverview Main entry point for kaitai-struct-ts library
2764
- * @module kaitai-struct-ts
3479
+ * @fileoverview Binary stream reading functionality
3480
+ * @module stream
2765
3481
  * @author Fabiano Pinto
2766
3482
  * @license MIT
2767
- * @version 0.2.0
2768
- *
2769
- * @description
2770
- * A runtime interpreter for Kaitai Struct binary format definitions in TypeScript.
2771
- * Parse any binary data format by providing a .ksy (Kaitai Struct YAML) definition file.
2772
- *
2773
- * @example
2774
- * ```typescript
2775
- * import { parse } from 'kaitai-struct-ts'
2776
- *
2777
- * const ksyDefinition = `
2778
- * meta:
2779
- * id: my_format
2780
- * endian: le
2781
- * seq:
2782
- * - id: magic
2783
- * contents: [0x4D, 0x5A]
2784
- * - id: version
2785
- * type: u2
2786
- * `
2787
- *
2788
- * const buffer = new Uint8Array([0x4D, 0x5A, 0x01, 0x00])
2789
- * const result = parse(ksyDefinition, buffer)
2790
- * console.log(result.version) // 1
2791
- * ```
2792
3483
  */
2793
3484
  /**
2794
3485
  * @fileoverview CLI utility for parsing binary files with Kaitai Struct definitions