@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/browser/index.mjs +272 -0
- package/dist/browser/index.mjs.map +1 -0
- package/dist/cli.js +1614 -923
- package/dist/index.d.mts +70 -7
- package/dist/index.d.ts +70 -7
- package/dist/index.js +704 -46
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +704 -46
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -3
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/
|
150
|
-
var
|
145
|
+
// src/parser/KsyParser.ts
|
146
|
+
var KsyParser = class {
|
151
147
|
/**
|
152
|
-
*
|
153
|
-
*
|
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
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
-
*
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
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
|
-
|
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
|
-
*
|
205
|
-
*
|
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
|
-
|
208
|
-
if (
|
209
|
-
|
210
|
-
|
211
|
-
|
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
|
-
*
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
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
|
-
*
|
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
|
-
|
227
|
-
|
228
|
-
const
|
229
|
-
|
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
|
-
*
|
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
|
-
|
236
|
-
this.
|
237
|
-
|
238
|
-
|
239
|
-
|
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
|
-
*
|
243
|
-
|
244
|
-
|
245
|
-
|
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
|
-
|
254
|
-
this.
|
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
|
-
*
|
261
|
-
*
|
494
|
+
* Get the root object.
|
495
|
+
* Accessible in expressions as `_root`.
|
496
|
+
*
|
497
|
+
* @returns Root object
|
262
498
|
*/
|
263
|
-
|
264
|
-
this.
|
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
|
-
*
|
271
|
-
*
|
503
|
+
* Get the parent object.
|
504
|
+
* Accessible in expressions as `_parent`.
|
505
|
+
*
|
506
|
+
* @returns Parent object or null if at root
|
272
507
|
*/
|
273
|
-
|
274
|
-
this.
|
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
|
-
*
|
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
|
-
|
284
|
-
this.
|
285
|
-
return this.view.getInt8(this._pos++);
|
517
|
+
get current() {
|
518
|
+
return this._current;
|
286
519
|
}
|
287
520
|
/**
|
288
|
-
*
|
521
|
+
* Set the current object.
|
522
|
+
*
|
523
|
+
* @param obj - Object to set as current
|
289
524
|
*/
|
290
|
-
|
291
|
-
this.
|
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
|
-
*
|
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
|
-
|
300
|
-
this.
|
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
|
-
*
|
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
|
-
|
309
|
-
this.
|
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
|
-
*
|
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
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
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
|
-
*
|
325
|
-
*
|
571
|
+
* Set a value in the current object.
|
572
|
+
*
|
573
|
+
* @param name - Field name
|
574
|
+
* @param value - Value to set
|
326
575
|
*/
|
327
|
-
|
328
|
-
this.
|
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
|
-
*
|
335
|
-
*
|
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
|
-
|
338
|
-
this.
|
339
|
-
|
340
|
-
|
341
|
-
|
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
|
-
*
|
601
|
+
* Check if an enum exists.
|
602
|
+
*
|
603
|
+
* @param enumName - Name of the enum
|
604
|
+
* @returns True if enum exists
|
346
605
|
*/
|
347
|
-
|
348
|
-
this.
|
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
|
-
*
|
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
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
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
|
-
*
|
626
|
+
* Clone this context.
|
627
|
+
* Creates a shallow copy with the same stream, root, and parent.
|
628
|
+
*
|
629
|
+
* @returns Cloned context
|
364
630
|
*/
|
365
|
-
|
366
|
-
this.
|
367
|
-
|
368
|
-
|
369
|
-
return
|
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
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
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
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
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
|
-
*
|
744
|
+
* Create a new KaitaiStream from a buffer
|
745
|
+
* @param buffer - ArrayBuffer or Uint8Array containing the binary data
|
393
746
|
*/
|
394
|
-
|
395
|
-
|
396
|
-
this.
|
397
|
-
|
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
|
-
*
|
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
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
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
|
-
|
413
|
-
|
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
|
-
`
|
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
|
-
// ====================
|
807
|
+
// ==================== Unsigned Integers ====================
|
429
808
|
/**
|
430
|
-
* Read
|
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
|
-
|
435
|
-
|
436
|
-
return
|
811
|
+
readU1() {
|
812
|
+
this.ensureBytes(1);
|
813
|
+
return this.buffer[this._pos++];
|
437
814
|
}
|
438
815
|
/**
|
439
|
-
* Read
|
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
|
-
|
447
|
-
|
448
|
-
|
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
|
-
*
|
825
|
+
* Read 2-byte unsigned integer, big-endian
|
453
826
|
*/
|
454
|
-
|
455
|
-
this.
|
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
|
459
|
-
* @param n - Number of bits to read (1-64)
|
834
|
+
* Read 4-byte unsigned integer, little-endian
|
460
835
|
*/
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
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
|
482
|
-
* @param n - Number of bits to read (1-64)
|
843
|
+
* Read 4-byte unsigned integer, big-endian
|
483
844
|
*/
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
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
|
-
*
|
852
|
+
* Read 8-byte unsigned integer, little-endian
|
853
|
+
* Returns BigInt for values > Number.MAX_SAFE_INTEGER
|
508
854
|
*/
|
509
|
-
|
510
|
-
|
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
|
-
*
|
514
|
-
*
|
862
|
+
* Read 8-byte unsigned integer, big-endian
|
863
|
+
* Returns BigInt for values > Number.MAX_SAFE_INTEGER
|
515
864
|
*/
|
516
|
-
|
517
|
-
this.ensureBytes(
|
518
|
-
const
|
519
|
-
this._pos +=
|
520
|
-
return
|
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
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
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
|
-
*
|
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
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
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
|
-
*
|
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
|
-
|
636
|
-
|
637
|
-
const
|
638
|
-
|
639
|
-
|
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
|
-
*
|
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
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
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
|
-
*
|
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
|
-
|
822
|
-
|
823
|
-
|
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
|
-
*
|
831
|
-
*
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
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
|
-
|
838
|
-
this.
|
839
|
-
this.
|
840
|
-
|
841
|
-
|
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
|
-
*
|
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
|
-
|
860
|
-
|
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
|
-
*
|
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
|
-
|
869
|
-
|
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
|
-
*
|
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
|
-
|
878
|
-
|
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
|
-
*
|
882
|
-
*
|
883
|
-
*
|
884
|
-
* @returns Current object
|
974
|
+
* Read a fixed number of bytes
|
975
|
+
* @param length - Number of bytes to read
|
885
976
|
*/
|
886
|
-
|
887
|
-
|
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
|
-
*
|
891
|
-
*
|
892
|
-
* @param obj - Object to set as current
|
984
|
+
* Read all remaining bytes until end of stream
|
893
985
|
*/
|
894
|
-
|
895
|
-
|
986
|
+
readBytesFull() {
|
987
|
+
const bytes = this.buffer.slice(this._pos);
|
988
|
+
this._pos = this.buffer.length;
|
989
|
+
return bytes;
|
896
990
|
}
|
897
991
|
/**
|
898
|
-
*
|
899
|
-
*
|
900
|
-
*
|
901
|
-
* @param
|
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
|
-
|
904
|
-
this.
|
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
|
-
*
|
908
|
-
*
|
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
|
-
|
913
|
-
|
1026
|
+
readStr(length, encoding = "UTF-8") {
|
1027
|
+
const bytes = this.readBytes(length);
|
1028
|
+
return decodeString(bytes, encoding);
|
914
1029
|
}
|
915
1030
|
/**
|
916
|
-
*
|
917
|
-
*
|
918
|
-
*
|
919
|
-
* @param
|
920
|
-
* @
|
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
|
-
|
923
|
-
|
924
|
-
|
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
|
-
*
|
941
|
-
*
|
942
|
-
* @param name - Field name
|
943
|
-
* @param value - Value to set
|
1044
|
+
* Align bit reading to byte boundary
|
944
1045
|
*/
|
945
|
-
|
946
|
-
this.
|
1046
|
+
alignToByte() {
|
1047
|
+
this._bitsLeft = 0;
|
947
1048
|
}
|
948
1049
|
/**
|
949
|
-
*
|
950
|
-
*
|
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
|
-
|
957
|
-
|
958
|
-
|
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
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
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
|
1070
|
+
return result;
|
968
1071
|
}
|
969
1072
|
/**
|
970
|
-
*
|
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
|
-
|
976
|
-
|
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
|
-
*
|
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
|
-
|
986
|
-
|
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
|
-
*
|
996
|
-
*
|
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
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
return
|
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.
|
1860
|
+
return this.bitwiseOp(leftVal, rightVal, (a, b) => a << b);
|
1726
1861
|
case ">>":
|
1727
|
-
return this.
|
1862
|
+
return this.bitwiseOp(leftVal, rightVal, (a, b) => a >> b);
|
1728
1863
|
case "&":
|
1729
|
-
return this.
|
1864
|
+
return this.bitwiseOp(leftVal, rightVal, (a, b) => a & b);
|
1730
1865
|
case "|":
|
1731
|
-
return this.
|
1866
|
+
return this.bitwiseOp(leftVal, rightVal, (a, b) => a | b);
|
1732
1867
|
case "^":
|
1733
|
-
return this.
|
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,
|
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
|
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
|
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
|
-
|
1959
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
2246
|
-
|
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 &&
|
2249
|
-
const typeSchema = this.schema.types[
|
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(
|
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,
|
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
|
-
|
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
|
-
*
|
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
|
-
|
2388
|
-
|
2389
|
-
|
2390
|
-
|
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
|
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
|
2566
|
-
|
2567
|
-
|
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
|
-
|
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
|
3275
|
+
return safeStringify(data, true).replace(/^{$/gm, "").replace(/^}$/gm, "").replace(/^\s*"([^"]+)":\s*/gm, "$1: ").replace(/,$/gm, "");
|
2574
3276
|
}
|
2575
|
-
return
|
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
|
-
|
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
|
2668
|
-
* @module
|
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
|
2680
|
-
* @module
|
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
|
2686
|
-
* @module
|
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
|
2692
|
-
* @module parser
|
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
|
2698
|
-
* @module
|
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
|
2704
|
-
* @module
|
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
|
2710
|
-
* @module
|
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
|
2764
|
-
* @module
|
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
|