@k67/kaitai-struct-ts 0.2.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/LICENSE +21 -0
- package/README.md +188 -0
- package/dist/index.d.mts +962 -0
- package/dist/index.d.ts +962 -0
- package/dist/index.js +1405 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1362 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +84 -0
package/dist/index.mjs
ADDED
@@ -0,0 +1,1362 @@
|
|
1
|
+
// src/utils/errors.ts
|
2
|
+
var KaitaiError = class _KaitaiError extends Error {
|
3
|
+
constructor(message, position) {
|
4
|
+
super(message);
|
5
|
+
this.position = position;
|
6
|
+
this.name = "KaitaiError";
|
7
|
+
Object.setPrototypeOf(this, _KaitaiError.prototype);
|
8
|
+
}
|
9
|
+
};
|
10
|
+
var ValidationError = class _ValidationError extends KaitaiError {
|
11
|
+
constructor(message, position) {
|
12
|
+
super(message, position);
|
13
|
+
this.name = "ValidationError";
|
14
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
15
|
+
}
|
16
|
+
};
|
17
|
+
var ParseError = class _ParseError extends KaitaiError {
|
18
|
+
constructor(message, position) {
|
19
|
+
super(message, position);
|
20
|
+
this.name = "ParseError";
|
21
|
+
Object.setPrototypeOf(this, _ParseError.prototype);
|
22
|
+
}
|
23
|
+
};
|
24
|
+
var EOFError = class _EOFError extends KaitaiError {
|
25
|
+
constructor(message = "Unexpected end of stream", position) {
|
26
|
+
super(message, position);
|
27
|
+
this.name = "EOFError";
|
28
|
+
Object.setPrototypeOf(this, _EOFError.prototype);
|
29
|
+
}
|
30
|
+
};
|
31
|
+
var NotImplementedError = class _NotImplementedError extends KaitaiError {
|
32
|
+
constructor(feature) {
|
33
|
+
super(`Feature not yet implemented: ${feature}`);
|
34
|
+
this.name = "NotImplementedError";
|
35
|
+
Object.setPrototypeOf(this, _NotImplementedError.prototype);
|
36
|
+
}
|
37
|
+
};
|
38
|
+
|
39
|
+
// src/utils/encoding.ts
|
40
|
+
function decodeString(bytes, encoding) {
|
41
|
+
const normalizedEncoding = encoding.toLowerCase().replace(/[-_]/g, "");
|
42
|
+
switch (normalizedEncoding) {
|
43
|
+
case "utf8":
|
44
|
+
case "utf-8":
|
45
|
+
return decodeUtf8(bytes);
|
46
|
+
case "ascii":
|
47
|
+
case "usascii":
|
48
|
+
return decodeAscii(bytes);
|
49
|
+
case "utf16":
|
50
|
+
case "utf16le":
|
51
|
+
case "utf-16le":
|
52
|
+
return decodeUtf16Le(bytes);
|
53
|
+
case "utf16be":
|
54
|
+
case "utf-16be":
|
55
|
+
return decodeUtf16Be(bytes);
|
56
|
+
case "latin1":
|
57
|
+
case "iso88591":
|
58
|
+
case "iso-8859-1":
|
59
|
+
return decodeLatin1(bytes);
|
60
|
+
default:
|
61
|
+
if (typeof TextDecoder !== "undefined") {
|
62
|
+
try {
|
63
|
+
return new TextDecoder(encoding).decode(bytes);
|
64
|
+
} catch {
|
65
|
+
throw new Error(`Unsupported encoding: ${encoding}`);
|
66
|
+
}
|
67
|
+
}
|
68
|
+
throw new Error(`Unsupported encoding: ${encoding}`);
|
69
|
+
}
|
70
|
+
}
|
71
|
+
function decodeUtf8(bytes) {
|
72
|
+
if (typeof TextDecoder !== "undefined") {
|
73
|
+
return new TextDecoder("utf-8").decode(bytes);
|
74
|
+
}
|
75
|
+
let result = "";
|
76
|
+
let i = 0;
|
77
|
+
while (i < bytes.length) {
|
78
|
+
const byte1 = bytes[i++];
|
79
|
+
if (byte1 < 128) {
|
80
|
+
result += String.fromCharCode(byte1);
|
81
|
+
} else if (byte1 < 224) {
|
82
|
+
const byte2 = bytes[i++];
|
83
|
+
result += String.fromCharCode((byte1 & 31) << 6 | byte2 & 63);
|
84
|
+
} else if (byte1 < 240) {
|
85
|
+
const byte2 = bytes[i++];
|
86
|
+
const byte3 = bytes[i++];
|
87
|
+
result += String.fromCharCode(
|
88
|
+
(byte1 & 15) << 12 | (byte2 & 63) << 6 | byte3 & 63
|
89
|
+
);
|
90
|
+
} else {
|
91
|
+
const byte2 = bytes[i++];
|
92
|
+
const byte3 = bytes[i++];
|
93
|
+
const byte4 = bytes[i++];
|
94
|
+
let codePoint = (byte1 & 7) << 18 | (byte2 & 63) << 12 | (byte3 & 63) << 6 | byte4 & 63;
|
95
|
+
codePoint -= 65536;
|
96
|
+
result += String.fromCharCode(
|
97
|
+
55296 + (codePoint >> 10),
|
98
|
+
56320 + (codePoint & 1023)
|
99
|
+
);
|
100
|
+
}
|
101
|
+
}
|
102
|
+
return result;
|
103
|
+
}
|
104
|
+
function decodeAscii(bytes) {
|
105
|
+
let result = "";
|
106
|
+
for (let i = 0; i < bytes.length; i++) {
|
107
|
+
result += String.fromCharCode(bytes[i] & 127);
|
108
|
+
}
|
109
|
+
return result;
|
110
|
+
}
|
111
|
+
function decodeLatin1(bytes) {
|
112
|
+
let result = "";
|
113
|
+
for (let i = 0; i < bytes.length; i++) {
|
114
|
+
result += String.fromCharCode(bytes[i]);
|
115
|
+
}
|
116
|
+
return result;
|
117
|
+
}
|
118
|
+
function decodeUtf16Le(bytes) {
|
119
|
+
if (typeof TextDecoder !== "undefined") {
|
120
|
+
return new TextDecoder("utf-16le").decode(bytes);
|
121
|
+
}
|
122
|
+
let result = "";
|
123
|
+
for (let i = 0; i < bytes.length; i += 2) {
|
124
|
+
const charCode = bytes[i] | bytes[i + 1] << 8;
|
125
|
+
result += String.fromCharCode(charCode);
|
126
|
+
}
|
127
|
+
return result;
|
128
|
+
}
|
129
|
+
function decodeUtf16Be(bytes) {
|
130
|
+
if (typeof TextDecoder !== "undefined") {
|
131
|
+
return new TextDecoder("utf-16be").decode(bytes);
|
132
|
+
}
|
133
|
+
let result = "";
|
134
|
+
for (let i = 0; i < bytes.length; i += 2) {
|
135
|
+
const charCode = bytes[i] << 8 | bytes[i + 1];
|
136
|
+
result += String.fromCharCode(charCode);
|
137
|
+
}
|
138
|
+
return result;
|
139
|
+
}
|
140
|
+
|
141
|
+
// src/stream/KaitaiStream.ts
|
142
|
+
var KaitaiStream = class _KaitaiStream {
|
143
|
+
/**
|
144
|
+
* Create a new KaitaiStream from a buffer
|
145
|
+
* @param buffer - ArrayBuffer or Uint8Array containing the binary data
|
146
|
+
*/
|
147
|
+
constructor(buffer) {
|
148
|
+
this._pos = 0;
|
149
|
+
this._bits = 0;
|
150
|
+
this._bitsLeft = 0;
|
151
|
+
if (buffer instanceof ArrayBuffer) {
|
152
|
+
this.buffer = new Uint8Array(buffer);
|
153
|
+
this.view = new DataView(buffer);
|
154
|
+
} else {
|
155
|
+
this.buffer = buffer;
|
156
|
+
this.view = new DataView(
|
157
|
+
buffer.buffer,
|
158
|
+
buffer.byteOffset,
|
159
|
+
buffer.byteLength
|
160
|
+
);
|
161
|
+
}
|
162
|
+
}
|
163
|
+
/**
|
164
|
+
* Current position in the stream
|
165
|
+
*/
|
166
|
+
get pos() {
|
167
|
+
return this._pos;
|
168
|
+
}
|
169
|
+
set pos(value) {
|
170
|
+
this._pos = value;
|
171
|
+
this._bitsLeft = 0;
|
172
|
+
}
|
173
|
+
/**
|
174
|
+
* Total size of the stream in bytes
|
175
|
+
*/
|
176
|
+
get size() {
|
177
|
+
return this.buffer.length;
|
178
|
+
}
|
179
|
+
/**
|
180
|
+
* Check if we've reached the end of the stream
|
181
|
+
*/
|
182
|
+
isEof() {
|
183
|
+
return this._pos >= this.buffer.length;
|
184
|
+
}
|
185
|
+
/**
|
186
|
+
* Seek to a specific position in the stream
|
187
|
+
* @param pos - Position to seek to
|
188
|
+
*/
|
189
|
+
seek(pos) {
|
190
|
+
if (pos < 0 || pos > this.buffer.length) {
|
191
|
+
throw new Error(`Invalid seek position: ${pos}`);
|
192
|
+
}
|
193
|
+
this.pos = pos;
|
194
|
+
}
|
195
|
+
/**
|
196
|
+
* Ensure we have enough bytes available
|
197
|
+
* @param count - Number of bytes needed
|
198
|
+
*/
|
199
|
+
ensureBytes(count) {
|
200
|
+
if (this._pos + count > this.buffer.length) {
|
201
|
+
throw new EOFError(
|
202
|
+
`Requested ${count} bytes at position ${this._pos}, but only ${this.buffer.length - this._pos} bytes available`,
|
203
|
+
this._pos
|
204
|
+
);
|
205
|
+
}
|
206
|
+
}
|
207
|
+
// ==================== Unsigned Integers ====================
|
208
|
+
/**
|
209
|
+
* Read 1-byte unsigned integer (0 to 255)
|
210
|
+
*/
|
211
|
+
readU1() {
|
212
|
+
this.ensureBytes(1);
|
213
|
+
return this.buffer[this._pos++];
|
214
|
+
}
|
215
|
+
/**
|
216
|
+
* Read 2-byte unsigned integer, little-endian
|
217
|
+
*/
|
218
|
+
readU2le() {
|
219
|
+
this.ensureBytes(2);
|
220
|
+
const value = this.view.getUint16(this._pos, true);
|
221
|
+
this._pos += 2;
|
222
|
+
return value;
|
223
|
+
}
|
224
|
+
/**
|
225
|
+
* Read 2-byte unsigned integer, big-endian
|
226
|
+
*/
|
227
|
+
readU2be() {
|
228
|
+
this.ensureBytes(2);
|
229
|
+
const value = this.view.getUint16(this._pos, false);
|
230
|
+
this._pos += 2;
|
231
|
+
return value;
|
232
|
+
}
|
233
|
+
/**
|
234
|
+
* Read 4-byte unsigned integer, little-endian
|
235
|
+
*/
|
236
|
+
readU4le() {
|
237
|
+
this.ensureBytes(4);
|
238
|
+
const value = this.view.getUint32(this._pos, true);
|
239
|
+
this._pos += 4;
|
240
|
+
return value;
|
241
|
+
}
|
242
|
+
/**
|
243
|
+
* Read 4-byte unsigned integer, big-endian
|
244
|
+
*/
|
245
|
+
readU4be() {
|
246
|
+
this.ensureBytes(4);
|
247
|
+
const value = this.view.getUint32(this._pos, false);
|
248
|
+
this._pos += 4;
|
249
|
+
return value;
|
250
|
+
}
|
251
|
+
/**
|
252
|
+
* Read 8-byte unsigned integer, little-endian
|
253
|
+
* Returns BigInt for values > Number.MAX_SAFE_INTEGER
|
254
|
+
*/
|
255
|
+
readU8le() {
|
256
|
+
this.ensureBytes(8);
|
257
|
+
const value = this.view.getBigUint64(this._pos, true);
|
258
|
+
this._pos += 8;
|
259
|
+
return value;
|
260
|
+
}
|
261
|
+
/**
|
262
|
+
* Read 8-byte unsigned integer, big-endian
|
263
|
+
* Returns BigInt for values > Number.MAX_SAFE_INTEGER
|
264
|
+
*/
|
265
|
+
readU8be() {
|
266
|
+
this.ensureBytes(8);
|
267
|
+
const value = this.view.getBigUint64(this._pos, false);
|
268
|
+
this._pos += 8;
|
269
|
+
return value;
|
270
|
+
}
|
271
|
+
// ==================== Signed Integers ====================
|
272
|
+
/**
|
273
|
+
* Read 1-byte signed integer (-128 to 127)
|
274
|
+
*/
|
275
|
+
readS1() {
|
276
|
+
this.ensureBytes(1);
|
277
|
+
return this.view.getInt8(this._pos++);
|
278
|
+
}
|
279
|
+
/**
|
280
|
+
* Read 2-byte signed integer, little-endian
|
281
|
+
*/
|
282
|
+
readS2le() {
|
283
|
+
this.ensureBytes(2);
|
284
|
+
const value = this.view.getInt16(this._pos, true);
|
285
|
+
this._pos += 2;
|
286
|
+
return value;
|
287
|
+
}
|
288
|
+
/**
|
289
|
+
* Read 2-byte signed integer, big-endian
|
290
|
+
*/
|
291
|
+
readS2be() {
|
292
|
+
this.ensureBytes(2);
|
293
|
+
const value = this.view.getInt16(this._pos, false);
|
294
|
+
this._pos += 2;
|
295
|
+
return value;
|
296
|
+
}
|
297
|
+
/**
|
298
|
+
* Read 4-byte signed integer, little-endian
|
299
|
+
*/
|
300
|
+
readS4le() {
|
301
|
+
this.ensureBytes(4);
|
302
|
+
const value = this.view.getInt32(this._pos, true);
|
303
|
+
this._pos += 4;
|
304
|
+
return value;
|
305
|
+
}
|
306
|
+
/**
|
307
|
+
* Read 4-byte signed integer, big-endian
|
308
|
+
*/
|
309
|
+
readS4be() {
|
310
|
+
this.ensureBytes(4);
|
311
|
+
const value = this.view.getInt32(this._pos, false);
|
312
|
+
this._pos += 4;
|
313
|
+
return value;
|
314
|
+
}
|
315
|
+
/**
|
316
|
+
* Read 8-byte signed integer, little-endian
|
317
|
+
* Returns BigInt for values outside Number.MAX_SAFE_INTEGER range
|
318
|
+
*/
|
319
|
+
readS8le() {
|
320
|
+
this.ensureBytes(8);
|
321
|
+
const value = this.view.getBigInt64(this._pos, true);
|
322
|
+
this._pos += 8;
|
323
|
+
return value;
|
324
|
+
}
|
325
|
+
/**
|
326
|
+
* Read 8-byte signed integer, big-endian
|
327
|
+
* Returns BigInt for values outside Number.MAX_SAFE_INTEGER range
|
328
|
+
*/
|
329
|
+
readS8be() {
|
330
|
+
this.ensureBytes(8);
|
331
|
+
const value = this.view.getBigInt64(this._pos, false);
|
332
|
+
this._pos += 8;
|
333
|
+
return value;
|
334
|
+
}
|
335
|
+
// ==================== Floating Point ====================
|
336
|
+
/**
|
337
|
+
* Read 4-byte IEEE 754 single-precision float, little-endian
|
338
|
+
*/
|
339
|
+
readF4le() {
|
340
|
+
this.ensureBytes(4);
|
341
|
+
const value = this.view.getFloat32(this._pos, true);
|
342
|
+
this._pos += 4;
|
343
|
+
return value;
|
344
|
+
}
|
345
|
+
/**
|
346
|
+
* Read 4-byte IEEE 754 single-precision float, big-endian
|
347
|
+
*/
|
348
|
+
readF4be() {
|
349
|
+
this.ensureBytes(4);
|
350
|
+
const value = this.view.getFloat32(this._pos, false);
|
351
|
+
this._pos += 4;
|
352
|
+
return value;
|
353
|
+
}
|
354
|
+
/**
|
355
|
+
* Read 8-byte IEEE 754 double-precision float, little-endian
|
356
|
+
*/
|
357
|
+
readF8le() {
|
358
|
+
this.ensureBytes(8);
|
359
|
+
const value = this.view.getFloat64(this._pos, true);
|
360
|
+
this._pos += 8;
|
361
|
+
return value;
|
362
|
+
}
|
363
|
+
/**
|
364
|
+
* Read 8-byte IEEE 754 double-precision float, big-endian
|
365
|
+
*/
|
366
|
+
readF8be() {
|
367
|
+
this.ensureBytes(8);
|
368
|
+
const value = this.view.getFloat64(this._pos, false);
|
369
|
+
this._pos += 8;
|
370
|
+
return value;
|
371
|
+
}
|
372
|
+
// ==================== Byte Arrays ====================
|
373
|
+
/**
|
374
|
+
* Read a fixed number of bytes
|
375
|
+
* @param length - Number of bytes to read
|
376
|
+
*/
|
377
|
+
readBytes(length) {
|
378
|
+
this.ensureBytes(length);
|
379
|
+
const bytes = this.buffer.slice(this._pos, this._pos + length);
|
380
|
+
this._pos += length;
|
381
|
+
return bytes;
|
382
|
+
}
|
383
|
+
/**
|
384
|
+
* Read all remaining bytes until end of stream
|
385
|
+
*/
|
386
|
+
readBytesFull() {
|
387
|
+
const bytes = this.buffer.slice(this._pos);
|
388
|
+
this._pos = this.buffer.length;
|
389
|
+
return bytes;
|
390
|
+
}
|
391
|
+
/**
|
392
|
+
* Read bytes until a terminator byte is found
|
393
|
+
* @param term - Terminator byte value
|
394
|
+
* @param include - Include terminator in result
|
395
|
+
* @param consume - Consume terminator from stream
|
396
|
+
* @param eosError - Throw error if EOS reached before terminator
|
397
|
+
*/
|
398
|
+
readBytesterm(term, include = false, consume = true, eosError = true) {
|
399
|
+
const start = this._pos;
|
400
|
+
let end = start;
|
401
|
+
while (end < this.buffer.length && this.buffer[end] !== term) {
|
402
|
+
end++;
|
403
|
+
}
|
404
|
+
const foundTerm = end < this.buffer.length;
|
405
|
+
if (!foundTerm && eosError) {
|
406
|
+
throw new EOFError(
|
407
|
+
`Terminator byte ${term} not found before end of stream`,
|
408
|
+
this._pos
|
409
|
+
);
|
410
|
+
}
|
411
|
+
const includeEnd = include && foundTerm ? end + 1 : end;
|
412
|
+
const bytes = this.buffer.slice(start, includeEnd);
|
413
|
+
if (foundTerm && consume) {
|
414
|
+
this._pos = end + 1;
|
415
|
+
} else {
|
416
|
+
this._pos = end;
|
417
|
+
}
|
418
|
+
return bytes;
|
419
|
+
}
|
420
|
+
// ==================== Strings ====================
|
421
|
+
/**
|
422
|
+
* Read a fixed-length string
|
423
|
+
* @param length - Number of bytes to read
|
424
|
+
* @param encoding - Character encoding (default: UTF-8)
|
425
|
+
*/
|
426
|
+
readStr(length, encoding = "UTF-8") {
|
427
|
+
const bytes = this.readBytes(length);
|
428
|
+
return decodeString(bytes, encoding);
|
429
|
+
}
|
430
|
+
/**
|
431
|
+
* Read a null-terminated string
|
432
|
+
* @param encoding - Character encoding (default: UTF-8)
|
433
|
+
* @param term - Terminator byte (default: 0)
|
434
|
+
* @param include - Include terminator in result
|
435
|
+
* @param consume - Consume terminator from stream
|
436
|
+
* @param eosError - Throw error if EOS reached before terminator
|
437
|
+
*/
|
438
|
+
readStrz(encoding = "UTF-8", term = 0, include = false, consume = true, eosError = true) {
|
439
|
+
const bytes = this.readBytesterm(term, include, consume, eosError);
|
440
|
+
return decodeString(bytes, encoding);
|
441
|
+
}
|
442
|
+
// ==================== Bit-level Reading ====================
|
443
|
+
/**
|
444
|
+
* Align bit reading to byte boundary
|
445
|
+
*/
|
446
|
+
alignToByte() {
|
447
|
+
this._bitsLeft = 0;
|
448
|
+
}
|
449
|
+
/**
|
450
|
+
* Read specified number of bits as unsigned integer (big-endian)
|
451
|
+
* @param n - Number of bits to read (1-64)
|
452
|
+
*/
|
453
|
+
readBitsIntBe(n) {
|
454
|
+
if (n < 1 || n > 64) {
|
455
|
+
throw new Error(`Invalid bit count: ${n}. Must be between 1 and 64`);
|
456
|
+
}
|
457
|
+
let result = 0n;
|
458
|
+
for (let bitsNeeded = n; bitsNeeded > 0; ) {
|
459
|
+
if (this._bitsLeft === 0) {
|
460
|
+
this._bits = this.readU1();
|
461
|
+
this._bitsLeft = 8;
|
462
|
+
}
|
463
|
+
const bitsToRead = Math.min(bitsNeeded, this._bitsLeft);
|
464
|
+
const mask = (1 << bitsToRead) - 1;
|
465
|
+
const shift = this._bitsLeft - bitsToRead;
|
466
|
+
result = result << BigInt(bitsToRead) | BigInt(this._bits >> shift & mask);
|
467
|
+
this._bitsLeft -= bitsToRead;
|
468
|
+
bitsNeeded -= bitsToRead;
|
469
|
+
}
|
470
|
+
return result;
|
471
|
+
}
|
472
|
+
/**
|
473
|
+
* Read specified number of bits as unsigned integer (little-endian)
|
474
|
+
* @param n - Number of bits to read (1-64)
|
475
|
+
*/
|
476
|
+
readBitsIntLe(n) {
|
477
|
+
if (n < 1 || n > 64) {
|
478
|
+
throw new Error(`Invalid bit count: ${n}. Must be between 1 and 64`);
|
479
|
+
}
|
480
|
+
let result = 0n;
|
481
|
+
let bitPos = 0;
|
482
|
+
for (let bitsNeeded = n; bitsNeeded > 0; ) {
|
483
|
+
if (this._bitsLeft === 0) {
|
484
|
+
this._bits = this.readU1();
|
485
|
+
this._bitsLeft = 8;
|
486
|
+
}
|
487
|
+
const bitsToRead = Math.min(bitsNeeded, this._bitsLeft);
|
488
|
+
const mask = (1 << bitsToRead) - 1;
|
489
|
+
result |= BigInt(this._bits & mask) << BigInt(bitPos);
|
490
|
+
this._bits >>= bitsToRead;
|
491
|
+
this._bitsLeft -= bitsToRead;
|
492
|
+
bitsNeeded -= bitsToRead;
|
493
|
+
bitPos += bitsToRead;
|
494
|
+
}
|
495
|
+
return result;
|
496
|
+
}
|
497
|
+
// ==================== Utility Methods ====================
|
498
|
+
/**
|
499
|
+
* Get the underlying buffer
|
500
|
+
*/
|
501
|
+
getBuffer() {
|
502
|
+
return this.buffer;
|
503
|
+
}
|
504
|
+
/**
|
505
|
+
* Create a substream from current position with specified size
|
506
|
+
* @param size - Size of the substream in bytes
|
507
|
+
*/
|
508
|
+
substream(size) {
|
509
|
+
this.ensureBytes(size);
|
510
|
+
const subBuffer = this.buffer.slice(this._pos, this._pos + size);
|
511
|
+
this._pos += size;
|
512
|
+
return new _KaitaiStream(subBuffer);
|
513
|
+
}
|
514
|
+
};
|
515
|
+
|
516
|
+
// src/parser/schema.ts
|
517
|
+
var BUILTIN_TYPES = [
|
518
|
+
// Unsigned integers
|
519
|
+
"u1",
|
520
|
+
"u2",
|
521
|
+
"u2le",
|
522
|
+
"u2be",
|
523
|
+
"u4",
|
524
|
+
"u4le",
|
525
|
+
"u4be",
|
526
|
+
"u8",
|
527
|
+
"u8le",
|
528
|
+
"u8be",
|
529
|
+
// Signed integers
|
530
|
+
"s1",
|
531
|
+
"s2",
|
532
|
+
"s2le",
|
533
|
+
"s2be",
|
534
|
+
"s4",
|
535
|
+
"s4le",
|
536
|
+
"s4be",
|
537
|
+
"s8",
|
538
|
+
"s8le",
|
539
|
+
"s8be",
|
540
|
+
// Floating point
|
541
|
+
"f4",
|
542
|
+
"f4le",
|
543
|
+
"f4be",
|
544
|
+
"f8",
|
545
|
+
"f8le",
|
546
|
+
"f8be",
|
547
|
+
// String
|
548
|
+
"str",
|
549
|
+
"strz"
|
550
|
+
];
|
551
|
+
function isBuiltinType(type) {
|
552
|
+
return BUILTIN_TYPES.includes(type);
|
553
|
+
}
|
554
|
+
function getTypeEndianness(type) {
|
555
|
+
if (type.endsWith("le")) return "le";
|
556
|
+
if (type.endsWith("be")) return "be";
|
557
|
+
return void 0;
|
558
|
+
}
|
559
|
+
function getBaseType(type) {
|
560
|
+
if (type.endsWith("le") || type.endsWith("be")) {
|
561
|
+
return type.slice(0, -2);
|
562
|
+
}
|
563
|
+
return type;
|
564
|
+
}
|
565
|
+
function isIntegerType(type) {
|
566
|
+
const base = getBaseType(type);
|
567
|
+
return /^[us][1248]$/.test(base);
|
568
|
+
}
|
569
|
+
function isFloatType(type) {
|
570
|
+
const base = getBaseType(type);
|
571
|
+
return /^f[48]$/.test(base);
|
572
|
+
}
|
573
|
+
function isStringType(type) {
|
574
|
+
return type === "str" || type === "strz";
|
575
|
+
}
|
576
|
+
|
577
|
+
// src/parser/KsyParser.ts
|
578
|
+
import { parse as parseYaml } from "yaml";
|
579
|
+
var KsyParser = class {
|
580
|
+
/**
|
581
|
+
* Parse a .ksy YAML string into a typed schema object.
|
582
|
+
*
|
583
|
+
* @param yaml - YAML string containing the .ksy definition
|
584
|
+
* @param options - Parsing options
|
585
|
+
* @returns Parsed and validated schema
|
586
|
+
* @throws {ParseError} If YAML parsing fails
|
587
|
+
* @throws {ValidationError} If schema validation fails
|
588
|
+
*/
|
589
|
+
parse(yaml, options = {}) {
|
590
|
+
const { validate = true, strict = false } = options;
|
591
|
+
let parsed;
|
592
|
+
try {
|
593
|
+
parsed = parseYaml(yaml);
|
594
|
+
} catch (error) {
|
595
|
+
throw new ParseError(
|
596
|
+
`Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`
|
597
|
+
);
|
598
|
+
}
|
599
|
+
if (typeof parsed !== "object" || parsed === null) {
|
600
|
+
throw new ParseError("KSY file must contain an object");
|
601
|
+
}
|
602
|
+
const schema = parsed;
|
603
|
+
if (validate) {
|
604
|
+
const result = this.validate(schema, { strict });
|
605
|
+
if (!result.valid) {
|
606
|
+
const errorMessages = result.errors.map((e) => e.message).join("; ");
|
607
|
+
throw new ValidationError(
|
608
|
+
`Schema validation failed: ${errorMessages}`
|
609
|
+
);
|
610
|
+
}
|
611
|
+
if (result.warnings.length > 0 && !strict) {
|
612
|
+
console.warn(
|
613
|
+
"Schema validation warnings:",
|
614
|
+
result.warnings.map((w) => w.message)
|
615
|
+
);
|
616
|
+
}
|
617
|
+
}
|
618
|
+
return schema;
|
619
|
+
}
|
620
|
+
/**
|
621
|
+
* Validate a schema object.
|
622
|
+
*
|
623
|
+
* @param schema - Schema to validate
|
624
|
+
* @param options - Validation options
|
625
|
+
* @returns Validation result with errors and warnings
|
626
|
+
*/
|
627
|
+
validate(schema, options = {}) {
|
628
|
+
const { strict = false, isNested = false } = options;
|
629
|
+
const errors = [];
|
630
|
+
const warnings = [];
|
631
|
+
if (!schema.meta && !isNested) {
|
632
|
+
errors.push({
|
633
|
+
message: 'Missing required "meta" section',
|
634
|
+
path: [],
|
635
|
+
code: "MISSING_META"
|
636
|
+
});
|
637
|
+
} else if (schema.meta) {
|
638
|
+
if (!schema.meta.id) {
|
639
|
+
errors.push({
|
640
|
+
message: 'Missing required "meta.id" field',
|
641
|
+
path: ["meta"],
|
642
|
+
code: "MISSING_META_ID"
|
643
|
+
});
|
644
|
+
} else if (typeof schema.meta.id !== "string") {
|
645
|
+
errors.push({
|
646
|
+
message: '"meta.id" must be a string',
|
647
|
+
path: ["meta", "id"],
|
648
|
+
code: "INVALID_META_ID_TYPE"
|
649
|
+
});
|
650
|
+
} else if (!/^[a-z][a-z0-9_]*$/.test(schema.meta.id)) {
|
651
|
+
warnings.push({
|
652
|
+
message: '"meta.id" should follow snake_case naming convention',
|
653
|
+
path: ["meta", "id"],
|
654
|
+
code: "META_ID_NAMING"
|
655
|
+
});
|
656
|
+
}
|
657
|
+
if (schema.meta.endian) {
|
658
|
+
if (typeof schema.meta.endian === "string" && schema.meta.endian !== "le" && schema.meta.endian !== "be") {
|
659
|
+
errors.push({
|
660
|
+
message: '"meta.endian" must be "le" or "be"',
|
661
|
+
path: ["meta", "endian"],
|
662
|
+
code: "INVALID_ENDIAN"
|
663
|
+
});
|
664
|
+
}
|
665
|
+
}
|
666
|
+
}
|
667
|
+
if (schema.seq) {
|
668
|
+
if (!Array.isArray(schema.seq)) {
|
669
|
+
errors.push({
|
670
|
+
message: '"seq" must be an array',
|
671
|
+
path: ["seq"],
|
672
|
+
code: "INVALID_SEQ_TYPE"
|
673
|
+
});
|
674
|
+
} else {
|
675
|
+
schema.seq.forEach((attr, index) => {
|
676
|
+
this.validateAttribute(
|
677
|
+
attr,
|
678
|
+
["seq", String(index)],
|
679
|
+
errors,
|
680
|
+
warnings,
|
681
|
+
strict
|
682
|
+
);
|
683
|
+
});
|
684
|
+
}
|
685
|
+
}
|
686
|
+
if (schema.instances) {
|
687
|
+
if (typeof schema.instances !== "object") {
|
688
|
+
errors.push({
|
689
|
+
message: '"instances" must be an object',
|
690
|
+
path: ["instances"],
|
691
|
+
code: "INVALID_INSTANCES_TYPE"
|
692
|
+
});
|
693
|
+
} else {
|
694
|
+
Object.entries(schema.instances).forEach(([key, instance]) => {
|
695
|
+
this.validateAttribute(
|
696
|
+
instance,
|
697
|
+
["instances", key],
|
698
|
+
errors,
|
699
|
+
warnings,
|
700
|
+
strict
|
701
|
+
);
|
702
|
+
});
|
703
|
+
}
|
704
|
+
}
|
705
|
+
if (schema.types) {
|
706
|
+
if (typeof schema.types !== "object") {
|
707
|
+
errors.push({
|
708
|
+
message: '"types" must be an object',
|
709
|
+
path: ["types"],
|
710
|
+
code: "INVALID_TYPES_TYPE"
|
711
|
+
});
|
712
|
+
} else {
|
713
|
+
Object.entries(schema.types).forEach(([key, type]) => {
|
714
|
+
const typeResult = this.validate(type, { ...options, isNested: true });
|
715
|
+
errors.push(
|
716
|
+
...typeResult.errors.map((e) => ({
|
717
|
+
...e,
|
718
|
+
path: ["types", key, ...e.path]
|
719
|
+
}))
|
720
|
+
);
|
721
|
+
warnings.push(
|
722
|
+
...typeResult.warnings.map((w) => ({
|
723
|
+
...w,
|
724
|
+
path: ["types", key, ...w.path]
|
725
|
+
}))
|
726
|
+
);
|
727
|
+
});
|
728
|
+
}
|
729
|
+
}
|
730
|
+
if (schema.enums) {
|
731
|
+
if (typeof schema.enums !== "object") {
|
732
|
+
errors.push({
|
733
|
+
message: '"enums" must be an object',
|
734
|
+
path: ["enums"],
|
735
|
+
code: "INVALID_ENUMS_TYPE"
|
736
|
+
});
|
737
|
+
}
|
738
|
+
}
|
739
|
+
return {
|
740
|
+
valid: errors.length === 0 && (strict ? warnings.length === 0 : true),
|
741
|
+
errors,
|
742
|
+
warnings
|
743
|
+
};
|
744
|
+
}
|
745
|
+
/**
|
746
|
+
* Validate an attribute specification.
|
747
|
+
*
|
748
|
+
* @param attr - Attribute to validate
|
749
|
+
* @param path - Path to this attribute in the schema
|
750
|
+
* @param errors - Array to collect errors
|
751
|
+
* @param warnings - Array to collect warnings
|
752
|
+
* @param strict - Whether to be strict about warnings
|
753
|
+
* @private
|
754
|
+
*/
|
755
|
+
validateAttribute(attr, path, errors, warnings, _strict) {
|
756
|
+
if (attr.id && typeof attr.id === "string") {
|
757
|
+
if (!/^[a-z][a-z0-9_]*$/.test(attr.id)) {
|
758
|
+
warnings.push({
|
759
|
+
message: `Field "${attr.id}" should follow snake_case naming convention`,
|
760
|
+
path: [...path, "id"],
|
761
|
+
code: "FIELD_ID_NAMING"
|
762
|
+
});
|
763
|
+
}
|
764
|
+
}
|
765
|
+
if (attr.repeat) {
|
766
|
+
if (attr.repeat !== "expr" && attr.repeat !== "eos" && attr.repeat !== "until") {
|
767
|
+
errors.push({
|
768
|
+
message: '"repeat" must be "expr", "eos", or "until"',
|
769
|
+
path: [...path, "repeat"],
|
770
|
+
code: "INVALID_REPEAT"
|
771
|
+
});
|
772
|
+
}
|
773
|
+
if (attr.repeat === "expr" && !attr["repeat-expr"]) {
|
774
|
+
errors.push({
|
775
|
+
message: '"repeat-expr" is required when repeat is "expr"',
|
776
|
+
path: [...path, "repeat-expr"],
|
777
|
+
code: "MISSING_REPEAT_EXPR"
|
778
|
+
});
|
779
|
+
}
|
780
|
+
if (attr.repeat === "until" && !attr["repeat-until"]) {
|
781
|
+
errors.push({
|
782
|
+
message: '"repeat-until" is required when repeat is "until"',
|
783
|
+
path: [...path, "repeat-until"],
|
784
|
+
code: "MISSING_REPEAT_UNTIL"
|
785
|
+
});
|
786
|
+
}
|
787
|
+
}
|
788
|
+
if (attr["size-eos"] && attr.size) {
|
789
|
+
warnings.push({
|
790
|
+
message: '"size-eos" and "size" are mutually exclusive',
|
791
|
+
path: [...path],
|
792
|
+
code: "SIZE_EOS_WITH_SIZE"
|
793
|
+
});
|
794
|
+
}
|
795
|
+
if (attr.contents) {
|
796
|
+
if (!Array.isArray(attr.contents) && typeof attr.contents !== "string") {
|
797
|
+
errors.push({
|
798
|
+
message: '"contents" must be an array or string',
|
799
|
+
path: [...path, "contents"],
|
800
|
+
code: "INVALID_CONTENTS_TYPE"
|
801
|
+
});
|
802
|
+
}
|
803
|
+
}
|
804
|
+
}
|
805
|
+
/**
|
806
|
+
* Parse multiple .ksy files and resolve imports.
|
807
|
+
*
|
808
|
+
* @param mainYaml - Main .ksy file content
|
809
|
+
* @param imports - Map of import names to their YAML content
|
810
|
+
* @param options - Parsing options
|
811
|
+
* @returns Parsed schema with resolved imports
|
812
|
+
*/
|
813
|
+
parseWithImports(mainYaml, _imports, options = {}) {
|
814
|
+
const mainSchema = this.parse(mainYaml, options);
|
815
|
+
return mainSchema;
|
816
|
+
}
|
817
|
+
};
|
818
|
+
|
819
|
+
// src/interpreter/Context.ts
|
820
|
+
var Context = class _Context {
|
821
|
+
/**
|
822
|
+
* Create a new execution context.
|
823
|
+
*
|
824
|
+
* @param _io - Binary stream being read
|
825
|
+
* @param _root - Root object of the parse tree
|
826
|
+
* @param _parent - Parent object (optional)
|
827
|
+
*/
|
828
|
+
constructor(_io, _root = null, _parent = null) {
|
829
|
+
this._io = _io;
|
830
|
+
this._root = _root;
|
831
|
+
/** Stack of parent objects */
|
832
|
+
this.parentStack = [];
|
833
|
+
/** Current object being parsed */
|
834
|
+
this._current = {};
|
835
|
+
if (_parent !== null) {
|
836
|
+
this.parentStack.push(_parent);
|
837
|
+
}
|
838
|
+
}
|
839
|
+
/**
|
840
|
+
* Get the current I/O stream.
|
841
|
+
* Accessible in expressions as `_io`.
|
842
|
+
*
|
843
|
+
* @returns Current stream
|
844
|
+
*/
|
845
|
+
get io() {
|
846
|
+
return this._io;
|
847
|
+
}
|
848
|
+
/**
|
849
|
+
* Get the root object.
|
850
|
+
* Accessible in expressions as `_root`.
|
851
|
+
*
|
852
|
+
* @returns Root object
|
853
|
+
*/
|
854
|
+
get root() {
|
855
|
+
return this._root;
|
856
|
+
}
|
857
|
+
/**
|
858
|
+
* Get the parent object.
|
859
|
+
* Accessible in expressions as `_parent`.
|
860
|
+
*
|
861
|
+
* @returns Parent object or null if at root
|
862
|
+
*/
|
863
|
+
get parent() {
|
864
|
+
return this.parentStack.length > 0 ? this.parentStack[this.parentStack.length - 1] : null;
|
865
|
+
}
|
866
|
+
/**
|
867
|
+
* Get the current object being parsed.
|
868
|
+
* Used to access fields defined earlier in the sequence.
|
869
|
+
*
|
870
|
+
* @returns Current object
|
871
|
+
*/
|
872
|
+
get current() {
|
873
|
+
return this._current;
|
874
|
+
}
|
875
|
+
/**
|
876
|
+
* Set the current object.
|
877
|
+
*
|
878
|
+
* @param obj - Object to set as current
|
879
|
+
*/
|
880
|
+
set current(obj) {
|
881
|
+
this._current = obj;
|
882
|
+
}
|
883
|
+
/**
|
884
|
+
* Push a new parent onto the stack.
|
885
|
+
* Used when entering a nested type.
|
886
|
+
*
|
887
|
+
* @param parent - Parent object to push
|
888
|
+
*/
|
889
|
+
pushParent(parent) {
|
890
|
+
this.parentStack.push(parent);
|
891
|
+
}
|
892
|
+
/**
|
893
|
+
* Pop the current parent from the stack.
|
894
|
+
* Used when exiting a nested type.
|
895
|
+
*
|
896
|
+
* @returns The popped parent object
|
897
|
+
*/
|
898
|
+
popParent() {
|
899
|
+
return this.parentStack.pop();
|
900
|
+
}
|
901
|
+
/**
|
902
|
+
* Get a value from the context by path.
|
903
|
+
* Supports special names: _io, _root, _parent, _index.
|
904
|
+
*
|
905
|
+
* @param name - Name or path to resolve
|
906
|
+
* @returns Resolved value
|
907
|
+
*/
|
908
|
+
resolve(name) {
|
909
|
+
switch (name) {
|
910
|
+
case "_io":
|
911
|
+
return this._io;
|
912
|
+
case "_root":
|
913
|
+
return this._root;
|
914
|
+
case "_parent":
|
915
|
+
return this.parent;
|
916
|
+
case "_index":
|
917
|
+
return this._current["_index"];
|
918
|
+
default:
|
919
|
+
if (name in this._current) {
|
920
|
+
return this._current[name];
|
921
|
+
}
|
922
|
+
return void 0;
|
923
|
+
}
|
924
|
+
}
|
925
|
+
/**
|
926
|
+
* Set a value in the current object.
|
927
|
+
*
|
928
|
+
* @param name - Field name
|
929
|
+
* @param value - Value to set
|
930
|
+
*/
|
931
|
+
set(name, value) {
|
932
|
+
this._current[name] = value;
|
933
|
+
}
|
934
|
+
/**
|
935
|
+
* Create a child context for nested parsing.
|
936
|
+
* The current object becomes the parent in the child context.
|
937
|
+
*
|
938
|
+
* @param stream - Stream for the child context (defaults to current stream)
|
939
|
+
* @returns New child context
|
940
|
+
*/
|
941
|
+
createChild(stream) {
|
942
|
+
const childContext = new _Context(
|
943
|
+
stream || this._io,
|
944
|
+
this._root || this._current,
|
945
|
+
this._current
|
946
|
+
);
|
947
|
+
return childContext;
|
948
|
+
}
|
949
|
+
/**
|
950
|
+
* Clone this context.
|
951
|
+
* Creates a shallow copy with the same stream, root, and parent.
|
952
|
+
*
|
953
|
+
* @returns Cloned context
|
954
|
+
*/
|
955
|
+
clone() {
|
956
|
+
const cloned = new _Context(this._io, this._root, this.parent);
|
957
|
+
cloned._current = { ...this._current };
|
958
|
+
cloned.parentStack = [...this.parentStack];
|
959
|
+
return cloned;
|
960
|
+
}
|
961
|
+
};
|
962
|
+
|
963
|
+
// src/interpreter/TypeInterpreter.ts
|
964
|
+
var TypeInterpreter = class _TypeInterpreter {
|
965
|
+
/**
|
966
|
+
* Create a new type interpreter.
|
967
|
+
*
|
968
|
+
* @param schema - Kaitai Struct schema to interpret
|
969
|
+
* @param parentMeta - Parent schema's meta (for nested types)
|
970
|
+
*/
|
971
|
+
constructor(schema, parentMeta) {
|
972
|
+
this.schema = schema;
|
973
|
+
this.parentMeta = parentMeta;
|
974
|
+
if (!schema.meta && !parentMeta) {
|
975
|
+
throw new ParseError("Schema must have meta section");
|
976
|
+
}
|
977
|
+
if (schema.meta && !schema.meta.id && !parentMeta) {
|
978
|
+
throw new ParseError("Root schema must have meta.id");
|
979
|
+
}
|
980
|
+
}
|
981
|
+
/**
|
982
|
+
* Parse binary data according to the schema.
|
983
|
+
*
|
984
|
+
* @param stream - Binary stream to parse
|
985
|
+
* @param parent - Parent object (for nested types)
|
986
|
+
* @returns Parsed object
|
987
|
+
*/
|
988
|
+
parse(stream, parent) {
|
989
|
+
const result = {};
|
990
|
+
const context = new Context(stream, result, parent);
|
991
|
+
context.current = result;
|
992
|
+
if (this.schema.seq) {
|
993
|
+
for (const attr of this.schema.seq) {
|
994
|
+
const value = this.parseAttribute(attr, context);
|
995
|
+
if (attr.id) {
|
996
|
+
result[attr.id] = value;
|
997
|
+
}
|
998
|
+
}
|
999
|
+
}
|
1000
|
+
return result;
|
1001
|
+
}
|
1002
|
+
/**
|
1003
|
+
* Parse a single attribute according to its specification.
|
1004
|
+
*
|
1005
|
+
* @param attr - Attribute specification
|
1006
|
+
* @param context - Execution context
|
1007
|
+
* @returns Parsed value
|
1008
|
+
* @private
|
1009
|
+
*/
|
1010
|
+
parseAttribute(attr, context) {
|
1011
|
+
const stream = context.io;
|
1012
|
+
if (attr.if) {
|
1013
|
+
throw new NotImplementedError("Conditional parsing (if)");
|
1014
|
+
}
|
1015
|
+
if (attr.pos !== void 0) {
|
1016
|
+
const pos = typeof attr.pos === "number" ? attr.pos : 0;
|
1017
|
+
stream.seek(pos);
|
1018
|
+
}
|
1019
|
+
if (attr.io) {
|
1020
|
+
throw new NotImplementedError("Custom I/O streams");
|
1021
|
+
}
|
1022
|
+
if (attr.repeat) {
|
1023
|
+
return this.parseRepeated(attr, context);
|
1024
|
+
}
|
1025
|
+
if (attr.contents) {
|
1026
|
+
return this.parseContents(attr, context);
|
1027
|
+
}
|
1028
|
+
return this.parseValue(attr, context);
|
1029
|
+
}
|
1030
|
+
/**
|
1031
|
+
* Parse a repeated attribute.
|
1032
|
+
*
|
1033
|
+
* @param attr - Attribute specification with repeat
|
1034
|
+
* @param context - Execution context
|
1035
|
+
* @returns Array of parsed values
|
1036
|
+
* @private
|
1037
|
+
*/
|
1038
|
+
parseRepeated(attr, context) {
|
1039
|
+
const result = [];
|
1040
|
+
const stream = context.io;
|
1041
|
+
switch (attr.repeat) {
|
1042
|
+
case "expr": {
|
1043
|
+
const count = typeof attr["repeat-expr"] === "number" ? attr["repeat-expr"] : 0;
|
1044
|
+
for (let i = 0; i < count; i++) {
|
1045
|
+
context.set("_index", i);
|
1046
|
+
result.push(this.parseValue(attr, context));
|
1047
|
+
}
|
1048
|
+
break;
|
1049
|
+
}
|
1050
|
+
case "eos": {
|
1051
|
+
while (!stream.isEof()) {
|
1052
|
+
context.set("_index", result.length);
|
1053
|
+
result.push(this.parseValue(attr, context));
|
1054
|
+
}
|
1055
|
+
break;
|
1056
|
+
}
|
1057
|
+
case "until": {
|
1058
|
+
if (!attr["repeat-until"]) {
|
1059
|
+
throw new ParseError("repeat-until expression is required");
|
1060
|
+
}
|
1061
|
+
throw new NotImplementedError("repeat-until");
|
1062
|
+
}
|
1063
|
+
default:
|
1064
|
+
throw new ParseError(`Unknown repeat type: ${attr.repeat}`);
|
1065
|
+
}
|
1066
|
+
return result;
|
1067
|
+
}
|
1068
|
+
/**
|
1069
|
+
* Parse and validate contents.
|
1070
|
+
*
|
1071
|
+
* @param attr - Attribute specification with contents
|
1072
|
+
* @param context - Execution context
|
1073
|
+
* @returns The validated contents
|
1074
|
+
* @private
|
1075
|
+
*/
|
1076
|
+
parseContents(attr, context) {
|
1077
|
+
const stream = context.io;
|
1078
|
+
const expected = attr.contents;
|
1079
|
+
if (Array.isArray(expected)) {
|
1080
|
+
const bytes = stream.readBytes(expected.length);
|
1081
|
+
for (let i = 0; i < expected.length; i++) {
|
1082
|
+
if (bytes[i] !== expected[i]) {
|
1083
|
+
throw new ValidationError(
|
1084
|
+
`Contents mismatch at byte ${i}: expected ${expected[i]}, got ${bytes[i]}`,
|
1085
|
+
stream.pos - expected.length + i
|
1086
|
+
);
|
1087
|
+
}
|
1088
|
+
}
|
1089
|
+
return bytes;
|
1090
|
+
} else {
|
1091
|
+
const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
|
1092
|
+
const str = stream.readStr(expected.length, encoding);
|
1093
|
+
if (str !== expected) {
|
1094
|
+
throw new ValidationError(
|
1095
|
+
`Contents mismatch: expected "${expected}", got "${str}"`,
|
1096
|
+
stream.pos - expected.length
|
1097
|
+
);
|
1098
|
+
}
|
1099
|
+
return str;
|
1100
|
+
}
|
1101
|
+
}
|
1102
|
+
/**
|
1103
|
+
* Parse a single value according to its type.
|
1104
|
+
*
|
1105
|
+
* @param attr - Attribute specification
|
1106
|
+
* @param context - Execution context
|
1107
|
+
* @returns Parsed value
|
1108
|
+
* @private
|
1109
|
+
*/
|
1110
|
+
parseValue(attr, context) {
|
1111
|
+
const stream = context.io;
|
1112
|
+
const type = attr.type;
|
1113
|
+
if (attr.size !== void 0) {
|
1114
|
+
const size = typeof attr.size === "number" ? attr.size : 0;
|
1115
|
+
if (type === "str" || !type) {
|
1116
|
+
const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
|
1117
|
+
if (type === "str") {
|
1118
|
+
return stream.readStr(size, encoding);
|
1119
|
+
} else {
|
1120
|
+
return stream.readBytes(size);
|
1121
|
+
}
|
1122
|
+
} else {
|
1123
|
+
const substream = stream.substream(size);
|
1124
|
+
return this.parseType(type, substream, context);
|
1125
|
+
}
|
1126
|
+
}
|
1127
|
+
if (attr["size-eos"]) {
|
1128
|
+
if (type === "str") {
|
1129
|
+
const encoding = attr.encoding || this.schema.meta.encoding || "UTF-8";
|
1130
|
+
const bytes = stream.readBytesFull();
|
1131
|
+
return new TextDecoder(encoding).decode(bytes);
|
1132
|
+
} else {
|
1133
|
+
return stream.readBytesFull();
|
1134
|
+
}
|
1135
|
+
}
|
1136
|
+
if (!type) {
|
1137
|
+
throw new ParseError("Attribute must have either type, size, or contents");
|
1138
|
+
}
|
1139
|
+
return this.parseType(type, stream, context);
|
1140
|
+
}
|
1141
|
+
/**
|
1142
|
+
* Parse a value of a specific type.
|
1143
|
+
*
|
1144
|
+
* @param type - Type name or switch specification
|
1145
|
+
* @param stream - Stream to read from
|
1146
|
+
* @param context - Execution context
|
1147
|
+
* @returns Parsed value
|
1148
|
+
* @private
|
1149
|
+
*/
|
1150
|
+
parseType(type, stream, context) {
|
1151
|
+
if (typeof type === "object") {
|
1152
|
+
throw new NotImplementedError("Switch types");
|
1153
|
+
}
|
1154
|
+
if (isBuiltinType(type)) {
|
1155
|
+
return this.parseBuiltinType(type, stream, context);
|
1156
|
+
}
|
1157
|
+
if (this.schema.types && type in this.schema.types) {
|
1158
|
+
const typeSchema = this.schema.types[type];
|
1159
|
+
const meta = this.schema.meta || this.parentMeta;
|
1160
|
+
const interpreter = new _TypeInterpreter(typeSchema, meta);
|
1161
|
+
return interpreter.parse(stream, context.current);
|
1162
|
+
}
|
1163
|
+
throw new ParseError(`Unknown type: ${type}`);
|
1164
|
+
}
|
1165
|
+
/**
|
1166
|
+
* Parse a built-in type.
|
1167
|
+
*
|
1168
|
+
* @param type - Built-in type name
|
1169
|
+
* @param stream - Stream to read from
|
1170
|
+
* @param context - Execution context
|
1171
|
+
* @returns Parsed value
|
1172
|
+
* @private
|
1173
|
+
*/
|
1174
|
+
parseBuiltinType(type, stream, _context) {
|
1175
|
+
const base = getBaseType(type);
|
1176
|
+
const typeEndian = getTypeEndianness(type);
|
1177
|
+
const meta = this.schema.meta || this.parentMeta;
|
1178
|
+
const metaEndian = meta?.endian;
|
1179
|
+
const endian = typeEndian || (typeof metaEndian === "string" ? metaEndian : "le");
|
1180
|
+
if (isIntegerType(type)) {
|
1181
|
+
return this.readInteger(base, endian, stream);
|
1182
|
+
}
|
1183
|
+
if (isFloatType(type)) {
|
1184
|
+
return this.readFloat(base, endian, stream);
|
1185
|
+
}
|
1186
|
+
if (isStringType(type)) {
|
1187
|
+
throw new ParseError("String types require size, size-eos, or terminator");
|
1188
|
+
}
|
1189
|
+
throw new ParseError(`Unknown built-in type: ${type}`);
|
1190
|
+
}
|
1191
|
+
/**
|
1192
|
+
* Read an integer value.
|
1193
|
+
*
|
1194
|
+
* @param type - Integer type (u1, u2, u4, u8, s1, s2, s4, s8)
|
1195
|
+
* @param endian - Endianness
|
1196
|
+
* @param stream - Stream to read from
|
1197
|
+
* @returns Integer value
|
1198
|
+
* @private
|
1199
|
+
*/
|
1200
|
+
readInteger(type, endian, stream) {
|
1201
|
+
switch (type) {
|
1202
|
+
case "u1":
|
1203
|
+
return stream.readU1();
|
1204
|
+
case "u2":
|
1205
|
+
return endian === "le" ? stream.readU2le() : stream.readU2be();
|
1206
|
+
case "u4":
|
1207
|
+
return endian === "le" ? stream.readU4le() : stream.readU4be();
|
1208
|
+
case "u8":
|
1209
|
+
return endian === "le" ? stream.readU8le() : stream.readU8be();
|
1210
|
+
case "s1":
|
1211
|
+
return stream.readS1();
|
1212
|
+
case "s2":
|
1213
|
+
return endian === "le" ? stream.readS2le() : stream.readS2be();
|
1214
|
+
case "s4":
|
1215
|
+
return endian === "le" ? stream.readS4le() : stream.readS4be();
|
1216
|
+
case "s8":
|
1217
|
+
return endian === "le" ? stream.readS8le() : stream.readS8be();
|
1218
|
+
default:
|
1219
|
+
throw new ParseError(`Unknown integer type: ${type}`);
|
1220
|
+
}
|
1221
|
+
}
|
1222
|
+
/**
|
1223
|
+
* Read a floating point value.
|
1224
|
+
*
|
1225
|
+
* @param type - Float type (f4, f8)
|
1226
|
+
* @param endian - Endianness
|
1227
|
+
* @param stream - Stream to read from
|
1228
|
+
* @returns Float value
|
1229
|
+
* @private
|
1230
|
+
*/
|
1231
|
+
readFloat(type, endian, stream) {
|
1232
|
+
switch (type) {
|
1233
|
+
case "f4":
|
1234
|
+
return endian === "le" ? stream.readF4le() : stream.readF4be();
|
1235
|
+
case "f8":
|
1236
|
+
return endian === "le" ? stream.readF8le() : stream.readF8be();
|
1237
|
+
default:
|
1238
|
+
throw new ParseError(`Unknown float type: ${type}`);
|
1239
|
+
}
|
1240
|
+
}
|
1241
|
+
};
|
1242
|
+
|
1243
|
+
// src/index.ts
|
1244
|
+
function parse(ksyYaml, buffer, options = {}) {
|
1245
|
+
const { validate = true, strict = false } = options;
|
1246
|
+
const parser = new KsyParser();
|
1247
|
+
const schema = parser.parse(ksyYaml, { validate, strict });
|
1248
|
+
const stream = new KaitaiStream(buffer);
|
1249
|
+
const interpreter = new TypeInterpreter(schema);
|
1250
|
+
return interpreter.parse(stream);
|
1251
|
+
}
|
1252
|
+
export {
|
1253
|
+
BUILTIN_TYPES,
|
1254
|
+
Context,
|
1255
|
+
EOFError,
|
1256
|
+
KaitaiError,
|
1257
|
+
KaitaiStream,
|
1258
|
+
KsyParser,
|
1259
|
+
NotImplementedError,
|
1260
|
+
ParseError,
|
1261
|
+
TypeInterpreter,
|
1262
|
+
ValidationError,
|
1263
|
+
getBaseType,
|
1264
|
+
getTypeEndianness,
|
1265
|
+
isBuiltinType,
|
1266
|
+
isFloatType,
|
1267
|
+
isIntegerType,
|
1268
|
+
isStringType,
|
1269
|
+
parse
|
1270
|
+
};
|
1271
|
+
/**
|
1272
|
+
* @fileoverview Custom error classes for Kaitai Struct parsing and validation
|
1273
|
+
* @module utils/errors
|
1274
|
+
* @author Fabiano Pinto
|
1275
|
+
* @license MIT
|
1276
|
+
*/
|
1277
|
+
/**
|
1278
|
+
* @fileoverview String encoding and decoding utilities for binary data
|
1279
|
+
* @module utils/encoding
|
1280
|
+
* @author Fabiano Pinto
|
1281
|
+
* @license MIT
|
1282
|
+
*/
|
1283
|
+
/**
|
1284
|
+
* @fileoverview Binary stream reader for Kaitai Struct
|
1285
|
+
* @module stream/KaitaiStream
|
1286
|
+
* @author Fabiano Pinto
|
1287
|
+
* @license MIT
|
1288
|
+
*/
|
1289
|
+
/**
|
1290
|
+
* @fileoverview Binary stream reading functionality
|
1291
|
+
* @module stream
|
1292
|
+
* @author Fabiano Pinto
|
1293
|
+
* @license MIT
|
1294
|
+
*/
|
1295
|
+
/**
|
1296
|
+
* @fileoverview Type definitions for Kaitai Struct YAML schema (.ksy files)
|
1297
|
+
* @module parser/schema
|
1298
|
+
* @author Fabiano Pinto
|
1299
|
+
* @license MIT
|
1300
|
+
*/
|
1301
|
+
/**
|
1302
|
+
* @fileoverview Parser for Kaitai Struct YAML (.ksy) files
|
1303
|
+
* @module parser/KsyParser
|
1304
|
+
* @author Fabiano Pinto
|
1305
|
+
* @license MIT
|
1306
|
+
*/
|
1307
|
+
/**
|
1308
|
+
* @fileoverview Parser module for Kaitai Struct YAML files
|
1309
|
+
* @module parser
|
1310
|
+
* @author Fabiano Pinto
|
1311
|
+
* @license MIT
|
1312
|
+
*/
|
1313
|
+
/**
|
1314
|
+
* @fileoverview Execution context for Kaitai Struct parsing
|
1315
|
+
* @module interpreter/Context
|
1316
|
+
* @author Fabiano Pinto
|
1317
|
+
* @license MIT
|
1318
|
+
*/
|
1319
|
+
/**
|
1320
|
+
* @fileoverview Type interpreter for executing Kaitai Struct schemas
|
1321
|
+
* @module interpreter/TypeInterpreter
|
1322
|
+
* @author Fabiano Pinto
|
1323
|
+
* @license MIT
|
1324
|
+
*/
|
1325
|
+
/**
|
1326
|
+
* @fileoverview Interpreter module for executing Kaitai Struct schemas
|
1327
|
+
* @module interpreter
|
1328
|
+
* @author Fabiano Pinto
|
1329
|
+
* @license MIT
|
1330
|
+
*/
|
1331
|
+
/**
|
1332
|
+
* @fileoverview Main entry point for kaitai-struct-ts library
|
1333
|
+
* @module kaitai-struct-ts
|
1334
|
+
* @author Fabiano Pinto
|
1335
|
+
* @license MIT
|
1336
|
+
* @version 0.2.0
|
1337
|
+
*
|
1338
|
+
* @description
|
1339
|
+
* A runtime interpreter for Kaitai Struct binary format definitions in TypeScript.
|
1340
|
+
* Parse any binary data format by providing a .ksy (Kaitai Struct YAML) definition file.
|
1341
|
+
*
|
1342
|
+
* @example
|
1343
|
+
* ```typescript
|
1344
|
+
* import { parse } from 'kaitai-struct-ts'
|
1345
|
+
*
|
1346
|
+
* const ksyDefinition = `
|
1347
|
+
* meta:
|
1348
|
+
* id: my_format
|
1349
|
+
* endian: le
|
1350
|
+
* seq:
|
1351
|
+
* - id: magic
|
1352
|
+
* contents: [0x4D, 0x5A]
|
1353
|
+
* - id: version
|
1354
|
+
* type: u2
|
1355
|
+
* `
|
1356
|
+
*
|
1357
|
+
* const buffer = new Uint8Array([0x4D, 0x5A, 0x01, 0x00])
|
1358
|
+
* const result = parse(ksyDefinition, buffer)
|
1359
|
+
* console.log(result.version) // 1
|
1360
|
+
* ```
|
1361
|
+
*/
|
1362
|
+
//# sourceMappingURL=index.mjs.map
|