@k67/kaitai-struct-ts 0.7.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -560,8 +560,26 @@ declare class KsyParser {
560
560
  * @param imports - Map of import names to their YAML content
561
561
  * @param options - Parsing options
562
562
  * @returns Parsed schema with resolved imports
563
+ * @throws {ParseError} If import resolution fails
564
+ * @example
565
+ * ```typescript
566
+ * const parser = new KsyParser()
567
+ * const imports = new Map([
568
+ * ['/common/riff', riffYamlContent]
569
+ * ])
570
+ * const schema = parser.parseWithImports(wavYaml, imports)
571
+ * ```
572
+ */
573
+ parseWithImports(mainYaml: string, imports: Map<string, string>, options?: ParseOptions$1): KsySchema;
574
+ /**
575
+ * Extract namespace from import path.
576
+ * Converts paths like '/common/riff' or 'common/riff' to 'riff'.
577
+ *
578
+ * @param importPath - Import path from meta.imports
579
+ * @returns Namespace identifier
580
+ * @private
563
581
  */
564
- parseWithImports(mainYaml: string, _imports: Map<string, string>, options?: ParseOptions$1): KsySchema;
582
+ private extractNamespace;
565
583
  }
566
584
  /**
567
585
  * Options for parsing .ksy files.
@@ -753,6 +771,11 @@ declare class TypeInterpreter {
753
771
  endian?: Endianness | object;
754
772
  encoding?: string;
755
773
  } | undefined);
774
+ /**
775
+ * Safely extract a KaitaiStream from an object that may expose `_io`.
776
+ * Avoids using `any` casts to satisfy linting.
777
+ */
778
+ private static getKaitaiIO;
756
779
  /**
757
780
  * Parse binary data according to the schema.
758
781
  *
package/dist/index.d.ts CHANGED
@@ -560,8 +560,26 @@ declare class KsyParser {
560
560
  * @param imports - Map of import names to their YAML content
561
561
  * @param options - Parsing options
562
562
  * @returns Parsed schema with resolved imports
563
+ * @throws {ParseError} If import resolution fails
564
+ * @example
565
+ * ```typescript
566
+ * const parser = new KsyParser()
567
+ * const imports = new Map([
568
+ * ['/common/riff', riffYamlContent]
569
+ * ])
570
+ * const schema = parser.parseWithImports(wavYaml, imports)
571
+ * ```
572
+ */
573
+ parseWithImports(mainYaml: string, imports: Map<string, string>, options?: ParseOptions$1): KsySchema;
574
+ /**
575
+ * Extract namespace from import path.
576
+ * Converts paths like '/common/riff' or 'common/riff' to 'riff'.
577
+ *
578
+ * @param importPath - Import path from meta.imports
579
+ * @returns Namespace identifier
580
+ * @private
563
581
  */
564
- parseWithImports(mainYaml: string, _imports: Map<string, string>, options?: ParseOptions$1): KsySchema;
582
+ private extractNamespace;
565
583
  }
566
584
  /**
567
585
  * Options for parsing .ksy files.
@@ -753,6 +771,11 @@ declare class TypeInterpreter {
753
771
  endian?: Endianness | object;
754
772
  encoding?: string;
755
773
  } | undefined);
774
+ /**
775
+ * Safely extract a KaitaiStream from an object that may expose `_io`.
776
+ * Avoids using `any` casts to satisfy linting.
777
+ */
778
+ private static getKaitaiIO;
756
779
  /**
757
780
  * Parse binary data according to the schema.
758
781
  *
package/dist/index.js CHANGED
@@ -591,7 +591,9 @@ var BUILTIN_TYPES = [
591
591
  "strz"
592
592
  ];
593
593
  function isBuiltinType(type) {
594
- return BUILTIN_TYPES.includes(type);
594
+ if (BUILTIN_TYPES.includes(type)) return true;
595
+ if (/^b\d+$/.test(type)) return true;
596
+ return false;
595
597
  }
596
598
  function getTypeEndianness(type) {
597
599
  if (type.endsWith("le")) return "le";
@@ -851,11 +853,83 @@ var KsyParser = class {
851
853
  * @param imports - Map of import names to their YAML content
852
854
  * @param options - Parsing options
853
855
  * @returns Parsed schema with resolved imports
854
- */
855
- parseWithImports(mainYaml, _imports, options = {}) {
856
+ * @throws {ParseError} If import resolution fails
857
+ * @example
858
+ * ```typescript
859
+ * const parser = new KsyParser()
860
+ * const imports = new Map([
861
+ * ['/common/riff', riffYamlContent]
862
+ * ])
863
+ * const schema = parser.parseWithImports(wavYaml, imports)
864
+ * ```
865
+ */
866
+ parseWithImports(mainYaml, imports, options = {}) {
856
867
  const mainSchema = this.parse(mainYaml, options);
868
+ if (!mainSchema.meta.imports || mainSchema.meta.imports.length === 0) {
869
+ return mainSchema;
870
+ }
871
+ const resolvedTypes = {};
872
+ for (const importPath of mainSchema.meta.imports) {
873
+ if (!imports.has(importPath)) {
874
+ throw new ParseError(
875
+ `Import not found: ${importPath}. Available imports: ${Array.from(imports.keys()).join(", ")}`
876
+ );
877
+ }
878
+ const importYaml = imports.get(importPath);
879
+ const importedSchema = this.parse(importYaml, {
880
+ ...options,
881
+ validate: false
882
+ // Skip validation for imported schemas
883
+ });
884
+ const namespace = this.extractNamespace(importPath);
885
+ if (importedSchema.types) {
886
+ for (const [typeName, typeSchema] of Object.entries(
887
+ importedSchema.types
888
+ )) {
889
+ const qualifiedName = `${namespace}::${typeName}`;
890
+ resolvedTypes[qualifiedName] = typeSchema;
891
+ }
892
+ }
893
+ resolvedTypes[namespace] = {
894
+ meta: importedSchema.meta,
895
+ seq: importedSchema.seq,
896
+ instances: importedSchema.instances,
897
+ types: importedSchema.types,
898
+ enums: importedSchema.enums
899
+ };
900
+ if (importedSchema.enums) {
901
+ if (!mainSchema.enums) {
902
+ mainSchema.enums = {};
903
+ }
904
+ for (const [enumName, enumSpec] of Object.entries(
905
+ importedSchema.enums
906
+ )) {
907
+ const qualifiedEnumName = `${namespace}::${enumName}`;
908
+ mainSchema.enums[qualifiedEnumName] = enumSpec;
909
+ }
910
+ }
911
+ }
912
+ if (Object.keys(resolvedTypes).length > 0) {
913
+ mainSchema.types = {
914
+ ...resolvedTypes,
915
+ ...mainSchema.types
916
+ };
917
+ }
857
918
  return mainSchema;
858
919
  }
920
+ /**
921
+ * Extract namespace from import path.
922
+ * Converts paths like '/common/riff' or 'common/riff' to 'riff'.
923
+ *
924
+ * @param importPath - Import path from meta.imports
925
+ * @returns Namespace identifier
926
+ * @private
927
+ */
928
+ extractNamespace(importPath) {
929
+ const normalized = importPath.startsWith("/") ? importPath.slice(1) : importPath;
930
+ const segments = normalized.split("/");
931
+ return segments[segments.length - 1];
932
+ }
859
933
  };
860
934
 
861
935
  // src/interpreter/Context.ts
@@ -1166,6 +1240,17 @@ var Lexer = class {
1166
1240
  }
1167
1241
  return createToken("NUMBER" /* NUMBER */, parseInt(value, 16), start);
1168
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
+ }
1169
1254
  while (this.current !== null && this.isDigit(this.current)) {
1170
1255
  value += this.current;
1171
1256
  this.advance();
@@ -1377,6 +1462,9 @@ function createMethodCall(object, method, args) {
1377
1462
  function createEnumAccess(enumName, value) {
1378
1463
  return { kind: "EnumAccess", enumName, value };
1379
1464
  }
1465
+ function createArrayLiteral(elements) {
1466
+ return { kind: "ArrayLiteral", elements };
1467
+ }
1380
1468
 
1381
1469
  // src/expression/Parser.ts
1382
1470
  var ExpressionParser = class {
@@ -1673,6 +1761,17 @@ var ExpressionParser = class {
1673
1761
  this.expect("RPAREN" /* RPAREN */, "Expected ) after expression");
1674
1762
  return expr;
1675
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
+ }
1676
1775
  throw new ParseError(
1677
1776
  `Unexpected token: ${this.current().type}`,
1678
1777
  this.current().position
@@ -1711,6 +1810,8 @@ var Evaluator = class {
1711
1810
  return this.evaluateMethodCall(n.object, n.method, n.args, context);
1712
1811
  case "EnumAccess":
1713
1812
  return this.evaluateEnumAccess(n.enumName, n.value, context);
1813
+ case "ArrayLiteral":
1814
+ return this.evaluateArrayLiteral(n.elements, context);
1714
1815
  default:
1715
1816
  throw new ParseError(`Unknown AST node kind: ${node.kind}`);
1716
1817
  }
@@ -1899,8 +2000,30 @@ var Evaluator = class {
1899
2000
  if (typeof left === "bigint" || typeof right === "bigint") {
1900
2001
  return BigInt(left) === BigInt(right);
1901
2002
  }
2003
+ const toArray = (v) => {
2004
+ if (Array.isArray(v))
2005
+ return v.map((x) => typeof x === "bigint" ? Number(x) : x);
2006
+ if (v instanceof Uint8Array) return Array.from(v);
2007
+ return null;
2008
+ };
2009
+ const leftArr = toArray(left);
2010
+ const rightArr = toArray(right);
2011
+ if (leftArr && rightArr) {
2012
+ if (leftArr.length !== rightArr.length) return false;
2013
+ for (let i = 0; i < leftArr.length; i++) {
2014
+ if (leftArr[i] !== rightArr[i]) return false;
2015
+ }
2016
+ return true;
2017
+ }
1902
2018
  return left === right;
1903
2019
  }
2020
+ /**
2021
+ * Evaluate an array literal.
2022
+ * @private
2023
+ */
2024
+ evaluateArrayLiteral(elements, context) {
2025
+ return elements.map((e) => this.evaluate(e, context));
2026
+ }
1904
2027
  /**
1905
2028
  * Helper: Convert to number.
1906
2029
  * @private
@@ -1935,7 +2058,8 @@ var Evaluator = class {
1935
2058
 
1936
2059
  // src/expression/index.ts
1937
2060
  function evaluateExpression(expression, context) {
1938
- const lexer = new Lexer(expression);
2061
+ const preprocessed = expression.replace(/\.as<[^>]+>/g, "");
2062
+ const lexer = new Lexer(preprocessed);
1939
2063
  const tokens = lexer.tokenize();
1940
2064
  const parser = new ExpressionParser(tokens);
1941
2065
  const ast = parser.parse();
@@ -1961,6 +2085,18 @@ var TypeInterpreter = class _TypeInterpreter {
1961
2085
  throw new ParseError("Root schema must have meta.id");
1962
2086
  }
1963
2087
  }
2088
+ /**
2089
+ * Safely extract a KaitaiStream from an object that may expose `_io`.
2090
+ * Avoids using `any` casts to satisfy linting.
2091
+ */
2092
+ static getKaitaiIO(val) {
2093
+ if (val && typeof val === "object") {
2094
+ const rec = val;
2095
+ const maybe = rec["_io"];
2096
+ if (maybe instanceof KaitaiStream) return maybe;
2097
+ }
2098
+ return null;
2099
+ }
1964
2100
  /**
1965
2101
  * Parse binary data according to the schema.
1966
2102
  *
@@ -1973,6 +2109,7 @@ var TypeInterpreter = class _TypeInterpreter {
1973
2109
  const result = {};
1974
2110
  const context = new Context(stream, result, parent, this.schema.enums);
1975
2111
  context.current = result;
2112
+ result["_io"] = stream;
1976
2113
  if (typeArgs && this.schema.params) {
1977
2114
  for (let i = 0; i < this.schema.params.length && i < typeArgs.length; i++) {
1978
2115
  const param = this.schema.params[i];
@@ -2075,7 +2212,22 @@ var TypeInterpreter = class _TypeInterpreter {
2075
2212
  * @private
2076
2213
  */
2077
2214
  parseAttribute(attr, context) {
2078
- const stream = context.io;
2215
+ let stream = context.io;
2216
+ if (attr.io !== void 0) {
2217
+ const ioVal = this.evaluateValue(attr.io, context);
2218
+ if (ioVal instanceof KaitaiStream) {
2219
+ stream = ioVal;
2220
+ } else {
2221
+ const kio = _TypeInterpreter.getKaitaiIO(ioVal);
2222
+ if (kio) {
2223
+ stream = kio;
2224
+ } else {
2225
+ throw new ParseError(
2226
+ "io must evaluate to a KaitaiStream or an object with _io"
2227
+ );
2228
+ }
2229
+ }
2230
+ }
2079
2231
  if (attr.if) {
2080
2232
  const condition = this.evaluateValue(attr.if, context);
2081
2233
  if (!condition) {
@@ -2092,9 +2244,6 @@ var TypeInterpreter = class _TypeInterpreter {
2092
2244
  throw new ParseError(`pos must evaluate to a number, got ${typeof pos}`);
2093
2245
  }
2094
2246
  }
2095
- if (attr.io) {
2096
- throw new NotImplementedError("Custom I/O streams");
2097
- }
2098
2247
  if (attr.repeat) {
2099
2248
  return this.parseRepeated(attr, context);
2100
2249
  }
@@ -2191,7 +2340,7 @@ var TypeInterpreter = class _TypeInterpreter {
2191
2340
  }
2192
2341
  return bytes;
2193
2342
  } else {
2194
- const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
2343
+ const encoding = attr.encoding || this.schema.meta?.encoding || this.parentMeta?.encoding || "UTF-8";
2195
2344
  const str = stream.readStr(expected.length, encoding);
2196
2345
  if (str !== expected) {
2197
2346
  throw new ValidationError(
@@ -2220,7 +2369,7 @@ var TypeInterpreter = class _TypeInterpreter {
2220
2369
  throw new ParseError(`size must be non-negative, got ${size}`);
2221
2370
  }
2222
2371
  if (type === "str" || !type) {
2223
- const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
2372
+ const encoding = attr.encoding || this.schema.meta?.encoding || this.parentMeta?.encoding || "UTF-8";
2224
2373
  let data;
2225
2374
  if (type === "str") {
2226
2375
  data = stream.readBytes(size);
@@ -2246,11 +2395,19 @@ var TypeInterpreter = class _TypeInterpreter {
2246
2395
  }
2247
2396
  if (attr["size-eos"]) {
2248
2397
  if (type === "str") {
2249
- const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
2398
+ const encoding = attr.encoding || this.schema.meta?.encoding || this.parentMeta?.encoding || "UTF-8";
2250
2399
  const bytes = stream.readBytesFull();
2251
2400
  return new TextDecoder(encoding).decode(bytes);
2252
2401
  } else {
2253
- return stream.readBytesFull();
2402
+ let bytes = stream.readBytesFull();
2403
+ if (attr.process) {
2404
+ bytes = this.applyProcessing(bytes, attr.process);
2405
+ }
2406
+ if (type) {
2407
+ const sub = new KaitaiStream(bytes);
2408
+ return this.parseType(type, sub, context, attr["type-args"]);
2409
+ }
2410
+ return bytes;
2254
2411
  }
2255
2412
  }
2256
2413
  if (!type) {
@@ -2346,8 +2503,21 @@ var TypeInterpreter = class _TypeInterpreter {
2346
2503
  if (isFloatType(type)) {
2347
2504
  return this.readFloat(base, endian, stream);
2348
2505
  }
2506
+ if (/^b\d+$/.test(type)) {
2507
+ const n = parseInt(type.slice(1), 10);
2508
+ const val = stream.readBitsIntBe(n);
2509
+ const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
2510
+ return val <= maxSafe ? Number(val) : val;
2511
+ }
2349
2512
  if (isStringType(type)) {
2350
- throw new ParseError("String types require size, size-eos, or terminator");
2513
+ const encoding = this.schema.meta?.encoding || "UTF-8";
2514
+ if (type === "strz") {
2515
+ return stream.readStrz(encoding, 0, false, true, true);
2516
+ } else if (type === "str") {
2517
+ throw new ParseError(
2518
+ "str type requires size, size-eos, or terminator attribute"
2519
+ );
2520
+ }
2351
2521
  }
2352
2522
  throw new ParseError(`Unknown built-in type: ${type}`);
2353
2523
  }