@zakkster/lite-bake-stream 1.1.0 → 1.3.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/CHANGELOG.md +58 -0
- package/README.md +1 -1
- package/llms.txt +2 -2
- package/package.json +5 -1
- package/src/FileIngest.js +25 -7
- package/src/MultiReader.js +20 -4
- package/src/Opts.js +189 -0
- package/src/PreserveReader.js +69 -13
- package/src/PreserveTokenizer.js +35 -4
- package/src/PreserveWriter.js +15 -2
- package/src/RangeReader.js +146 -16
- package/src/Reader.js +125 -25
- package/src/Split.js +40 -8
- package/src/StringTable.js +1 -1
- package/src/Tokenizer.js +68 -30
- package/src/Views.js +25 -0
- package/src/Writer.js +87 -15
- package/src/index.js +30 -24
package/src/Reader.js
CHANGED
|
@@ -10,15 +10,22 @@
|
|
|
10
10
|
// R_BAD_MAGIC - header magic mismatch
|
|
11
11
|
// R_UNSUPPORTED_VERSION - format_version > 1
|
|
12
12
|
// R_UNSUPPORTED_ENDIAN - endian byte is neither LE nor BE
|
|
13
|
+
// R_WRONG_MODE - preserve-mode container fed to the schema Reader
|
|
14
|
+
// R_BAD_FLAGS - unknown header flag bits set
|
|
13
15
|
// R_BAD_FIELD_FLAGS - a FieldDescriptor.flags is non-zero (v2+ reserved)
|
|
14
16
|
// R_UNSUPPORTED_LANE - lane_kind is not F64 or U32
|
|
15
17
|
// R_SHARD_VERSION_TOO_NEW - a shard's min_reader_version > this reader
|
|
16
|
-
// R_TRUNCATED -
|
|
17
|
-
//
|
|
18
|
+
// R_TRUNCATED - a structure claims bytes past the buffer end
|
|
19
|
+
// R_RESERVED_NONZERO - a reserved header/descriptor/shard field is non-zero
|
|
20
|
+
// R_BAD_FOOTER - footer magic_end or footer_len is malformed
|
|
21
|
+
// R_BAD_METADATA - metadata_off is non-zero but the zone-map segment is unparseable
|
|
22
|
+
// R_INVALID - a structure is internally inconsistent but in-bounds
|
|
23
|
+
// R_UNKNOWN_FIELD - get()/findShards()/shardBounds() called with an unknown field name
|
|
18
24
|
|
|
19
25
|
import { StringTable } from './StringTable.js';
|
|
26
|
+
import { toContainerBuffer } from './Views.js';
|
|
20
27
|
|
|
21
|
-
export const VERSION = '1.
|
|
28
|
+
export const VERSION = '1.3.0';
|
|
22
29
|
|
|
23
30
|
const CONTAINER_HEADER_BYTES = 48;
|
|
24
31
|
const SHARD_ENTRY_BYTES = 40;
|
|
@@ -29,16 +36,43 @@ const LANE_F64 = 1;
|
|
|
29
36
|
const LANE_U32 = 3;
|
|
30
37
|
const READER_VERSION = 1;
|
|
31
38
|
|
|
39
|
+
function laneBytesOf(k) { return k === LANE_F64 ? 8 : (k === LANE_U32 ? 4 : 0); }
|
|
40
|
+
|
|
32
41
|
export class ReaderError extends Error {
|
|
33
42
|
constructor(code, msg) { super(msg); this.code = code; this.name = 'ReaderError'; }
|
|
34
43
|
}
|
|
35
44
|
|
|
45
|
+
// Validate a local string table's shape BEFORE StringTable.parse casts a
|
|
46
|
+
// Uint32Array over it (T-1..T-4): the offsets array must be in bounds, the
|
|
47
|
+
// count/blob must fit, offsets must be non-decreasing, and the trailing
|
|
48
|
+
// sentinel must equal blob_length. A lie here is otherwise a raw RangeError
|
|
49
|
+
// from `new Uint32Array(buffer, off, ...)` or a silent out-of-range read.
|
|
50
|
+
function validateStringTable(bytes, off, len, label) {
|
|
51
|
+
if (len < 8)
|
|
52
|
+
throw new ReaderError('R_TRUNCATED', label + ' string table shorter than its 8-byte header');
|
|
53
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset + off, len);
|
|
54
|
+
const entryCount = dv.getUint32(0, true);
|
|
55
|
+
const blobLen = dv.getUint32(4, true);
|
|
56
|
+
const offsetsBytes = (entryCount + 1) * 4;
|
|
57
|
+
if (8 + offsetsBytes + blobLen > len)
|
|
58
|
+
throw new ReaderError('R_TRUNCATED', label + ' string table claims ' + entryCount +
|
|
59
|
+
' entries + ' + blobLen + ' blob bytes past its ' + len + '-byte extent');
|
|
60
|
+
let prev = dv.getUint32(8, true);
|
|
61
|
+
if (prev !== 0)
|
|
62
|
+
throw new ReaderError('R_INVALID', label + ' string table offsets[0] is ' + prev + ', must be 0');
|
|
63
|
+
for (let i = 1; i <= entryCount; i++) {
|
|
64
|
+
const cur = dv.getUint32(8 + i * 4, true);
|
|
65
|
+
if (cur < prev)
|
|
66
|
+
throw new ReaderError('R_INVALID', label + ' string table offsets not monotonic at index ' + i);
|
|
67
|
+
prev = cur;
|
|
68
|
+
}
|
|
69
|
+
if (prev !== blobLen)
|
|
70
|
+
throw new ReaderError('R_INVALID', label + ' string table sentinel ' + prev + ' != blob_length ' + blobLen);
|
|
71
|
+
}
|
|
72
|
+
|
|
36
73
|
export class Reader {
|
|
37
|
-
static fromBuffer(
|
|
38
|
-
|
|
39
|
-
? bufferOrArrayBuffer
|
|
40
|
-
: bufferOrArrayBuffer.buffer;
|
|
41
|
-
return new Reader(buffer);
|
|
74
|
+
static fromBuffer(input) {
|
|
75
|
+
return new Reader(toContainerBuffer(input, 'Reader.fromBuffer'));
|
|
42
76
|
}
|
|
43
77
|
|
|
44
78
|
constructor(buffer) {
|
|
@@ -46,9 +80,10 @@ export class Reader {
|
|
|
46
80
|
this._dv = new DataView(buffer);
|
|
47
81
|
this._bytes = new Uint8Array(buffer);
|
|
48
82
|
this._parseHeader();
|
|
83
|
+
this._parseFooter();
|
|
49
84
|
this._parseSchema();
|
|
50
85
|
this._parseShardDirectory();
|
|
51
|
-
this._parseZoneMaps(); // M7
|
|
86
|
+
this._parseZoneMaps(); // M7 -- no-op if metadata_off is 0
|
|
52
87
|
this._buildFieldIndex();
|
|
53
88
|
}
|
|
54
89
|
|
|
@@ -79,13 +114,42 @@ export class Reader {
|
|
|
79
114
|
}
|
|
80
115
|
this._flags = flags;
|
|
81
116
|
|
|
117
|
+
const reserved1 = this._dv.getUint32(36, true);
|
|
118
|
+
if (reserved1 !== 0)
|
|
119
|
+
throw new ReaderError('R_RESERVED_NONZERO', 'header reserved1 at offset 36 must be 0, got ' + reserved1);
|
|
120
|
+
|
|
82
121
|
this._schemaBlockOff = Number(this._dv.getBigUint64(8, true));
|
|
83
122
|
this._metadataOff = Number(this._dv.getBigUint64(16, true)); // 0 = no metadata block; M7+ zone maps
|
|
84
123
|
this._shardDirOff = Number(this._dv.getBigUint64(24, true));
|
|
85
124
|
this._shardCount = this._dv.getUint32(32, true);
|
|
86
|
-
// 4 bytes reserved at 36
|
|
87
125
|
this._totalRows = Number(this._dv.getBigUint64(40, true));
|
|
88
126
|
this._formatVersion = version;
|
|
127
|
+
|
|
128
|
+
const len = this._buffer.byteLength;
|
|
129
|
+
if (this._schemaBlockOff < CONTAINER_HEADER_BYTES || this._schemaBlockOff >= len)
|
|
130
|
+
throw new ReaderError('R_TRUNCATED', 'schema_block_off ' + this._schemaBlockOff + ' out of range [48, ' + len + ')');
|
|
131
|
+
if (this._shardDirOff < CONTAINER_HEADER_BYTES || this._shardDirOff >= len)
|
|
132
|
+
throw new ReaderError('R_TRUNCATED', 'shard_directory_off ' + this._shardDirOff + ' out of range [48, ' + len + ')');
|
|
133
|
+
if (this._metadataOff !== 0 && (this._metadataOff < CONTAINER_HEADER_BYTES || this._metadataOff >= len))
|
|
134
|
+
throw new ReaderError('R_TRUNCATED', 'metadata_off ' + this._metadataOff + ' out of range {0} u [48, ' + len + ')');
|
|
135
|
+
if (this._shardCount * SHARD_ENTRY_BYTES > len - CONTAINER_HEADER_BYTES)
|
|
136
|
+
throw new ReaderError('R_TRUNCATED', 'shard_count ' + this._shardCount + ' exceeds the bytes available for a shard directory');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Footer (SPEC 3.7): magic_end 'LBK1'-reversed = '1KBL' at footer_off+8,
|
|
140
|
+
// footer_len >= 16 at footer_off+12. Runs once, at construction.
|
|
141
|
+
_parseFooter() {
|
|
142
|
+
const len = this._buffer.byteLength;
|
|
143
|
+
const footerOff = len - FOOTER_BYTES;
|
|
144
|
+
const b = this._bytes;
|
|
145
|
+
if (b[footerOff + 8] !== 0x31 || b[footerOff + 9] !== 0x4B ||
|
|
146
|
+
b[footerOff + 10] !== 0x42 || b[footerOff + 11] !== 0x4C)
|
|
147
|
+
throw new ReaderError('R_BAD_FOOTER', 'footer magic_end at offset ' + (footerOff + 8) + ' is not 1KBL');
|
|
148
|
+
const footerLen = this._dv.getUint32(footerOff + 12, true);
|
|
149
|
+
if (footerLen < FOOTER_BYTES)
|
|
150
|
+
throw new ReaderError('R_BAD_FOOTER', 'footer_len ' + footerLen + ' is less than the minimum ' + FOOTER_BYTES);
|
|
151
|
+
if (footerLen > len - CONTAINER_HEADER_BYTES)
|
|
152
|
+
throw new ReaderError('R_BAD_FOOTER', 'footer_len ' + footerLen + ' exceeds the container body size');
|
|
89
153
|
}
|
|
90
154
|
|
|
91
155
|
_parseSchema() {
|
|
@@ -93,6 +157,7 @@ export class Reader {
|
|
|
93
157
|
if (off + 8 > this._buffer.byteLength) throw new ReaderError('R_TRUNCATED', 'schema header truncated');
|
|
94
158
|
const fieldCount = this._dv.getUint32(off + 0, true);
|
|
95
159
|
const rowStride = this._dv.getUint32(off + 4, true);
|
|
160
|
+
if (rowStride < 1) throw new ReaderError('R_INVALID', 'schema row_stride is 0');
|
|
96
161
|
const descriptorsOff = off + 8;
|
|
97
162
|
const descriptorsBytes = fieldCount * FIELD_DESCRIPTOR_BYTES;
|
|
98
163
|
if (descriptorsOff + descriptorsBytes + 4 > this._buffer.byteLength) {
|
|
@@ -114,11 +179,21 @@ export class Reader {
|
|
|
114
179
|
const offsetInRow = this._dv.getUint16(descOff + 2, true);
|
|
115
180
|
const laneKind = this._bytes[descOff + 4];
|
|
116
181
|
const flags = this._bytes[descOff + 5];
|
|
182
|
+
const reserved2 = this._dv.getUint16(descOff + 6, true);
|
|
117
183
|
const nameStrOff = Number(this._dv.getBigUint64(descOff + 8, true));
|
|
184
|
+
const reserved3 = this._dv.getBigUint64(descOff + 16, true);
|
|
118
185
|
|
|
119
186
|
if (flags !== 0) throw new ReaderError('R_BAD_FIELD_FLAGS', 'field ' + i + ' has non-zero flags (v2+ reserved)');
|
|
187
|
+
if (reserved2 !== 0) throw new ReaderError('R_RESERVED_NONZERO', 'field ' + i + ' reserved2 must be 0, got ' + reserved2);
|
|
188
|
+
if (reserved3 !== 0n) throw new ReaderError('R_RESERVED_NONZERO', 'field ' + i + ' reserved3 must be 0, got ' + reserved3);
|
|
120
189
|
if (laneKind !== LANE_F64 && laneKind !== LANE_U32)
|
|
121
190
|
throw new ReaderError('R_UNSUPPORTED_LANE', 'field ' + i + ' lane_kind=' + laneKind);
|
|
191
|
+
if (nameLen > 255) throw new ReaderError('R_INVALID', 'field ' + i + ' name_len ' + nameLen + ' exceeds 255');
|
|
192
|
+
if (offsetInRow + laneBytesOf(laneKind) > rowStride)
|
|
193
|
+
throw new ReaderError('R_INVALID', 'field ' + i + ' offset_in_row ' + offsetInRow +
|
|
194
|
+
' + lane bytes exceeds row_stride ' + rowStride);
|
|
195
|
+
if (nameStrOff + nameLen > nameBlobLen)
|
|
196
|
+
throw new ReaderError('R_TRUNCATED', 'field ' + i + ' name range past the name blob');
|
|
122
197
|
|
|
123
198
|
const nameBytes = this._bytes.subarray(nameBlobOff + nameStrOff, nameBlobOff + nameStrOff + nameLen);
|
|
124
199
|
const name = decoder.decode(nameBytes);
|
|
@@ -133,22 +208,35 @@ export class Reader {
|
|
|
133
208
|
const need = this._shardCount * SHARD_ENTRY_BYTES;
|
|
134
209
|
if (off + need > this._buffer.byteLength) throw new ReaderError('R_TRUNCATED', 'shard directory truncated');
|
|
135
210
|
const shards = new Array(this._shardCount);
|
|
211
|
+
const rowStride = this._schema.rowStride;
|
|
212
|
+
let rowSum = 0;
|
|
136
213
|
for (let i = 0; i < this._shardCount; i++) {
|
|
137
214
|
const entryOff = off + i * SHARD_ENTRY_BYTES;
|
|
138
215
|
const payloadOff = Number(this._dv.getBigUint64(entryOff + 0, true));
|
|
139
216
|
const payloadLen = this._dv.getUint32(entryOff + 8, true);
|
|
140
217
|
const rowCount = this._dv.getUint32(entryOff + 12, true);
|
|
141
218
|
const minReaderVer = this._dv.getUint16(entryOff + 16, true);
|
|
142
|
-
|
|
219
|
+
const shardFlags = this._dv.getUint16(entryOff + 18, true);
|
|
220
|
+
const shardReserved = this._dv.getUint32(entryOff + 20, true);
|
|
143
221
|
const localStrOff = Number(this._dv.getBigUint64(entryOff + 24, true));
|
|
144
222
|
const localStrLen = Number(this._dv.getBigUint64(entryOff + 32, true));
|
|
145
223
|
if (minReaderVer > READER_VERSION) {
|
|
146
224
|
throw new ReaderError('R_SHARD_VERSION_TOO_NEW',
|
|
147
225
|
'shard ' + i + ' requires reader version ' + minReaderVer);
|
|
148
226
|
}
|
|
227
|
+
if (shardFlags !== 0) throw new ReaderError('R_RESERVED_NONZERO', 'shard ' + i + ' flags must be 0, got ' + shardFlags);
|
|
228
|
+
if (shardReserved !== 0) throw new ReaderError('R_RESERVED_NONZERO', 'shard ' + i + ' reserved must be 0, got ' + shardReserved);
|
|
229
|
+
if (payloadOff < CONTAINER_HEADER_BYTES)
|
|
230
|
+
throw new ReaderError('R_INVALID', 'shard ' + i + ' payload_off ' + payloadOff + ' overlaps the header');
|
|
149
231
|
if (payloadOff + payloadLen > this._buffer.byteLength) {
|
|
150
232
|
throw new ReaderError('R_TRUNCATED', 'shard ' + i + ' payload truncated');
|
|
151
233
|
}
|
|
234
|
+
if (rowCount * rowStride > payloadLen)
|
|
235
|
+
throw new ReaderError('R_INVALID', 'shard ' + i + ' row_count ' + rowCount +
|
|
236
|
+
' * row_stride ' + rowStride + ' exceeds payload_len ' + payloadLen);
|
|
237
|
+
if (localStrLen === 0 && localStrOff !== 0)
|
|
238
|
+
throw new ReaderError('R_INVALID', 'shard ' + i + ' has local_string_len 0 but local_string_off ' + localStrOff);
|
|
239
|
+
rowSum += rowCount;
|
|
152
240
|
// Byte view over payload (no F64 view; row stride can be non-multiple-of-8 with U32 fields)
|
|
153
241
|
const payloadBytes = new Uint8Array(this._buffer, payloadOff, payloadLen);
|
|
154
242
|
const payloadDv = new DataView(this._buffer, payloadOff, payloadLen);
|
|
@@ -158,10 +246,13 @@ export class Reader {
|
|
|
158
246
|
if (localStrOff + localStrLen > this._buffer.byteLength) {
|
|
159
247
|
throw new ReaderError('R_TRUNCATED', 'shard ' + i + ' string table truncated');
|
|
160
248
|
}
|
|
249
|
+
validateStringTable(this._bytes, localStrOff, localStrLen, 'shard ' + i);
|
|
161
250
|
stringTable = StringTable.parse(new Uint8Array(this._buffer, localStrOff, localStrLen), 0);
|
|
162
251
|
}
|
|
163
252
|
shards[i] = { payloadOff, payloadLen, rowCount, payloadBytes, payloadDv, stringTable };
|
|
164
253
|
}
|
|
254
|
+
if (rowSum !== this._totalRows)
|
|
255
|
+
throw new ReaderError('R_INVALID', 'shard row_count sum ' + rowSum + ' != header total_rows ' + this._totalRows);
|
|
165
256
|
this._shards = shards;
|
|
166
257
|
}
|
|
167
258
|
|
|
@@ -178,33 +269,38 @@ export class Reader {
|
|
|
178
269
|
// _zoneMapsMins/Maxes: Float64Array -- length shardCount * T, row-major
|
|
179
270
|
_parseZoneMaps() {
|
|
180
271
|
this._zoneMapsTrackedFields = null;
|
|
181
|
-
if (this._metadataOff === 0) return;
|
|
182
|
-
if (this._metadataOff + 16 > this._buffer.byteLength) return;
|
|
272
|
+
if (this._metadataOff === 0) return; // legal absence
|
|
183
273
|
const off = this._metadataOff;
|
|
184
|
-
//
|
|
274
|
+
// D1: a non-zero metadata_off is a producer assertion that a parseable
|
|
275
|
+
// segment lives here. Any way it fails to parse is corruption, not absence.
|
|
276
|
+
if (off + 16 > this._buffer.byteLength)
|
|
277
|
+
throw new ReaderError('R_BAD_METADATA', 'zone maps segment header at ' + off + ' runs past the buffer');
|
|
185
278
|
if (this._bytes[off] !== 0x30 || this._bytes[off + 1] !== 0x5A ||
|
|
186
|
-
this._bytes[off + 2] !== 0x4D || this._bytes[off + 3] !== 0x31)
|
|
279
|
+
this._bytes[off + 2] !== 0x4D || this._bytes[off + 3] !== 0x31)
|
|
280
|
+
throw new ReaderError('R_BAD_METADATA', 'zone maps magic at ' + off + ' is not ZM01');
|
|
187
281
|
const shardCount = this._dv.getUint32(off + 4, true);
|
|
188
282
|
const T = this._dv.getUint32(off + 8, true);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (
|
|
283
|
+
const reserved0 = this._dv.getUint32(off + 12, true);
|
|
284
|
+
if (shardCount !== this._shardCount)
|
|
285
|
+
throw new ReaderError('R_BAD_METADATA', 'zone maps shard_count ' + shardCount + ' != header ' + this._shardCount);
|
|
286
|
+
if (reserved0 !== 0)
|
|
287
|
+
throw new ReaderError('R_BAD_METADATA', 'zone maps reserved0 must be 0, got ' + reserved0);
|
|
288
|
+
if (T === 0) return; // a well-formed segment tracking nothing: hasZoneMaps false
|
|
193
289
|
const fieldTableOff = off + 16;
|
|
194
290
|
const fieldTablePad = (8 - ((T * 2) & 7)) & 7;
|
|
195
291
|
const minsOff = fieldTableOff + T * 2 + fieldTablePad;
|
|
196
292
|
const maxesOff = minsOff + shardCount * T * 8;
|
|
197
293
|
const requiredEnd = maxesOff + shardCount * T * 8;
|
|
198
|
-
if (requiredEnd > this._buffer.byteLength)
|
|
199
|
-
throw new ReaderError('
|
|
200
|
-
|
|
294
|
+
if (requiredEnd > this._buffer.byteLength)
|
|
295
|
+
throw new ReaderError('R_BAD_METADATA', 'zone maps segment runs past the buffer');
|
|
296
|
+
if ((minsOff & 7) !== 0)
|
|
297
|
+
throw new ReaderError('R_BAD_METADATA', 'zone maps mins offset ' + minsOff + ' is not 8-byte aligned');
|
|
201
298
|
const tracked = new Array(T);
|
|
202
299
|
const fieldToPos = new Map();
|
|
203
300
|
for (let t = 0; t < T; t++) {
|
|
204
301
|
const schemaFieldIdx = this._dv.getUint16(fieldTableOff + t * 2, true);
|
|
205
|
-
if (schemaFieldIdx >= this._schema.fields.length)
|
|
206
|
-
throw new ReaderError('
|
|
207
|
-
}
|
|
302
|
+
if (schemaFieldIdx >= this._schema.fields.length)
|
|
303
|
+
throw new ReaderError('R_BAD_METADATA', 'zone maps references field index ' + schemaFieldIdx + ' past the schema');
|
|
208
304
|
tracked[t] = schemaFieldIdx;
|
|
209
305
|
fieldToPos.set(this._schema.fields[schemaFieldIdx].name, t);
|
|
210
306
|
}
|
|
@@ -259,6 +355,8 @@ export class Reader {
|
|
|
259
355
|
// - the field is not tracked (currently: U32/string fields are never tracked in v1).
|
|
260
356
|
// For empty ranges (a shard where the field defaulted to 0), returns {min:0, max:0}.
|
|
261
357
|
shardBounds(shardIdx, fieldName) {
|
|
358
|
+
if (!this._fieldIndex.has(fieldName))
|
|
359
|
+
throw new ReaderError('R_UNKNOWN_FIELD', 'no field named ' + JSON.stringify(fieldName));
|
|
262
360
|
if (!this._zoneMapsTrackedFields) return null;
|
|
263
361
|
const t = this._zoneMapsFieldToPos.get(fieldName);
|
|
264
362
|
if (t === undefined) return null;
|
|
@@ -273,6 +371,8 @@ export class Reader {
|
|
|
273
371
|
// Semantics: a shard is INCLUDED iff its [smin, smax] overlaps [min, max].
|
|
274
372
|
// A pass-through range ({min: -Inf, max: +Inf} or omitted) returns all shards.
|
|
275
373
|
findShards(fieldName, opts) {
|
|
374
|
+
if (!this._fieldIndex.has(fieldName))
|
|
375
|
+
throw new ReaderError('R_UNKNOWN_FIELD', 'no field named ' + JSON.stringify(fieldName));
|
|
276
376
|
const all = () => { const a = new Array(this._shardCount); for (let i = 0; i < this._shardCount; i++) a[i] = i; return a; };
|
|
277
377
|
if (!this._zoneMapsTrackedFields) return all();
|
|
278
378
|
const t = this._zoneMapsFieldToPos.get(fieldName);
|
package/src/Split.js
CHANGED
|
@@ -35,8 +35,9 @@ import { Tokenizer, TokenizerError } from './Tokenizer.js';
|
|
|
35
35
|
import { Writer, WriterError } from './Writer.js';
|
|
36
36
|
import { Reader, ReaderError } from './Reader.js';
|
|
37
37
|
import { StringTable } from './StringTable.js';
|
|
38
|
+
import { checkOpts } from './Opts.js';
|
|
38
39
|
|
|
39
|
-
export const VERSION = '1.
|
|
40
|
+
export const VERSION = '1.3.0';
|
|
40
41
|
|
|
41
42
|
const LF = 0x0A;
|
|
42
43
|
const CONTAINER_HEADER_BYTES = 48;
|
|
@@ -44,10 +45,28 @@ const SHARD_ENTRY_BYTES = 40;
|
|
|
44
45
|
const FIELD_DESCRIPTOR_BYTES = 24;
|
|
45
46
|
const FOOTER_BYTES = 16;
|
|
46
47
|
const LANE_F64 = 1;
|
|
48
|
+
const LANE_U32 = 3;
|
|
49
|
+
const U32_MAX = 4294967295;
|
|
50
|
+
|
|
51
|
+
const SPLIT_NDJSON_OPTS = {
|
|
52
|
+
targetParts: { t: 'int', min: 1, max: U32_MAX },
|
|
53
|
+
maxPartBytes: { t: 'int', min: 1, max: U32_MAX, inf: true },
|
|
54
|
+
};
|
|
55
|
+
const COMPILE_PART_OPTS = {
|
|
56
|
+
framing: { t: 'enum', values: ['auto', 'array', 'ndjson'] },
|
|
57
|
+
writer: { t: 'obj' },
|
|
58
|
+
};
|
|
59
|
+
const COMPILE_IN_PARTS_OPTS = {
|
|
60
|
+
targetParts: { t: 'int', min: 1, max: U32_MAX },
|
|
61
|
+
maxPartBytes: { t: 'int', min: 1, max: U32_MAX, inf: true },
|
|
62
|
+
framing: { t: 'enum', values: ['auto', 'array', 'ndjson'] },
|
|
63
|
+
writer: { t: 'obj' },
|
|
64
|
+
};
|
|
47
65
|
|
|
48
66
|
export class SplitError extends Error {
|
|
49
67
|
constructor(code, msg) { super(msg); this.code = code; this.name = 'SplitError'; }
|
|
50
68
|
}
|
|
69
|
+
function raiseSplit(code, msg) { throw new SplitError(code, msg); }
|
|
51
70
|
|
|
52
71
|
// ---------- splitNDJSON ----------
|
|
53
72
|
|
|
@@ -67,8 +86,9 @@ export function splitNDJSON(bytes, opts) {
|
|
|
67
86
|
throw new TypeError('splitNDJSON: expected Uint8Array');
|
|
68
87
|
}
|
|
69
88
|
opts = opts || {};
|
|
70
|
-
|
|
71
|
-
const
|
|
89
|
+
checkOpts('splitNDJSON', opts, SPLIT_NDJSON_OPTS, raiseSplit);
|
|
90
|
+
const targetParts = Math.max(1, opts.targetParts !== undefined ? opts.targetParts : 4);
|
|
91
|
+
const maxPartBytes = opts.maxPartBytes !== undefined ? opts.maxPartBytes : Infinity;
|
|
72
92
|
const total = bytes.length;
|
|
73
93
|
if (total === 0) return [];
|
|
74
94
|
|
|
@@ -110,8 +130,9 @@ export function compilePart(bytes, opts) {
|
|
|
110
130
|
throw new TypeError('compilePart: expected Uint8Array');
|
|
111
131
|
}
|
|
112
132
|
opts = opts || {};
|
|
113
|
-
|
|
114
|
-
const
|
|
133
|
+
checkOpts('compilePart', opts, COMPILE_PART_OPTS, raiseSplit);
|
|
134
|
+
const framing = opts.framing !== undefined ? opts.framing : 'ndjson';
|
|
135
|
+
const w = new Writer(opts.writer !== undefined ? opts.writer : {});
|
|
115
136
|
const t = new Tokenizer(w, { framing });
|
|
116
137
|
t.feed(bytes);
|
|
117
138
|
t.end();
|
|
@@ -126,13 +147,14 @@ export function compilePart(bytes, opts) {
|
|
|
126
147
|
// aren't available or for testing.
|
|
127
148
|
export function compileInParts(bytes, opts) {
|
|
128
149
|
opts = opts || {};
|
|
150
|
+
checkOpts('compileInParts', opts, COMPILE_IN_PARTS_OPTS, raiseSplit);
|
|
129
151
|
const splitOpts = {
|
|
130
152
|
targetParts: opts.targetParts,
|
|
131
153
|
maxPartBytes: opts.maxPartBytes,
|
|
132
154
|
};
|
|
133
155
|
const partOpts = {
|
|
134
|
-
framing: opts.framing
|
|
135
|
-
writer: opts.writer
|
|
156
|
+
framing: opts.framing !== undefined ? opts.framing : 'ndjson',
|
|
157
|
+
writer: opts.writer !== undefined ? opts.writer : {},
|
|
136
158
|
};
|
|
137
159
|
const ranges = splitNDJSON(bytes, splitOpts);
|
|
138
160
|
const containers = new Array(ranges.length);
|
|
@@ -180,6 +202,16 @@ export function mergeContainers(containers) {
|
|
|
180
202
|
}
|
|
181
203
|
}
|
|
182
204
|
|
|
205
|
+
// BS-15 conformance: a merged container whose schema has no U32 lane must
|
|
206
|
+
// emit NO local string tables (local_string_off/len = 0), regardless of what
|
|
207
|
+
// the source containers carry. A pre-M2 F64-only input holds a stale 16-byte
|
|
208
|
+
// empty table; without this, merge would launder it into the output and the
|
|
209
|
+
// t8 spec-checker would reject the result. Computed once from the shared schema.
|
|
210
|
+
let mergedHasU32 = false;
|
|
211
|
+
for (let i = 0; i < schema0.fields.length; i++) {
|
|
212
|
+
if (schema0.fields[i].laneKind === LANE_U32) { mergedHasU32 = true; break; }
|
|
213
|
+
}
|
|
214
|
+
|
|
183
215
|
// Total shards and rows
|
|
184
216
|
let totalShards = 0, totalRows = 0;
|
|
185
217
|
for (const r of readers) { totalShards += r.shardCount; totalRows += r.totalRows; }
|
|
@@ -225,7 +257,7 @@ export function mergeContainers(containers) {
|
|
|
225
257
|
// Source shard payload lives at shard.payloadOff in the source container.
|
|
226
258
|
const payloadLen = shard.payloadLen;
|
|
227
259
|
const payloadPad = (8 - (payloadLen & 7)) & 7;
|
|
228
|
-
const stringLen = shard.stringTable ? _sourceStringTableLen(readers[ci], si) : 0;
|
|
260
|
+
const stringLen = (mergedHasU32 && shard.stringTable) ? _sourceStringTableLen(readers[ci], si) : 0;
|
|
229
261
|
const outPayloadOff = cursor;
|
|
230
262
|
const outStringOff = outPayloadOff + payloadLen + payloadPad;
|
|
231
263
|
shardOutMap.push({
|
package/src/StringTable.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// and at every reset(), so an absent U32 row cell (which is 0) decodes as ""
|
|
21
21
|
// rather than aliasing the shard's first-interned string (SPEC 3.3, SPEC 7).
|
|
22
22
|
|
|
23
|
-
export const VERSION = '1.
|
|
23
|
+
export const VERSION = '1.3.0';
|
|
24
24
|
|
|
25
25
|
const EMPTY_SLOT = 0xFFFFFFFF; // MUST be unsigned; typed-array reads are unsigned
|
|
26
26
|
const INITIAL_BLOB_BYTES = 64 * 1024;
|
package/src/Tokenizer.js
CHANGED
|
@@ -16,8 +16,22 @@
|
|
|
16
16
|
// E_KEYWORD_MISMATCH - true/false/null spelled wrong
|
|
17
17
|
// E_TRAILING_INPUT - non-whitespace bytes after top-level value(s) (array mode only)
|
|
18
18
|
// E_DEPTH_LIMIT - nesting depth exceeded MAX_DEPTH
|
|
19
|
+
// E_STRING_TOO_LONG - string exceeds maxStringBytes cap
|
|
20
|
+
// E_ENDED - feed()/end() after end()
|
|
21
|
+
// E_POISONED - feed()/end() after a thrown error
|
|
22
|
+
// E_UNKNOWN_OPTION - unknown constructor option key
|
|
23
|
+
// E_OPTION_VALUE - constructor option value out of domain
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
import { checkOpts } from './Opts.js';
|
|
26
|
+
|
|
27
|
+
export const VERSION = '1.3.0';
|
|
28
|
+
|
|
29
|
+
const U32_MAX = 4294967295;
|
|
30
|
+
const TOKENIZER_OPTS = {
|
|
31
|
+
framing: { t: 'enum', values: ['auto', 'array', 'ndjson'] },
|
|
32
|
+
maxStringBytes: { t: 'int', min: 1, max: U32_MAX },
|
|
33
|
+
};
|
|
34
|
+
function raiseTok(code, msg) { throw new TokenizerError(code, 0, msg); }
|
|
21
35
|
|
|
22
36
|
// ---------- byte constants ----------
|
|
23
37
|
const B_SPACE = 0x20, B_TAB = 0x09, B_LF = 0x0A, B_CR = 0x0D;
|
|
@@ -104,24 +118,33 @@ const NOOP = () => {};
|
|
|
104
118
|
export class Tokenizer {
|
|
105
119
|
constructor(sink, opts) {
|
|
106
120
|
if (!sink) throw new Error('sink required');
|
|
121
|
+
checkOpts('Tokenizer', opts, TOKENIZER_OPTS, raiseTok);
|
|
107
122
|
this.sink = sink;
|
|
108
123
|
|
|
109
124
|
// top-level framing: 'auto' (default), 'array', or 'ndjson'
|
|
110
|
-
this._framing = (opts && opts.framing)
|
|
111
|
-
this._maxStringBytes = (opts && opts.maxStringBytes)
|
|
112
|
-
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
this._framing = (opts && opts.framing !== undefined) ? opts.framing : 'auto';
|
|
126
|
+
this._maxStringBytes = (opts && opts.maxStringBytes !== undefined) ? opts.maxStringBytes : (1 << 20); // 1 MB default cap
|
|
127
|
+
|
|
128
|
+
// Per-instance dispatch fields: bind each present sink method to the sink
|
|
129
|
+
// once at construction (cold path), defaulting to NOOP. The caller's sink
|
|
130
|
+
// object is never mutated (BS-19), so a frozen sink constructs and drives
|
|
131
|
+
// fine. Binding preserves the sink as receiver while the hot call sites read
|
|
132
|
+
// a single monomorphic field (this._onX) instead of this.sink.onX.
|
|
133
|
+
this._onStartObject = sink.onStartObject ? sink.onStartObject.bind(sink) : NOOP;
|
|
134
|
+
this._onEndObject = sink.onEndObject ? sink.onEndObject.bind(sink) : NOOP;
|
|
135
|
+
this._onStartArray = sink.onStartArray ? sink.onStartArray.bind(sink) : NOOP;
|
|
136
|
+
this._onEndArray = sink.onEndArray ? sink.onEndArray.bind(sink) : NOOP;
|
|
137
|
+
this._onKey = sink.onKey ? sink.onKey.bind(sink) : NOOP;
|
|
138
|
+
this._onString = sink.onString ? sink.onString.bind(sink) : NOOP;
|
|
139
|
+
this._onNumber = sink.onNumber ? sink.onNumber.bind(sink) : NOOP;
|
|
140
|
+
this._onTrue = sink.onTrue ? sink.onTrue.bind(sink) : NOOP;
|
|
141
|
+
this._onFalse = sink.onFalse ? sink.onFalse.bind(sink) : NOOP;
|
|
142
|
+
this._onNull = sink.onNull ? sink.onNull.bind(sink) : NOOP;
|
|
143
|
+
this._onEnd = sink.onEnd ? sink.onEnd.bind(sink) : NOOP;
|
|
144
|
+
|
|
145
|
+
// Terminal state (BS-14): once ended or poisoned, the instance is dead.
|
|
146
|
+
this._ended = false;
|
|
147
|
+
this._poisoned = false;
|
|
125
148
|
|
|
126
149
|
// parser state
|
|
127
150
|
this._state = S_TOP;
|
|
@@ -158,6 +181,12 @@ export class Tokenizer {
|
|
|
158
181
|
|
|
159
182
|
// Feed a chunk. Bytes are consumed synchronously; sink events fire during this call.
|
|
160
183
|
feed(chunk) {
|
|
184
|
+
if (this._poisoned) throw new TokenizerError('E_POISONED', this._absOffset, 'tokenizer poisoned by a previous error or an in-flight feed(); construct a new instance');
|
|
185
|
+
if (this._ended) throw new TokenizerError('E_ENDED', this._absOffset, 'tokenizer already ended; construct a new instance');
|
|
186
|
+
// D3 armed-flag: a sink throw mid-feed unwinds through here with the flag
|
|
187
|
+
// set, poisoning the instance. Cleared on the successful fall-through below.
|
|
188
|
+
// Two boolean stores per chunk; the byte loop is untouched.
|
|
189
|
+
this._poisoned = true;
|
|
161
190
|
const len = chunk.length;
|
|
162
191
|
let i = 0;
|
|
163
192
|
while (i < len) {
|
|
@@ -306,9 +335,9 @@ export class Tokenizer {
|
|
|
306
335
|
this._kwPos++;
|
|
307
336
|
i++; this._absOffset++;
|
|
308
337
|
if (this._kwPos === tmpl.length) {
|
|
309
|
-
if (this._kwId === KW_ID_TRUE) this.
|
|
310
|
-
else if (this._kwId === KW_ID_FALSE) this.
|
|
311
|
-
else this.
|
|
338
|
+
if (this._kwId === KW_ID_TRUE) this._onTrue();
|
|
339
|
+
else if (this._kwId === KW_ID_FALSE) this._onFalse();
|
|
340
|
+
else this._onNull();
|
|
312
341
|
this._afterValue();
|
|
313
342
|
}
|
|
314
343
|
continue;
|
|
@@ -317,9 +346,13 @@ export class Tokenizer {
|
|
|
317
346
|
// unreachable
|
|
318
347
|
this._err('E_UNEXPECTED_BYTE', 'internal: unknown state ' + st);
|
|
319
348
|
}
|
|
349
|
+
this._poisoned = false; // D3: clean fall-through un-arms the flag
|
|
320
350
|
}
|
|
321
351
|
|
|
322
352
|
end() {
|
|
353
|
+
if (this._poisoned) throw new TokenizerError('E_POISONED', this._absOffset, 'tokenizer poisoned by a previous error or an in-flight feed(); construct a new instance');
|
|
354
|
+
if (this._ended) throw new TokenizerError('E_ENDED', this._absOffset, 'tokenizer already ended; construct a new instance');
|
|
355
|
+
this._poisoned = true; // D3: armed until a clean exit below
|
|
323
356
|
// A trailing number may still be pending
|
|
324
357
|
if (this._state === S_NUMBER) {
|
|
325
358
|
this._emitNumber();
|
|
@@ -328,7 +361,9 @@ export class Tokenizer {
|
|
|
328
361
|
if (this._arrayMode && this._arrayModeOuterOpen) {
|
|
329
362
|
this._err('E_UNEXPECTED_EOF', 'unclosed top-level array');
|
|
330
363
|
}
|
|
331
|
-
this.
|
|
364
|
+
this._ended = true;
|
|
365
|
+
this._onEnd();
|
|
366
|
+
this._poisoned = false; // clear AFTER _onEnd(): a throwing onEnd stays poisoned
|
|
332
367
|
return;
|
|
333
368
|
}
|
|
334
369
|
this._err('E_UNEXPECTED_EOF', 'input ended mid-token (state=' + this._state + ')');
|
|
@@ -378,7 +413,7 @@ export class Tokenizer {
|
|
|
378
413
|
|
|
379
414
|
_handleObjectStartByte(b) {
|
|
380
415
|
if (b === B_RBRACE) {
|
|
381
|
-
this.
|
|
416
|
+
this._onEndObject();
|
|
382
417
|
this._popContainer();
|
|
383
418
|
return;
|
|
384
419
|
}
|
|
@@ -389,7 +424,7 @@ export class Tokenizer {
|
|
|
389
424
|
_handleObjectValueEndByte(b) {
|
|
390
425
|
if (b === B_COMMA) { this._state = S_OBJECT_NEXT_KEY; }
|
|
391
426
|
else if (b === B_RBRACE) {
|
|
392
|
-
this.
|
|
427
|
+
this._onEndObject();
|
|
393
428
|
this._popContainer();
|
|
394
429
|
} else {
|
|
395
430
|
this._err('E_UNEXPECTED_BYTE', 'expected , or }');
|
|
@@ -404,7 +439,7 @@ export class Tokenizer {
|
|
|
404
439
|
this._state = S_TOP;
|
|
405
440
|
return;
|
|
406
441
|
}
|
|
407
|
-
this.
|
|
442
|
+
this._onEndArray();
|
|
408
443
|
this._popContainer();
|
|
409
444
|
return;
|
|
410
445
|
}
|
|
@@ -419,7 +454,7 @@ export class Tokenizer {
|
|
|
419
454
|
this._state = S_TOP;
|
|
420
455
|
return;
|
|
421
456
|
}
|
|
422
|
-
this.
|
|
457
|
+
this._onEndArray();
|
|
423
458
|
this._popContainer();
|
|
424
459
|
} else {
|
|
425
460
|
this._err('E_UNEXPECTED_BYTE', 'expected , or ]');
|
|
@@ -430,13 +465,13 @@ export class Tokenizer {
|
|
|
430
465
|
|
|
431
466
|
_beginValue(b) {
|
|
432
467
|
if (b === B_LBRACE) {
|
|
433
|
-
this.
|
|
468
|
+
this._onStartObject();
|
|
434
469
|
this._pushContainer(C_OBJECT);
|
|
435
470
|
this._state = S_OBJECT_START;
|
|
436
471
|
return;
|
|
437
472
|
}
|
|
438
473
|
if (b === B_LBRACKET) {
|
|
439
|
-
this.
|
|
474
|
+
this._onStartArray();
|
|
440
475
|
this._pushContainer(C_ARRAY);
|
|
441
476
|
this._state = S_ARRAY_START;
|
|
442
477
|
return;
|
|
@@ -578,16 +613,17 @@ export class Tokenizer {
|
|
|
578
613
|
v = this._numSign * v;
|
|
579
614
|
}
|
|
580
615
|
if (!Number.isFinite(v)) this._err('E_NUMBER_OVERFLOW', 'number exceeds F64 range');
|
|
581
|
-
this.
|
|
616
|
+
this._onNumber(v);
|
|
582
617
|
this._afterValue();
|
|
583
618
|
}
|
|
584
619
|
|
|
585
620
|
_emitString() {
|
|
621
|
+
if (this._strLen > this._maxStringBytes) this._err('E_STRING_TOO_LONG', 'string exceeds maxStringBytes cap (' + this._strLen + ' > ' + this._maxStringBytes + ')');
|
|
586
622
|
if (this._strIsKey) {
|
|
587
|
-
this.
|
|
623
|
+
this._onKey(this._strBuf, 0, this._strLen);
|
|
588
624
|
this._state = S_OBJECT_KEY_END;
|
|
589
625
|
} else {
|
|
590
|
-
this.
|
|
626
|
+
this._onString(this._strBuf, 0, this._strLen);
|
|
591
627
|
this._afterValue();
|
|
592
628
|
}
|
|
593
629
|
}
|
|
@@ -636,6 +672,7 @@ export class Tokenizer {
|
|
|
636
672
|
|
|
637
673
|
_appendStrRange(chunk, from, to) {
|
|
638
674
|
const need = to - from;
|
|
675
|
+
if (this._strLen + need > this._maxStringBytes) this._err('E_STRING_TOO_LONG', 'string exceeds maxStringBytes cap (' + (this._strLen + need) + ' > ' + this._maxStringBytes + ')');
|
|
639
676
|
if (this._strLen + need > this._strBuf.length) this._growStrBuf(need);
|
|
640
677
|
// Manual copy loop — Uint8Array.set(source) via subarray allocates a small
|
|
641
678
|
// view header per call, which turns into MB-scale GC pressure across a
|
|
@@ -668,9 +705,9 @@ export class Tokenizer {
|
|
|
668
705
|
}
|
|
669
706
|
|
|
670
707
|
_growStrBuf(need) {
|
|
708
|
+
if (this._strLen + need > this._maxStringBytes) this._err('E_STRING_TOO_LONG', 'string exceeds maxStringBytes cap (' + (this._strLen + need) + ' > ' + this._maxStringBytes + ')');
|
|
671
709
|
let cap = this._strBuf.length;
|
|
672
710
|
while (cap < this._strLen + need) cap *= 2;
|
|
673
|
-
if (cap > this._maxStringBytes) this._err('E_UNEXPECTED_BYTE', 'string exceeds maxStringBytes cap');
|
|
674
711
|
const nb = new Uint8Array(cap);
|
|
675
712
|
nb.set(this._strBuf);
|
|
676
713
|
this._strBuf = nb;
|
|
@@ -684,6 +721,7 @@ export class Tokenizer {
|
|
|
684
721
|
}
|
|
685
722
|
|
|
686
723
|
_err(code, msg) {
|
|
724
|
+
this._poisoned = true;
|
|
687
725
|
throw new TokenizerError(code, this._absOffset, msg);
|
|
688
726
|
}
|
|
689
727
|
}
|
package/src/Views.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// @zakkster/lite-bake-stream / Views (internal)
|
|
2
|
+
// Copyright (c) 2026 Zahary Shinikchiev. MIT.
|
|
3
|
+
//
|
|
4
|
+
// One shared helper so every container entry point (deserialize,
|
|
5
|
+
// Reader.fromBuffer, PreserveReader.fromBuffer) resolves caller-supplied bytes
|
|
6
|
+
// to an ArrayBuffer by exactly the SAME rule (BS-09). A Uint8Array that is a
|
|
7
|
+
// full-buffer view is unwrapped in place (zero copy); a partial view (a pooled
|
|
8
|
+
// Buffer with byteOffset > 0, or a subarray) is COPIED so the reader never sees
|
|
9
|
+
// bytes outside the caller's window. Anything else fails closed.
|
|
10
|
+
//
|
|
11
|
+
// Not a public export. Internal to the package.
|
|
12
|
+
|
|
13
|
+
export function toContainerBuffer(input, label) {
|
|
14
|
+
if (input instanceof ArrayBuffer) return input;
|
|
15
|
+
if (input instanceof Uint8Array) {
|
|
16
|
+
if (input.byteOffset === 0 && input.byteLength === input.buffer.byteLength) {
|
|
17
|
+
return input.buffer;
|
|
18
|
+
}
|
|
19
|
+
const copy = new Uint8Array(input.byteLength);
|
|
20
|
+
copy.set(input);
|
|
21
|
+
return copy.buffer;
|
|
22
|
+
}
|
|
23
|
+
throw new TypeError(label + ': expected ArrayBuffer or Uint8Array, got ' +
|
|
24
|
+
(input === null ? 'null' : typeof input));
|
|
25
|
+
}
|