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