@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.
@@ -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.5.0';
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 = { maxCachedShards: { t: 'int', min: 0, max: U32_MAX } };
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.5.0';
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.5.0';
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
- outDv.setUint32(footerOff + 0, 0xFFFFFFFF, true); // CRC absent
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;
@@ -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.5.0';
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
@@ -30,7 +30,7 @@
30
30
 
31
31
  import { checkOpts } from './Opts.js';
32
32
 
33
- export const VERSION = '1.5.0';
33
+ export const VERSION = '1.6.0';
34
34
 
35
35
  const U32_MAX = 4294967295;
36
36
  const TOKENIZER_OPTS = {
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
+ }