@zakkster/lite-bake-stream 1.5.0 → 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 +8 -0
- package/README.md +18 -1
- package/SPEC.md +3 -1
- package/llms.txt +10 -2
- package/package.json +1 -1
- package/src/Crc32c.js +96 -0
- package/src/FileIngest.js +1 -1
- package/src/MultiReader.js +1 -1
- package/src/PreserveReader.js +27 -4
- package/src/PreserveTokenizer.js +1 -1
- package/src/PreserveWriter.js +211 -60
- package/src/RangeReader.js +27 -2
- package/src/Reader.js +32 -4
- package/src/Split.js +15 -3
- package/src/StringTable.js +1 -1
- package/src/Tokenizer.js +1 -1
- package/src/Views.js +62 -0
- package/src/Writer.js +382 -143
- package/src/index.js +4 -4
- 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/Writer.d.ts +37 -0
- package/types/index.d.ts +2 -1
package/src/RangeReader.js
CHANGED
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
|
|
45
45
|
import { StringTable } from './StringTable.js';
|
|
46
46
|
import { checkOpts } from './Opts.js';
|
|
47
|
+
import { crc32cInit, crc32cUpdate, crc32cFinal } from './Crc32c.js';
|
|
47
48
|
|
|
48
|
-
export const VERSION = '1.
|
|
49
|
+
export const VERSION = '1.6.0';
|
|
49
50
|
|
|
50
51
|
const CONTAINER_HEADER_BYTES = 48;
|
|
51
52
|
const SHARD_ENTRY_BYTES = 40;
|
|
@@ -56,10 +57,14 @@ const LANE_U32 = 3;
|
|
|
56
57
|
const READER_VERSION = 1;
|
|
57
58
|
const U32_MAX = 4294967295;
|
|
58
59
|
|
|
59
|
-
const RANGE_READER_OPTS = {
|
|
60
|
+
const RANGE_READER_OPTS = {
|
|
61
|
+
maxCachedShards: { t: 'int', min: 0, max: U32_MAX },
|
|
62
|
+
verifyCrc: { t: 'bool' },
|
|
63
|
+
};
|
|
60
64
|
const HTTP_ADAPTER_OPTS = { fetch: { t: 'fn' } };
|
|
61
65
|
|
|
62
66
|
const CONTAINER_FOOTER_BYTES = 16;
|
|
67
|
+
const CRC_ABSENT = 0xFFFFFFFF;
|
|
63
68
|
|
|
64
69
|
function laneBytesOf(k) { return k === LANE_F64 ? 8 : (k === LANE_U32 ? 4 : 0); }
|
|
65
70
|
|
|
@@ -201,6 +206,11 @@ export class RangeReader {
|
|
|
201
206
|
static async open(adapter, opts) {
|
|
202
207
|
const r = new RangeReader(adapter, opts);
|
|
203
208
|
await r._loadHeaderAndSchema();
|
|
209
|
+
if (opts && opts.verifyCrc === true) {
|
|
210
|
+
const status = await r.verifyCrc();
|
|
211
|
+
if (status === 'absent')
|
|
212
|
+
throw new RangeReaderError('R_CRC_ABSENT', 'verifyCrc:true but the container carries no CRC (footer CRC is absent, 0xFFFFFFFF)');
|
|
213
|
+
}
|
|
204
214
|
return r;
|
|
205
215
|
}
|
|
206
216
|
|
|
@@ -209,6 +219,7 @@ export class RangeReader {
|
|
|
209
219
|
opts = opts || {};
|
|
210
220
|
this.adapter = adapter;
|
|
211
221
|
this.maxCachedShards = opts.maxCachedShards !== undefined ? opts.maxCachedShards : 8;
|
|
222
|
+
this._footerCrc = CRC_ABSENT;
|
|
212
223
|
// shard cache: shardIdx -> { payloadBytes, payloadDv, stringTable, lastAccess }
|
|
213
224
|
this._shardCache = new Map();
|
|
214
225
|
this._accessCounter = 0;
|
|
@@ -298,6 +309,20 @@ export class RangeReader {
|
|
|
298
309
|
throw new RangeReaderError('R_BAD_FOOTER', 'footer_len ' + footerLen + ' is less than the minimum ' + CONTAINER_FOOTER_BYTES);
|
|
299
310
|
if (footerLen > size - CONTAINER_HEADER_BYTES)
|
|
300
311
|
throw new RangeReaderError('R_BAD_FOOTER', 'footer_len ' + footerLen + ' exceeds the container body size');
|
|
312
|
+
this._footerCrc = fDv.getUint32(0, true) >>> 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// SPEC 3.7 integrity over a ranged source: 'ok' | 'absent'; a mismatch throws
|
|
316
|
+
// R_BAD_CRC. Fetches the body [0, footer_off) in one range (a full-integrity
|
|
317
|
+
// check is inherently whole-body). The footer CRC was captured at open.
|
|
318
|
+
async verifyCrc() {
|
|
319
|
+
if (this._footerCrc === CRC_ABSENT) return 'absent';
|
|
320
|
+
const footerOff = this.adapter.size - CONTAINER_FOOTER_BYTES;
|
|
321
|
+
const body = await this._fetchExact(0, footerOff);
|
|
322
|
+
const actual = crc32cFinal(crc32cUpdate(crc32cInit(), body, 0, footerOff));
|
|
323
|
+
if (actual !== this._footerCrc)
|
|
324
|
+
throw new RangeReaderError('R_BAD_CRC', 'container CRC mismatch: stored 0x' + this._footerCrc.toString(16) + ' != computed 0x' + actual.toString(16));
|
|
325
|
+
return 'ok';
|
|
301
326
|
}
|
|
302
327
|
|
|
303
328
|
async _loadZoneMaps() {
|
package/src/Reader.js
CHANGED
|
@@ -33,13 +33,19 @@
|
|
|
33
33
|
|
|
34
34
|
import { StringTable } from './StringTable.js';
|
|
35
35
|
import { toContainerBuffer } from './Views.js';
|
|
36
|
+
import { checkOpts } from './Opts.js';
|
|
37
|
+
import { crc32cInit, crc32cUpdate, crc32cFinal } from './Crc32c.js';
|
|
36
38
|
|
|
37
|
-
export const VERSION = '1.
|
|
39
|
+
export const VERSION = '1.6.0';
|
|
38
40
|
|
|
39
41
|
const CONTAINER_HEADER_BYTES = 48;
|
|
40
42
|
const SHARD_ENTRY_BYTES = 40;
|
|
41
43
|
const FIELD_DESCRIPTOR_BYTES = 24;
|
|
42
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); }
|
|
43
49
|
|
|
44
50
|
const LANE_F64 = 1;
|
|
45
51
|
const LANE_U32 = 3;
|
|
@@ -93,11 +99,12 @@ function validateStringTable(bytes, off, len, label) {
|
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
export class Reader {
|
|
96
|
-
static fromBuffer(input) {
|
|
97
|
-
return new Reader(toContainerBuffer(input, 'Reader.fromBuffer'));
|
|
102
|
+
static fromBuffer(input, opts) {
|
|
103
|
+
return new Reader(toContainerBuffer(input, 'Reader.fromBuffer'), opts);
|
|
98
104
|
}
|
|
99
105
|
|
|
100
|
-
constructor(buffer) {
|
|
106
|
+
constructor(buffer, opts) {
|
|
107
|
+
checkOpts('Reader', opts, READER_OPTS, raiseReaderOpt);
|
|
101
108
|
this._buffer = buffer;
|
|
102
109
|
this._dv = new DataView(buffer);
|
|
103
110
|
this._bytes = new Uint8Array(buffer);
|
|
@@ -107,6 +114,27 @@ export class Reader {
|
|
|
107
114
|
this._parseShardDirectory();
|
|
108
115
|
this._parseZoneMaps(); // M7 -- no-op if metadata_off is 0
|
|
109
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';
|
|
110
138
|
}
|
|
111
139
|
|
|
112
140
|
_parseHeader() {
|
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;
|
|
@@ -355,9 +356,20 @@ export function mergeContainers(containers) {
|
|
|
355
356
|
}
|
|
356
357
|
}
|
|
357
358
|
|
|
358
|
-
// 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).
|
|
359
363
|
const footerOff = totalBytes - FOOTER_BYTES;
|
|
360
|
-
|
|
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);
|
|
361
373
|
outDv.setUint32(footerOff + 4, 0, true);
|
|
362
374
|
out[footerOff + 8] = 0x31;
|
|
363
375
|
out[footerOff + 9] = 0x4B;
|
package/src/StringTable.js
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// TEST-ONLY: not re-exported from index.js, absent from the .d.ts and docs,
|
|
39
39
|
// and carrying no semver guarantee.
|
|
40
40
|
|
|
41
|
-
export const VERSION = '1.
|
|
41
|
+
export const VERSION = '1.6.0';
|
|
42
42
|
|
|
43
43
|
const EMPTY_SLOT = 0xFFFFFFFF; // MUST be unsigned; typed-array reads are unsigned
|
|
44
44
|
const INITIAL_BLOB_BYTES = 64 * 1024;
|
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
|
+
}
|