@zakkster/lite-bake-stream 1.4.1 → 1.6.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 +70 -0
- package/README.md +18 -1
- package/SPEC.md +3 -1
- package/llms.txt +21 -2
- package/package.json +13 -1
- package/src/Crc32c.js +96 -0
- package/src/FileIngest.js +1 -1
- package/src/MultiReader.js +5 -2
- package/src/PreserveReader.js +51 -13
- package/src/PreserveTokenizer.js +1 -1
- package/src/PreserveWriter.js +248 -67
- package/src/RangeReader.js +82 -21
- package/src/Reader.js +77 -13
- package/src/Split.js +24 -4
- package/src/StringTable.js +62 -6
- package/src/Tokenizer.js +1 -1
- package/src/Views.js +62 -0
- package/src/Writer.js +496 -149
- package/src/index.js +6 -6
- package/types/PreserveReader.d.ts +10 -2
- package/types/PreserveWriter.d.ts +30 -0
- package/types/RangeReader.d.ts +5 -0
- package/types/Reader.d.ts +10 -2
- package/types/StringTable.d.ts +5 -0
- package/types/Writer.d.ts +37 -0
- package/types/index.d.ts +3 -2
package/src/Reader.js
CHANGED
|
@@ -21,16 +21,31 @@
|
|
|
21
21
|
// R_BAD_METADATA - metadata_off is non-zero but the zone-map segment is unparseable
|
|
22
22
|
// R_INVALID - a structure is internally inconsistent but in-bounds
|
|
23
23
|
// R_UNKNOWN_FIELD - get()/findShards()/shardBounds() called with an unknown field name
|
|
24
|
+
// R_OFFSET_TOO_LARGE - a u64 header/schema/directory offset exceeds 2^53-1
|
|
25
|
+
// (Number() would lose precision; fail closed, BS-05)
|
|
26
|
+
// R_ROW_OUT_OF_RANGE - get(rowIdx) with rowIdx negative, fractional, NaN,
|
|
27
|
+
// or >= totalRows (BS-32)
|
|
28
|
+
//
|
|
29
|
+
// Row-index policy (BS-32): get() range-checks rowIdx; the SHARD-index escape
|
|
30
|
+
// hatches (shardPayload, shardF64, shardStringTable) do NOT -- their contract is
|
|
31
|
+
// raw indexed access with no per-call checking. shardBounds returns null for an
|
|
32
|
+
// out-of-range shard by decided policy.
|
|
24
33
|
|
|
25
34
|
import { StringTable } from './StringTable.js';
|
|
26
35
|
import { toContainerBuffer } from './Views.js';
|
|
36
|
+
import { checkOpts } from './Opts.js';
|
|
37
|
+
import { crc32cInit, crc32cUpdate, crc32cFinal } from './Crc32c.js';
|
|
27
38
|
|
|
28
|
-
export const VERSION = '1.
|
|
39
|
+
export const VERSION = '1.6.0';
|
|
29
40
|
|
|
30
41
|
const CONTAINER_HEADER_BYTES = 48;
|
|
31
42
|
const SHARD_ENTRY_BYTES = 40;
|
|
32
43
|
const FIELD_DESCRIPTOR_BYTES = 24;
|
|
33
44
|
const FOOTER_BYTES = 16;
|
|
45
|
+
const CRC_ABSENT = 0xFFFFFFFF;
|
|
46
|
+
|
|
47
|
+
const READER_OPTS = { verifyCrc: { t: 'bool' } };
|
|
48
|
+
function raiseReaderOpt(code, msg) { throw new ReaderError(code, msg); }
|
|
34
49
|
|
|
35
50
|
const LANE_F64 = 1;
|
|
36
51
|
const LANE_U32 = 3;
|
|
@@ -42,6 +57,19 @@ export class ReaderError extends Error {
|
|
|
42
57
|
constructor(code, msg) { super(msg); this.code = code; this.name = 'ReaderError'; }
|
|
43
58
|
}
|
|
44
59
|
|
|
60
|
+
// Read a u64 header/directory/schema field and narrow it to a JS number, failing
|
|
61
|
+
// closed if it exceeds Number.MAX_SAFE_INTEGER (2^53-1). Past that a Number()
|
|
62
|
+
// cast loses precision and every downstream bounds check reads a corrupted
|
|
63
|
+
// offset -- an in-bounds lie is worse than a loud refusal (BS-05). Cold: called
|
|
64
|
+
// only during header/schema/directory parse, never per row.
|
|
65
|
+
function u64(dv, off, what) {
|
|
66
|
+
const v = dv.getBigUint64(off, true);
|
|
67
|
+
if (v > 9007199254740991n)
|
|
68
|
+
throw new ReaderError('R_OFFSET_TOO_LARGE',
|
|
69
|
+
what + ' value ' + v + ' exceeds the safe-integer ceiling 9007199254740991');
|
|
70
|
+
return Number(v);
|
|
71
|
+
}
|
|
72
|
+
|
|
45
73
|
// Validate a local string table's shape BEFORE StringTable.parse casts a
|
|
46
74
|
// Uint32Array over it (T-1..T-4): the offsets array must be in bounds, the
|
|
47
75
|
// count/blob must fit, offsets must be non-decreasing, and the trailing
|
|
@@ -71,11 +99,12 @@ function validateStringTable(bytes, off, len, label) {
|
|
|
71
99
|
}
|
|
72
100
|
|
|
73
101
|
export class Reader {
|
|
74
|
-
static fromBuffer(input) {
|
|
75
|
-
return new Reader(toContainerBuffer(input, 'Reader.fromBuffer'));
|
|
102
|
+
static fromBuffer(input, opts) {
|
|
103
|
+
return new Reader(toContainerBuffer(input, 'Reader.fromBuffer'), opts);
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
constructor(buffer) {
|
|
106
|
+
constructor(buffer, opts) {
|
|
107
|
+
checkOpts('Reader', opts, READER_OPTS, raiseReaderOpt);
|
|
79
108
|
this._buffer = buffer;
|
|
80
109
|
this._dv = new DataView(buffer);
|
|
81
110
|
this._bytes = new Uint8Array(buffer);
|
|
@@ -85,6 +114,27 @@ export class Reader {
|
|
|
85
114
|
this._parseShardDirectory();
|
|
86
115
|
this._parseZoneMaps(); // M7 -- no-op if metadata_off is 0
|
|
87
116
|
this._buildFieldIndex();
|
|
117
|
+
// Optional open-time verification (fail closed on BOTH mismatch AND absence:
|
|
118
|
+
// the caller demanded verification, so an unverifiable container is an
|
|
119
|
+
// unverified state -- null is not zero).
|
|
120
|
+
if (opts && opts.verifyCrc === true) {
|
|
121
|
+
const status = this.verifyCrc();
|
|
122
|
+
if (status === 'absent')
|
|
123
|
+
throw new ReaderError('R_CRC_ABSENT', 'verifyCrc:true but the container carries no CRC (footer CRC is absent, 0xFFFFFFFF)');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// SPEC 3.7 integrity: recompute CRC-32C over [0, footer_off) and compare to the
|
|
128
|
+
// stored footer CRC. Returns 'ok' when they match, 'absent' when the container
|
|
129
|
+
// carries no CRC (footer CRC == 0xFFFFFFFF). A MISMATCH throws R_BAD_CRC.
|
|
130
|
+
verifyCrc() {
|
|
131
|
+
const footerOff = this._buffer.byteLength - FOOTER_BYTES;
|
|
132
|
+
const stored = this._dv.getUint32(footerOff, true) >>> 0;
|
|
133
|
+
if (stored === CRC_ABSENT) return 'absent';
|
|
134
|
+
const actual = crc32cFinal(crc32cUpdate(crc32cInit(), this._bytes, 0, footerOff));
|
|
135
|
+
if (actual !== stored)
|
|
136
|
+
throw new ReaderError('R_BAD_CRC', 'container CRC mismatch: stored 0x' + stored.toString(16) + ' != computed 0x' + actual.toString(16));
|
|
137
|
+
return 'ok';
|
|
88
138
|
}
|
|
89
139
|
|
|
90
140
|
_parseHeader() {
|
|
@@ -118,11 +168,11 @@ export class Reader {
|
|
|
118
168
|
if (reserved1 !== 0)
|
|
119
169
|
throw new ReaderError('R_RESERVED_NONZERO', 'header reserved1 at offset 36 must be 0, got ' + reserved1);
|
|
120
170
|
|
|
121
|
-
this._schemaBlockOff =
|
|
122
|
-
this._metadataOff =
|
|
123
|
-
this._shardDirOff =
|
|
171
|
+
this._schemaBlockOff = u64(this._dv, 8, 'schema_block_off');
|
|
172
|
+
this._metadataOff = u64(this._dv, 16, 'metadata_off'); // 0 = no metadata block; M7+ zone maps
|
|
173
|
+
this._shardDirOff = u64(this._dv, 24, 'shard_directory_off');
|
|
124
174
|
this._shardCount = this._dv.getUint32(32, true);
|
|
125
|
-
this._totalRows =
|
|
175
|
+
this._totalRows = u64(this._dv, 40, 'total_rows');
|
|
126
176
|
this._formatVersion = version;
|
|
127
177
|
|
|
128
178
|
const len = this._buffer.byteLength;
|
|
@@ -180,7 +230,10 @@ export class Reader {
|
|
|
180
230
|
const laneKind = this._bytes[descOff + 4];
|
|
181
231
|
const flags = this._bytes[descOff + 5];
|
|
182
232
|
const reserved2 = this._dv.getUint16(descOff + 6, true);
|
|
183
|
-
const nameStrOff =
|
|
233
|
+
const nameStrOff = u64(this._dv, descOff + 8, 'field ' + i + ' name_str_off');
|
|
234
|
+
// reserved3 is compared as BigInt vs 0n below and never Number()-cast, so
|
|
235
|
+
// it needs no u64() narrowing guard -- a value past 2^53 is caught by the
|
|
236
|
+
// reserved-must-be-zero check, not by precision loss (BS-05).
|
|
184
237
|
const reserved3 = this._dv.getBigUint64(descOff + 16, true);
|
|
185
238
|
|
|
186
239
|
if (flags !== 0) throw new ReaderError('R_BAD_FIELD_FLAGS', 'field ' + i + ' has non-zero flags (v2+ reserved)');
|
|
@@ -212,14 +265,14 @@ export class Reader {
|
|
|
212
265
|
let rowSum = 0;
|
|
213
266
|
for (let i = 0; i < this._shardCount; i++) {
|
|
214
267
|
const entryOff = off + i * SHARD_ENTRY_BYTES;
|
|
215
|
-
const payloadOff =
|
|
268
|
+
const payloadOff = u64(this._dv, entryOff + 0, 'shard ' + i + ' payload_off');
|
|
216
269
|
const payloadLen = this._dv.getUint32(entryOff + 8, true);
|
|
217
270
|
const rowCount = this._dv.getUint32(entryOff + 12, true);
|
|
218
271
|
const minReaderVer = this._dv.getUint16(entryOff + 16, true);
|
|
219
272
|
const shardFlags = this._dv.getUint16(entryOff + 18, true);
|
|
220
273
|
const shardReserved = this._dv.getUint32(entryOff + 20, true);
|
|
221
|
-
const localStrOff =
|
|
222
|
-
const localStrLen =
|
|
274
|
+
const localStrOff = u64(this._dv, entryOff + 24, 'shard ' + i + ' local_string_off');
|
|
275
|
+
const localStrLen = u64(this._dv, entryOff + 32, 'shard ' + i + ' local_string_len');
|
|
223
276
|
if (minReaderVer > READER_VERSION) {
|
|
224
277
|
throw new ReaderError('R_SHARD_VERSION_TOO_NEW',
|
|
225
278
|
'shard ' + i + ' requires reader version ' + minReaderVer);
|
|
@@ -396,13 +449,24 @@ export class Reader {
|
|
|
396
449
|
return i;
|
|
397
450
|
}
|
|
398
451
|
|
|
452
|
+
// Cold: the BS-32 row-range refusal body, kept out of get()'s hot frame.
|
|
453
|
+
_badRow(rowIdx) {
|
|
454
|
+
throw new ReaderError('R_ROW_OUT_OF_RANGE',
|
|
455
|
+
'rowIdx ' + rowIdx + ' out of range [0, ' + this._totalRows + ')');
|
|
456
|
+
}
|
|
457
|
+
|
|
399
458
|
// Random-access get across all shards. Returns the field's decoded value:
|
|
400
459
|
// F64 lanes -> number
|
|
401
460
|
// U32 lanes -> JS string (resolved via the shard's local string table)
|
|
402
461
|
// Not zero-alloc; intended for testing/introspection.
|
|
403
462
|
get(rowIdx, fieldName) {
|
|
404
|
-
let remaining = rowIdx;
|
|
405
463
|
const fieldIdx = this.fieldIndex(fieldName);
|
|
464
|
+
// BS-32 row-range door. `(rowIdx >>> 0) !== rowIdx` rejects negative,
|
|
465
|
+
// fractional, NaN and >= 2^32 in one compare; a whole ArrayBuffer cannot hold
|
|
466
|
+
// 2^32 rows, so the totalRows compare fires first for every reachable
|
|
467
|
+
// container. Throw body hoisted to the cold _badRow helper.
|
|
468
|
+
if ((rowIdx >>> 0) !== rowIdx || rowIdx >= this._totalRows) this._badRow(rowIdx);
|
|
469
|
+
let remaining = rowIdx;
|
|
406
470
|
const field = this._schema.fields[fieldIdx];
|
|
407
471
|
for (let s = 0; s < this._shards.length; s++) {
|
|
408
472
|
const shard = this._shards[s];
|
package/src/Split.js
CHANGED
|
@@ -36,8 +36,9 @@ import { Writer, WriterError } from './Writer.js';
|
|
|
36
36
|
import { Reader, ReaderError } from './Reader.js';
|
|
37
37
|
import { StringTable } from './StringTable.js';
|
|
38
38
|
import { checkOpts } from './Opts.js';
|
|
39
|
+
import { crc32cInit, crc32cUpdate, crc32cFinal } from './Crc32c.js';
|
|
39
40
|
|
|
40
|
-
export const VERSION = '1.
|
|
41
|
+
export const VERSION = '1.6.0';
|
|
41
42
|
|
|
42
43
|
const LF = 0x0A;
|
|
43
44
|
const CONTAINER_HEADER_BYTES = 48;
|
|
@@ -219,6 +220,11 @@ export function mergeContainers(containers) {
|
|
|
219
220
|
// Compute the target layout. Schema block is copied from container 0 verbatim.
|
|
220
221
|
// Reader gives us schemaBlockOff and shardDirOff; the schema-block byte range
|
|
221
222
|
// is [schemaBlockOff, shardDirOff). We reuse this slice.
|
|
223
|
+
// These Number(getBigUint64) reads carry NO u64 narrowing guard by design
|
|
224
|
+
// (BS-05 dominance): `new Reader(...)` at :193 above parsed every part first,
|
|
225
|
+
// and Reader's own guarded u64() over the same header bytes (schema_block_off,
|
|
226
|
+
// shard_directory_off) already threw R_OFFSET_TOO_LARGE for any over-2^53
|
|
227
|
+
// value. Split mints nothing here -- an untriggerable code would be dead.
|
|
222
228
|
const src0 = parts[0];
|
|
223
229
|
const dv0 = new DataView(src0.buffer, src0.byteOffset, src0.byteLength);
|
|
224
230
|
const schemaBlockOff = Number(dv0.getBigUint64(8, true));
|
|
@@ -350,9 +356,20 @@ export function mergeContainers(containers) {
|
|
|
350
356
|
}
|
|
351
357
|
}
|
|
352
358
|
|
|
353
|
-
// Footer
|
|
359
|
+
// Footer. CRC policy (D4): recompute a fresh CRC-32C over the merged body iff
|
|
360
|
+
// EVERY input carried one; if any input's CRC is absent the merged CRC is
|
|
361
|
+
// absent too (an integrity guarantee only the inputs all shared can be
|
|
362
|
+
// honestly re-asserted -- never fabricated over an unverified part).
|
|
354
363
|
const footerOff = totalBytes - FOOTER_BYTES;
|
|
355
|
-
|
|
364
|
+
let mergedCrc = 0xFFFFFFFF;
|
|
365
|
+
let allHaveCrc = true;
|
|
366
|
+
for (const p of parts) {
|
|
367
|
+
const pDv = new DataView(p.buffer, p.byteOffset, p.byteLength);
|
|
368
|
+
const pCrc = pDv.getUint32(p.byteLength - FOOTER_BYTES, true) >>> 0;
|
|
369
|
+
if (pCrc === 0xFFFFFFFF) { allHaveCrc = false; break; }
|
|
370
|
+
}
|
|
371
|
+
if (allHaveCrc) mergedCrc = crc32cFinal(crc32cUpdate(crc32cInit(), out, 0, footerOff));
|
|
372
|
+
outDv.setUint32(footerOff + 0, mergedCrc >>> 0, true);
|
|
356
373
|
outDv.setUint32(footerOff + 4, 0, true);
|
|
357
374
|
out[footerOff + 8] = 0x31;
|
|
358
375
|
out[footerOff + 9] = 0x4B;
|
|
@@ -383,7 +400,10 @@ function _schemasEqual(a, b) {
|
|
|
383
400
|
return true;
|
|
384
401
|
}
|
|
385
402
|
|
|
386
|
-
// Look up the source string-table byte length for a shard.
|
|
403
|
+
// Look up the source string-table byte length for a shard. No u64 narrowing
|
|
404
|
+
// guard here (BS-05 dominance): `reader` is a fully-parsed Reader, so Reader's
|
|
405
|
+
// guarded u64() over this same local_string_len field already threw
|
|
406
|
+
// R_OFFSET_TOO_LARGE for any over-2^53 value during construction.
|
|
387
407
|
function _sourceStringTableLen(reader, shardIdx) {
|
|
388
408
|
const dv = new DataView(reader.buffer);
|
|
389
409
|
const entryOff = reader.shardDirectoryOffset + shardIdx * SHARD_ENTRY_BYTES;
|
package/src/StringTable.js
CHANGED
|
@@ -19,8 +19,26 @@
|
|
|
19
19
|
// Entry 0 is always the empty string: the table reserves it in the constructor
|
|
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
|
+
//
|
|
23
|
+
// Serialization ceilings (BS-31). serialize() writes entry_count and
|
|
24
|
+
// blob_length as u32; a table that grew past either would silently wrap on
|
|
25
|
+
// setUint32 and mint a corrupt-but-in-bounds container. Two cold guards in
|
|
26
|
+
// _insertNew fail closed instead:
|
|
27
|
+
// ST_BLOB_OVERFLOW - the blob would exceed the u32 blob_length ceiling, OR
|
|
28
|
+
// the entry count would exceed the entry_count ceiling
|
|
29
|
+
// (one code, two messages -- both name their ceiling).
|
|
30
|
+
// The doubling clamps in _growBlob / _growOffsets cap allocation at the
|
|
31
|
+
// ceiling so a crossing write ALWAYS re-enters the grow branch where the guard
|
|
32
|
+
// lives; guard placement is then provably complete (no path reaches serialize
|
|
33
|
+
// with an out-of-range count/blob). Zero hot cost: the guards read only inside
|
|
34
|
+
// the cold grow arms.
|
|
35
|
+
//
|
|
36
|
+
// __setStringTableLimits({blobBytes, entryCount}) lowers the ceilings for a
|
|
37
|
+
// cheap gated crossing on the real grow path and returns the previous pair.
|
|
38
|
+
// TEST-ONLY: not re-exported from index.js, absent from the .d.ts and docs,
|
|
39
|
+
// and carrying no semver guarantee.
|
|
22
40
|
|
|
23
|
-
export const VERSION = '1.
|
|
41
|
+
export const VERSION = '1.6.0';
|
|
24
42
|
|
|
25
43
|
const EMPTY_SLOT = 0xFFFFFFFF; // MUST be unsigned; typed-array reads are unsigned
|
|
26
44
|
const INITIAL_BLOB_BYTES = 64 * 1024;
|
|
@@ -29,6 +47,26 @@ const INITIAL_HASH_CAP = 2048; // load factor target 50%
|
|
|
29
47
|
const HASH_MAX_LOAD_NUM = 1; // 50% load factor: num/den = 1/2
|
|
30
48
|
const HASH_MAX_LOAD_DEN = 2;
|
|
31
49
|
|
|
50
|
+
// u32 serialization ceilings. blob_length is a u32 (max 4294967295). entry_count
|
|
51
|
+
// is a u32, but the offsets array carries entry_count+1 slots (the sentinel), so
|
|
52
|
+
// the last addressable entry_count is 4294967294 to keep offsets_len a valid u32.
|
|
53
|
+
// `let`, not `const`: __setStringTableLimits lowers them for gated crossings.
|
|
54
|
+
let LIMIT_BLOB = 4294967295;
|
|
55
|
+
let LIMIT_ENTRIES = 4294967294;
|
|
56
|
+
|
|
57
|
+
// TEST-ONLY seam (BS-31). Assigns whichever of {blobBytes, entryCount} are
|
|
58
|
+
// present, returns the previous pair. Not part of the public API.
|
|
59
|
+
export function __setStringTableLimits(next) {
|
|
60
|
+
const prev = { blobBytes: LIMIT_BLOB, entryCount: LIMIT_ENTRIES };
|
|
61
|
+
if (next && next.blobBytes !== undefined) LIMIT_BLOB = next.blobBytes;
|
|
62
|
+
if (next && next.entryCount !== undefined) LIMIT_ENTRIES = next.entryCount;
|
|
63
|
+
return prev;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class StringTableError extends Error {
|
|
67
|
+
constructor(code, msg) { super(msg); this.code = code; this.name = 'StringTableError'; }
|
|
68
|
+
}
|
|
69
|
+
|
|
32
70
|
// Reserved entry 0. A zero-length range: no blob bytes, no growth, one hash
|
|
33
71
|
// slot. Module-level so reset() reserves without allocating.
|
|
34
72
|
const EMPTY = new Uint8Array(0);
|
|
@@ -94,10 +132,23 @@ export class StringTable {
|
|
|
94
132
|
|
|
95
133
|
_insertNew(bytes, from, to, slot) {
|
|
96
134
|
const n = to - from;
|
|
97
|
-
// Grow blob if needed
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
if (this.
|
|
135
|
+
// Grow blob if needed. The clamp keeps _blob.length <= LIMIT_BLOB, so any
|
|
136
|
+
// write pushing past the ceiling necessarily re-enters this branch and is
|
|
137
|
+
// caught by the cold guard (fail closed -- serialize would wrap otherwise).
|
|
138
|
+
if (this._blobLen + n > this._blob.length) {
|
|
139
|
+
if (this._blobLen + n > LIMIT_BLOB)
|
|
140
|
+
throw new StringTableError('ST_BLOB_OVERFLOW',
|
|
141
|
+
'string blob ' + this._blobLen + ' + ' + n + ' would exceed the u32 blob_length ceiling ' + LIMIT_BLOB);
|
|
142
|
+
this._growBlob(n);
|
|
143
|
+
}
|
|
144
|
+
// Grow offsets if needed (leave room for +1 sentinel). Same completeness
|
|
145
|
+
// argument via the _growOffsets clamp on the entry_count ceiling.
|
|
146
|
+
if (this._count + 1 >= this._offsets.length) {
|
|
147
|
+
if (this._count >= LIMIT_ENTRIES)
|
|
148
|
+
throw new StringTableError('ST_BLOB_OVERFLOW',
|
|
149
|
+
'string table entry count ' + this._count + ' would exceed the entry_count ceiling ' + LIMIT_ENTRIES);
|
|
150
|
+
this._growOffsets();
|
|
151
|
+
}
|
|
101
152
|
|
|
102
153
|
const idx = this._count;
|
|
103
154
|
this._offsets[idx] = this._blobLen;
|
|
@@ -123,13 +174,18 @@ export class StringTable {
|
|
|
123
174
|
_growBlob(needed) {
|
|
124
175
|
let cap = this._blob.length;
|
|
125
176
|
while (cap < this._blobLen + needed) cap *= 2;
|
|
177
|
+
// Clamp so allocated length never exceeds the u32 ceiling (see _insertNew).
|
|
178
|
+
if (cap > LIMIT_BLOB) cap = LIMIT_BLOB;
|
|
126
179
|
const nb = new Uint8Array(cap);
|
|
127
180
|
nb.set(this._blob);
|
|
128
181
|
this._blob = nb;
|
|
129
182
|
}
|
|
130
183
|
|
|
131
184
|
_growOffsets() {
|
|
132
|
-
|
|
185
|
+
let len = this._offsets.length * 2;
|
|
186
|
+
// Clamp so the offsets array (entry_count + 1 slots) never exceeds a valid u32.
|
|
187
|
+
if (len > LIMIT_ENTRIES + 1) len = LIMIT_ENTRIES + 1;
|
|
188
|
+
const nb = new Uint32Array(len);
|
|
133
189
|
nb.set(this._offsets);
|
|
134
190
|
this._offsets = nb;
|
|
135
191
|
}
|
package/src/Tokenizer.js
CHANGED
package/src/Views.js
CHANGED
|
@@ -23,3 +23,65 @@ export function toContainerBuffer(input, label) {
|
|
|
23
23
|
throw new TypeError(label + ': expected ArrayBuffer or Uint8Array, got ' +
|
|
24
24
|
(input === null ? 'null' : typeof input));
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
// ---- streaming sink support (M6) --------------------------------------------
|
|
28
|
+
//
|
|
29
|
+
// A sink is any object with a synchronous write(bytes) method; the streaming
|
|
30
|
+
// (layout:'stream') path additionally needs writeAt(bytes, position) for the
|
|
31
|
+
// single header backpatch. MemorySink is the in-RAM reference implementation
|
|
32
|
+
// that finalize() drives internally so it can return the identical container
|
|
33
|
+
// buffer the classic assembler produced.
|
|
34
|
+
|
|
35
|
+
// Growable in-RAM sink. write() appends; writeAt() places bytes at an absolute
|
|
36
|
+
// position (growing the logical length if needed); toArrayBuffer() returns an
|
|
37
|
+
// EXACT-length ArrayBuffer copy of the bytes written.
|
|
38
|
+
export class MemorySink {
|
|
39
|
+
constructor(hint) {
|
|
40
|
+
const cap = (typeof hint === 'number' && hint > 0) ? hint : 64 * 1024;
|
|
41
|
+
this._buf = new Uint8Array(cap);
|
|
42
|
+
this._len = 0;
|
|
43
|
+
}
|
|
44
|
+
_ensure(need) {
|
|
45
|
+
if (need <= this._buf.length) return;
|
|
46
|
+
let cap = this._buf.length;
|
|
47
|
+
while (cap < need) cap *= 2;
|
|
48
|
+
const next = new Uint8Array(cap);
|
|
49
|
+
next.set(this._buf.subarray(0, this._len));
|
|
50
|
+
this._buf = next;
|
|
51
|
+
}
|
|
52
|
+
write(bytes) {
|
|
53
|
+
this._ensure(this._len + bytes.length);
|
|
54
|
+
this._buf.set(bytes, this._len);
|
|
55
|
+
this._len += bytes.length;
|
|
56
|
+
}
|
|
57
|
+
writeAt(bytes, position) {
|
|
58
|
+
this._ensure(position + bytes.length);
|
|
59
|
+
this._buf.set(bytes, position);
|
|
60
|
+
if (position + bytes.length > this._len) this._len = position + bytes.length;
|
|
61
|
+
}
|
|
62
|
+
toArrayBuffer() {
|
|
63
|
+
return this._buf.buffer.slice(0, this._len);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Cold prologue guard for finalizeToSink (raised as W_BAD_SINK by the writer):
|
|
68
|
+
// the sink must be an object with write(); layout:'stream' also needs writeAt().
|
|
69
|
+
export function validateSink(sink, needsWriteAt, raise) {
|
|
70
|
+
if (sink === null || typeof sink !== 'object') {
|
|
71
|
+
raise('W_BAD_SINK', 'sink must be an object with a write(bytes) method; got ' +
|
|
72
|
+
(sink === null ? 'null' : typeof sink));
|
|
73
|
+
}
|
|
74
|
+
if (typeof sink.write !== 'function') {
|
|
75
|
+
raise('W_BAD_SINK', 'sink is missing a write(bytes) method');
|
|
76
|
+
}
|
|
77
|
+
if (needsWriteAt && typeof sink.writeAt !== 'function') {
|
|
78
|
+
raise('W_BAD_SINK', "sink is missing a writeAt(bytes, position) method required for layout:'stream'");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A sink write that returns a thenable is an async sink; the contract is
|
|
83
|
+
// synchronous, so the writer treats it as a malformed sink (W_BAD_SINK).
|
|
84
|
+
export function isThenable(v) {
|
|
85
|
+
return v !== null && (typeof v === 'object' || typeof v === 'function') &&
|
|
86
|
+
typeof v.then === 'function';
|
|
87
|
+
}
|