@k67/kaitai-struct-ts 0.6.0 → 0.7.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/README.md +110 -37
- package/dist/cli.js +2776 -0
- package/dist/index.d.mts +197 -229
- package/dist/index.d.ts +197 -229
- package/dist/index.js +5 -110
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5 -110
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -1
package/dist/cli.js
ADDED
@@ -0,0 +1,2776 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
"use strict";
|
3
|
+
|
4
|
+
// src/cli.ts
|
5
|
+
var import_fs = require("fs");
|
6
|
+
var import_path = require("path");
|
7
|
+
var import_util = require("util");
|
8
|
+
|
9
|
+
// src/utils/errors.ts
|
10
|
+
var KaitaiError = class _KaitaiError extends Error {
|
11
|
+
constructor(message, position) {
|
12
|
+
super(message);
|
13
|
+
this.position = position;
|
14
|
+
this.name = "KaitaiError";
|
15
|
+
Object.setPrototypeOf(this, _KaitaiError.prototype);
|
16
|
+
}
|
17
|
+
};
|
18
|
+
var ValidationError = class _ValidationError extends KaitaiError {
|
19
|
+
constructor(message, position) {
|
20
|
+
super(message, position);
|
21
|
+
this.name = "ValidationError";
|
22
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
23
|
+
}
|
24
|
+
};
|
25
|
+
var ParseError = class _ParseError extends KaitaiError {
|
26
|
+
constructor(message, position) {
|
27
|
+
super(message, position);
|
28
|
+
this.name = "ParseError";
|
29
|
+
Object.setPrototypeOf(this, _ParseError.prototype);
|
30
|
+
}
|
31
|
+
};
|
32
|
+
var EOFError = class _EOFError extends KaitaiError {
|
33
|
+
constructor(message = "Unexpected end of stream", position) {
|
34
|
+
super(message, position);
|
35
|
+
this.name = "EOFError";
|
36
|
+
Object.setPrototypeOf(this, _EOFError.prototype);
|
37
|
+
}
|
38
|
+
};
|
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
|
+
|
149
|
+
// src/stream/KaitaiStream.ts
|
150
|
+
var KaitaiStream = class _KaitaiStream {
|
151
|
+
/**
|
152
|
+
* Create a new KaitaiStream from a buffer
|
153
|
+
* @param buffer - ArrayBuffer or Uint8Array containing the binary data
|
154
|
+
*/
|
155
|
+
constructor(buffer) {
|
156
|
+
this._pos = 0;
|
157
|
+
this._bits = 0;
|
158
|
+
this._bitsLeft = 0;
|
159
|
+
if (buffer instanceof ArrayBuffer) {
|
160
|
+
this.buffer = new Uint8Array(buffer);
|
161
|
+
this.view = new DataView(buffer);
|
162
|
+
} else {
|
163
|
+
this.buffer = buffer;
|
164
|
+
this.view = new DataView(
|
165
|
+
buffer.buffer,
|
166
|
+
buffer.byteOffset,
|
167
|
+
buffer.byteLength
|
168
|
+
);
|
169
|
+
}
|
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}`);
|
200
|
+
}
|
201
|
+
this.pos = pos;
|
202
|
+
}
|
203
|
+
/**
|
204
|
+
* Ensure we have enough bytes available
|
205
|
+
* @param count - Number of bytes needed
|
206
|
+
*/
|
207
|
+
ensureBytes(count) {
|
208
|
+
if (this._pos + count > this.buffer.length) {
|
209
|
+
throw new EOFError(
|
210
|
+
`Requested ${count} bytes at position ${this._pos}, but only ${this.buffer.length - this._pos} bytes available`,
|
211
|
+
this._pos
|
212
|
+
);
|
213
|
+
}
|
214
|
+
}
|
215
|
+
// ==================== Unsigned Integers ====================
|
216
|
+
/**
|
217
|
+
* Read 1-byte unsigned integer (0 to 255)
|
218
|
+
*/
|
219
|
+
readU1() {
|
220
|
+
this.ensureBytes(1);
|
221
|
+
return this.buffer[this._pos++];
|
222
|
+
}
|
223
|
+
/**
|
224
|
+
* Read 2-byte unsigned integer, little-endian
|
225
|
+
*/
|
226
|
+
readU2le() {
|
227
|
+
this.ensureBytes(2);
|
228
|
+
const value = this.view.getUint16(this._pos, true);
|
229
|
+
this._pos += 2;
|
230
|
+
return value;
|
231
|
+
}
|
232
|
+
/**
|
233
|
+
* Read 2-byte unsigned integer, big-endian
|
234
|
+
*/
|
235
|
+
readU2be() {
|
236
|
+
this.ensureBytes(2);
|
237
|
+
const value = this.view.getUint16(this._pos, false);
|
238
|
+
this._pos += 2;
|
239
|
+
return value;
|
240
|
+
}
|
241
|
+
/**
|
242
|
+
* Read 4-byte unsigned integer, little-endian
|
243
|
+
*/
|
244
|
+
readU4le() {
|
245
|
+
this.ensureBytes(4);
|
246
|
+
const value = this.view.getUint32(this._pos, true);
|
247
|
+
this._pos += 4;
|
248
|
+
return value;
|
249
|
+
}
|
250
|
+
/**
|
251
|
+
* Read 4-byte unsigned integer, big-endian
|
252
|
+
*/
|
253
|
+
readU4be() {
|
254
|
+
this.ensureBytes(4);
|
255
|
+
const value = this.view.getUint32(this._pos, false);
|
256
|
+
this._pos += 4;
|
257
|
+
return value;
|
258
|
+
}
|
259
|
+
/**
|
260
|
+
* Read 8-byte unsigned integer, little-endian
|
261
|
+
* Returns BigInt for values > Number.MAX_SAFE_INTEGER
|
262
|
+
*/
|
263
|
+
readU8le() {
|
264
|
+
this.ensureBytes(8);
|
265
|
+
const value = this.view.getBigUint64(this._pos, true);
|
266
|
+
this._pos += 8;
|
267
|
+
return value;
|
268
|
+
}
|
269
|
+
/**
|
270
|
+
* Read 8-byte unsigned integer, big-endian
|
271
|
+
* Returns BigInt for values > Number.MAX_SAFE_INTEGER
|
272
|
+
*/
|
273
|
+
readU8be() {
|
274
|
+
this.ensureBytes(8);
|
275
|
+
const value = this.view.getBigUint64(this._pos, false);
|
276
|
+
this._pos += 8;
|
277
|
+
return value;
|
278
|
+
}
|
279
|
+
// ==================== Signed Integers ====================
|
280
|
+
/**
|
281
|
+
* Read 1-byte signed integer (-128 to 127)
|
282
|
+
*/
|
283
|
+
readS1() {
|
284
|
+
this.ensureBytes(1);
|
285
|
+
return this.view.getInt8(this._pos++);
|
286
|
+
}
|
287
|
+
/**
|
288
|
+
* Read 2-byte signed integer, little-endian
|
289
|
+
*/
|
290
|
+
readS2le() {
|
291
|
+
this.ensureBytes(2);
|
292
|
+
const value = this.view.getInt16(this._pos, true);
|
293
|
+
this._pos += 2;
|
294
|
+
return value;
|
295
|
+
}
|
296
|
+
/**
|
297
|
+
* Read 2-byte signed integer, big-endian
|
298
|
+
*/
|
299
|
+
readS2be() {
|
300
|
+
this.ensureBytes(2);
|
301
|
+
const value = this.view.getInt16(this._pos, false);
|
302
|
+
this._pos += 2;
|
303
|
+
return value;
|
304
|
+
}
|
305
|
+
/**
|
306
|
+
* Read 4-byte signed integer, little-endian
|
307
|
+
*/
|
308
|
+
readS4le() {
|
309
|
+
this.ensureBytes(4);
|
310
|
+
const value = this.view.getInt32(this._pos, true);
|
311
|
+
this._pos += 4;
|
312
|
+
return value;
|
313
|
+
}
|
314
|
+
/**
|
315
|
+
* Read 4-byte signed integer, big-endian
|
316
|
+
*/
|
317
|
+
readS4be() {
|
318
|
+
this.ensureBytes(4);
|
319
|
+
const value = this.view.getInt32(this._pos, false);
|
320
|
+
this._pos += 4;
|
321
|
+
return value;
|
322
|
+
}
|
323
|
+
/**
|
324
|
+
* Read 8-byte signed integer, little-endian
|
325
|
+
* Returns BigInt for values outside Number.MAX_SAFE_INTEGER range
|
326
|
+
*/
|
327
|
+
readS8le() {
|
328
|
+
this.ensureBytes(8);
|
329
|
+
const value = this.view.getBigInt64(this._pos, true);
|
330
|
+
this._pos += 8;
|
331
|
+
return value;
|
332
|
+
}
|
333
|
+
/**
|
334
|
+
* Read 8-byte signed integer, big-endian
|
335
|
+
* Returns BigInt for values outside Number.MAX_SAFE_INTEGER range
|
336
|
+
*/
|
337
|
+
readS8be() {
|
338
|
+
this.ensureBytes(8);
|
339
|
+
const value = this.view.getBigInt64(this._pos, false);
|
340
|
+
this._pos += 8;
|
341
|
+
return value;
|
342
|
+
}
|
343
|
+
// ==================== Floating Point ====================
|
344
|
+
/**
|
345
|
+
* Read 4-byte IEEE 754 single-precision float, little-endian
|
346
|
+
*/
|
347
|
+
readF4le() {
|
348
|
+
this.ensureBytes(4);
|
349
|
+
const value = this.view.getFloat32(this._pos, true);
|
350
|
+
this._pos += 4;
|
351
|
+
return value;
|
352
|
+
}
|
353
|
+
/**
|
354
|
+
* Read 4-byte IEEE 754 single-precision float, big-endian
|
355
|
+
*/
|
356
|
+
readF4be() {
|
357
|
+
this.ensureBytes(4);
|
358
|
+
const value = this.view.getFloat32(this._pos, false);
|
359
|
+
this._pos += 4;
|
360
|
+
return value;
|
361
|
+
}
|
362
|
+
/**
|
363
|
+
* Read 8-byte IEEE 754 double-precision float, little-endian
|
364
|
+
*/
|
365
|
+
readF8le() {
|
366
|
+
this.ensureBytes(8);
|
367
|
+
const value = this.view.getFloat64(this._pos, true);
|
368
|
+
this._pos += 8;
|
369
|
+
return value;
|
370
|
+
}
|
371
|
+
/**
|
372
|
+
* Read 8-byte IEEE 754 double-precision float, big-endian
|
373
|
+
*/
|
374
|
+
readF8be() {
|
375
|
+
this.ensureBytes(8);
|
376
|
+
const value = this.view.getFloat64(this._pos, false);
|
377
|
+
this._pos += 8;
|
378
|
+
return value;
|
379
|
+
}
|
380
|
+
// ==================== Byte Arrays ====================
|
381
|
+
/**
|
382
|
+
* Read a fixed number of bytes
|
383
|
+
* @param length - Number of bytes to read
|
384
|
+
*/
|
385
|
+
readBytes(length) {
|
386
|
+
this.ensureBytes(length);
|
387
|
+
const bytes = this.buffer.slice(this._pos, this._pos + length);
|
388
|
+
this._pos += length;
|
389
|
+
return bytes;
|
390
|
+
}
|
391
|
+
/**
|
392
|
+
* Read all remaining bytes until end of stream
|
393
|
+
*/
|
394
|
+
readBytesFull() {
|
395
|
+
const bytes = this.buffer.slice(this._pos);
|
396
|
+
this._pos = this.buffer.length;
|
397
|
+
return bytes;
|
398
|
+
}
|
399
|
+
/**
|
400
|
+
* Read bytes until a terminator byte is found
|
401
|
+
* @param term - Terminator byte value
|
402
|
+
* @param include - Include terminator in result
|
403
|
+
* @param consume - Consume terminator from stream
|
404
|
+
* @param eosError - Throw error if EOS reached before terminator
|
405
|
+
*/
|
406
|
+
readBytesterm(term, include = false, consume = true, eosError = true) {
|
407
|
+
const start = this._pos;
|
408
|
+
let end = start;
|
409
|
+
while (end < this.buffer.length && this.buffer[end] !== term) {
|
410
|
+
end++;
|
411
|
+
}
|
412
|
+
const foundTerm = end < this.buffer.length;
|
413
|
+
if (!foundTerm && eosError) {
|
414
|
+
throw new EOFError(
|
415
|
+
`Terminator byte ${term} not found before end of stream`,
|
416
|
+
this._pos
|
417
|
+
);
|
418
|
+
}
|
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
|
+
}
|
428
|
+
// ==================== Strings ====================
|
429
|
+
/**
|
430
|
+
* Read a fixed-length string
|
431
|
+
* @param length - Number of bytes to read
|
432
|
+
* @param encoding - Character encoding (default: UTF-8)
|
433
|
+
*/
|
434
|
+
readStr(length, encoding = "UTF-8") {
|
435
|
+
const bytes = this.readBytes(length);
|
436
|
+
return decodeString(bytes, encoding);
|
437
|
+
}
|
438
|
+
/**
|
439
|
+
* Read a null-terminated string
|
440
|
+
* @param encoding - Character encoding (default: UTF-8)
|
441
|
+
* @param term - Terminator byte (default: 0)
|
442
|
+
* @param include - Include terminator in result
|
443
|
+
* @param consume - Consume terminator from stream
|
444
|
+
* @param eosError - Throw error if EOS reached before terminator
|
445
|
+
*/
|
446
|
+
readStrz(encoding = "UTF-8", term = 0, include = false, consume = true, eosError = true) {
|
447
|
+
const bytes = this.readBytesterm(term, include, consume, eosError);
|
448
|
+
return decodeString(bytes, encoding);
|
449
|
+
}
|
450
|
+
// ==================== Bit-level Reading ====================
|
451
|
+
/**
|
452
|
+
* Align bit reading to byte boundary
|
453
|
+
*/
|
454
|
+
alignToByte() {
|
455
|
+
this._bitsLeft = 0;
|
456
|
+
}
|
457
|
+
/**
|
458
|
+
* Read specified number of bits as unsigned integer (big-endian)
|
459
|
+
* @param n - Number of bits to read (1-64)
|
460
|
+
*/
|
461
|
+
readBitsIntBe(n) {
|
462
|
+
if (n < 1 || n > 64) {
|
463
|
+
throw new Error(`Invalid bit count: ${n}. Must be between 1 and 64`);
|
464
|
+
}
|
465
|
+
let result = 0n;
|
466
|
+
for (let bitsNeeded = n; bitsNeeded > 0; ) {
|
467
|
+
if (this._bitsLeft === 0) {
|
468
|
+
this._bits = this.readU1();
|
469
|
+
this._bitsLeft = 8;
|
470
|
+
}
|
471
|
+
const bitsToRead = Math.min(bitsNeeded, this._bitsLeft);
|
472
|
+
const mask = (1 << bitsToRead) - 1;
|
473
|
+
const shift = this._bitsLeft - bitsToRead;
|
474
|
+
result = result << BigInt(bitsToRead) | BigInt(this._bits >> shift & mask);
|
475
|
+
this._bitsLeft -= bitsToRead;
|
476
|
+
bitsNeeded -= bitsToRead;
|
477
|
+
}
|
478
|
+
return result;
|
479
|
+
}
|
480
|
+
/**
|
481
|
+
* Read specified number of bits as unsigned integer (little-endian)
|
482
|
+
* @param n - Number of bits to read (1-64)
|
483
|
+
*/
|
484
|
+
readBitsIntLe(n) {
|
485
|
+
if (n < 1 || n > 64) {
|
486
|
+
throw new Error(`Invalid bit count: ${n}. Must be between 1 and 64`);
|
487
|
+
}
|
488
|
+
let result = 0n;
|
489
|
+
let bitPos = 0;
|
490
|
+
for (let bitsNeeded = n; bitsNeeded > 0; ) {
|
491
|
+
if (this._bitsLeft === 0) {
|
492
|
+
this._bits = this.readU1();
|
493
|
+
this._bitsLeft = 8;
|
494
|
+
}
|
495
|
+
const bitsToRead = Math.min(bitsNeeded, this._bitsLeft);
|
496
|
+
const mask = (1 << bitsToRead) - 1;
|
497
|
+
result |= BigInt(this._bits & mask) << BigInt(bitPos);
|
498
|
+
this._bits >>= bitsToRead;
|
499
|
+
this._bitsLeft -= bitsToRead;
|
500
|
+
bitsNeeded -= bitsToRead;
|
501
|
+
bitPos += bitsToRead;
|
502
|
+
}
|
503
|
+
return result;
|
504
|
+
}
|
505
|
+
// ==================== Utility Methods ====================
|
506
|
+
/**
|
507
|
+
* Get the underlying buffer
|
508
|
+
*/
|
509
|
+
getBuffer() {
|
510
|
+
return this.buffer;
|
511
|
+
}
|
512
|
+
/**
|
513
|
+
* Create a substream from current position with specified size
|
514
|
+
* @param size - Size of the substream in bytes
|
515
|
+
*/
|
516
|
+
substream(size) {
|
517
|
+
this.ensureBytes(size);
|
518
|
+
const subBuffer = this.buffer.slice(this._pos, this._pos + size);
|
519
|
+
this._pos += size;
|
520
|
+
return new _KaitaiStream(subBuffer);
|
521
|
+
}
|
522
|
+
};
|
523
|
+
|
524
|
+
// src/parser/schema.ts
|
525
|
+
var BUILTIN_TYPES = [
|
526
|
+
// Unsigned integers
|
527
|
+
"u1",
|
528
|
+
"u2",
|
529
|
+
"u2le",
|
530
|
+
"u2be",
|
531
|
+
"u4",
|
532
|
+
"u4le",
|
533
|
+
"u4be",
|
534
|
+
"u8",
|
535
|
+
"u8le",
|
536
|
+
"u8be",
|
537
|
+
// Signed integers
|
538
|
+
"s1",
|
539
|
+
"s2",
|
540
|
+
"s2le",
|
541
|
+
"s2be",
|
542
|
+
"s4",
|
543
|
+
"s4le",
|
544
|
+
"s4be",
|
545
|
+
"s8",
|
546
|
+
"s8le",
|
547
|
+
"s8be",
|
548
|
+
// Floating point
|
549
|
+
"f4",
|
550
|
+
"f4le",
|
551
|
+
"f4be",
|
552
|
+
"f8",
|
553
|
+
"f8le",
|
554
|
+
"f8be",
|
555
|
+
// String
|
556
|
+
"str",
|
557
|
+
"strz"
|
558
|
+
];
|
559
|
+
function isBuiltinType(type) {
|
560
|
+
return BUILTIN_TYPES.includes(type);
|
561
|
+
}
|
562
|
+
function getTypeEndianness(type) {
|
563
|
+
if (type.endsWith("le")) return "le";
|
564
|
+
if (type.endsWith("be")) return "be";
|
565
|
+
return void 0;
|
566
|
+
}
|
567
|
+
function getBaseType(type) {
|
568
|
+
if (type.endsWith("le") || type.endsWith("be")) {
|
569
|
+
return type.slice(0, -2);
|
570
|
+
}
|
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
|
+
/**
|
589
|
+
* Parse a .ksy YAML string into a typed schema object.
|
590
|
+
*
|
591
|
+
* @param yaml - YAML string containing the .ksy definition
|
592
|
+
* @param options - Parsing options
|
593
|
+
* @returns Parsed and validated schema
|
594
|
+
* @throws {ParseError} If YAML parsing fails
|
595
|
+
* @throws {ValidationError} If schema validation fails
|
596
|
+
*/
|
597
|
+
parse(yaml, options = {}) {
|
598
|
+
const { validate = true, strict = false } = options;
|
599
|
+
let parsed;
|
600
|
+
try {
|
601
|
+
parsed = (0, import_yaml.parse)(yaml);
|
602
|
+
} catch (error) {
|
603
|
+
throw new ParseError(
|
604
|
+
`Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`
|
605
|
+
);
|
606
|
+
}
|
607
|
+
if (typeof parsed !== "object" || parsed === null) {
|
608
|
+
throw new ParseError("KSY file must contain an object");
|
609
|
+
}
|
610
|
+
const schema = parsed;
|
611
|
+
if (validate) {
|
612
|
+
const result = this.validate(schema, { strict });
|
613
|
+
if (!result.valid) {
|
614
|
+
const errorMessages = result.errors.map((e) => e.message).join("; ");
|
615
|
+
throw new ValidationError(
|
616
|
+
`Schema validation failed: ${errorMessages}`
|
617
|
+
);
|
618
|
+
}
|
619
|
+
if (result.warnings.length > 0 && !strict) {
|
620
|
+
console.warn(
|
621
|
+
"Schema validation warnings:",
|
622
|
+
result.warnings.map((w) => w.message)
|
623
|
+
);
|
624
|
+
}
|
625
|
+
}
|
626
|
+
return schema;
|
627
|
+
}
|
628
|
+
/**
|
629
|
+
* Validate a schema object.
|
630
|
+
*
|
631
|
+
* @param schema - Schema to validate
|
632
|
+
* @param options - Validation options
|
633
|
+
* @returns Validation result with errors and warnings
|
634
|
+
*/
|
635
|
+
validate(schema, options = {}) {
|
636
|
+
const { strict = false, isNested = false } = options;
|
637
|
+
const errors = [];
|
638
|
+
const warnings = [];
|
639
|
+
if (!schema.meta && !isNested) {
|
640
|
+
errors.push({
|
641
|
+
message: 'Missing required "meta" section',
|
642
|
+
path: [],
|
643
|
+
code: "MISSING_META"
|
644
|
+
});
|
645
|
+
} else if (schema.meta) {
|
646
|
+
if (!schema.meta.id) {
|
647
|
+
errors.push({
|
648
|
+
message: 'Missing required "meta.id" field',
|
649
|
+
path: ["meta"],
|
650
|
+
code: "MISSING_META_ID"
|
651
|
+
});
|
652
|
+
} else if (typeof schema.meta.id !== "string") {
|
653
|
+
errors.push({
|
654
|
+
message: '"meta.id" must be a string',
|
655
|
+
path: ["meta", "id"],
|
656
|
+
code: "INVALID_META_ID_TYPE"
|
657
|
+
});
|
658
|
+
} else if (!/^[a-z][a-z0-9_]*$/.test(schema.meta.id)) {
|
659
|
+
warnings.push({
|
660
|
+
message: '"meta.id" should follow snake_case naming convention',
|
661
|
+
path: ["meta", "id"],
|
662
|
+
code: "META_ID_NAMING"
|
663
|
+
});
|
664
|
+
}
|
665
|
+
if (schema.meta.endian) {
|
666
|
+
if (typeof schema.meta.endian === "string" && schema.meta.endian !== "le" && schema.meta.endian !== "be") {
|
667
|
+
errors.push({
|
668
|
+
message: '"meta.endian" must be "le" or "be"',
|
669
|
+
path: ["meta", "endian"],
|
670
|
+
code: "INVALID_ENDIAN"
|
671
|
+
});
|
672
|
+
}
|
673
|
+
}
|
674
|
+
}
|
675
|
+
if (schema.seq) {
|
676
|
+
if (!Array.isArray(schema.seq)) {
|
677
|
+
errors.push({
|
678
|
+
message: '"seq" must be an array',
|
679
|
+
path: ["seq"],
|
680
|
+
code: "INVALID_SEQ_TYPE"
|
681
|
+
});
|
682
|
+
} else {
|
683
|
+
schema.seq.forEach((attr, index) => {
|
684
|
+
this.validateAttribute(
|
685
|
+
attr,
|
686
|
+
["seq", String(index)],
|
687
|
+
errors,
|
688
|
+
warnings,
|
689
|
+
strict
|
690
|
+
);
|
691
|
+
});
|
692
|
+
}
|
693
|
+
}
|
694
|
+
if (schema.instances) {
|
695
|
+
if (typeof schema.instances !== "object") {
|
696
|
+
errors.push({
|
697
|
+
message: '"instances" must be an object',
|
698
|
+
path: ["instances"],
|
699
|
+
code: "INVALID_INSTANCES_TYPE"
|
700
|
+
});
|
701
|
+
} else {
|
702
|
+
Object.entries(schema.instances).forEach(([key, instance]) => {
|
703
|
+
this.validateAttribute(
|
704
|
+
instance,
|
705
|
+
["instances", key],
|
706
|
+
errors,
|
707
|
+
warnings,
|
708
|
+
strict
|
709
|
+
);
|
710
|
+
});
|
711
|
+
}
|
712
|
+
}
|
713
|
+
if (schema.types) {
|
714
|
+
if (typeof schema.types !== "object") {
|
715
|
+
errors.push({
|
716
|
+
message: '"types" must be an object',
|
717
|
+
path: ["types"],
|
718
|
+
code: "INVALID_TYPES_TYPE"
|
719
|
+
});
|
720
|
+
} else {
|
721
|
+
Object.entries(schema.types).forEach(([key, type]) => {
|
722
|
+
const typeResult = this.validate(type, { ...options, isNested: true });
|
723
|
+
errors.push(
|
724
|
+
...typeResult.errors.map((e) => ({
|
725
|
+
...e,
|
726
|
+
path: ["types", key, ...e.path]
|
727
|
+
}))
|
728
|
+
);
|
729
|
+
warnings.push(
|
730
|
+
...typeResult.warnings.map((w) => ({
|
731
|
+
...w,
|
732
|
+
path: ["types", key, ...w.path]
|
733
|
+
}))
|
734
|
+
);
|
735
|
+
});
|
736
|
+
}
|
737
|
+
}
|
738
|
+
if (schema.enums) {
|
739
|
+
if (typeof schema.enums !== "object") {
|
740
|
+
errors.push({
|
741
|
+
message: '"enums" must be an object',
|
742
|
+
path: ["enums"],
|
743
|
+
code: "INVALID_ENUMS_TYPE"
|
744
|
+
});
|
745
|
+
}
|
746
|
+
}
|
747
|
+
return {
|
748
|
+
valid: errors.length === 0 && (strict ? warnings.length === 0 : true),
|
749
|
+
errors,
|
750
|
+
warnings
|
751
|
+
};
|
752
|
+
}
|
753
|
+
/**
|
754
|
+
* Validate an attribute specification.
|
755
|
+
*
|
756
|
+
* @param attr - Attribute to validate
|
757
|
+
* @param path - Path to this attribute in the schema
|
758
|
+
* @param errors - Array to collect errors
|
759
|
+
* @param warnings - Array to collect warnings
|
760
|
+
* @param strict - Whether to be strict about warnings
|
761
|
+
* @private
|
762
|
+
*/
|
763
|
+
validateAttribute(attr, path, errors, warnings, _strict) {
|
764
|
+
if (attr.id && typeof attr.id === "string") {
|
765
|
+
if (!/^[a-z][a-z0-9_]*$/.test(attr.id)) {
|
766
|
+
warnings.push({
|
767
|
+
message: `Field "${attr.id}" should follow snake_case naming convention`,
|
768
|
+
path: [...path, "id"],
|
769
|
+
code: "FIELD_ID_NAMING"
|
770
|
+
});
|
771
|
+
}
|
772
|
+
}
|
773
|
+
if (attr.repeat) {
|
774
|
+
if (attr.repeat !== "expr" && attr.repeat !== "eos" && attr.repeat !== "until") {
|
775
|
+
errors.push({
|
776
|
+
message: '"repeat" must be "expr", "eos", or "until"',
|
777
|
+
path: [...path, "repeat"],
|
778
|
+
code: "INVALID_REPEAT"
|
779
|
+
});
|
780
|
+
}
|
781
|
+
if (attr.repeat === "expr" && !attr["repeat-expr"]) {
|
782
|
+
errors.push({
|
783
|
+
message: '"repeat-expr" is required when repeat is "expr"',
|
784
|
+
path: [...path, "repeat-expr"],
|
785
|
+
code: "MISSING_REPEAT_EXPR"
|
786
|
+
});
|
787
|
+
}
|
788
|
+
if (attr.repeat === "until" && !attr["repeat-until"]) {
|
789
|
+
errors.push({
|
790
|
+
message: '"repeat-until" is required when repeat is "until"',
|
791
|
+
path: [...path, "repeat-until"],
|
792
|
+
code: "MISSING_REPEAT_UNTIL"
|
793
|
+
});
|
794
|
+
}
|
795
|
+
}
|
796
|
+
if (attr["size-eos"] && attr.size) {
|
797
|
+
warnings.push({
|
798
|
+
message: '"size-eos" and "size" are mutually exclusive',
|
799
|
+
path: [...path],
|
800
|
+
code: "SIZE_EOS_WITH_SIZE"
|
801
|
+
});
|
802
|
+
}
|
803
|
+
if (attr.contents) {
|
804
|
+
if (!Array.isArray(attr.contents) && typeof attr.contents !== "string") {
|
805
|
+
errors.push({
|
806
|
+
message: '"contents" must be an array or string',
|
807
|
+
path: [...path, "contents"],
|
808
|
+
code: "INVALID_CONTENTS_TYPE"
|
809
|
+
});
|
810
|
+
}
|
811
|
+
}
|
812
|
+
}
|
813
|
+
/**
|
814
|
+
* Parse multiple .ksy files and resolve imports.
|
815
|
+
*
|
816
|
+
* @param mainYaml - Main .ksy file content
|
817
|
+
* @param imports - Map of import names to their YAML content
|
818
|
+
* @param options - Parsing options
|
819
|
+
* @returns Parsed schema with resolved imports
|
820
|
+
*/
|
821
|
+
parseWithImports(mainYaml, _imports, options = {}) {
|
822
|
+
const mainSchema = this.parse(mainYaml, options);
|
823
|
+
return mainSchema;
|
824
|
+
}
|
825
|
+
};
|
826
|
+
|
827
|
+
// src/interpreter/Context.ts
|
828
|
+
var Context = class _Context {
|
829
|
+
/**
|
830
|
+
* Create a new execution context.
|
831
|
+
*
|
832
|
+
* @param _io - Binary stream being read
|
833
|
+
* @param _root - Root object of the parse tree
|
834
|
+
* @param _parent - Parent object (optional)
|
835
|
+
* @param enums - Enum definitions from schema (optional)
|
836
|
+
*/
|
837
|
+
constructor(_io, _root = null, _parent = null, enums) {
|
838
|
+
this._io = _io;
|
839
|
+
this._root = _root;
|
840
|
+
/** Stack of parent objects */
|
841
|
+
this.parentStack = [];
|
842
|
+
/** Current object being parsed */
|
843
|
+
this._current = {};
|
844
|
+
/** Enum definitions from schema */
|
845
|
+
this._enums = {};
|
846
|
+
if (_parent !== null) {
|
847
|
+
this.parentStack.push(_parent);
|
848
|
+
}
|
849
|
+
if (enums) {
|
850
|
+
this._enums = enums;
|
851
|
+
}
|
852
|
+
}
|
853
|
+
/**
|
854
|
+
* Get the current I/O stream.
|
855
|
+
* Accessible in expressions as `_io`.
|
856
|
+
*
|
857
|
+
* @returns Current stream
|
858
|
+
*/
|
859
|
+
get io() {
|
860
|
+
return this._io;
|
861
|
+
}
|
862
|
+
/**
|
863
|
+
* Get the root object.
|
864
|
+
* Accessible in expressions as `_root`.
|
865
|
+
*
|
866
|
+
* @returns Root object
|
867
|
+
*/
|
868
|
+
get root() {
|
869
|
+
return this._root;
|
870
|
+
}
|
871
|
+
/**
|
872
|
+
* Get the parent object.
|
873
|
+
* Accessible in expressions as `_parent`.
|
874
|
+
*
|
875
|
+
* @returns Parent object or null if at root
|
876
|
+
*/
|
877
|
+
get parent() {
|
878
|
+
return this.parentStack.length > 0 ? this.parentStack[this.parentStack.length - 1] : null;
|
879
|
+
}
|
880
|
+
/**
|
881
|
+
* Get the current object being parsed.
|
882
|
+
* Used to access fields defined earlier in the sequence.
|
883
|
+
*
|
884
|
+
* @returns Current object
|
885
|
+
*/
|
886
|
+
get current() {
|
887
|
+
return this._current;
|
888
|
+
}
|
889
|
+
/**
|
890
|
+
* Set the current object.
|
891
|
+
*
|
892
|
+
* @param obj - Object to set as current
|
893
|
+
*/
|
894
|
+
set current(obj) {
|
895
|
+
this._current = obj;
|
896
|
+
}
|
897
|
+
/**
|
898
|
+
* Push a new parent onto the stack.
|
899
|
+
* Used when entering a nested type.
|
900
|
+
*
|
901
|
+
* @param parent - Parent object to push
|
902
|
+
*/
|
903
|
+
pushParent(parent) {
|
904
|
+
this.parentStack.push(parent);
|
905
|
+
}
|
906
|
+
/**
|
907
|
+
* Pop the current parent from the stack.
|
908
|
+
* Used when exiting a nested type.
|
909
|
+
*
|
910
|
+
* @returns The popped parent object
|
911
|
+
*/
|
912
|
+
popParent() {
|
913
|
+
return this.parentStack.pop();
|
914
|
+
}
|
915
|
+
/**
|
916
|
+
* Get a value from the context by path.
|
917
|
+
* Supports special names: _io, _root, _parent, _index.
|
918
|
+
*
|
919
|
+
* @param name - Name or path to resolve
|
920
|
+
* @returns Resolved value
|
921
|
+
*/
|
922
|
+
resolve(name) {
|
923
|
+
switch (name) {
|
924
|
+
case "_io":
|
925
|
+
return this._io;
|
926
|
+
case "_root":
|
927
|
+
return this._root;
|
928
|
+
case "_parent":
|
929
|
+
return this.parent;
|
930
|
+
case "_index":
|
931
|
+
return this._current["_index"];
|
932
|
+
default:
|
933
|
+
if (name in this._current) {
|
934
|
+
return this._current[name];
|
935
|
+
}
|
936
|
+
return void 0;
|
937
|
+
}
|
938
|
+
}
|
939
|
+
/**
|
940
|
+
* Set a value in the current object.
|
941
|
+
*
|
942
|
+
* @param name - Field name
|
943
|
+
* @param value - Value to set
|
944
|
+
*/
|
945
|
+
set(name, value) {
|
946
|
+
this._current[name] = value;
|
947
|
+
}
|
948
|
+
/**
|
949
|
+
* Get enum value by name.
|
950
|
+
* Used for enum access in expressions (EnumName::value).
|
951
|
+
*
|
952
|
+
* @param enumName - Name of the enum
|
953
|
+
* @param valueName - Name of the enum value
|
954
|
+
* @returns Enum value (number) or undefined
|
955
|
+
*/
|
956
|
+
getEnumValue(enumName, valueName) {
|
957
|
+
const enumDef = this._enums[enumName];
|
958
|
+
if (!enumDef) {
|
959
|
+
return void 0;
|
960
|
+
}
|
961
|
+
for (const [key, value] of Object.entries(enumDef)) {
|
962
|
+
if (value === valueName) {
|
963
|
+
const numKey = Number(key);
|
964
|
+
return isNaN(numKey) ? key : numKey;
|
965
|
+
}
|
966
|
+
}
|
967
|
+
return void 0;
|
968
|
+
}
|
969
|
+
/**
|
970
|
+
* Check if an enum exists.
|
971
|
+
*
|
972
|
+
* @param enumName - Name of the enum
|
973
|
+
* @returns True if enum exists
|
974
|
+
*/
|
975
|
+
hasEnum(enumName) {
|
976
|
+
return enumName in this._enums;
|
977
|
+
}
|
978
|
+
/**
|
979
|
+
* Create a child context for nested parsing.
|
980
|
+
* The current object becomes the parent in the child context.
|
981
|
+
*
|
982
|
+
* @param stream - Stream for the child context (defaults to current stream)
|
983
|
+
* @returns New child context
|
984
|
+
*/
|
985
|
+
createChild(stream) {
|
986
|
+
const childContext = new _Context(
|
987
|
+
stream || this._io,
|
988
|
+
this._root || this._current,
|
989
|
+
this._current,
|
990
|
+
this._enums
|
991
|
+
);
|
992
|
+
return childContext;
|
993
|
+
}
|
994
|
+
/**
|
995
|
+
* Clone this context.
|
996
|
+
* Creates a shallow copy with the same stream, root, and parent.
|
997
|
+
*
|
998
|
+
* @returns Cloned context
|
999
|
+
*/
|
1000
|
+
clone() {
|
1001
|
+
const cloned = new _Context(this._io, this._root, this.parent, this._enums);
|
1002
|
+
cloned._current = { ...this._current };
|
1003
|
+
cloned.parentStack = [...this.parentStack];
|
1004
|
+
return cloned;
|
1005
|
+
}
|
1006
|
+
};
|
1007
|
+
|
1008
|
+
// src/expression/Token.ts
|
1009
|
+
function createToken(type, value = null, position = 0) {
|
1010
|
+
return { type, value, position };
|
1011
|
+
}
|
1012
|
+
|
1013
|
+
// src/expression/Lexer.ts
|
1014
|
+
var Lexer = class {
|
1015
|
+
/**
|
1016
|
+
* Create a new lexer.
|
1017
|
+
*
|
1018
|
+
* @param input - Expression string to tokenize
|
1019
|
+
*/
|
1020
|
+
constructor(input) {
|
1021
|
+
this.position = 0;
|
1022
|
+
this.current = null;
|
1023
|
+
this.input = input;
|
1024
|
+
this.current = input.length > 0 ? input[0] : null;
|
1025
|
+
}
|
1026
|
+
/**
|
1027
|
+
* Tokenize the entire input string.
|
1028
|
+
*
|
1029
|
+
* @returns Array of tokens
|
1030
|
+
* @throws {ParseError} If invalid syntax is encountered
|
1031
|
+
*/
|
1032
|
+
tokenize() {
|
1033
|
+
const tokens = [];
|
1034
|
+
while (this.current !== null) {
|
1035
|
+
if (this.isWhitespace(this.current)) {
|
1036
|
+
this.skipWhitespace();
|
1037
|
+
continue;
|
1038
|
+
}
|
1039
|
+
if (this.isDigit(this.current)) {
|
1040
|
+
tokens.push(this.readNumber());
|
1041
|
+
continue;
|
1042
|
+
}
|
1043
|
+
if (this.isIdentifierStart(this.current)) {
|
1044
|
+
tokens.push(this.readIdentifierOrKeyword());
|
1045
|
+
continue;
|
1046
|
+
}
|
1047
|
+
if (this.current === '"' || this.current === "'") {
|
1048
|
+
tokens.push(this.readString());
|
1049
|
+
continue;
|
1050
|
+
}
|
1051
|
+
const token = this.readOperator();
|
1052
|
+
if (token) {
|
1053
|
+
tokens.push(token);
|
1054
|
+
continue;
|
1055
|
+
}
|
1056
|
+
throw new ParseError(
|
1057
|
+
`Unexpected character: ${this.current}`,
|
1058
|
+
this.position
|
1059
|
+
);
|
1060
|
+
}
|
1061
|
+
tokens.push(createToken("EOF" /* EOF */, null, this.position));
|
1062
|
+
return tokens;
|
1063
|
+
}
|
1064
|
+
/**
|
1065
|
+
* Advance to the next character.
|
1066
|
+
* @private
|
1067
|
+
*/
|
1068
|
+
advance() {
|
1069
|
+
this.position++;
|
1070
|
+
this.current = this.position < this.input.length ? this.input[this.position] : null;
|
1071
|
+
}
|
1072
|
+
/**
|
1073
|
+
* Peek at the next character without advancing.
|
1074
|
+
* @private
|
1075
|
+
*/
|
1076
|
+
peek(offset = 1) {
|
1077
|
+
const pos = this.position + offset;
|
1078
|
+
return pos < this.input.length ? this.input[pos] : null;
|
1079
|
+
}
|
1080
|
+
/**
|
1081
|
+
* Check if character is whitespace.
|
1082
|
+
* @private
|
1083
|
+
*/
|
1084
|
+
isWhitespace(char) {
|
1085
|
+
return /\s/.test(char);
|
1086
|
+
}
|
1087
|
+
/**
|
1088
|
+
* Check if character is a digit.
|
1089
|
+
* @private
|
1090
|
+
*/
|
1091
|
+
isDigit(char) {
|
1092
|
+
return /[0-9]/.test(char);
|
1093
|
+
}
|
1094
|
+
/**
|
1095
|
+
* Check if character can start an identifier.
|
1096
|
+
* @private
|
1097
|
+
*/
|
1098
|
+
isIdentifierStart(char) {
|
1099
|
+
return /[a-zA-Z_]/.test(char);
|
1100
|
+
}
|
1101
|
+
/**
|
1102
|
+
* Check if character can be part of an identifier.
|
1103
|
+
* @private
|
1104
|
+
*/
|
1105
|
+
isIdentifierPart(char) {
|
1106
|
+
return /[a-zA-Z0-9_]/.test(char);
|
1107
|
+
}
|
1108
|
+
/**
|
1109
|
+
* Skip whitespace characters.
|
1110
|
+
* @private
|
1111
|
+
*/
|
1112
|
+
skipWhitespace() {
|
1113
|
+
while (this.current !== null && this.isWhitespace(this.current)) {
|
1114
|
+
this.advance();
|
1115
|
+
}
|
1116
|
+
}
|
1117
|
+
/**
|
1118
|
+
* Read a number token.
|
1119
|
+
* @private
|
1120
|
+
*/
|
1121
|
+
readNumber() {
|
1122
|
+
const start = this.position;
|
1123
|
+
let value = "";
|
1124
|
+
if (this.current === "0" && this.peek() === "x") {
|
1125
|
+
value += this.current;
|
1126
|
+
this.advance();
|
1127
|
+
value += this.current;
|
1128
|
+
this.advance();
|
1129
|
+
while (this.current !== null && /[0-9a-fA-F]/.test(this.current)) {
|
1130
|
+
value += this.current;
|
1131
|
+
this.advance();
|
1132
|
+
}
|
1133
|
+
return createToken("NUMBER" /* NUMBER */, parseInt(value, 16), start);
|
1134
|
+
}
|
1135
|
+
while (this.current !== null && this.isDigit(this.current)) {
|
1136
|
+
value += this.current;
|
1137
|
+
this.advance();
|
1138
|
+
}
|
1139
|
+
if (this.current === "." && this.peek() && this.isDigit(this.peek())) {
|
1140
|
+
value += this.current;
|
1141
|
+
this.advance();
|
1142
|
+
while (this.current !== null && this.isDigit(this.current)) {
|
1143
|
+
value += this.current;
|
1144
|
+
this.advance();
|
1145
|
+
}
|
1146
|
+
return createToken("NUMBER" /* NUMBER */, parseFloat(value), start);
|
1147
|
+
}
|
1148
|
+
return createToken("NUMBER" /* NUMBER */, parseInt(value, 10), start);
|
1149
|
+
}
|
1150
|
+
/**
|
1151
|
+
* Read an identifier or keyword token.
|
1152
|
+
* @private
|
1153
|
+
*/
|
1154
|
+
readIdentifierOrKeyword() {
|
1155
|
+
const start = this.position;
|
1156
|
+
let value = "";
|
1157
|
+
while (this.current !== null && this.isIdentifierPart(this.current)) {
|
1158
|
+
value += this.current;
|
1159
|
+
this.advance();
|
1160
|
+
}
|
1161
|
+
switch (value) {
|
1162
|
+
case "true":
|
1163
|
+
return createToken("BOOLEAN" /* BOOLEAN */, true, start);
|
1164
|
+
case "false":
|
1165
|
+
return createToken("BOOLEAN" /* BOOLEAN */, false, start);
|
1166
|
+
case "and":
|
1167
|
+
return createToken("AND" /* AND */, value, start);
|
1168
|
+
case "or":
|
1169
|
+
return createToken("OR" /* OR */, value, start);
|
1170
|
+
case "not":
|
1171
|
+
return createToken("NOT" /* NOT */, value, start);
|
1172
|
+
default:
|
1173
|
+
return createToken("IDENTIFIER" /* IDENTIFIER */, value, start);
|
1174
|
+
}
|
1175
|
+
}
|
1176
|
+
/**
|
1177
|
+
* Read a string token.
|
1178
|
+
* @private
|
1179
|
+
*/
|
1180
|
+
readString() {
|
1181
|
+
const start = this.position;
|
1182
|
+
const quote = this.current;
|
1183
|
+
let value = "";
|
1184
|
+
this.advance();
|
1185
|
+
while (this.current !== null && this.current !== quote) {
|
1186
|
+
if (this.current === "\\") {
|
1187
|
+
this.advance();
|
1188
|
+
if (this.current === null) {
|
1189
|
+
throw new ParseError("Unterminated string", start);
|
1190
|
+
}
|
1191
|
+
const ch = this.current;
|
1192
|
+
if (ch === "n") {
|
1193
|
+
value += "\n";
|
1194
|
+
} else if (ch === "t") {
|
1195
|
+
value += " ";
|
1196
|
+
} else if (ch === "r") {
|
1197
|
+
value += "\r";
|
1198
|
+
} else if (ch === "\\") {
|
1199
|
+
value += "\\";
|
1200
|
+
} else if (ch === '"') {
|
1201
|
+
value += '"';
|
1202
|
+
} else if (ch === "'") {
|
1203
|
+
value += "'";
|
1204
|
+
} else {
|
1205
|
+
value += ch;
|
1206
|
+
}
|
1207
|
+
} else {
|
1208
|
+
value += this.current;
|
1209
|
+
}
|
1210
|
+
this.advance();
|
1211
|
+
}
|
1212
|
+
if (this.current === null) {
|
1213
|
+
throw new ParseError("Unterminated string", start);
|
1214
|
+
}
|
1215
|
+
this.advance();
|
1216
|
+
return createToken("STRING" /* STRING */, value, start);
|
1217
|
+
}
|
1218
|
+
/**
|
1219
|
+
* Read an operator or punctuation token.
|
1220
|
+
* @private
|
1221
|
+
*/
|
1222
|
+
readOperator() {
|
1223
|
+
const start = this.position;
|
1224
|
+
const char = this.current;
|
1225
|
+
switch (char) {
|
1226
|
+
case "+":
|
1227
|
+
this.advance();
|
1228
|
+
return createToken("PLUS" /* PLUS */, char, start);
|
1229
|
+
case "-":
|
1230
|
+
this.advance();
|
1231
|
+
return createToken("MINUS" /* MINUS */, char, start);
|
1232
|
+
case "*":
|
1233
|
+
this.advance();
|
1234
|
+
return createToken("STAR" /* STAR */, char, start);
|
1235
|
+
case "/":
|
1236
|
+
this.advance();
|
1237
|
+
return createToken("SLASH" /* SLASH */, char, start);
|
1238
|
+
case "%":
|
1239
|
+
this.advance();
|
1240
|
+
return createToken("PERCENT" /* PERCENT */, char, start);
|
1241
|
+
case "<":
|
1242
|
+
this.advance();
|
1243
|
+
if (this.current === "=") {
|
1244
|
+
this.advance();
|
1245
|
+
return createToken("LE" /* LE */, "<=", start);
|
1246
|
+
} else if (this.current === "<") {
|
1247
|
+
this.advance();
|
1248
|
+
return createToken("LSHIFT" /* LSHIFT */, "<<", start);
|
1249
|
+
}
|
1250
|
+
return createToken("LT" /* LT */, "<", start);
|
1251
|
+
case ">":
|
1252
|
+
this.advance();
|
1253
|
+
if (this.current === "=") {
|
1254
|
+
this.advance();
|
1255
|
+
return createToken("GE" /* GE */, ">=", start);
|
1256
|
+
} else if (this.current === ">") {
|
1257
|
+
this.advance();
|
1258
|
+
return createToken("RSHIFT" /* RSHIFT */, ">>", start);
|
1259
|
+
}
|
1260
|
+
return createToken("GT" /* GT */, ">", start);
|
1261
|
+
case "=":
|
1262
|
+
this.advance();
|
1263
|
+
if (this.current === "=") {
|
1264
|
+
this.advance();
|
1265
|
+
return createToken("EQ" /* EQ */, "==", start);
|
1266
|
+
}
|
1267
|
+
throw new ParseError("Expected == for equality", start);
|
1268
|
+
case "!":
|
1269
|
+
this.advance();
|
1270
|
+
if (this.current === "=") {
|
1271
|
+
this.advance();
|
1272
|
+
return createToken("NE" /* NE */, "!=", start);
|
1273
|
+
}
|
1274
|
+
throw new ParseError("Expected != for inequality", start);
|
1275
|
+
case "&":
|
1276
|
+
this.advance();
|
1277
|
+
return createToken("AMPERSAND" /* AMPERSAND */, char, start);
|
1278
|
+
case "|":
|
1279
|
+
this.advance();
|
1280
|
+
return createToken("PIPE" /* PIPE */, char, start);
|
1281
|
+
case "^":
|
1282
|
+
this.advance();
|
1283
|
+
return createToken("CARET" /* CARET */, char, start);
|
1284
|
+
case "?":
|
1285
|
+
this.advance();
|
1286
|
+
return createToken("QUESTION" /* QUESTION */, char, start);
|
1287
|
+
case ":":
|
1288
|
+
this.advance();
|
1289
|
+
if (this.current === ":") {
|
1290
|
+
this.advance();
|
1291
|
+
return createToken("DOUBLE_COLON" /* DOUBLE_COLON */, "::", start);
|
1292
|
+
}
|
1293
|
+
return createToken("COLON" /* COLON */, char, start);
|
1294
|
+
case "(":
|
1295
|
+
this.advance();
|
1296
|
+
return createToken("LPAREN" /* LPAREN */, char, start);
|
1297
|
+
case ")":
|
1298
|
+
this.advance();
|
1299
|
+
return createToken("RPAREN" /* RPAREN */, char, start);
|
1300
|
+
case "[":
|
1301
|
+
this.advance();
|
1302
|
+
return createToken("LBRACKET" /* LBRACKET */, char, start);
|
1303
|
+
case "]":
|
1304
|
+
this.advance();
|
1305
|
+
return createToken("RBRACKET" /* RBRACKET */, char, start);
|
1306
|
+
case ".":
|
1307
|
+
this.advance();
|
1308
|
+
return createToken("DOT" /* DOT */, char, start);
|
1309
|
+
case ",":
|
1310
|
+
this.advance();
|
1311
|
+
return createToken("COMMA" /* COMMA */, char, start);
|
1312
|
+
default:
|
1313
|
+
return null;
|
1314
|
+
}
|
1315
|
+
}
|
1316
|
+
};
|
1317
|
+
|
1318
|
+
// src/expression/AST.ts
|
1319
|
+
function createLiteral(value) {
|
1320
|
+
return { kind: "Literal", value };
|
1321
|
+
}
|
1322
|
+
function createIdentifier(name) {
|
1323
|
+
return { kind: "Identifier", name };
|
1324
|
+
}
|
1325
|
+
function createBinaryOp(operator, left, right) {
|
1326
|
+
return { kind: "BinaryOp", operator, left, right };
|
1327
|
+
}
|
1328
|
+
function createUnaryOp(operator, operand) {
|
1329
|
+
return { kind: "UnaryOp", operator, operand };
|
1330
|
+
}
|
1331
|
+
function createTernary(condition, ifTrue, ifFalse) {
|
1332
|
+
return { kind: "Ternary", condition, ifTrue, ifFalse };
|
1333
|
+
}
|
1334
|
+
function createMemberAccess(object, property) {
|
1335
|
+
return { kind: "MemberAccess", object, property };
|
1336
|
+
}
|
1337
|
+
function createIndexAccess(object, index) {
|
1338
|
+
return { kind: "IndexAccess", object, index };
|
1339
|
+
}
|
1340
|
+
function createMethodCall(object, method, args) {
|
1341
|
+
return { kind: "MethodCall", object, method, args };
|
1342
|
+
}
|
1343
|
+
function createEnumAccess(enumName, value) {
|
1344
|
+
return { kind: "EnumAccess", enumName, value };
|
1345
|
+
}
|
1346
|
+
|
1347
|
+
// src/expression/Parser.ts
|
1348
|
+
var ExpressionParser = class {
|
1349
|
+
/**
|
1350
|
+
* Create a new expression parser.
|
1351
|
+
*
|
1352
|
+
* @param tokens - Array of tokens to parse
|
1353
|
+
*/
|
1354
|
+
constructor(tokens) {
|
1355
|
+
this.position = 0;
|
1356
|
+
this.tokens = tokens;
|
1357
|
+
}
|
1358
|
+
/**
|
1359
|
+
* Parse the tokens into an AST.
|
1360
|
+
*
|
1361
|
+
* @returns Root AST node
|
1362
|
+
* @throws {ParseError} If invalid syntax is encountered
|
1363
|
+
*/
|
1364
|
+
parse() {
|
1365
|
+
const expr = this.parseTernary();
|
1366
|
+
if (!this.isAtEnd()) {
|
1367
|
+
throw new ParseError(
|
1368
|
+
`Unexpected token: ${this.current().type}`,
|
1369
|
+
this.current().position
|
1370
|
+
);
|
1371
|
+
}
|
1372
|
+
return expr;
|
1373
|
+
}
|
1374
|
+
/**
|
1375
|
+
* Get the current token.
|
1376
|
+
* @private
|
1377
|
+
*/
|
1378
|
+
current() {
|
1379
|
+
return this.tokens[this.position];
|
1380
|
+
}
|
1381
|
+
/**
|
1382
|
+
* Check if we're at the end of tokens.
|
1383
|
+
* @private
|
1384
|
+
*/
|
1385
|
+
isAtEnd() {
|
1386
|
+
return this.current().type === "EOF" /* EOF */;
|
1387
|
+
}
|
1388
|
+
/**
|
1389
|
+
* Advance to the next token.
|
1390
|
+
* @private
|
1391
|
+
*/
|
1392
|
+
advance() {
|
1393
|
+
if (!this.isAtEnd()) {
|
1394
|
+
this.position++;
|
1395
|
+
}
|
1396
|
+
return this.tokens[this.position - 1];
|
1397
|
+
}
|
1398
|
+
/**
|
1399
|
+
* Check if current token matches any of the given types.
|
1400
|
+
* @private
|
1401
|
+
*/
|
1402
|
+
match(...types) {
|
1403
|
+
for (const type of types) {
|
1404
|
+
if (this.current().type === type) {
|
1405
|
+
this.advance();
|
1406
|
+
return true;
|
1407
|
+
}
|
1408
|
+
}
|
1409
|
+
return false;
|
1410
|
+
}
|
1411
|
+
/**
|
1412
|
+
* Expect a specific token type and advance.
|
1413
|
+
* @private
|
1414
|
+
*/
|
1415
|
+
expect(type, message) {
|
1416
|
+
if (this.current().type !== type) {
|
1417
|
+
throw new ParseError(message, this.current().position);
|
1418
|
+
}
|
1419
|
+
return this.advance();
|
1420
|
+
}
|
1421
|
+
/**
|
1422
|
+
* Parse ternary conditional (lowest precedence).
|
1423
|
+
* condition ? ifTrue : ifFalse
|
1424
|
+
* @private
|
1425
|
+
*/
|
1426
|
+
parseTernary() {
|
1427
|
+
let expr = this.parseLogicalOr();
|
1428
|
+
if (this.match("QUESTION" /* QUESTION */)) {
|
1429
|
+
const ifTrue = this.parseTernary();
|
1430
|
+
this.expect("COLON" /* COLON */, "Expected : in ternary expression");
|
1431
|
+
const ifFalse = this.parseTernary();
|
1432
|
+
return createTernary(expr, ifTrue, ifFalse);
|
1433
|
+
}
|
1434
|
+
return expr;
|
1435
|
+
}
|
1436
|
+
/**
|
1437
|
+
* Parse logical OR.
|
1438
|
+
* @private
|
1439
|
+
*/
|
1440
|
+
parseLogicalOr() {
|
1441
|
+
let left = this.parseLogicalAnd();
|
1442
|
+
while (this.match("OR" /* OR */)) {
|
1443
|
+
const operator = "or";
|
1444
|
+
const right = this.parseLogicalAnd();
|
1445
|
+
left = createBinaryOp(operator, left, right);
|
1446
|
+
}
|
1447
|
+
return left;
|
1448
|
+
}
|
1449
|
+
/**
|
1450
|
+
* Parse logical AND.
|
1451
|
+
* @private
|
1452
|
+
*/
|
1453
|
+
parseLogicalAnd() {
|
1454
|
+
let left = this.parseBitwiseOr();
|
1455
|
+
while (this.match("AND" /* AND */)) {
|
1456
|
+
const operator = "and";
|
1457
|
+
const right = this.parseBitwiseOr();
|
1458
|
+
left = createBinaryOp(operator, left, right);
|
1459
|
+
}
|
1460
|
+
return left;
|
1461
|
+
}
|
1462
|
+
/**
|
1463
|
+
* Parse bitwise OR.
|
1464
|
+
* @private
|
1465
|
+
*/
|
1466
|
+
parseBitwiseOr() {
|
1467
|
+
let left = this.parseBitwiseXor();
|
1468
|
+
while (this.match("PIPE" /* PIPE */)) {
|
1469
|
+
const operator = "|";
|
1470
|
+
const right = this.parseBitwiseXor();
|
1471
|
+
left = createBinaryOp(operator, left, right);
|
1472
|
+
}
|
1473
|
+
return left;
|
1474
|
+
}
|
1475
|
+
/**
|
1476
|
+
* Parse bitwise XOR.
|
1477
|
+
* @private
|
1478
|
+
*/
|
1479
|
+
parseBitwiseXor() {
|
1480
|
+
let left = this.parseBitwiseAnd();
|
1481
|
+
while (this.match("CARET" /* CARET */)) {
|
1482
|
+
const operator = "^";
|
1483
|
+
const right = this.parseBitwiseAnd();
|
1484
|
+
left = createBinaryOp(operator, left, right);
|
1485
|
+
}
|
1486
|
+
return left;
|
1487
|
+
}
|
1488
|
+
/**
|
1489
|
+
* Parse bitwise AND.
|
1490
|
+
* @private
|
1491
|
+
*/
|
1492
|
+
parseBitwiseAnd() {
|
1493
|
+
let left = this.parseEquality();
|
1494
|
+
while (this.match("AMPERSAND" /* AMPERSAND */)) {
|
1495
|
+
const operator = "&";
|
1496
|
+
const right = this.parseEquality();
|
1497
|
+
left = createBinaryOp(operator, left, right);
|
1498
|
+
}
|
1499
|
+
return left;
|
1500
|
+
}
|
1501
|
+
/**
|
1502
|
+
* Parse equality operators (==, !=).
|
1503
|
+
* @private
|
1504
|
+
*/
|
1505
|
+
parseEquality() {
|
1506
|
+
let left = this.parseRelational();
|
1507
|
+
while (this.match("EQ" /* EQ */, "NE" /* NE */)) {
|
1508
|
+
const operator = this.tokens[this.position - 1].value;
|
1509
|
+
const right = this.parseRelational();
|
1510
|
+
left = createBinaryOp(operator, left, right);
|
1511
|
+
}
|
1512
|
+
return left;
|
1513
|
+
}
|
1514
|
+
/**
|
1515
|
+
* Parse relational operators (<, <=, >, >=).
|
1516
|
+
* @private
|
1517
|
+
*/
|
1518
|
+
parseRelational() {
|
1519
|
+
let left = this.parseBitShift();
|
1520
|
+
while (this.match("LT" /* LT */, "LE" /* LE */, "GT" /* GT */, "GE" /* GE */)) {
|
1521
|
+
const operator = this.tokens[this.position - 1].value;
|
1522
|
+
const right = this.parseBitShift();
|
1523
|
+
left = createBinaryOp(operator, left, right);
|
1524
|
+
}
|
1525
|
+
return left;
|
1526
|
+
}
|
1527
|
+
/**
|
1528
|
+
* Parse bit shift operators (<<, >>).
|
1529
|
+
* @private
|
1530
|
+
*/
|
1531
|
+
parseBitShift() {
|
1532
|
+
let left = this.parseAdditive();
|
1533
|
+
while (this.match("LSHIFT" /* LSHIFT */, "RSHIFT" /* RSHIFT */)) {
|
1534
|
+
const operator = this.tokens[this.position - 1].value;
|
1535
|
+
const right = this.parseAdditive();
|
1536
|
+
left = createBinaryOp(operator, left, right);
|
1537
|
+
}
|
1538
|
+
return left;
|
1539
|
+
}
|
1540
|
+
/**
|
1541
|
+
* Parse additive operators (+, -).
|
1542
|
+
* @private
|
1543
|
+
*/
|
1544
|
+
parseAdditive() {
|
1545
|
+
let left = this.parseMultiplicative();
|
1546
|
+
while (this.match("PLUS" /* PLUS */, "MINUS" /* MINUS */)) {
|
1547
|
+
const operator = this.tokens[this.position - 1].value;
|
1548
|
+
const right = this.parseMultiplicative();
|
1549
|
+
left = createBinaryOp(operator, left, right);
|
1550
|
+
}
|
1551
|
+
return left;
|
1552
|
+
}
|
1553
|
+
/**
|
1554
|
+
* Parse multiplicative operators (*, /, %).
|
1555
|
+
* @private
|
1556
|
+
*/
|
1557
|
+
parseMultiplicative() {
|
1558
|
+
let left = this.parseUnary();
|
1559
|
+
while (this.match("STAR" /* STAR */, "SLASH" /* SLASH */, "PERCENT" /* PERCENT */)) {
|
1560
|
+
const operator = this.tokens[this.position - 1].value;
|
1561
|
+
const right = this.parseUnary();
|
1562
|
+
left = createBinaryOp(operator, left, right);
|
1563
|
+
}
|
1564
|
+
return left;
|
1565
|
+
}
|
1566
|
+
/**
|
1567
|
+
* Parse unary operators (-, not).
|
1568
|
+
* @private
|
1569
|
+
*/
|
1570
|
+
parseUnary() {
|
1571
|
+
if (this.match("MINUS" /* MINUS */, "NOT" /* NOT */)) {
|
1572
|
+
const operator = this.tokens[this.position - 1].value;
|
1573
|
+
const operand = this.parseUnary();
|
1574
|
+
return createUnaryOp(operator, operand);
|
1575
|
+
}
|
1576
|
+
return this.parsePostfix();
|
1577
|
+
}
|
1578
|
+
/**
|
1579
|
+
* Parse postfix operators (., [], ()).
|
1580
|
+
* @private
|
1581
|
+
*/
|
1582
|
+
parsePostfix() {
|
1583
|
+
let expr = this.parsePrimary();
|
1584
|
+
while (true) {
|
1585
|
+
if (this.match("DOT" /* DOT */)) {
|
1586
|
+
const property = this.expect(
|
1587
|
+
"IDENTIFIER" /* IDENTIFIER */,
|
1588
|
+
"Expected property name after ."
|
1589
|
+
);
|
1590
|
+
expr = createMemberAccess(expr, property.value);
|
1591
|
+
} else if (this.match("LBRACKET" /* LBRACKET */)) {
|
1592
|
+
const index = this.parseTernary();
|
1593
|
+
this.expect("RBRACKET" /* RBRACKET */, "Expected ] after array index");
|
1594
|
+
expr = createIndexAccess(expr, index);
|
1595
|
+
} else if (this.current().type === "LPAREN" /* LPAREN */) {
|
1596
|
+
if (expr.kind === "MemberAccess") {
|
1597
|
+
this.advance();
|
1598
|
+
const args = [];
|
1599
|
+
if (this.current().type !== "RPAREN" /* RPAREN */) {
|
1600
|
+
args.push(this.parseTernary());
|
1601
|
+
while (this.match("COMMA" /* COMMA */)) {
|
1602
|
+
args.push(this.parseTernary());
|
1603
|
+
}
|
1604
|
+
}
|
1605
|
+
this.expect("RPAREN" /* RPAREN */, "Expected ) after arguments");
|
1606
|
+
const memberExpr = expr;
|
1607
|
+
expr = createMethodCall(memberExpr.object, memberExpr.property, args);
|
1608
|
+
} else {
|
1609
|
+
break;
|
1610
|
+
}
|
1611
|
+
} else {
|
1612
|
+
break;
|
1613
|
+
}
|
1614
|
+
}
|
1615
|
+
return expr;
|
1616
|
+
}
|
1617
|
+
/**
|
1618
|
+
* Parse primary expressions (literals, identifiers, grouping).
|
1619
|
+
* @private
|
1620
|
+
*/
|
1621
|
+
parsePrimary() {
|
1622
|
+
if (this.match("NUMBER" /* NUMBER */, "STRING" /* STRING */, "BOOLEAN" /* BOOLEAN */)) {
|
1623
|
+
const token = this.tokens[this.position - 1];
|
1624
|
+
return createLiteral(token.value);
|
1625
|
+
}
|
1626
|
+
if (this.match("IDENTIFIER" /* IDENTIFIER */)) {
|
1627
|
+
const name = this.tokens[this.position - 1].value;
|
1628
|
+
if (this.match("DOUBLE_COLON" /* DOUBLE_COLON */)) {
|
1629
|
+
const value = this.expect(
|
1630
|
+
"IDENTIFIER" /* IDENTIFIER */,
|
1631
|
+
"Expected enum value after ::"
|
1632
|
+
);
|
1633
|
+
return createEnumAccess(name, value.value);
|
1634
|
+
}
|
1635
|
+
return createIdentifier(name);
|
1636
|
+
}
|
1637
|
+
if (this.match("LPAREN" /* LPAREN */)) {
|
1638
|
+
const expr = this.parseTernary();
|
1639
|
+
this.expect("RPAREN" /* RPAREN */, "Expected ) after expression");
|
1640
|
+
return expr;
|
1641
|
+
}
|
1642
|
+
throw new ParseError(
|
1643
|
+
`Unexpected token: ${this.current().type}`,
|
1644
|
+
this.current().position
|
1645
|
+
);
|
1646
|
+
}
|
1647
|
+
};
|
1648
|
+
|
1649
|
+
// src/expression/Evaluator.ts
|
1650
|
+
var Evaluator = class {
|
1651
|
+
/**
|
1652
|
+
* Evaluate an AST node in the given context.
|
1653
|
+
*
|
1654
|
+
* @param node - AST node to evaluate
|
1655
|
+
* @param context - Execution context
|
1656
|
+
* @returns Evaluated value
|
1657
|
+
* @throws {ParseError} If evaluation fails
|
1658
|
+
*/
|
1659
|
+
evaluate(node, context) {
|
1660
|
+
const n = node;
|
1661
|
+
switch (node.kind) {
|
1662
|
+
case "Literal":
|
1663
|
+
return n.value;
|
1664
|
+
case "Identifier":
|
1665
|
+
return this.evaluateIdentifier(n.name, context);
|
1666
|
+
case "BinaryOp":
|
1667
|
+
return this.evaluateBinaryOp(n.operator, n.left, n.right, context);
|
1668
|
+
case "UnaryOp":
|
1669
|
+
return this.evaluateUnaryOp(n.operator, n.operand, context);
|
1670
|
+
case "Ternary":
|
1671
|
+
return this.evaluateTernary(n.condition, n.ifTrue, n.ifFalse, context);
|
1672
|
+
case "MemberAccess":
|
1673
|
+
return this.evaluateMemberAccess(n.object, n.property, context);
|
1674
|
+
case "IndexAccess":
|
1675
|
+
return this.evaluateIndexAccess(n.object, n.index, context);
|
1676
|
+
case "MethodCall":
|
1677
|
+
return this.evaluateMethodCall(n.object, n.method, n.args, context);
|
1678
|
+
case "EnumAccess":
|
1679
|
+
return this.evaluateEnumAccess(n.enumName, n.value, context);
|
1680
|
+
default:
|
1681
|
+
throw new ParseError(`Unknown AST node kind: ${node.kind}`);
|
1682
|
+
}
|
1683
|
+
}
|
1684
|
+
/**
|
1685
|
+
* Evaluate an identifier.
|
1686
|
+
* @private
|
1687
|
+
*/
|
1688
|
+
evaluateIdentifier(name, context) {
|
1689
|
+
return context.resolve(name);
|
1690
|
+
}
|
1691
|
+
/**
|
1692
|
+
* Evaluate a binary operation.
|
1693
|
+
* @private
|
1694
|
+
*/
|
1695
|
+
evaluateBinaryOp(operator, left, right, context) {
|
1696
|
+
const leftVal = this.evaluate(left, context);
|
1697
|
+
const rightVal = this.evaluate(right, context);
|
1698
|
+
switch (operator) {
|
1699
|
+
// Arithmetic
|
1700
|
+
case "+":
|
1701
|
+
return this.add(leftVal, rightVal);
|
1702
|
+
case "-":
|
1703
|
+
return this.toNumber(leftVal) - this.toNumber(rightVal);
|
1704
|
+
case "*":
|
1705
|
+
return this.toNumber(leftVal) * this.toNumber(rightVal);
|
1706
|
+
case "/":
|
1707
|
+
return this.toNumber(leftVal) / this.toNumber(rightVal);
|
1708
|
+
case "%":
|
1709
|
+
return this.modulo(this.toNumber(leftVal), this.toNumber(rightVal));
|
1710
|
+
// Comparison
|
1711
|
+
case "<":
|
1712
|
+
return this.compare(leftVal, rightVal) < 0;
|
1713
|
+
case "<=":
|
1714
|
+
return this.compare(leftVal, rightVal) <= 0;
|
1715
|
+
case ">":
|
1716
|
+
return this.compare(leftVal, rightVal) > 0;
|
1717
|
+
case ">=":
|
1718
|
+
return this.compare(leftVal, rightVal) >= 0;
|
1719
|
+
case "==":
|
1720
|
+
return this.equals(leftVal, rightVal);
|
1721
|
+
case "!=":
|
1722
|
+
return !this.equals(leftVal, rightVal);
|
1723
|
+
// Bitwise
|
1724
|
+
case "<<":
|
1725
|
+
return this.toInt(leftVal) << this.toInt(rightVal);
|
1726
|
+
case ">>":
|
1727
|
+
return this.toInt(leftVal) >> this.toInt(rightVal);
|
1728
|
+
case "&":
|
1729
|
+
return this.toInt(leftVal) & this.toInt(rightVal);
|
1730
|
+
case "|":
|
1731
|
+
return this.toInt(leftVal) | this.toInt(rightVal);
|
1732
|
+
case "^":
|
1733
|
+
return this.toInt(leftVal) ^ this.toInt(rightVal);
|
1734
|
+
// Logical
|
1735
|
+
case "and":
|
1736
|
+
return this.toBoolean(leftVal) && this.toBoolean(rightVal);
|
1737
|
+
case "or":
|
1738
|
+
return this.toBoolean(leftVal) || this.toBoolean(rightVal);
|
1739
|
+
default:
|
1740
|
+
throw new ParseError(`Unknown binary operator: ${operator}`);
|
1741
|
+
}
|
1742
|
+
}
|
1743
|
+
/**
|
1744
|
+
* Evaluate a unary operation.
|
1745
|
+
* @private
|
1746
|
+
*/
|
1747
|
+
evaluateUnaryOp(operator, operand, context) {
|
1748
|
+
const value = this.evaluate(operand, context);
|
1749
|
+
switch (operator) {
|
1750
|
+
case "-":
|
1751
|
+
return -this.toNumber(value);
|
1752
|
+
case "not":
|
1753
|
+
return !this.toBoolean(value);
|
1754
|
+
default:
|
1755
|
+
throw new ParseError(`Unknown unary operator: ${operator}`);
|
1756
|
+
}
|
1757
|
+
}
|
1758
|
+
/**
|
1759
|
+
* Evaluate a ternary conditional.
|
1760
|
+
* @private
|
1761
|
+
*/
|
1762
|
+
evaluateTernary(condition, ifTrue, ifFalse, context) {
|
1763
|
+
const condValue = this.evaluate(condition, context);
|
1764
|
+
return this.toBoolean(condValue) ? this.evaluate(ifTrue, context) : this.evaluate(ifFalse, context);
|
1765
|
+
}
|
1766
|
+
/**
|
1767
|
+
* Evaluate member access (object.property).
|
1768
|
+
* @private
|
1769
|
+
*/
|
1770
|
+
evaluateMemberAccess(object, property, context) {
|
1771
|
+
const obj = this.evaluate(object, context);
|
1772
|
+
if (obj === null || obj === void 0) {
|
1773
|
+
throw new ParseError(
|
1774
|
+
`Cannot access property ${property} of null/undefined`
|
1775
|
+
);
|
1776
|
+
}
|
1777
|
+
if (typeof obj === "object") {
|
1778
|
+
return obj[property];
|
1779
|
+
}
|
1780
|
+
throw new ParseError(`Cannot access property ${property} of non-object`);
|
1781
|
+
}
|
1782
|
+
/**
|
1783
|
+
* Evaluate index access (array[index]).
|
1784
|
+
* @private
|
1785
|
+
*/
|
1786
|
+
evaluateIndexAccess(object, index, context) {
|
1787
|
+
const obj = this.evaluate(object, context);
|
1788
|
+
const idx = this.evaluate(index, context);
|
1789
|
+
if (Array.isArray(obj)) {
|
1790
|
+
const numIdx = this.toInt(idx);
|
1791
|
+
return obj[numIdx];
|
1792
|
+
}
|
1793
|
+
if (obj instanceof Uint8Array) {
|
1794
|
+
const numIdx = this.toInt(idx);
|
1795
|
+
return obj[numIdx];
|
1796
|
+
}
|
1797
|
+
throw new ParseError("Index access requires an array");
|
1798
|
+
}
|
1799
|
+
/**
|
1800
|
+
* Evaluate method call (object.method()).
|
1801
|
+
* @private
|
1802
|
+
*/
|
1803
|
+
evaluateMethodCall(object, method, _args, context) {
|
1804
|
+
const obj = this.evaluate(object, context);
|
1805
|
+
if (method === "length" || method === "size") {
|
1806
|
+
if (Array.isArray(obj)) return obj.length;
|
1807
|
+
if (obj instanceof Uint8Array) return obj.length;
|
1808
|
+
if (typeof obj === "string") return obj.length;
|
1809
|
+
throw new ParseError(`Object does not have a ${method} property`);
|
1810
|
+
}
|
1811
|
+
if (method === "to_i") {
|
1812
|
+
return this.toInt(obj);
|
1813
|
+
}
|
1814
|
+
if (method === "to_s") {
|
1815
|
+
return String(obj);
|
1816
|
+
}
|
1817
|
+
throw new ParseError(`Unknown method: ${method}`);
|
1818
|
+
}
|
1819
|
+
/**
|
1820
|
+
* Evaluate enum access (EnumName::value).
|
1821
|
+
* @private
|
1822
|
+
*/
|
1823
|
+
evaluateEnumAccess(enumName, valueName, context) {
|
1824
|
+
const value = context.getEnumValue(enumName, valueName);
|
1825
|
+
if (value === void 0) {
|
1826
|
+
throw new ParseError(`Enum value "${enumName}::${valueName}" not found`);
|
1827
|
+
}
|
1828
|
+
return value;
|
1829
|
+
}
|
1830
|
+
/**
|
1831
|
+
* Helper: Add two values (handles strings and numbers).
|
1832
|
+
* @private
|
1833
|
+
*/
|
1834
|
+
add(left, right) {
|
1835
|
+
if (typeof left === "string" || typeof right === "string") {
|
1836
|
+
return String(left) + String(right);
|
1837
|
+
}
|
1838
|
+
return this.toNumber(left) + this.toNumber(right);
|
1839
|
+
}
|
1840
|
+
/**
|
1841
|
+
* Helper: Modulo operation (Kaitai-style, not remainder).
|
1842
|
+
* @private
|
1843
|
+
*/
|
1844
|
+
modulo(a, b) {
|
1845
|
+
const result = a % b;
|
1846
|
+
return result < 0 ? result + b : result;
|
1847
|
+
}
|
1848
|
+
/**
|
1849
|
+
* Helper: Compare two values.
|
1850
|
+
* @private
|
1851
|
+
*/
|
1852
|
+
compare(left, right) {
|
1853
|
+
if (typeof left === "string" && typeof right === "string") {
|
1854
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
1855
|
+
}
|
1856
|
+
const leftNum = this.toNumber(left);
|
1857
|
+
const rightNum = this.toNumber(right);
|
1858
|
+
return leftNum < rightNum ? -1 : leftNum > rightNum ? 1 : 0;
|
1859
|
+
}
|
1860
|
+
/**
|
1861
|
+
* Helper: Check equality.
|
1862
|
+
* @private
|
1863
|
+
*/
|
1864
|
+
equals(left, right) {
|
1865
|
+
if (typeof left === "bigint" || typeof right === "bigint") {
|
1866
|
+
return BigInt(left) === BigInt(right);
|
1867
|
+
}
|
1868
|
+
return left === right;
|
1869
|
+
}
|
1870
|
+
/**
|
1871
|
+
* Helper: Convert to number.
|
1872
|
+
* @private
|
1873
|
+
*/
|
1874
|
+
toNumber(value) {
|
1875
|
+
if (typeof value === "number") return value;
|
1876
|
+
if (typeof value === "bigint") return Number(value);
|
1877
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
1878
|
+
if (typeof value === "string") return parseFloat(value);
|
1879
|
+
throw new ParseError(`Cannot convert ${typeof value} to number`);
|
1880
|
+
}
|
1881
|
+
/**
|
1882
|
+
* Helper: Convert to integer.
|
1883
|
+
* @private
|
1884
|
+
*/
|
1885
|
+
toInt(value) {
|
1886
|
+
return Math.floor(this.toNumber(value));
|
1887
|
+
}
|
1888
|
+
/**
|
1889
|
+
* Helper: Convert to boolean.
|
1890
|
+
* @private
|
1891
|
+
*/
|
1892
|
+
toBoolean(value) {
|
1893
|
+
if (typeof value === "boolean") return value;
|
1894
|
+
if (typeof value === "number") return value !== 0;
|
1895
|
+
if (typeof value === "bigint") return value !== 0n;
|
1896
|
+
if (typeof value === "string") return value.length > 0;
|
1897
|
+
if (value === null || value === void 0) return false;
|
1898
|
+
return true;
|
1899
|
+
}
|
1900
|
+
};
|
1901
|
+
|
1902
|
+
// src/expression/index.ts
|
1903
|
+
function evaluateExpression(expression, context) {
|
1904
|
+
const lexer = new Lexer(expression);
|
1905
|
+
const tokens = lexer.tokenize();
|
1906
|
+
const parser = new ExpressionParser(tokens);
|
1907
|
+
const ast = parser.parse();
|
1908
|
+
const evaluator = new Evaluator();
|
1909
|
+
return evaluator.evaluate(ast, context);
|
1910
|
+
}
|
1911
|
+
|
1912
|
+
// src/interpreter/TypeInterpreter.ts
|
1913
|
+
var TypeInterpreter = class _TypeInterpreter {
|
1914
|
+
/**
|
1915
|
+
* Create a new type interpreter.
|
1916
|
+
*
|
1917
|
+
* @param schema - Kaitai Struct schema to interpret
|
1918
|
+
* @param parentMeta - Parent schema's meta (for nested types)
|
1919
|
+
*/
|
1920
|
+
constructor(schema, parentMeta) {
|
1921
|
+
this.schema = schema;
|
1922
|
+
this.parentMeta = parentMeta;
|
1923
|
+
if (!schema.meta && !parentMeta) {
|
1924
|
+
throw new ParseError("Schema must have meta section");
|
1925
|
+
}
|
1926
|
+
if (schema.meta && !schema.meta.id && !parentMeta) {
|
1927
|
+
throw new ParseError("Root schema must have meta.id");
|
1928
|
+
}
|
1929
|
+
}
|
1930
|
+
/**
|
1931
|
+
* Parse binary data according to the schema.
|
1932
|
+
*
|
1933
|
+
* @param stream - Binary stream to parse
|
1934
|
+
* @param parent - Parent object (for nested types)
|
1935
|
+
* @param typeArgs - Arguments for parametric types
|
1936
|
+
* @returns Parsed object
|
1937
|
+
*/
|
1938
|
+
parse(stream, parent, typeArgs) {
|
1939
|
+
const result = {};
|
1940
|
+
const context = new Context(stream, result, parent, this.schema.enums);
|
1941
|
+
context.current = result;
|
1942
|
+
if (typeArgs && this.schema.params) {
|
1943
|
+
for (let i = 0; i < this.schema.params.length && i < typeArgs.length; i++) {
|
1944
|
+
const param = this.schema.params[i];
|
1945
|
+
const argValue = typeArgs[i];
|
1946
|
+
const evaluatedArg = typeof argValue === "string" ? this.evaluateValue(argValue, context) : argValue;
|
1947
|
+
context.set(param.id, evaluatedArg);
|
1948
|
+
}
|
1949
|
+
}
|
1950
|
+
if (this.schema.seq) {
|
1951
|
+
for (const attr of this.schema.seq) {
|
1952
|
+
const value = this.parseAttribute(attr, context);
|
1953
|
+
if (attr.id) {
|
1954
|
+
result[attr.id] = value;
|
1955
|
+
}
|
1956
|
+
}
|
1957
|
+
}
|
1958
|
+
if (this.schema.instances) {
|
1959
|
+
this.setupInstances(result, stream, context);
|
1960
|
+
}
|
1961
|
+
return result;
|
1962
|
+
}
|
1963
|
+
/**
|
1964
|
+
* Set up lazy-evaluated instance getters.
|
1965
|
+
* Instances are computed on first access and cached.
|
1966
|
+
*
|
1967
|
+
* @param result - Result object to add getters to
|
1968
|
+
* @param stream - Stream for parsing
|
1969
|
+
* @param context - Execution context
|
1970
|
+
* @private
|
1971
|
+
*/
|
1972
|
+
setupInstances(result, stream, context) {
|
1973
|
+
if (!this.schema.instances) return;
|
1974
|
+
for (const [name, instance] of Object.entries(this.schema.instances)) {
|
1975
|
+
let cached = void 0;
|
1976
|
+
let evaluated = false;
|
1977
|
+
Object.defineProperty(result, name, {
|
1978
|
+
get: () => {
|
1979
|
+
if (!evaluated) {
|
1980
|
+
cached = this.parseInstance(
|
1981
|
+
instance,
|
1982
|
+
stream,
|
1983
|
+
context
|
1984
|
+
);
|
1985
|
+
evaluated = true;
|
1986
|
+
}
|
1987
|
+
return cached;
|
1988
|
+
},
|
1989
|
+
enumerable: true,
|
1990
|
+
configurable: true
|
1991
|
+
});
|
1992
|
+
}
|
1993
|
+
}
|
1994
|
+
/**
|
1995
|
+
* Parse an instance (lazy-evaluated field).
|
1996
|
+
*
|
1997
|
+
* @param instance - Instance specification
|
1998
|
+
* @param stream - Stream to read from
|
1999
|
+
* @param context - Execution context
|
2000
|
+
* @returns Parsed or calculated value
|
2001
|
+
* @private
|
2002
|
+
*/
|
2003
|
+
parseInstance(instance, stream, context) {
|
2004
|
+
if ("value" in instance) {
|
2005
|
+
return this.evaluateValue(
|
2006
|
+
instance.value,
|
2007
|
+
context
|
2008
|
+
);
|
2009
|
+
}
|
2010
|
+
const savedPos = stream.pos;
|
2011
|
+
try {
|
2012
|
+
if (instance.pos !== void 0) {
|
2013
|
+
const pos = this.evaluateValue(
|
2014
|
+
instance.pos,
|
2015
|
+
context
|
2016
|
+
);
|
2017
|
+
if (typeof pos === "number") {
|
2018
|
+
stream.seek(pos);
|
2019
|
+
} else if (typeof pos === "bigint") {
|
2020
|
+
stream.seek(Number(pos));
|
2021
|
+
} else {
|
2022
|
+
throw new ParseError(
|
2023
|
+
`pos must evaluate to a number, got ${typeof pos}`
|
2024
|
+
);
|
2025
|
+
}
|
2026
|
+
}
|
2027
|
+
const value = this.parseAttribute(instance, context);
|
2028
|
+
return value;
|
2029
|
+
} finally {
|
2030
|
+
if (instance.pos !== void 0) {
|
2031
|
+
stream.seek(savedPos);
|
2032
|
+
}
|
2033
|
+
}
|
2034
|
+
}
|
2035
|
+
/**
|
2036
|
+
* Parse a single attribute according to its specification.
|
2037
|
+
*
|
2038
|
+
* @param attr - Attribute specification
|
2039
|
+
* @param context - Execution context
|
2040
|
+
* @returns Parsed value
|
2041
|
+
* @private
|
2042
|
+
*/
|
2043
|
+
parseAttribute(attr, context) {
|
2044
|
+
const stream = context.io;
|
2045
|
+
if (attr.if) {
|
2046
|
+
const condition = this.evaluateValue(attr.if, context);
|
2047
|
+
if (!condition) {
|
2048
|
+
return void 0;
|
2049
|
+
}
|
2050
|
+
}
|
2051
|
+
if (attr.pos !== void 0) {
|
2052
|
+
const pos = this.evaluateValue(attr.pos, context);
|
2053
|
+
if (typeof pos === "number") {
|
2054
|
+
stream.seek(pos);
|
2055
|
+
} else if (typeof pos === "bigint") {
|
2056
|
+
stream.seek(Number(pos));
|
2057
|
+
} else {
|
2058
|
+
throw new ParseError(`pos must evaluate to a number, got ${typeof pos}`);
|
2059
|
+
}
|
2060
|
+
}
|
2061
|
+
if (attr.io) {
|
2062
|
+
throw new NotImplementedError("Custom I/O streams");
|
2063
|
+
}
|
2064
|
+
if (attr.repeat) {
|
2065
|
+
return this.parseRepeated(attr, context);
|
2066
|
+
}
|
2067
|
+
if (attr.contents) {
|
2068
|
+
return this.parseContents(attr, context);
|
2069
|
+
}
|
2070
|
+
const value = this.parseValue(attr, context);
|
2071
|
+
return value;
|
2072
|
+
}
|
2073
|
+
/**
|
2074
|
+
* Parse a repeated attribute.
|
2075
|
+
*
|
2076
|
+
* @param attr - Attribute specification with repeat
|
2077
|
+
* @param context - Execution context
|
2078
|
+
* @returns Array of parsed values
|
2079
|
+
* @private
|
2080
|
+
*/
|
2081
|
+
parseRepeated(attr, context) {
|
2082
|
+
const stream = context.io;
|
2083
|
+
const result = [];
|
2084
|
+
switch (attr.repeat) {
|
2085
|
+
case "expr": {
|
2086
|
+
const countValue = this.evaluateValue(attr["repeat-expr"], context);
|
2087
|
+
const count = typeof countValue === "number" ? countValue : typeof countValue === "bigint" ? Number(countValue) : 0;
|
2088
|
+
if (count < 0) {
|
2089
|
+
throw new ParseError(`repeat-expr must be non-negative, got ${count}`);
|
2090
|
+
}
|
2091
|
+
for (let i = 0; i < count; i++) {
|
2092
|
+
context.set("_index", i);
|
2093
|
+
const value = this.parseAttribute(
|
2094
|
+
{ ...attr, repeat: void 0, "repeat-expr": void 0 },
|
2095
|
+
context
|
2096
|
+
);
|
2097
|
+
result.push(value);
|
2098
|
+
}
|
2099
|
+
break;
|
2100
|
+
}
|
2101
|
+
case "eos": {
|
2102
|
+
while (!stream.isEof()) {
|
2103
|
+
context.set("_index", result.length);
|
2104
|
+
result.push(this.parseValue(attr, context));
|
2105
|
+
}
|
2106
|
+
break;
|
2107
|
+
}
|
2108
|
+
case "until": {
|
2109
|
+
if (!attr["repeat-until"]) {
|
2110
|
+
throw new ParseError("repeat-until expression is required");
|
2111
|
+
}
|
2112
|
+
let index = 0;
|
2113
|
+
while (true) {
|
2114
|
+
context.set("_index", index);
|
2115
|
+
const value = this.parseAttribute(
|
2116
|
+
{ ...attr, repeat: void 0, "repeat-until": void 0 },
|
2117
|
+
context
|
2118
|
+
);
|
2119
|
+
result.push(value);
|
2120
|
+
context.set("_", value);
|
2121
|
+
const condition = this.evaluateValue(attr["repeat-until"], context);
|
2122
|
+
if (condition) {
|
2123
|
+
break;
|
2124
|
+
}
|
2125
|
+
if (stream.isEof()) {
|
2126
|
+
break;
|
2127
|
+
}
|
2128
|
+
index++;
|
2129
|
+
}
|
2130
|
+
break;
|
2131
|
+
}
|
2132
|
+
default:
|
2133
|
+
throw new ParseError(`Unknown repeat type: ${attr.repeat}`);
|
2134
|
+
}
|
2135
|
+
return result;
|
2136
|
+
}
|
2137
|
+
/**
|
2138
|
+
* Parse and validate contents.
|
2139
|
+
*
|
2140
|
+
* @param attr - Attribute specification with contents
|
2141
|
+
* @param context - Execution context
|
2142
|
+
* @returns The validated contents
|
2143
|
+
* @private
|
2144
|
+
*/
|
2145
|
+
parseContents(attr, context) {
|
2146
|
+
const stream = context.io;
|
2147
|
+
const expected = attr.contents;
|
2148
|
+
if (Array.isArray(expected)) {
|
2149
|
+
const bytes = stream.readBytes(expected.length);
|
2150
|
+
for (let i = 0; i < expected.length; i++) {
|
2151
|
+
if (bytes[i] !== expected[i]) {
|
2152
|
+
throw new ValidationError(
|
2153
|
+
`Contents mismatch at byte ${i}: expected ${expected[i]}, got ${bytes[i]}`,
|
2154
|
+
stream.pos - expected.length + i
|
2155
|
+
);
|
2156
|
+
}
|
2157
|
+
}
|
2158
|
+
return bytes;
|
2159
|
+
} else {
|
2160
|
+
const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
|
2161
|
+
const str = stream.readStr(expected.length, encoding);
|
2162
|
+
if (str !== expected) {
|
2163
|
+
throw new ValidationError(
|
2164
|
+
`Contents mismatch: expected "${expected}", got "${str}"`,
|
2165
|
+
stream.pos - expected.length
|
2166
|
+
);
|
2167
|
+
}
|
2168
|
+
return str;
|
2169
|
+
}
|
2170
|
+
}
|
2171
|
+
/**
|
2172
|
+
* Parse a single value according to its type.
|
2173
|
+
*
|
2174
|
+
* @param attr - Attribute specification
|
2175
|
+
* @param context - Execution context
|
2176
|
+
* @returns Parsed value
|
2177
|
+
* @private
|
2178
|
+
*/
|
2179
|
+
parseValue(attr, context) {
|
2180
|
+
const stream = context.io;
|
2181
|
+
const type = attr.type;
|
2182
|
+
if (attr.size !== void 0) {
|
2183
|
+
const sizeValue = this.evaluateValue(attr.size, context);
|
2184
|
+
const size = typeof sizeValue === "number" ? sizeValue : typeof sizeValue === "bigint" ? Number(sizeValue) : 0;
|
2185
|
+
if (size < 0) {
|
2186
|
+
throw new ParseError(`size must be non-negative, got ${size}`);
|
2187
|
+
}
|
2188
|
+
if (type === "str" || !type) {
|
2189
|
+
const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
|
2190
|
+
let data;
|
2191
|
+
if (type === "str") {
|
2192
|
+
data = stream.readBytes(size);
|
2193
|
+
if (attr.process) {
|
2194
|
+
data = this.applyProcessing(data, attr.process);
|
2195
|
+
}
|
2196
|
+
return new TextDecoder(encoding).decode(data);
|
2197
|
+
} else {
|
2198
|
+
data = stream.readBytes(size);
|
2199
|
+
if (attr.process) {
|
2200
|
+
data = this.applyProcessing(data, attr.process);
|
2201
|
+
}
|
2202
|
+
return data;
|
2203
|
+
}
|
2204
|
+
} else {
|
2205
|
+
let data = stream.readBytes(size);
|
2206
|
+
if (attr.process) {
|
2207
|
+
data = this.applyProcessing(data, attr.process);
|
2208
|
+
}
|
2209
|
+
const substream = new KaitaiStream(data);
|
2210
|
+
return this.parseType(type, substream, context, attr["type-args"]);
|
2211
|
+
}
|
2212
|
+
}
|
2213
|
+
if (attr["size-eos"]) {
|
2214
|
+
if (type === "str") {
|
2215
|
+
const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
|
2216
|
+
const bytes = stream.readBytesFull();
|
2217
|
+
return new TextDecoder(encoding).decode(bytes);
|
2218
|
+
} else {
|
2219
|
+
return stream.readBytesFull();
|
2220
|
+
}
|
2221
|
+
}
|
2222
|
+
if (!type) {
|
2223
|
+
throw new ParseError("Attribute must have either type, size, or contents");
|
2224
|
+
}
|
2225
|
+
return this.parseType(type, stream, context, attr["type-args"]);
|
2226
|
+
}
|
2227
|
+
/**
|
2228
|
+
* Parse a value of a specific type.
|
2229
|
+
*
|
2230
|
+
* @param type - Type name or switch specification
|
2231
|
+
* @param stream - Stream to read from
|
2232
|
+
* @param context - Execution context
|
2233
|
+
* @param typeArgs - Arguments for parametric types
|
2234
|
+
* @returns Parsed value
|
2235
|
+
* @private
|
2236
|
+
*/
|
2237
|
+
parseType(type, stream, context, typeArgs) {
|
2238
|
+
if (typeof type === "object") {
|
2239
|
+
return this.parseSwitchType(
|
2240
|
+
type,
|
2241
|
+
stream,
|
2242
|
+
context
|
2243
|
+
);
|
2244
|
+
}
|
2245
|
+
if (isBuiltinType(type)) {
|
2246
|
+
return this.parseBuiltinType(type, stream, context);
|
2247
|
+
}
|
2248
|
+
if (this.schema.types && type in this.schema.types) {
|
2249
|
+
const typeSchema = this.schema.types[type];
|
2250
|
+
const meta = this.schema.meta || this.parentMeta;
|
2251
|
+
if (this.schema.enums && !typeSchema.enums) {
|
2252
|
+
typeSchema.enums = this.schema.enums;
|
2253
|
+
}
|
2254
|
+
if (this.schema.types && !typeSchema.types) {
|
2255
|
+
typeSchema.types = this.schema.types;
|
2256
|
+
}
|
2257
|
+
const interpreter = new _TypeInterpreter(typeSchema, meta);
|
2258
|
+
return interpreter.parse(stream, context.current, typeArgs);
|
2259
|
+
}
|
2260
|
+
throw new ParseError(`Unknown type: ${type}`);
|
2261
|
+
}
|
2262
|
+
/**
|
2263
|
+
* Parse a switch type (type selection based on expression).
|
2264
|
+
*
|
2265
|
+
* @param switchType - Switch type specification
|
2266
|
+
* @param stream - Stream to read from
|
2267
|
+
* @param context - Execution context
|
2268
|
+
* @returns Parsed value
|
2269
|
+
* @private
|
2270
|
+
*/
|
2271
|
+
parseSwitchType(switchType, stream, context) {
|
2272
|
+
const switchOn = switchType["switch-on"];
|
2273
|
+
const cases = switchType["cases"];
|
2274
|
+
const defaultType = switchType["default"];
|
2275
|
+
if (!switchOn || typeof switchOn !== "string") {
|
2276
|
+
throw new ParseError("switch-on expression is required for switch types");
|
2277
|
+
}
|
2278
|
+
if (!cases) {
|
2279
|
+
throw new ParseError("cases are required for switch types");
|
2280
|
+
}
|
2281
|
+
const switchValue = this.evaluateValue(switchOn, context);
|
2282
|
+
const switchKey = String(switchValue);
|
2283
|
+
let selectedType = cases[switchKey];
|
2284
|
+
if (selectedType === void 0 && defaultType) {
|
2285
|
+
selectedType = defaultType;
|
2286
|
+
}
|
2287
|
+
if (selectedType === void 0) {
|
2288
|
+
throw new ParseError(
|
2289
|
+
`No matching case for switch value "${switchKey}" and no default type specified`
|
2290
|
+
);
|
2291
|
+
}
|
2292
|
+
return this.parseType(selectedType, stream, context);
|
2293
|
+
}
|
2294
|
+
/**
|
2295
|
+
* Parse a built-in type.
|
2296
|
+
*
|
2297
|
+
* @param type - Built-in type name
|
2298
|
+
* @param stream - Stream to read from
|
2299
|
+
* @param context - Execution context
|
2300
|
+
* @returns Parsed value
|
2301
|
+
* @private
|
2302
|
+
*/
|
2303
|
+
parseBuiltinType(type, stream, _context) {
|
2304
|
+
const base = getBaseType(type);
|
2305
|
+
const typeEndian = getTypeEndianness(type);
|
2306
|
+
const meta = this.schema.meta || this.parentMeta;
|
2307
|
+
const metaEndian = meta?.endian;
|
2308
|
+
const endian = typeEndian || (typeof metaEndian === "string" ? metaEndian : "le");
|
2309
|
+
if (isIntegerType(type)) {
|
2310
|
+
return this.readInteger(base, endian, stream);
|
2311
|
+
}
|
2312
|
+
if (isFloatType(type)) {
|
2313
|
+
return this.readFloat(base, endian, stream);
|
2314
|
+
}
|
2315
|
+
if (isStringType(type)) {
|
2316
|
+
throw new ParseError("String types require size, size-eos, or terminator");
|
2317
|
+
}
|
2318
|
+
throw new ParseError(`Unknown built-in type: ${type}`);
|
2319
|
+
}
|
2320
|
+
/**
|
2321
|
+
* Read an integer value.
|
2322
|
+
*
|
2323
|
+
* @param type - Integer type (u1, u2, u4, u8, s1, s2, s4, s8)
|
2324
|
+
* @param endian - Endianness
|
2325
|
+
* @param stream - Stream to read from
|
2326
|
+
* @returns Integer value
|
2327
|
+
* @private
|
2328
|
+
*/
|
2329
|
+
readInteger(type, endian, stream) {
|
2330
|
+
switch (type) {
|
2331
|
+
case "u1":
|
2332
|
+
return stream.readU1();
|
2333
|
+
case "u2":
|
2334
|
+
return endian === "le" ? stream.readU2le() : stream.readU2be();
|
2335
|
+
case "u4":
|
2336
|
+
return endian === "le" ? stream.readU4le() : stream.readU4be();
|
2337
|
+
case "u8":
|
2338
|
+
return endian === "le" ? stream.readU8le() : stream.readU8be();
|
2339
|
+
case "s1":
|
2340
|
+
return stream.readS1();
|
2341
|
+
case "s2":
|
2342
|
+
return endian === "le" ? stream.readS2le() : stream.readS2be();
|
2343
|
+
case "s4":
|
2344
|
+
return endian === "le" ? stream.readS4le() : stream.readS4be();
|
2345
|
+
case "s8":
|
2346
|
+
return endian === "le" ? stream.readS8le() : stream.readS8be();
|
2347
|
+
default:
|
2348
|
+
throw new ParseError(`Unknown integer type: ${type}`);
|
2349
|
+
}
|
2350
|
+
}
|
2351
|
+
/**
|
2352
|
+
* Read a floating point value.
|
2353
|
+
*
|
2354
|
+
* @param type - Float type (f4, f8)
|
2355
|
+
* @param endian - Endianness
|
2356
|
+
* @param stream - Stream to read from
|
2357
|
+
* @returns Float value
|
2358
|
+
* @private
|
2359
|
+
*/
|
2360
|
+
readFloat(type, endian, stream) {
|
2361
|
+
switch (type) {
|
2362
|
+
case "f4":
|
2363
|
+
return endian === "le" ? stream.readF4le() : stream.readF4be();
|
2364
|
+
case "f8":
|
2365
|
+
return endian === "le" ? stream.readF8le() : stream.readF8be();
|
2366
|
+
default:
|
2367
|
+
throw new ParseError(`Unknown float type: ${type}`);
|
2368
|
+
}
|
2369
|
+
}
|
2370
|
+
/**
|
2371
|
+
* Apply processing transformation to data.
|
2372
|
+
* Supports basic transformations like zlib decompression.
|
2373
|
+
*
|
2374
|
+
* @param data - Data to process
|
2375
|
+
* @param process - Processing specification
|
2376
|
+
* @returns Processed data
|
2377
|
+
* @private
|
2378
|
+
*/
|
2379
|
+
applyProcessing(data, process2) {
|
2380
|
+
const processType = typeof process2 === "string" ? process2 : process2.algorithm;
|
2381
|
+
if (processType) {
|
2382
|
+
throw new NotImplementedError(
|
2383
|
+
`Processing type "${processType}" is not yet implemented. Supported in future versions with zlib, encryption, etc.`
|
2384
|
+
);
|
2385
|
+
}
|
2386
|
+
return data;
|
2387
|
+
}
|
2388
|
+
/**
|
2389
|
+
* Evaluate a value that can be an expression or literal.
|
2390
|
+
*
|
2391
|
+
* @param value - Value to evaluate (expression string, number, or boolean)
|
2392
|
+
* @param context - Execution context
|
2393
|
+
* @returns Evaluated result
|
2394
|
+
* @private
|
2395
|
+
*/
|
2396
|
+
evaluateValue(value, context) {
|
2397
|
+
if (value === void 0) {
|
2398
|
+
return void 0;
|
2399
|
+
}
|
2400
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
2401
|
+
return value;
|
2402
|
+
}
|
2403
|
+
if (typeof value === "string") {
|
2404
|
+
try {
|
2405
|
+
return evaluateExpression(value, context);
|
2406
|
+
} catch (error) {
|
2407
|
+
throw new ParseError(
|
2408
|
+
`Failed to evaluate expression "${value}": ${error instanceof Error ? error.message : String(error)}`
|
2409
|
+
);
|
2410
|
+
}
|
2411
|
+
}
|
2412
|
+
return value;
|
2413
|
+
}
|
2414
|
+
};
|
2415
|
+
|
2416
|
+
// src/index.ts
|
2417
|
+
function parse(ksyYaml, buffer, options = {}) {
|
2418
|
+
const { validate = true, strict = false } = options;
|
2419
|
+
const parser = new KsyParser();
|
2420
|
+
const schema = parser.parse(ksyYaml, { validate, strict });
|
2421
|
+
const stream = new KaitaiStream(buffer);
|
2422
|
+
const interpreter = new TypeInterpreter(schema);
|
2423
|
+
return interpreter.parse(stream);
|
2424
|
+
}
|
2425
|
+
|
2426
|
+
// src/cli.ts
|
2427
|
+
var VERSION = "0.6.0";
|
2428
|
+
var HELP_TEXT = `
|
2429
|
+
kaitai - Parse binary files using Kaitai Struct definitions
|
2430
|
+
|
2431
|
+
USAGE:
|
2432
|
+
kaitai <ksy-file> <binary-file> [options]
|
2433
|
+
pnpx kaitai <ksy-file> <binary-file> [options]
|
2434
|
+
|
2435
|
+
ARGUMENTS:
|
2436
|
+
<ksy-file> Path to .ksy definition file (YAML format)
|
2437
|
+
<binary-file> Path to binary file to parse
|
2438
|
+
|
2439
|
+
OPTIONS:
|
2440
|
+
-o, --output <file> Write output to file instead of stdout
|
2441
|
+
-p, --pretty Pretty-print JSON output (default: true for stdout)
|
2442
|
+
-f, --format <format> Output format: json or yaml (default: json)
|
2443
|
+
--field <path> Extract specific field (dot notation: e.g., "header.version")
|
2444
|
+
--no-validate Skip schema validation
|
2445
|
+
--strict Treat schema warnings as errors
|
2446
|
+
-q, --quiet Suppress non-error output
|
2447
|
+
-h, --help Show this help message
|
2448
|
+
-v, --version Show version number
|
2449
|
+
|
2450
|
+
EXAMPLES:
|
2451
|
+
# Parse a binary file and output JSON
|
2452
|
+
kaitai format.ksy data.bin
|
2453
|
+
|
2454
|
+
# Parse and save to file
|
2455
|
+
kaitai format.ksy data.bin -o output.json
|
2456
|
+
|
2457
|
+
# Parse with pretty printing disabled
|
2458
|
+
kaitai format.ksy data.bin --no-pretty
|
2459
|
+
|
2460
|
+
# Extract specific field
|
2461
|
+
kaitai format.ksy data.bin --field header.magic
|
2462
|
+
|
2463
|
+
# Strict validation
|
2464
|
+
kaitai format.ksy data.bin --strict
|
2465
|
+
|
2466
|
+
# Output as YAML
|
2467
|
+
kaitai format.ksy data.bin --format yaml
|
2468
|
+
|
2469
|
+
EXIT CODES:
|
2470
|
+
0 Success
|
2471
|
+
1 General error (file not found, parse error, etc.)
|
2472
|
+
2 Invalid arguments or usage
|
2473
|
+
3 Schema validation error
|
2474
|
+
|
2475
|
+
For more information, visit: https://github.com/fabianopinto/kaitai-struct-ts
|
2476
|
+
`;
|
2477
|
+
function showHelp() {
|
2478
|
+
console.log(HELP_TEXT);
|
2479
|
+
}
|
2480
|
+
function showVersion() {
|
2481
|
+
console.log(`kaitai v${VERSION}`);
|
2482
|
+
}
|
2483
|
+
function parseCliArgs() {
|
2484
|
+
try {
|
2485
|
+
const { values, positionals } = (0, import_util.parseArgs)({
|
2486
|
+
args: process.argv.slice(2),
|
2487
|
+
options: {
|
2488
|
+
output: { type: "string", short: "o" },
|
2489
|
+
pretty: { type: "boolean", short: "p", default: void 0 },
|
2490
|
+
"no-pretty": { type: "boolean", default: false },
|
2491
|
+
format: { type: "string", short: "f", default: "json" },
|
2492
|
+
field: { type: "string" },
|
2493
|
+
validate: { type: "boolean", default: true },
|
2494
|
+
"no-validate": { type: "boolean", default: false },
|
2495
|
+
strict: { type: "boolean", default: false },
|
2496
|
+
quiet: { type: "boolean", short: "q", default: false },
|
2497
|
+
help: { type: "boolean", short: "h", default: false },
|
2498
|
+
version: { type: "boolean", short: "v", default: false }
|
2499
|
+
},
|
2500
|
+
allowPositionals: true
|
2501
|
+
});
|
2502
|
+
const pretty = values.pretty !== void 0 ? values.pretty : values["no-pretty"] ? false : void 0;
|
2503
|
+
const validate = values["no-validate"] ? false : values.validate;
|
2504
|
+
const options = {
|
2505
|
+
output: values.output,
|
2506
|
+
pretty,
|
2507
|
+
format: values.format || "json",
|
2508
|
+
field: values.field,
|
2509
|
+
validate,
|
2510
|
+
strict: values.strict,
|
2511
|
+
quiet: values.quiet,
|
2512
|
+
help: values.help,
|
2513
|
+
version: values.version
|
2514
|
+
};
|
2515
|
+
return { options, positional: positionals };
|
2516
|
+
} catch (error) {
|
2517
|
+
if (error instanceof Error) {
|
2518
|
+
console.error(`Error parsing arguments: ${error.message}`);
|
2519
|
+
}
|
2520
|
+
process.exit(2);
|
2521
|
+
}
|
2522
|
+
}
|
2523
|
+
function readFile(filePath, description) {
|
2524
|
+
const resolvedPath = (0, import_path.resolve)(filePath);
|
2525
|
+
if (!(0, import_fs.existsSync)(resolvedPath)) {
|
2526
|
+
console.error(`Error: ${description} not found: ${filePath}`);
|
2527
|
+
process.exit(1);
|
2528
|
+
}
|
2529
|
+
try {
|
2530
|
+
return (0, import_fs.readFileSync)(resolvedPath);
|
2531
|
+
} catch (error) {
|
2532
|
+
console.error(
|
2533
|
+
`Error reading ${description}: ${error instanceof Error ? error.message : String(error)}`
|
2534
|
+
);
|
2535
|
+
process.exit(1);
|
2536
|
+
}
|
2537
|
+
}
|
2538
|
+
function extractField(obj, path) {
|
2539
|
+
const parts = path.split(".");
|
2540
|
+
let current = obj;
|
2541
|
+
for (const part of parts) {
|
2542
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
2543
|
+
throw new Error(`Cannot access property '${part}' of ${typeof current}`);
|
2544
|
+
}
|
2545
|
+
current = current[part];
|
2546
|
+
}
|
2547
|
+
return current;
|
2548
|
+
}
|
2549
|
+
function formatOutput(data, format, pretty) {
|
2550
|
+
if (format === "yaml") {
|
2551
|
+
return JSON.stringify(data, null, 2).replace(/^{$/gm, "").replace(/^}$/gm, "").replace(/^\s*"([^"]+)":\s*/gm, "$1: ").replace(/,$/gm, "");
|
2552
|
+
}
|
2553
|
+
return pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
|
2554
|
+
}
|
2555
|
+
function main() {
|
2556
|
+
const { options, positional } = parseCliArgs();
|
2557
|
+
if (options.help) {
|
2558
|
+
showHelp();
|
2559
|
+
process.exit(0);
|
2560
|
+
}
|
2561
|
+
if (options.version) {
|
2562
|
+
showVersion();
|
2563
|
+
process.exit(0);
|
2564
|
+
}
|
2565
|
+
if (positional.length < 2) {
|
2566
|
+
console.error("Error: Missing required arguments");
|
2567
|
+
console.error("Usage: kaitai <ksy-file> <binary-file> [options]");
|
2568
|
+
console.error('Run "kaitai --help" for more information');
|
2569
|
+
process.exit(2);
|
2570
|
+
}
|
2571
|
+
if (positional.length > 2) {
|
2572
|
+
console.error("Error: Too many arguments");
|
2573
|
+
console.error("Usage: kaitai <ksy-file> <binary-file> [options]");
|
2574
|
+
process.exit(2);
|
2575
|
+
}
|
2576
|
+
const [ksyFile, binaryFile] = positional;
|
2577
|
+
if (options.format && !["json", "yaml"].includes(options.format)) {
|
2578
|
+
console.error(
|
2579
|
+
`Error: Invalid format '${options.format}'. Must be 'json' or 'yaml'`
|
2580
|
+
);
|
2581
|
+
process.exit(2);
|
2582
|
+
}
|
2583
|
+
if (!options.quiet) {
|
2584
|
+
console.error(`Reading KSY definition: ${ksyFile}`);
|
2585
|
+
console.error(`Reading binary file: ${binaryFile}`);
|
2586
|
+
}
|
2587
|
+
const ksyContent = readFile(ksyFile, "KSY definition file").toString("utf-8");
|
2588
|
+
const binaryData = readFile(binaryFile, "Binary file");
|
2589
|
+
const parseOptions = {
|
2590
|
+
validate: options.validate,
|
2591
|
+
strict: options.strict
|
2592
|
+
};
|
2593
|
+
if (!options.quiet) {
|
2594
|
+
console.error("Parsing...");
|
2595
|
+
}
|
2596
|
+
let result;
|
2597
|
+
try {
|
2598
|
+
result = parse(ksyContent, binaryData, parseOptions);
|
2599
|
+
} catch (error) {
|
2600
|
+
console.error(
|
2601
|
+
`Parse error: ${error instanceof Error ? error.message : String(error)}`
|
2602
|
+
);
|
2603
|
+
if (error instanceof Error && error.name === "ValidationError") {
|
2604
|
+
process.exit(3);
|
2605
|
+
}
|
2606
|
+
process.exit(1);
|
2607
|
+
}
|
2608
|
+
let output = result;
|
2609
|
+
if (options.field) {
|
2610
|
+
try {
|
2611
|
+
output = extractField(result, options.field);
|
2612
|
+
if (!options.quiet) {
|
2613
|
+
console.error(`Extracted field: ${options.field}`);
|
2614
|
+
}
|
2615
|
+
} catch (error) {
|
2616
|
+
console.error(
|
2617
|
+
`Error extracting field '${options.field}': ${error instanceof Error ? error.message : String(error)}`
|
2618
|
+
);
|
2619
|
+
process.exit(1);
|
2620
|
+
}
|
2621
|
+
}
|
2622
|
+
const shouldPretty = options.pretty !== void 0 ? options.pretty : !options.output;
|
2623
|
+
const formatted = formatOutput(output, options.format || "json", shouldPretty);
|
2624
|
+
if (options.output) {
|
2625
|
+
try {
|
2626
|
+
(0, import_fs.writeFileSync)(options.output, formatted + "\n", "utf-8");
|
2627
|
+
if (!options.quiet) {
|
2628
|
+
console.error(`Output written to: ${options.output}`);
|
2629
|
+
}
|
2630
|
+
} catch (error) {
|
2631
|
+
console.error(
|
2632
|
+
`Error writing output file: ${error instanceof Error ? error.message : String(error)}`
|
2633
|
+
);
|
2634
|
+
process.exit(1);
|
2635
|
+
}
|
2636
|
+
} else {
|
2637
|
+
console.log(formatted);
|
2638
|
+
}
|
2639
|
+
if (!options.quiet) {
|
2640
|
+
console.error("Done!");
|
2641
|
+
}
|
2642
|
+
}
|
2643
|
+
main();
|
2644
|
+
/**
|
2645
|
+
* @fileoverview Custom error classes for Kaitai Struct parsing and validation
|
2646
|
+
* @module utils/errors
|
2647
|
+
* @author Fabiano Pinto
|
2648
|
+
* @license MIT
|
2649
|
+
*/
|
2650
|
+
/**
|
2651
|
+
* @fileoverview String encoding and decoding utilities for binary data
|
2652
|
+
* @module utils/encoding
|
2653
|
+
* @author Fabiano Pinto
|
2654
|
+
* @license MIT
|
2655
|
+
*/
|
2656
|
+
/**
|
2657
|
+
* @fileoverview Binary stream reader for Kaitai Struct
|
2658
|
+
* @module stream/KaitaiStream
|
2659
|
+
* @author Fabiano Pinto
|
2660
|
+
* @license MIT
|
2661
|
+
*/
|
2662
|
+
/**
|
2663
|
+
* @fileoverview Binary stream reading functionality
|
2664
|
+
* @module stream
|
2665
|
+
* @author Fabiano Pinto
|
2666
|
+
* @license MIT
|
2667
|
+
*/
|
2668
|
+
/**
|
2669
|
+
* @fileoverview Type definitions for Kaitai Struct YAML schema (.ksy files)
|
2670
|
+
* @module parser/schema
|
2671
|
+
* @author Fabiano Pinto
|
2672
|
+
* @license MIT
|
2673
|
+
*/
|
2674
|
+
/**
|
2675
|
+
* @fileoverview Parser for Kaitai Struct YAML (.ksy) files
|
2676
|
+
* @module parser/KsyParser
|
2677
|
+
* @author Fabiano Pinto
|
2678
|
+
* @license MIT
|
2679
|
+
*/
|
2680
|
+
/**
|
2681
|
+
* @fileoverview Parser module for Kaitai Struct YAML files
|
2682
|
+
* @module parser
|
2683
|
+
* @author Fabiano Pinto
|
2684
|
+
* @license MIT
|
2685
|
+
*/
|
2686
|
+
/**
|
2687
|
+
* @fileoverview Execution context for Kaitai Struct parsing
|
2688
|
+
* @module interpreter/Context
|
2689
|
+
* @author Fabiano Pinto
|
2690
|
+
* @license MIT
|
2691
|
+
*/
|
2692
|
+
/**
|
2693
|
+
* @fileoverview Token types for Kaitai Struct expression language
|
2694
|
+
* @module expression/Token
|
2695
|
+
* @author Fabiano Pinto
|
2696
|
+
* @license MIT
|
2697
|
+
*/
|
2698
|
+
/**
|
2699
|
+
* @fileoverview Lexer for Kaitai Struct expression language
|
2700
|
+
* @module expression/Lexer
|
2701
|
+
* @author Fabiano Pinto
|
2702
|
+
* @license MIT
|
2703
|
+
*/
|
2704
|
+
/**
|
2705
|
+
* @fileoverview Abstract Syntax Tree nodes for expression language
|
2706
|
+
* @module expression/AST
|
2707
|
+
* @author Fabiano Pinto
|
2708
|
+
* @license MIT
|
2709
|
+
*/
|
2710
|
+
/**
|
2711
|
+
* @fileoverview Parser for Kaitai Struct expression language
|
2712
|
+
* @module expression/Parser
|
2713
|
+
* @author Fabiano Pinto
|
2714
|
+
* @license MIT
|
2715
|
+
*/
|
2716
|
+
/**
|
2717
|
+
* @fileoverview Evaluator for Kaitai Struct expression AST
|
2718
|
+
* @module expression/Evaluator
|
2719
|
+
* @author Fabiano Pinto
|
2720
|
+
* @license MIT
|
2721
|
+
*/
|
2722
|
+
/**
|
2723
|
+
* @fileoverview Expression evaluation module
|
2724
|
+
* @module expression
|
2725
|
+
* @author Fabiano Pinto
|
2726
|
+
* @license MIT
|
2727
|
+
*/
|
2728
|
+
/**
|
2729
|
+
* @fileoverview Type interpreter for executing Kaitai Struct schemas
|
2730
|
+
* @module interpreter/TypeInterpreter
|
2731
|
+
* @author Fabiano Pinto
|
2732
|
+
* @license MIT
|
2733
|
+
*/
|
2734
|
+
/**
|
2735
|
+
* @fileoverview Interpreter module for executing Kaitai Struct schemas
|
2736
|
+
* @module interpreter
|
2737
|
+
* @author Fabiano Pinto
|
2738
|
+
* @license MIT
|
2739
|
+
*/
|
2740
|
+
/**
|
2741
|
+
* @fileoverview Main entry point for kaitai-struct-ts library
|
2742
|
+
* @module kaitai-struct-ts
|
2743
|
+
* @author Fabiano Pinto
|
2744
|
+
* @license MIT
|
2745
|
+
* @version 0.2.0
|
2746
|
+
*
|
2747
|
+
* @description
|
2748
|
+
* A runtime interpreter for Kaitai Struct binary format definitions in TypeScript.
|
2749
|
+
* Parse any binary data format by providing a .ksy (Kaitai Struct YAML) definition file.
|
2750
|
+
*
|
2751
|
+
* @example
|
2752
|
+
* ```typescript
|
2753
|
+
* import { parse } from 'kaitai-struct-ts'
|
2754
|
+
*
|
2755
|
+
* const ksyDefinition = `
|
2756
|
+
* meta:
|
2757
|
+
* id: my_format
|
2758
|
+
* endian: le
|
2759
|
+
* seq:
|
2760
|
+
* - id: magic
|
2761
|
+
* contents: [0x4D, 0x5A]
|
2762
|
+
* - id: version
|
2763
|
+
* type: u2
|
2764
|
+
* `
|
2765
|
+
*
|
2766
|
+
* const buffer = new Uint8Array([0x4D, 0x5A, 0x01, 0x00])
|
2767
|
+
* const result = parse(ksyDefinition, buffer)
|
2768
|
+
* console.log(result.version) // 1
|
2769
|
+
* ```
|
2770
|
+
*/
|
2771
|
+
/**
|
2772
|
+
* @fileoverview CLI utility for parsing binary files with Kaitai Struct definitions
|
2773
|
+
* @module kaitai-struct-ts/cli
|
2774
|
+
* @author Fabiano Pinto
|
2775
|
+
* @license MIT
|
2776
|
+
*/
|