@zakkster/lite-bake-stream 1.2.0 → 1.3.1
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 +40 -0
- package/README.md +1 -1
- package/SPEC.md +2 -2
- package/llms.txt +2 -2
- package/package.json +5 -1
- package/src/FileIngest.js +1 -1
- package/src/MultiReader.js +20 -4
- package/src/PreserveReader.js +69 -13
- package/src/PreserveTokenizer.js +10 -4
- package/src/PreserveWriter.js +1 -1
- package/src/RangeReader.js +136 -14
- package/src/Reader.js +125 -25
- package/src/Split.js +13 -2
- package/src/StringTable.js +1 -1
- package/src/Tokenizer.js +10 -4
- package/src/Views.js +25 -0
- package/src/Writer.js +18 -5
- package/src/index.js +3 -15
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
4
|
|
|
5
|
+
## [1.3.1] -- 2026-09-01
|
|
6
|
+
|
|
7
|
+
M3 remainder (BS-05) -- the UTF-8 door ends the promise-A/behavior-B split with a measured decision. Option A (validate at the tokenizer, refuse invalid UTF-8 with `E_INVALID_UTF8`) was built, proven correctness-green (460/460, torture fast 42/42), and rejected on its pre-committed budget: unicode-heavy throughput regressed 229.7 -> 213.1 MB/s (-7.2%, over the 3% ceiling), because the baseline scan bulk-copies `>= 0x80` bytes with zero inspection so validation is pure added cost, while ASCII stayed within noise. SPEC 5.5 is instead rewritten to match the long-standing behavior (documented-permissive), and the dead code is removed. No bytes move at runtime. See decisions/0006-utf8-door.md for the before/after numbers and the rejected option. Suite: 453 tests / 453 pass / 0 fail / 0 todo. Torture: 42/42 fast, 44/44 full, arrayBuffers growth 0. Inverted mutation check: all 49 battery and pin tests pass on baseline src as well -- zero behavior change, proven.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- SPEC 5.5 and SPEC 7's U32 row rewritten to documented-permissive: string bytes between the quotes are stored byte-exact through U32 lanes regardless of UTF-8 validity; invalid sequences are not a parse error. `get()`/`getString()` decode with replacement (U+FFFD); `shardStringTable(s).bytesAt(i)` returns the exact stored bytes, the schema-mode twin of `PreserveReader.getBytes(i)`. This is a documentation-and-dead-code change only -- no runtime behavior changed, no new public surface (the accessor already existed).
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- BS-05: the SPEC no longer promises a UTF-8 parse error the tokenizer never enforced, and `Tokenizer.js` no longer declares an unreachable `E_INVALID_UTF8` code. The byte-exact string contract is now stated truthfully: invalid bytes round-trip byte-exact and decode with U+FFFD, in schema mode exactly as in preserve mode. Lone-surrogate escapes still refuse with `E_UNPAIRED_SURROGATE` (an escape-grammar rule, unrelated to input-UTF-8 validity).
|
|
16
|
+
|
|
17
|
+
## [1.3.0] -- 2026-09-01
|
|
18
|
+
|
|
19
|
+
M2 -- a container is verified at the door or refused at the door. The four readers (`Reader`, `PreserveReader`, `RangeReader`, `MultiReader`) now agree with each other and with the `Writer` about the bytes between them; every malformed container names its refusal with a code, and a standalone SPEC byte-checker (reads SPEC.md offsets only, imports nothing from `src/`) is the contract's executable form. All new validation runs at open/parse/construction time -- the `get`/`syncRange`/`findShards` row loops and both tokenizer byte loops are byte-identical. Suite: 432 tests / 431 pass / 0 fail / 1 todo. Torture: 37/37 fast, 39/39 full, arrayBuffers growth 0. Mutation check: all 52 door tests fail on source revert; all 23 behavior-unchanged pins hold.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- New stable refusal codes: `R_RESERVED_NONZERO` (a reserved header/descriptor/shard field is non-zero), `R_BAD_FOOTER` (footer `magic_end` or `footer_len` malformed), `R_BAD_METADATA` (a non-zero `metadata_off` pointing at an unparseable zone-map segment), and `M_TOO_MANY_ROWS` (a `MultiReader` union whose cumulative row count exceeds 2^53).
|
|
24
|
+
- `src/Views.js` (internal, not exported): a shared `toContainerBuffer` helper so `deserialize`, `Reader.fromBuffer`, and `PreserveReader.fromBuffer` resolve caller bytes by one rule -- a full-buffer view is unwrapped in place, a partial view is copied.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Malformed containers that previously opened, half-opened, or failed later with a raw `RangeError` now refuse at the door with a code. Reserved bytes, footer shape, structural bounds (`row_count * row_stride <= payload_len`, `offset_in_row + lane_bytes <= row_stride`, name-blob and payload extents, monotonic string-table offsets with a `blob_length` sentinel, `total_rows == sum(row_count)`) are all checked at parse time. Every byte-prefix of a valid container either opens fully or throws an `R_`/`M_` code -- no raw `RangeError`, no clamped subarray, no silent short read.
|
|
29
|
+
- `findShards`/`shardBounds` with a field name absent from the schema now throw `R_UNKNOWN_FIELD` (`M_UNKNOWN_FIELD` on `MultiReader`) instead of silently returning all shards / `null`. A field present in the schema but untracked by zone maps still returns all shards / `null` (decisions/0005).
|
|
30
|
+
- F64-only shards no longer emit a 16-byte empty local string table -- such containers are `16 * shardCount` bytes smaller and their ShardEntries carry `local_string_off = 0` / `local_string_len = 0` (SPEC 3.4). `mergeContainers` applies the same rule to its output even when fed a pre-M2 container that violated it.
|
|
31
|
+
- `RangeReader.open` performs one additional 16-byte footer fetch (open-phase fetch count 5 -> 6 for a container with zone maps), so a wrong-footer or short container is refused before the schema fetch.
|
|
32
|
+
- `MultiReader` cumulative row/shard offsets moved `Uint32Array` -> `Float64Array`, so a union totaling more than 2^32 rows locates rows correctly instead of wrapping to `null`.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- BS-06: a truncated preserve container no longer returns a clamped short `subarray`; the shard directory is validated at open.
|
|
37
|
+
- BS-08: reserved header/descriptor/shard bytes and the footer are now enforced; a lying `metadata_off` throws `R_BAD_METADATA` instead of silently degrading or throwing a raw `RangeError`.
|
|
38
|
+
- BS-09: `Reader.fromBuffer` now respects a `Uint8Array`'s `byteOffset`/`byteLength`, copying a partial view instead of reading a sibling's bytes from a pooled buffer.
|
|
39
|
+
- BS-10: `RangeReader.open` on a preserve-mode container throws `R_WRONG_MODE` naming `PreserveReader`, instead of misreading it as schema mode.
|
|
40
|
+
- BS-15: F64-only shards emit no local string table, matching SPEC 3.4's 0/0 rule.
|
|
41
|
+
- BS-16: `MultiReader` over sub-readers totaling more than 2^32 rows addresses every row exactly; beyond 2^53 it refuses with `M_TOO_MANY_ROWS`.
|
|
42
|
+
- BS-17: one unknown-field policy across `get`/`findShards`/`shardBounds` on all readers (decisions/0005).
|
|
43
|
+
- Sink-poison rider (M1 deferred): a sink callback that throws mid-`feed()`/`end()` now leaves the tokenizer poisoned; the next `feed()`/`end()` throws `E_POISONED`. Both tokenizers, via an armed-flag pattern with zero `try`/`catch` and two boolean stores per chunk.
|
|
44
|
+
|
|
5
45
|
## [1.2.0] -- 2026-08-31
|
|
6
46
|
|
|
7
47
|
M1 -- the write path refuses what it cannot store. Eight findings closed by a set of doors that all run at construction, freeze, or terminal transition; the byte and record hot paths are unchanged (proven by the torture t6 gate, numbers within noise of M0). Suite: 359 tests / 358 pass / 0 fail / 1 todo. Torture: 37/37 fast, 39/39 full, arrayBuffers growth 0. Mutation check: all 63 door tests fail on source revert; all 13 behavior-unchanged pins hold.
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
> Streaming byte-level JSON compiler for [`@zakkster/lite-bake`](https://github.com/PeshoVurtoleta/lite-bake). Zero-GC, tree-shakeable, gigabyte-scale.
|
|
15
15
|
|
|
16
|
-
**Status:** v1.
|
|
16
|
+
**Status:** v1.3.1. LBK1 format frozen at `format_version: 1`. Qualified on an 8 GB soak with zero GC and byte-exact preservation across 590 million cells.
|
|
17
17
|
|
|
18
18
|
## Two modes, one API
|
|
19
19
|
|
package/SPEC.md
CHANGED
|
@@ -274,7 +274,7 @@ The boundary is exercised explicitly by `test/NumericBoundary.test.js`, which pi
|
|
|
274
274
|
|
|
275
275
|
### 5.5 String semantics (v1)
|
|
276
276
|
|
|
277
|
-
-
|
|
277
|
+
- String bytes between the quotes are stored byte-exact through U32 lanes, regardless of UTF-8 validity. Invalid sequences are **not** a parse error; the prior rule declaring them one (and its `E_INVALID_UTF8` code) is withdrawn. `get()` / `getString()` decode with replacement (U+FFFD); `shardStringTable(s).bytesAt(i)` returns the exact stored bytes, as `PreserveReader.getBytes(i)` does in preserve mode.
|
|
278
278
|
- All standard JSON escapes are supported: `\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, `\t`, `\uXXXX`.
|
|
279
279
|
- Surrogate pairs (`\uD800`..`\uDBFF` followed by `\uDC00`..`\uDFFF`) are combined and emitted as UTF-8.
|
|
280
280
|
- Unpaired surrogates are a parse error.
|
|
@@ -334,7 +334,7 @@ Given source NDJSON `S` and a schema `Σ` (either explicit or inferred), the LBK
|
|
|
334
334
|
| :-- | :-- | :-- |
|
|
335
335
|
| JSON number in fast-path domain (≤15 sig digits, `|exp| ≤ 22`) | F64 | **bit-exact IEEE 754 double** |
|
|
336
336
|
| JSON number in slow-path domain | F64 | within 1 ULP of the correctly-rounded value |
|
|
337
|
-
| JSON string (any valid UTF-8) | U32 | **byte-exact
|
|
337
|
+
| JSON string (any bytes, valid UTF-8 or not) | U32 | **byte-exact bytes; validity not asserted** |
|
|
338
338
|
| JSON `true` / `false` / `null` | F64 | `1` / `0` / `0` (documented coercion) |
|
|
339
339
|
| absent field (missing in source) | F64 | `0` (default) |
|
|
340
340
|
| absent field | U32 | `""` (index 0 is the reserved empty-string entry) |
|
package/llms.txt
CHANGED
|
@@ -8,13 +8,13 @@ Ingest gigabyte-scale JSON (top-level array or NDJSON) into the `lite-bake` LBK1
|
|
|
8
8
|
|
|
9
9
|
## Status
|
|
10
10
|
|
|
11
|
-
v1.
|
|
11
|
+
v1.3.1 — stable. LBK1 format frozen at `format_version: 1`. Qualified on an 8 GB soak (M1 MacBook Pro): 98.37M rows, 4.89 GB container, zero major GC, zero minor GC, 499 KB total heap allocation, 590.21M cells verified byte-exact, zero mismatches. Tokenizer benches at 222-237 MB/s (~55% of JSON.parse, with no object graph allocated).
|
|
12
12
|
|
|
13
13
|
Public API follows semver from 1.0.0. Future additions (I64 lane, columnar payload mode, container-level string table) land via the format's forward-compat seams -- `min_reader_version` on ShardEntry, reserved FieldDescriptor flags, the `metadata_off` block wrapper -- without a format_version bump.
|
|
14
14
|
|
|
15
15
|
See SPEC.md for the LBK1 container format, section 3.6 for zone maps, section 4.3 for the reserved field flags.
|
|
16
16
|
|
|
17
|
-
## Public API (v1.
|
|
17
|
+
## Public API (v1.3.1)
|
|
18
18
|
|
|
19
19
|
Two ingest modes share one top-level API:
|
|
20
20
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-bake-stream",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Streaming byte-level JSON to lite-bake binary compiler. Zero-GC, tree-shakeable, gigabyte-scale.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -109,6 +109,10 @@
|
|
|
109
109
|
"engines": {
|
|
110
110
|
"node": ">=18"
|
|
111
111
|
},
|
|
112
|
+
"funding": {
|
|
113
|
+
"type": "github",
|
|
114
|
+
"url": "https://github.com/sponsors/PeshoVurtoleta"
|
|
115
|
+
},
|
|
112
116
|
"publishConfig": {
|
|
113
117
|
"access": "public"
|
|
114
118
|
},
|
package/src/FileIngest.js
CHANGED
|
@@ -27,7 +27,7 @@ import { PreserveWriter } from './PreserveWriter.js';
|
|
|
27
27
|
import { PreserveReader } from './PreserveReader.js';
|
|
28
28
|
import { checkOpts } from './Opts.js';
|
|
29
29
|
|
|
30
|
-
export const VERSION = '1.
|
|
30
|
+
export const VERSION = '1.3.1';
|
|
31
31
|
|
|
32
32
|
const U32_MAX = 4294967295;
|
|
33
33
|
const INGEST_OPTS = {
|
package/src/MultiReader.js
CHANGED
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
// Error codes:
|
|
22
22
|
// M_EMPTY - no readers passed
|
|
23
23
|
// M_SCHEMA_MISMATCH - schemas differ across readers
|
|
24
|
-
// M_UNKNOWN_FIELD - .get() / .fieldIndex()
|
|
24
|
+
// M_UNKNOWN_FIELD - .get() / .fieldIndex() / findShards / shardBounds with unknown name
|
|
25
25
|
// M_ROW_OUT_OF_RANGE - rowIdx >= totalRows
|
|
26
|
+
// M_TOO_MANY_ROWS - cumulative row count exceeds Number.MAX_SAFE_INTEGER
|
|
26
27
|
|
|
27
|
-
export const VERSION = '1.
|
|
28
|
+
export const VERSION = '1.3.1';
|
|
28
29
|
|
|
29
30
|
export class MultiReaderError extends Error {
|
|
30
31
|
constructor(code, msg) { super(msg); this.code = code; this.name = 'MultiReaderError'; }
|
|
@@ -60,15 +61,26 @@ export class MultiReader {
|
|
|
60
61
|
|
|
61
62
|
// Cumulative row offsets and shard offsets. Row rowIdx belongs to reader
|
|
62
63
|
// r iff _rowOffsets[r] <= rowIdx < _rowOffsets[r+1].
|
|
64
|
+
// Float64Array (not Uint32Array): cumulative row/shard offsets can exceed
|
|
65
|
+
// 2^32 across many large sub-readers. F64 is exact to 2^53 and stays flat
|
|
66
|
+
// and monotonic, so the linear scan in _locateRow/_locateShard is unchanged
|
|
67
|
+
// apart from the element type (BS-16).
|
|
63
68
|
let cumRows = 0, cumShards = 0;
|
|
64
|
-
this._rowOffsets = new
|
|
65
|
-
this._shardOffsets = new
|
|
69
|
+
this._rowOffsets = new Float64Array(readers.length + 1);
|
|
70
|
+
this._shardOffsets = new Float64Array(readers.length + 1);
|
|
66
71
|
for (let i = 0; i < readers.length; i++) {
|
|
67
72
|
this._rowOffsets[i] = cumRows;
|
|
68
73
|
this._shardOffsets[i] = cumShards;
|
|
69
74
|
cumRows += readers[i].totalRows;
|
|
70
75
|
cumShards += readers[i].shardCount;
|
|
71
76
|
}
|
|
77
|
+
// Boundary is 2^53 (BRIEF T6 / A8): total_rows == 2^53 keeps the largest
|
|
78
|
+
// row index at 2^53-1 (= MAX_SAFE_INTEGER), still exactly representable;
|
|
79
|
+
// beyond 2^53 an index collides with its neighbour in F64.
|
|
80
|
+
if (cumRows > 2 ** 53) {
|
|
81
|
+
throw new MultiReaderError('M_TOO_MANY_ROWS',
|
|
82
|
+
'cumulative row count ' + cumRows + ' exceeds 2^53; row indices past 2^53 are not exactly addressable in F64');
|
|
83
|
+
}
|
|
72
84
|
this._rowOffsets[readers.length] = cumRows;
|
|
73
85
|
this._shardOffsets[readers.length] = cumShards;
|
|
74
86
|
this._totalRows = cumRows;
|
|
@@ -129,6 +141,8 @@ export class MultiReader {
|
|
|
129
141
|
// reader; results are concatenated and re-numbered into the global shard
|
|
130
142
|
// index space.
|
|
131
143
|
findShards(fieldName, opts) {
|
|
144
|
+
if (!this._fieldIndex.has(fieldName))
|
|
145
|
+
throw new MultiReaderError('M_UNKNOWN_FIELD', 'no field named ' + JSON.stringify(fieldName));
|
|
132
146
|
const out = [];
|
|
133
147
|
for (let r = 0; r < this._readers.length; r++) {
|
|
134
148
|
const localShards = this._readers[r].findShards(fieldName, opts);
|
|
@@ -139,6 +153,8 @@ export class MultiReader {
|
|
|
139
153
|
}
|
|
140
154
|
|
|
141
155
|
shardBounds(globalShardIdx, fieldName) {
|
|
156
|
+
if (!this._fieldIndex.has(fieldName))
|
|
157
|
+
throw new MultiReaderError('M_UNKNOWN_FIELD', 'no field named ' + JSON.stringify(fieldName));
|
|
142
158
|
const loc = this._locateShard(globalShardIdx);
|
|
143
159
|
if (!loc) return null;
|
|
144
160
|
return this._readers[loc.readerIdx].shardBounds(loc.localShard, fieldName);
|
package/src/PreserveReader.js
CHANGED
|
@@ -16,11 +16,27 @@
|
|
|
16
16
|
// The Uint8Array returned by getBytes is a live subarray view of the container
|
|
17
17
|
// buffer. It is valid for as long as the Reader instance holds the container.
|
|
18
18
|
// Do NOT mutate it, and do NOT retain it past the Reader's lifetime.
|
|
19
|
+
//
|
|
20
|
+
// Error codes:
|
|
21
|
+
// R_BAD_MAGIC - header magic mismatch
|
|
22
|
+
// R_UNSUPPORTED_VERSION - format_version > 1
|
|
23
|
+
// R_UNSUPPORTED_ENDIAN - endian byte is not LE
|
|
24
|
+
// R_WRONG_MODE - schema-mode container fed to the PreserveReader
|
|
25
|
+
// R_BAD_FLAGS - unknown header flag bits set
|
|
26
|
+
// R_TRUNCATED - a structure claims bytes past the buffer end
|
|
27
|
+
// R_RESERVED_NONZERO - a reserved header/shard field is non-zero
|
|
28
|
+
// R_BAD_FOOTER - footer magic_end or footer_len is malformed
|
|
29
|
+
// R_INVALID - a structure is internally inconsistent but in-bounds
|
|
30
|
+
// R_SHARD_VERSION_TOO_NEW - a shard's min_reader_version > this reader
|
|
31
|
+
// R_ROW_OUT_OF_RANGE - rowIdx >= totalRows
|
|
32
|
+
|
|
33
|
+
import { toContainerBuffer } from './Views.js';
|
|
19
34
|
|
|
20
|
-
export const VERSION = '1.
|
|
35
|
+
export const VERSION = '1.3.1';
|
|
21
36
|
|
|
22
37
|
const CONTAINER_HEADER_BYTES = 48;
|
|
23
38
|
const SHARD_ENTRY_BYTES = 40;
|
|
39
|
+
const FOOTER_BYTES = 16;
|
|
24
40
|
|
|
25
41
|
export class PreserveReaderError extends Error {
|
|
26
42
|
constructor(code, msg) { super(msg); this.code = code; this.name = 'PreserveReaderError'; }
|
|
@@ -28,16 +44,7 @@ export class PreserveReaderError extends Error {
|
|
|
28
44
|
|
|
29
45
|
export class PreserveReader {
|
|
30
46
|
static fromBuffer(input) {
|
|
31
|
-
|
|
32
|
-
if (input instanceof Uint8Array) {
|
|
33
|
-
if (input.byteOffset === 0 && input.byteLength === input.buffer.byteLength) {
|
|
34
|
-
return new PreserveReader(input.buffer);
|
|
35
|
-
}
|
|
36
|
-
const copy = new Uint8Array(input.byteLength);
|
|
37
|
-
copy.set(input);
|
|
38
|
-
return new PreserveReader(copy.buffer);
|
|
39
|
-
}
|
|
40
|
-
throw new TypeError('PreserveReader.fromBuffer: expected ArrayBuffer or Uint8Array');
|
|
47
|
+
return new PreserveReader(toContainerBuffer(input, 'PreserveReader.fromBuffer'));
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
constructor(buffer) {
|
|
@@ -49,6 +56,7 @@ export class PreserveReader {
|
|
|
49
56
|
this._bytes = new Uint8Array(buffer);
|
|
50
57
|
this._decoder = new TextDecoder('utf-8', { fatal: false });
|
|
51
58
|
this._parseHeader();
|
|
59
|
+
this._parseFooter();
|
|
52
60
|
this._parseShardDirectory();
|
|
53
61
|
}
|
|
54
62
|
|
|
@@ -72,17 +80,40 @@ export class PreserveReader {
|
|
|
72
80
|
if (flags & ~0x01) {
|
|
73
81
|
throw new PreserveReaderError('R_BAD_FLAGS', 'unknown flag bits set: 0x' + flags.toString(16));
|
|
74
82
|
}
|
|
83
|
+
const reserved1 = this._dv.getUint32(36, true);
|
|
84
|
+
if (reserved1 !== 0)
|
|
85
|
+
throw new PreserveReaderError('R_RESERVED_NONZERO', 'header reserved1 at offset 36 must be 0, got ' + reserved1);
|
|
75
86
|
|
|
76
87
|
this._schemaBlockOff = Number(this._dv.getBigUint64(8, true));
|
|
77
88
|
this._metadataOff = Number(this._dv.getBigUint64(16, true));
|
|
78
89
|
this._shardDirOff = Number(this._dv.getBigUint64(24, true));
|
|
79
90
|
this._shardCount = this._dv.getUint32(32, true);
|
|
80
|
-
// 4 bytes reserved at 36
|
|
81
91
|
this._totalRows = Number(this._dv.getBigUint64(40, true));
|
|
82
92
|
|
|
83
93
|
if (this._schemaBlockOff !== 0) {
|
|
84
94
|
throw new PreserveReaderError('R_INVALID', 'preserve container has non-zero schema_block_off');
|
|
85
95
|
}
|
|
96
|
+
if (this._metadataOff !== 0) {
|
|
97
|
+
throw new PreserveReaderError('R_INVALID', 'preserve container has non-zero metadata_off (SPEC 3.8)');
|
|
98
|
+
}
|
|
99
|
+
const len = this._buffer.byteLength;
|
|
100
|
+
if (this._shardDirOff < CONTAINER_HEADER_BYTES || this._shardDirOff >= len)
|
|
101
|
+
throw new PreserveReaderError('R_TRUNCATED', 'shard_directory_off ' + this._shardDirOff + ' out of range [48, ' + len + ')');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Footer (SPEC 3.7): magic_end '1KBL' + footer_len >= 16.
|
|
105
|
+
_parseFooter() {
|
|
106
|
+
const len = this._buffer.byteLength;
|
|
107
|
+
const footerOff = len - FOOTER_BYTES;
|
|
108
|
+
const b = this._bytes;
|
|
109
|
+
if (b[footerOff + 8] !== 0x31 || b[footerOff + 9] !== 0x4B ||
|
|
110
|
+
b[footerOff + 10] !== 0x42 || b[footerOff + 11] !== 0x4C)
|
|
111
|
+
throw new PreserveReaderError('R_BAD_FOOTER', 'footer magic_end at offset ' + (footerOff + 8) + ' is not 1KBL');
|
|
112
|
+
const footerLen = this._dv.getUint32(footerOff + 12, true);
|
|
113
|
+
if (footerLen < FOOTER_BYTES)
|
|
114
|
+
throw new PreserveReaderError('R_BAD_FOOTER', 'footer_len ' + footerLen + ' is less than the minimum ' + FOOTER_BYTES);
|
|
115
|
+
if (footerLen > len - CONTAINER_HEADER_BYTES)
|
|
116
|
+
throw new PreserveReaderError('R_BAD_FOOTER', 'footer_len ' + footerLen + ' exceeds the container body size');
|
|
86
117
|
}
|
|
87
118
|
|
|
88
119
|
_parseShardDirectory() {
|
|
@@ -100,21 +131,46 @@ export class PreserveReader {
|
|
|
100
131
|
const payloadLen = this._dv.getUint32(entryOff + 8, true);
|
|
101
132
|
const rowCount = this._dv.getUint32(entryOff + 12, true);
|
|
102
133
|
const minReaderVer = this._dv.getUint16(entryOff + 16, true);
|
|
134
|
+
const shardFlags = this._dv.getUint16(entryOff + 18, true);
|
|
135
|
+
const shardReserved = this._dv.getUint32(entryOff + 20, true);
|
|
136
|
+
const localStrOff = Number(this._dv.getBigUint64(entryOff + 24, true));
|
|
137
|
+
const localStrLen = Number(this._dv.getBigUint64(entryOff + 32, true));
|
|
103
138
|
if (minReaderVer > 1) {
|
|
104
139
|
throw new PreserveReaderError('R_SHARD_VERSION_TOO_NEW',
|
|
105
140
|
'shard ' + i + ' requires reader version ' + minReaderVer);
|
|
106
141
|
}
|
|
142
|
+
if (shardFlags !== 0) throw new PreserveReaderError('R_RESERVED_NONZERO', 'shard ' + i + ' flags must be 0, got ' + shardFlags);
|
|
143
|
+
if (shardReserved !== 0) throw new PreserveReaderError('R_RESERVED_NONZERO', 'shard ' + i + ' reserved must be 0, got ' + shardReserved);
|
|
144
|
+
if (localStrOff !== 0 || localStrLen !== 0)
|
|
145
|
+
throw new PreserveReaderError('R_INVALID', 'shard ' + i + ' local_string_* must be 0 in preserve mode (SPEC 3.8)');
|
|
146
|
+
if (payloadOff < CONTAINER_HEADER_BYTES || payloadOff + payloadLen > this._buffer.byteLength)
|
|
147
|
+
throw new PreserveReaderError('R_TRUNCATED', 'shard ' + i + ' payload [' + payloadOff + ', ' + (payloadOff + payloadLen) + ') out of range');
|
|
107
148
|
// Trailing u32 offset table.
|
|
108
149
|
const offsetTableOff = payloadOff + payloadLen - rowCount * 4;
|
|
109
150
|
if (offsetTableOff < payloadOff) {
|
|
110
151
|
throw new PreserveReaderError('R_INVALID',
|
|
111
152
|
'shard ' + i + ' payload_len (' + payloadLen + ') too small for offset table of ' + rowCount + ' rows');
|
|
112
153
|
}
|
|
154
|
+
const blobLen = offsetTableOff - payloadOff;
|
|
155
|
+
// Offset-table walk: offsets[0] must be 0, non-decreasing, last <= blobLen.
|
|
156
|
+
if (rowCount > 0) {
|
|
157
|
+
let prev = this._dv.getUint32(offsetTableOff, true);
|
|
158
|
+
if (prev !== 0)
|
|
159
|
+
throw new PreserveReaderError('R_INVALID', 'shard ' + i + ' offset table[0] is ' + prev + ', must be 0');
|
|
160
|
+
for (let r = 1; r < rowCount; r++) {
|
|
161
|
+
const cur = this._dv.getUint32(offsetTableOff + r * 4, true);
|
|
162
|
+
if (cur < prev)
|
|
163
|
+
throw new PreserveReaderError('R_INVALID', 'shard ' + i + ' offset table not monotonic at record ' + r);
|
|
164
|
+
prev = cur;
|
|
165
|
+
}
|
|
166
|
+
if (prev > blobLen)
|
|
167
|
+
throw new PreserveReaderError('R_INVALID', 'shard ' + i + ' offset table last entry ' + prev + ' exceeds blob length ' + blobLen);
|
|
168
|
+
}
|
|
113
169
|
shards[i] = {
|
|
114
170
|
payloadOff,
|
|
115
171
|
payloadLen,
|
|
116
172
|
rowCount,
|
|
117
|
-
blobLen
|
|
173
|
+
blobLen,
|
|
118
174
|
offsetTableOff,
|
|
119
175
|
firstRow: cumulativeRow,
|
|
120
176
|
endRow: cumulativeRow + rowCount,
|
package/src/PreserveTokenizer.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
import { checkOpts } from './Opts.js';
|
|
34
34
|
|
|
35
|
-
export const VERSION = '1.
|
|
35
|
+
export const VERSION = '1.3.1';
|
|
36
36
|
|
|
37
37
|
const U32_MAX = 4294967295;
|
|
38
38
|
const PRESERVE_TOKENIZER_OPTS = {
|
|
@@ -88,23 +88,28 @@ export class PreserveTokenizer {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
feed(chunk) {
|
|
91
|
-
if (this._poisoned) throw new PreserveTokenizerError('E_POISONED', 'tokenizer poisoned by a previous error; construct a new instance');
|
|
91
|
+
if (this._poisoned) throw new PreserveTokenizerError('E_POISONED', 'tokenizer poisoned by a previous error or an in-flight feed(); construct a new instance');
|
|
92
92
|
if (this._ended) throw new PreserveTokenizerError('E_ENDED', 'tokenizer already ended; construct a new instance');
|
|
93
93
|
if (!(chunk instanceof Uint8Array)) {
|
|
94
94
|
throw new TypeError('PreserveTokenizer.feed: expected Uint8Array');
|
|
95
95
|
}
|
|
96
|
-
|
|
96
|
+
// D3 armed-flag: a sink throw inside _scan unwinds with the flag set,
|
|
97
|
+
// poisoning the instance. Cleared at every clean exit (empty chunk and end).
|
|
98
|
+
this._poisoned = true;
|
|
99
|
+
if (chunk.length === 0) { this._poisoned = false; return; }
|
|
97
100
|
this._ensureCapacity(this._bufLen + chunk.length);
|
|
98
101
|
this._buf.set(chunk, this._bufLen);
|
|
99
102
|
const scanStart = this._bufLen;
|
|
100
103
|
this._bufLen += chunk.length;
|
|
101
104
|
this._scan(scanStart);
|
|
102
105
|
this._compact();
|
|
106
|
+
this._poisoned = false;
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
end() {
|
|
106
|
-
if (this._poisoned) throw new PreserveTokenizerError('E_POISONED', 'tokenizer poisoned by a previous error; construct a new instance');
|
|
110
|
+
if (this._poisoned) throw new PreserveTokenizerError('E_POISONED', 'tokenizer poisoned by a previous error or an in-flight feed(); construct a new instance');
|
|
107
111
|
if (this._ended) throw new PreserveTokenizerError('E_ENDED', 'tokenizer already ended; construct a new instance');
|
|
112
|
+
this._poisoned = true; // D3: armed until the clean exit below
|
|
108
113
|
if (this._bufLen > this._recordStart) {
|
|
109
114
|
// There's pending content that never terminated.
|
|
110
115
|
if (this._depth !== 0 || this._inString || this._escape) {
|
|
@@ -119,6 +124,7 @@ export class PreserveTokenizer {
|
|
|
119
124
|
this._recordStart = this._bufLen;
|
|
120
125
|
}
|
|
121
126
|
this._ended = true;
|
|
127
|
+
this._poisoned = false; // clean end; a throwing onRecord above stays poisoned
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
_scan(from) {
|
package/src/PreserveWriter.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { checkOpts } from './Opts.js';
|
|
20
20
|
|
|
21
|
-
export const VERSION = '1.
|
|
21
|
+
export const VERSION = '1.3.1';
|
|
22
22
|
|
|
23
23
|
export class PreserveWriterError extends Error {
|
|
24
24
|
constructor(code, msg) { super(msg); this.code = code; this.name = 'PreserveWriterError'; }
|
package/src/RangeReader.js
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
// table offsets exist precisely so a client can fetch only what it needs.
|
|
10
10
|
//
|
|
11
11
|
// RangeReader implements that story:
|
|
12
|
-
// 1. On open, fetch the header (
|
|
13
|
-
// That gives us the whole schema and every shard's byte extents
|
|
14
|
-
// touching a single row.
|
|
12
|
+
// 1. On open, fetch the header (48 bytes) + footer + schema block + shard
|
|
13
|
+
// directory. That gives us the whole schema and every shard's byte extents
|
|
14
|
+
// without touching a single row.
|
|
15
15
|
// 2. On get(rowIdx, fieldName), locate the shard containing rowIdx, fetch
|
|
16
16
|
// its payload + local string table in ONE range request (they are
|
|
17
17
|
// contiguous by SPEC 3.4), cache it, decode.
|
|
@@ -32,11 +32,17 @@
|
|
|
32
32
|
// R_UNSUPPORTED_LANE / R_SHARD_VERSION_TOO_NEW / R_TRUNCATED / R_UNKNOWN_FIELD
|
|
33
33
|
// R_ADAPTER_SHORT_READ -- adapter returned fewer bytes than requested
|
|
34
34
|
// R_ROW_OUT_OF_RANGE -- rowIdx >= totalRows
|
|
35
|
+
// R_WRONG_MODE -- preserve-mode container fed to the schema RangeReader
|
|
36
|
+
// R_BAD_FLAGS -- unknown header flag bits set
|
|
37
|
+
// R_RESERVED_NONZERO -- a reserved header/descriptor/shard field is non-zero
|
|
38
|
+
// R_BAD_FOOTER -- footer magic_end or footer_len is malformed
|
|
39
|
+
// R_BAD_METADATA -- metadata_off non-zero but the zone-map segment is unparseable
|
|
40
|
+
// R_INVALID -- a structure is internally inconsistent but in-bounds
|
|
35
41
|
|
|
36
42
|
import { StringTable } from './StringTable.js';
|
|
37
43
|
import { checkOpts } from './Opts.js';
|
|
38
44
|
|
|
39
|
-
export const VERSION = '1.
|
|
45
|
+
export const VERSION = '1.3.1';
|
|
40
46
|
|
|
41
47
|
const CONTAINER_HEADER_BYTES = 48;
|
|
42
48
|
const SHARD_ENTRY_BYTES = 40;
|
|
@@ -50,11 +56,40 @@ const U32_MAX = 4294967295;
|
|
|
50
56
|
const RANGE_READER_OPTS = { maxCachedShards: { t: 'int', min: 0, max: U32_MAX } };
|
|
51
57
|
const HTTP_ADAPTER_OPTS = { fetch: { t: 'fn' } };
|
|
52
58
|
|
|
59
|
+
const CONTAINER_FOOTER_BYTES = 16;
|
|
60
|
+
|
|
61
|
+
function laneBytesOf(k) { return k === LANE_F64 ? 8 : (k === LANE_U32 ? 4 : 0); }
|
|
62
|
+
|
|
53
63
|
export class RangeReaderError extends Error {
|
|
54
64
|
constructor(code, msg) { super(msg); this.code = code; this.name = 'RangeReaderError'; }
|
|
55
65
|
}
|
|
56
66
|
function raiseRange(code, msg) { throw new RangeReaderError(code, msg); }
|
|
57
67
|
|
|
68
|
+
// Validate a local string table's shape before StringTable.parse casts a
|
|
69
|
+
// Uint32Array over it (T-1..T-4). `bytes` is the already-fetched shard slice;
|
|
70
|
+
// `off` and `len` locate the table within it.
|
|
71
|
+
function validateStringTable(bytes, off, len, label) {
|
|
72
|
+
if (len < 8)
|
|
73
|
+
throw new RangeReaderError('R_TRUNCATED', label + ' string table shorter than its 8-byte header');
|
|
74
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset + off, len);
|
|
75
|
+
const entryCount = dv.getUint32(0, true);
|
|
76
|
+
const blobLen = dv.getUint32(4, true);
|
|
77
|
+
if (8 + (entryCount + 1) * 4 + blobLen > len)
|
|
78
|
+
throw new RangeReaderError('R_TRUNCATED', label + ' string table claims ' + entryCount +
|
|
79
|
+
' entries + ' + blobLen + ' blob bytes past its ' + len + '-byte extent');
|
|
80
|
+
let prev = dv.getUint32(8, true);
|
|
81
|
+
if (prev !== 0)
|
|
82
|
+
throw new RangeReaderError('R_INVALID', label + ' string table offsets[0] is ' + prev + ', must be 0');
|
|
83
|
+
for (let i = 1; i <= entryCount; i++) {
|
|
84
|
+
const cur = dv.getUint32(8 + i * 4, true);
|
|
85
|
+
if (cur < prev)
|
|
86
|
+
throw new RangeReaderError('R_INVALID', label + ' string table offsets not monotonic at index ' + i);
|
|
87
|
+
prev = cur;
|
|
88
|
+
}
|
|
89
|
+
if (prev !== blobLen)
|
|
90
|
+
throw new RangeReaderError('R_INVALID', label + ' string table sentinel ' + prev + ' != blob_length ' + blobLen);
|
|
91
|
+
}
|
|
92
|
+
|
|
58
93
|
// ----- Adapters --------------------------------------------------------------
|
|
59
94
|
|
|
60
95
|
// Uses fetch() with Range headers. Content-Length is discovered on open() via
|
|
@@ -169,7 +204,7 @@ export class RangeReader {
|
|
|
169
204
|
throw new RangeReaderError('R_TRUNCATED', 'adapter size smaller than header');
|
|
170
205
|
}
|
|
171
206
|
|
|
172
|
-
// Step 1: header (
|
|
207
|
+
// Step 1: header (48 bytes) tells us schema + shard-dir offsets.
|
|
173
208
|
const headerBytes = await this.adapter.fetch(0, CONTAINER_HEADER_BYTES);
|
|
174
209
|
const hdrDv = new DataView(headerBytes.buffer, headerBytes.byteOffset, headerBytes.byteLength);
|
|
175
210
|
if (headerBytes[0] !== 0x4C || headerBytes[1] !== 0x42 || headerBytes[2] !== 0x4B || headerBytes[3] !== 0x31) {
|
|
@@ -179,21 +214,48 @@ export class RangeReader {
|
|
|
179
214
|
if (version > 1) throw new RangeReaderError('R_UNSUPPORTED_VERSION', 'format_version=' + version);
|
|
180
215
|
const endian = headerBytes[6];
|
|
181
216
|
if (endian !== 1) throw new RangeReaderError('R_UNSUPPORTED_ENDIAN', 'BE payloads not implemented in v1 reader');
|
|
217
|
+
const flags = headerBytes[7];
|
|
218
|
+
if (flags & 0x01) {
|
|
219
|
+
throw new RangeReaderError('R_WRONG_MODE',
|
|
220
|
+
'container is preserve-mode; RangeReader is unsupported in preserve mode (SPEC 3.8) -- use PreserveReader');
|
|
221
|
+
}
|
|
222
|
+
if (flags & ~0x01) {
|
|
223
|
+
throw new RangeReaderError('R_BAD_FLAGS', 'unknown flag bits set in header byte 7: 0x' + flags.toString(16));
|
|
224
|
+
}
|
|
225
|
+
const reserved1 = hdrDv.getUint32(36, true);
|
|
226
|
+
if (reserved1 !== 0)
|
|
227
|
+
throw new RangeReaderError('R_RESERVED_NONZERO', 'header reserved1 at offset 36 must be 0, got ' + reserved1);
|
|
182
228
|
|
|
183
229
|
this._schemaBlockOff = Number(hdrDv.getBigUint64(8, true));
|
|
184
230
|
this._metadataOff = Number(hdrDv.getBigUint64(16, true));
|
|
185
231
|
this._shardDirOff = Number(hdrDv.getBigUint64(24, true));
|
|
186
232
|
this._shardCount = hdrDv.getUint32(32, true);
|
|
187
|
-
// 4 bytes reserved at 36
|
|
188
233
|
this._totalRows = Number(hdrDv.getBigUint64(40, true));
|
|
189
234
|
|
|
235
|
+
const size = this.adapter.size;
|
|
236
|
+
if (this._schemaBlockOff < CONTAINER_HEADER_BYTES || this._schemaBlockOff >= size)
|
|
237
|
+
throw new RangeReaderError('R_TRUNCATED', 'schema_block_off ' + this._schemaBlockOff + ' out of range [48, ' + size + ')');
|
|
238
|
+
if (this._shardDirOff < CONTAINER_HEADER_BYTES || this._shardDirOff >= size)
|
|
239
|
+
throw new RangeReaderError('R_TRUNCATED', 'shard_directory_off ' + this._shardDirOff + ' out of range [48, ' + size + ')');
|
|
240
|
+
if (this._metadataOff !== 0 && (this._metadataOff < CONTAINER_HEADER_BYTES || this._metadataOff >= size))
|
|
241
|
+
throw new RangeReaderError('R_TRUNCATED', 'metadata_off ' + this._metadataOff + ' out of range {0} u [48, ' + size + ')');
|
|
242
|
+
if (this._shardCount * SHARD_ENTRY_BYTES > size - CONTAINER_HEADER_BYTES)
|
|
243
|
+
throw new RangeReaderError('R_TRUNCATED', 'shard_count ' + this._shardCount + ' exceeds the bytes available for a shard directory');
|
|
244
|
+
|
|
245
|
+
// Footer: one 16-byte suffix fetch. Refuse a wrong-footer container before
|
|
246
|
+
// spending fetches on the schema. Gated on a body big enough to hold a
|
|
247
|
+
// header and a footer that do not overlap.
|
|
248
|
+
if (size >= CONTAINER_HEADER_BYTES + CONTAINER_FOOTER_BYTES) await this._loadFooter();
|
|
249
|
+
|
|
190
250
|
// Step 2: schema block. We know its start but not its length; compute
|
|
191
251
|
// upper bound: schemaBlockOff .. shardDirOff.
|
|
252
|
+
if (this._shardDirOff <= this._schemaBlockOff)
|
|
253
|
+
throw new RangeReaderError('R_TRUNCATED', 'shard_directory_off ' + this._shardDirOff + ' not after schema_block_off ' + this._schemaBlockOff);
|
|
192
254
|
const schemaBlockLen = this._shardDirOff - this._schemaBlockOff;
|
|
193
255
|
const schemaBytes = await this.adapter.fetch(this._schemaBlockOff, schemaBlockLen);
|
|
194
256
|
this._parseSchema(schemaBytes);
|
|
195
257
|
|
|
196
|
-
// Step 3: shard directory (fixed size = shardCount *
|
|
258
|
+
// Step 3: shard directory (fixed size = shardCount * 40).
|
|
197
259
|
const dirBytes = await this.adapter.fetch(this._shardDirOff, this._shardCount * SHARD_ENTRY_BYTES);
|
|
198
260
|
this._parseShardDirectory(dirBytes);
|
|
199
261
|
|
|
@@ -210,21 +272,43 @@ export class RangeReader {
|
|
|
210
272
|
}
|
|
211
273
|
}
|
|
212
274
|
|
|
275
|
+
async _loadFooter() {
|
|
276
|
+
const size = this.adapter.size;
|
|
277
|
+
const footer = await this.adapter.fetch(size - CONTAINER_FOOTER_BYTES, CONTAINER_FOOTER_BYTES);
|
|
278
|
+
if (footer[8] !== 0x31 || footer[9] !== 0x4B || footer[10] !== 0x42 || footer[11] !== 0x4C)
|
|
279
|
+
throw new RangeReaderError('R_BAD_FOOTER', 'footer magic_end is not 1KBL');
|
|
280
|
+
const fDv = new DataView(footer.buffer, footer.byteOffset, footer.byteLength);
|
|
281
|
+
const footerLen = fDv.getUint32(12, true);
|
|
282
|
+
if (footerLen < CONTAINER_FOOTER_BYTES)
|
|
283
|
+
throw new RangeReaderError('R_BAD_FOOTER', 'footer_len ' + footerLen + ' is less than the minimum ' + CONTAINER_FOOTER_BYTES);
|
|
284
|
+
if (footerLen > size - CONTAINER_HEADER_BYTES)
|
|
285
|
+
throw new RangeReaderError('R_BAD_FOOTER', 'footer_len ' + footerLen + ' exceeds the container body size');
|
|
286
|
+
}
|
|
287
|
+
|
|
213
288
|
async _loadZoneMaps() {
|
|
214
|
-
// Fetch the segment header first (16 bytes) so we can size the rest.
|
|
289
|
+
// Fetch the segment header first (16 bytes) so we can size the rest. A
|
|
290
|
+
// non-zero metadata_off asserts a parseable segment; any failure is
|
|
291
|
+
// corruption (D1), not absence.
|
|
292
|
+
if (this._metadataOff + 16 > this.adapter.size)
|
|
293
|
+
throw new RangeReaderError('R_BAD_METADATA', 'zone maps segment header runs past the container');
|
|
215
294
|
const hdr = await this.adapter.fetch(this._metadataOff, 16);
|
|
216
|
-
if (hdr[0] !== 0x30 || hdr[1] !== 0x5A || hdr[2] !== 0x4D || hdr[3] !== 0x31)
|
|
295
|
+
if (hdr[0] !== 0x30 || hdr[1] !== 0x5A || hdr[2] !== 0x4D || hdr[3] !== 0x31)
|
|
296
|
+
throw new RangeReaderError('R_BAD_METADATA', 'zone maps magic is not ZM01');
|
|
217
297
|
const hdrDv = new DataView(hdr.buffer, hdr.byteOffset, hdr.byteLength);
|
|
218
298
|
const shardCount = hdrDv.getUint32(4, true);
|
|
219
299
|
const T = hdrDv.getUint32(8, true);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
300
|
+
const reserved0 = hdrDv.getUint32(12, true);
|
|
301
|
+
if (shardCount !== this._shardCount)
|
|
302
|
+
throw new RangeReaderError('R_BAD_METADATA', 'zone maps shard_count ' + shardCount + ' != header ' + this._shardCount);
|
|
303
|
+
if (reserved0 !== 0)
|
|
304
|
+
throw new RangeReaderError('R_BAD_METADATA', 'zone maps reserved0 must be 0, got ' + reserved0);
|
|
223
305
|
if (T === 0) return;
|
|
224
306
|
// Fetch the rest in one range: field table + pad + mins + maxes.
|
|
225
307
|
const fieldTableLen = T * 2;
|
|
226
308
|
const fieldTablePad = (8 - (fieldTableLen & 7)) & 7;
|
|
227
309
|
const restLen = fieldTableLen + fieldTablePad + shardCount * T * 8 * 2;
|
|
310
|
+
if (this._metadataOff + 16 + restLen > this.adapter.size)
|
|
311
|
+
throw new RangeReaderError('R_BAD_METADATA', 'zone maps segment runs past the container');
|
|
228
312
|
const rest = await this.adapter.fetch(this._metadataOff + 16, restLen);
|
|
229
313
|
const restDv = new DataView(rest.buffer, rest.byteOffset, rest.byteLength);
|
|
230
314
|
const tracked = new Array(T);
|
|
@@ -232,7 +316,7 @@ export class RangeReader {
|
|
|
232
316
|
for (let t = 0; t < T; t++) {
|
|
233
317
|
const schemaFieldIdx = restDv.getUint16(t * 2, true);
|
|
234
318
|
if (schemaFieldIdx >= this._schema.fields.length) {
|
|
235
|
-
throw new RangeReaderError('
|
|
319
|
+
throw new RangeReaderError('R_BAD_METADATA', 'zone maps references field index ' + schemaFieldIdx + ' past the schema');
|
|
236
320
|
}
|
|
237
321
|
tracked[t] = schemaFieldIdx;
|
|
238
322
|
fieldToPos.set(this._schema.fields[schemaFieldIdx].name, t);
|
|
@@ -250,13 +334,20 @@ export class RangeReader {
|
|
|
250
334
|
|
|
251
335
|
_parseSchema(bytes) {
|
|
252
336
|
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
337
|
+
const total = bytes.byteLength;
|
|
338
|
+
if (total < 8) throw new RangeReaderError('R_TRUNCATED', 'schema block shorter than its 8-byte header');
|
|
253
339
|
const fieldCount = dv.getUint32(0, true);
|
|
254
340
|
const rowStride = dv.getUint32(4, true);
|
|
341
|
+
if (rowStride < 1) throw new RangeReaderError('R_INVALID', 'schema row_stride is 0');
|
|
255
342
|
const descOff = 8;
|
|
256
343
|
const descBytes = fieldCount * FIELD_DESCRIPTOR_BYTES;
|
|
344
|
+
if (descOff + descBytes + 4 > total)
|
|
345
|
+
throw new RangeReaderError('R_TRUNCATED', 'schema descriptors run past the fetched schema block');
|
|
257
346
|
const nameBlobLenOff = descOff + descBytes;
|
|
258
347
|
const nameBlobLen = dv.getUint32(nameBlobLenOff, true);
|
|
259
348
|
const nameBlobOff = nameBlobLenOff + 4;
|
|
349
|
+
if (nameBlobOff + nameBlobLen > total)
|
|
350
|
+
throw new RangeReaderError('R_TRUNCATED', 'schema name blob runs past the fetched schema block');
|
|
260
351
|
const decoder = new TextDecoder('utf-8');
|
|
261
352
|
const fields = new Array(fieldCount);
|
|
262
353
|
for (let i = 0; i < fieldCount; i++) {
|
|
@@ -265,10 +356,19 @@ export class RangeReader {
|
|
|
265
356
|
const offsetInRow = dv.getUint16(off + 2, true);
|
|
266
357
|
const laneKind = bytes[off + 4];
|
|
267
358
|
const flags = bytes[off + 5];
|
|
359
|
+
const reserved2 = dv.getUint16(off + 6, true);
|
|
268
360
|
const nameStrOff = Number(dv.getBigUint64(off + 8, true));
|
|
361
|
+
const reserved3 = dv.getBigUint64(off + 16, true);
|
|
269
362
|
if (flags !== 0) throw new RangeReaderError('R_BAD_FIELD_FLAGS', 'field ' + i + ' has non-zero flags');
|
|
363
|
+
if (reserved2 !== 0) throw new RangeReaderError('R_RESERVED_NONZERO', 'field ' + i + ' reserved2 must be 0, got ' + reserved2);
|
|
364
|
+
if (reserved3 !== 0n) throw new RangeReaderError('R_RESERVED_NONZERO', 'field ' + i + ' reserved3 must be 0, got ' + reserved3);
|
|
270
365
|
if (laneKind !== LANE_F64 && laneKind !== LANE_U32)
|
|
271
366
|
throw new RangeReaderError('R_UNSUPPORTED_LANE', 'field ' + i + ' lane_kind=' + laneKind);
|
|
367
|
+
if (nameLen > 255) throw new RangeReaderError('R_INVALID', 'field ' + i + ' name_len ' + nameLen + ' exceeds 255');
|
|
368
|
+
if (offsetInRow + laneBytesOf(laneKind) > rowStride)
|
|
369
|
+
throw new RangeReaderError('R_INVALID', 'field ' + i + ' offset_in_row ' + offsetInRow + ' + lane bytes exceeds row_stride ' + rowStride);
|
|
370
|
+
if (nameStrOff + nameLen > nameBlobLen)
|
|
371
|
+
throw new RangeReaderError('R_TRUNCATED', 'field ' + i + ' name range past the name blob');
|
|
272
372
|
const nb = bytes.subarray(nameBlobOff + nameStrOff, nameBlobOff + nameStrOff + nameLen);
|
|
273
373
|
fields[i] = { name: decoder.decode(nb), laneKind, offsetInRow };
|
|
274
374
|
}
|
|
@@ -278,6 +378,8 @@ export class RangeReader {
|
|
|
278
378
|
_parseShardDirectory(bytes) {
|
|
279
379
|
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
280
380
|
const shards = new Array(this._shardCount);
|
|
381
|
+
const size = this.adapter.size;
|
|
382
|
+
const rowStride = this._schema.rowStride;
|
|
281
383
|
let cumulativeRow = 0;
|
|
282
384
|
for (let i = 0; i < this._shardCount; i++) {
|
|
283
385
|
const off = i * SHARD_ENTRY_BYTES;
|
|
@@ -285,12 +387,25 @@ export class RangeReader {
|
|
|
285
387
|
const payloadLen = dv.getUint32(off + 8, true);
|
|
286
388
|
const rowCount = dv.getUint32(off + 12, true);
|
|
287
389
|
const minReaderVer = dv.getUint16(off + 16, true);
|
|
288
|
-
|
|
390
|
+
const shardFlags = dv.getUint16(off + 18, true);
|
|
391
|
+
const shardReserved = dv.getUint32(off + 20, true);
|
|
289
392
|
const localStrOff = Number(dv.getBigUint64(off + 24, true));
|
|
290
393
|
const localStrLen = Number(dv.getBigUint64(off + 32, true));
|
|
291
394
|
if (minReaderVer > READER_VERSION)
|
|
292
395
|
throw new RangeReaderError('R_SHARD_VERSION_TOO_NEW',
|
|
293
396
|
'shard ' + i + ' requires reader version ' + minReaderVer);
|
|
397
|
+
if (shardFlags !== 0) throw new RangeReaderError('R_RESERVED_NONZERO', 'shard ' + i + ' flags must be 0, got ' + shardFlags);
|
|
398
|
+
if (shardReserved !== 0) throw new RangeReaderError('R_RESERVED_NONZERO', 'shard ' + i + ' reserved must be 0, got ' + shardReserved);
|
|
399
|
+
if (payloadOff < CONTAINER_HEADER_BYTES)
|
|
400
|
+
throw new RangeReaderError('R_INVALID', 'shard ' + i + ' payload_off ' + payloadOff + ' overlaps the header');
|
|
401
|
+
if (payloadOff + payloadLen > size)
|
|
402
|
+
throw new RangeReaderError('R_TRUNCATED', 'shard ' + i + ' payload past the container size ' + size);
|
|
403
|
+
if (rowCount * rowStride > payloadLen)
|
|
404
|
+
throw new RangeReaderError('R_INVALID', 'shard ' + i + ' row_count ' + rowCount + ' * row_stride ' + rowStride + ' exceeds payload_len ' + payloadLen);
|
|
405
|
+
if (localStrLen === 0 && localStrOff !== 0)
|
|
406
|
+
throw new RangeReaderError('R_INVALID', 'shard ' + i + ' has local_string_len 0 but local_string_off ' + localStrOff);
|
|
407
|
+
if (localStrLen > 0 && localStrOff + localStrLen > size)
|
|
408
|
+
throw new RangeReaderError('R_TRUNCATED', 'shard ' + i + ' string table past the container size ' + size);
|
|
294
409
|
shards[i] = {
|
|
295
410
|
payloadOff, payloadLen, rowCount, localStrOff, localStrLen,
|
|
296
411
|
firstRow: cumulativeRow,
|
|
@@ -298,6 +413,8 @@ export class RangeReader {
|
|
|
298
413
|
};
|
|
299
414
|
cumulativeRow += rowCount;
|
|
300
415
|
}
|
|
416
|
+
if (cumulativeRow !== this._totalRows)
|
|
417
|
+
throw new RangeReaderError('R_INVALID', 'shard row_count sum ' + cumulativeRow + ' != header total_rows ' + this._totalRows);
|
|
301
418
|
this._shards = shards;
|
|
302
419
|
}
|
|
303
420
|
|
|
@@ -354,6 +471,7 @@ export class RangeReader {
|
|
|
354
471
|
let stringTable = null;
|
|
355
472
|
if (s.localStrLen > 0) {
|
|
356
473
|
const stBytes = combined.subarray(s.payloadLen, s.payloadLen + s.localStrLen);
|
|
474
|
+
validateStringTable(stBytes, 0, s.localStrLen, 'shard ' + shardIdx);
|
|
357
475
|
stringTable = StringTable.parse(stBytes, 0);
|
|
358
476
|
}
|
|
359
477
|
const record = {
|
|
@@ -448,6 +566,8 @@ export class RangeReader {
|
|
|
448
566
|
// the container has no zone maps or the field is not tracked. Synchronous
|
|
449
567
|
// -- zone maps were fetched during open().
|
|
450
568
|
shardBounds(shardIdx, fieldName) {
|
|
569
|
+
if (!this._fieldIndex.has(fieldName))
|
|
570
|
+
throw new RangeReaderError('R_UNKNOWN_FIELD', 'no field named ' + JSON.stringify(fieldName));
|
|
451
571
|
if (!this._zoneMapsTrackedFields) return null;
|
|
452
572
|
const t = this._zoneMapsFieldToPos.get(fieldName);
|
|
453
573
|
if (t === undefined) return null;
|
|
@@ -460,6 +580,8 @@ export class RangeReader {
|
|
|
460
580
|
// When no zone maps are present or the field is not tracked, returns every
|
|
461
581
|
// shard (query planner must fall back to full scan).
|
|
462
582
|
findShards(fieldName, opts) {
|
|
583
|
+
if (!this._fieldIndex.has(fieldName))
|
|
584
|
+
throw new RangeReaderError('R_UNKNOWN_FIELD', 'no field named ' + JSON.stringify(fieldName));
|
|
463
585
|
const all = () => { const a = new Array(this._shardCount); for (let i = 0; i < this._shardCount; i++) a[i] = i; return a; };
|
|
464
586
|
if (!this._zoneMapsTrackedFields) return all();
|
|
465
587
|
const t = this._zoneMapsFieldToPos.get(fieldName);
|
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.1';
|
|
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
|
@@ -37,7 +37,7 @@ import { Reader, ReaderError } from './Reader.js';
|
|
|
37
37
|
import { StringTable } from './StringTable.js';
|
|
38
38
|
import { checkOpts } from './Opts.js';
|
|
39
39
|
|
|
40
|
-
export const VERSION = '1.
|
|
40
|
+
export const VERSION = '1.3.1';
|
|
41
41
|
|
|
42
42
|
const LF = 0x0A;
|
|
43
43
|
const CONTAINER_HEADER_BYTES = 48;
|
|
@@ -45,6 +45,7 @@ const SHARD_ENTRY_BYTES = 40;
|
|
|
45
45
|
const FIELD_DESCRIPTOR_BYTES = 24;
|
|
46
46
|
const FOOTER_BYTES = 16;
|
|
47
47
|
const LANE_F64 = 1;
|
|
48
|
+
const LANE_U32 = 3;
|
|
48
49
|
const U32_MAX = 4294967295;
|
|
49
50
|
|
|
50
51
|
const SPLIT_NDJSON_OPTS = {
|
|
@@ -201,6 +202,16 @@ export function mergeContainers(containers) {
|
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
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
|
+
|
|
204
215
|
// Total shards and rows
|
|
205
216
|
let totalShards = 0, totalRows = 0;
|
|
206
217
|
for (const r of readers) { totalShards += r.shardCount; totalRows += r.totalRows; }
|
|
@@ -246,7 +257,7 @@ export function mergeContainers(containers) {
|
|
|
246
257
|
// Source shard payload lives at shard.payloadOff in the source container.
|
|
247
258
|
const payloadLen = shard.payloadLen;
|
|
248
259
|
const payloadPad = (8 - (payloadLen & 7)) & 7;
|
|
249
|
-
const stringLen = shard.stringTable ? _sourceStringTableLen(readers[ci], si) : 0;
|
|
260
|
+
const stringLen = (mergedHasU32 && shard.stringTable) ? _sourceStringTableLen(readers[ci], si) : 0;
|
|
250
261
|
const outPayloadOff = cursor;
|
|
251
262
|
const outStringOff = outPayloadOff + payloadLen + payloadPad;
|
|
252
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.1';
|
|
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
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
// E_INVALID_ESCAPE - unknown \ sequence
|
|
11
11
|
// E_INVALID_HEX - non-hex digit in \uXXXX
|
|
12
12
|
// E_UNPAIRED_SURROGATE - lone high/low surrogate in \u escape
|
|
13
|
-
// E_INVALID_UTF8 - malformed UTF-8 byte sequence
|
|
14
13
|
// E_NUMBER_OVERFLOW - number magnitude exceeds F64 range
|
|
15
14
|
// E_NUMBER_INVALID - malformed number literal
|
|
16
15
|
// E_KEYWORD_MISMATCH - true/false/null spelled wrong
|
|
@@ -24,7 +23,7 @@
|
|
|
24
23
|
|
|
25
24
|
import { checkOpts } from './Opts.js';
|
|
26
25
|
|
|
27
|
-
export const VERSION = '1.
|
|
26
|
+
export const VERSION = '1.3.1';
|
|
28
27
|
|
|
29
28
|
const U32_MAX = 4294967295;
|
|
30
29
|
const TOKENIZER_OPTS = {
|
|
@@ -181,8 +180,12 @@ export class Tokenizer {
|
|
|
181
180
|
|
|
182
181
|
// Feed a chunk. Bytes are consumed synchronously; sink events fire during this call.
|
|
183
182
|
feed(chunk) {
|
|
184
|
-
if (this._poisoned) throw new TokenizerError('E_POISONED', this._absOffset, 'tokenizer poisoned by a previous error; construct a new instance');
|
|
183
|
+
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
184
|
if (this._ended) throw new TokenizerError('E_ENDED', this._absOffset, 'tokenizer already ended; construct a new instance');
|
|
185
|
+
// D3 armed-flag: a sink throw mid-feed unwinds through here with the flag
|
|
186
|
+
// set, poisoning the instance. Cleared on the successful fall-through below.
|
|
187
|
+
// Two boolean stores per chunk; the byte loop is untouched.
|
|
188
|
+
this._poisoned = true;
|
|
186
189
|
const len = chunk.length;
|
|
187
190
|
let i = 0;
|
|
188
191
|
while (i < len) {
|
|
@@ -342,11 +345,13 @@ export class Tokenizer {
|
|
|
342
345
|
// unreachable
|
|
343
346
|
this._err('E_UNEXPECTED_BYTE', 'internal: unknown state ' + st);
|
|
344
347
|
}
|
|
348
|
+
this._poisoned = false; // D3: clean fall-through un-arms the flag
|
|
345
349
|
}
|
|
346
350
|
|
|
347
351
|
end() {
|
|
348
|
-
if (this._poisoned) throw new TokenizerError('E_POISONED', this._absOffset, 'tokenizer poisoned by a previous error; construct a new instance');
|
|
352
|
+
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');
|
|
349
353
|
if (this._ended) throw new TokenizerError('E_ENDED', this._absOffset, 'tokenizer already ended; construct a new instance');
|
|
354
|
+
this._poisoned = true; // D3: armed until a clean exit below
|
|
350
355
|
// A trailing number may still be pending
|
|
351
356
|
if (this._state === S_NUMBER) {
|
|
352
357
|
this._emitNumber();
|
|
@@ -357,6 +362,7 @@ export class Tokenizer {
|
|
|
357
362
|
}
|
|
358
363
|
this._ended = true;
|
|
359
364
|
this._onEnd();
|
|
365
|
+
this._poisoned = false; // clear AFTER _onEnd(): a throwing onEnd stays poisoned
|
|
360
366
|
return;
|
|
361
367
|
}
|
|
362
368
|
this._err('E_UNEXPECTED_EOF', 'input ended mid-token (state=' + this._state + ')');
|
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
|
+
}
|
package/src/Writer.js
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
import { StringTable } from './StringTable.js';
|
|
37
37
|
import { checkOpts } from './Opts.js';
|
|
38
38
|
|
|
39
|
-
export const VERSION = '1.
|
|
39
|
+
export const VERSION = '1.3.1';
|
|
40
40
|
|
|
41
41
|
const U32_MAX = 4294967295;
|
|
42
42
|
// Post-finalize sentinel for _recordDepth. Chosen = 2 so every post-finalize
|
|
@@ -64,6 +64,11 @@ const FIELD_FLAGS_NONE = 0;
|
|
|
64
64
|
const SHARD_MIN_READER_VERSION = 1;
|
|
65
65
|
const SHARD_FLAGS_NONE = 0;
|
|
66
66
|
|
|
67
|
+
// Shared empty view for F64-only shards, which emit NO local string table
|
|
68
|
+
// (SPEC 3.4: local_string_off/local_string_len are 0 when a shard has no
|
|
69
|
+
// U32-lane fields). Read-only; never written into.
|
|
70
|
+
const EMPTY_STRING_TABLE_BYTES = new Uint8Array(0);
|
|
71
|
+
|
|
67
72
|
const FORMAT_VERSION = 1;
|
|
68
73
|
const ENDIAN_LE = 1;
|
|
69
74
|
|
|
@@ -359,6 +364,12 @@ export class Writer {
|
|
|
359
364
|
|
|
360
365
|
_finalizeSchema(fields, rowStride) {
|
|
361
366
|
this._schema = { fields, rowStride };
|
|
367
|
+
// BS-15: a shard with no U32-lane field emits NO local string table.
|
|
368
|
+
// Computed once at freeze, consulted per shard at finalize / assembly.
|
|
369
|
+
this._hasU32 = false;
|
|
370
|
+
for (let i = 0; i < fields.length; i++) {
|
|
371
|
+
if (fields[i].laneKind === LANE_U32) { this._hasU32 = true; break; }
|
|
372
|
+
}
|
|
362
373
|
|
|
363
374
|
const enc = new TextEncoder();
|
|
364
375
|
this._fieldNamesUtf8 = new Array(fields.length);
|
|
@@ -492,9 +503,11 @@ export class Writer {
|
|
|
492
503
|
const usedBytes = this._currentShardRowCount * this._schema.rowStride;
|
|
493
504
|
const copy = new Uint8Array(usedBytes);
|
|
494
505
|
copy.set(this._currentShardBytes.subarray(0, usedBytes));
|
|
495
|
-
// Serialize the current shard's string table
|
|
506
|
+
// Serialize the current shard's string table. A shard whose schema has no
|
|
507
|
+
// U32 lane emits NO table at all (BS-15 / SPEC 3.4): zero bytes here means
|
|
508
|
+
// the ShardEntry carries local_string_off = 0 / local_string_len = 0.
|
|
496
509
|
const st = this._perShardStringTable || this._stringTable;
|
|
497
|
-
const
|
|
510
|
+
const stBytes = this._hasU32 ? st.serialize().bytes : EMPTY_STRING_TABLE_BYTES;
|
|
498
511
|
// Snapshot zone-map bounds for this shard. Copy so subsequent shards can
|
|
499
512
|
// reuse _currentShardMins/Maxes without clobbering the pushed record.
|
|
500
513
|
const T = this._trackedFieldIndices.length;
|
|
@@ -505,7 +518,7 @@ export class Writer {
|
|
|
505
518
|
this._shards.push({
|
|
506
519
|
bytes: copy,
|
|
507
520
|
rowCount: this._currentShardRowCount,
|
|
508
|
-
stringTableBytes:
|
|
521
|
+
stringTableBytes: stBytes,
|
|
509
522
|
mins: shardMins,
|
|
510
523
|
maxes: shardMaxes,
|
|
511
524
|
});
|
|
@@ -721,7 +734,7 @@ export class Writer {
|
|
|
721
734
|
dv.setUint16(entryOff + 16, SHARD_MIN_READER_VERSION, true);
|
|
722
735
|
dv.setUint16(entryOff + 18, SHARD_FLAGS_NONE, true);
|
|
723
736
|
dv.setUint32(entryOff + 20, 0, true); // reserved
|
|
724
|
-
dv.setBigUint64(entryOff + 24, BigInt(shardStringTableOffsets[i]), true);
|
|
737
|
+
dv.setBigUint64(entryOff + 24, BigInt(shardStringTableLens[i] > 0 ? shardStringTableOffsets[i] : 0), true);
|
|
725
738
|
dv.setBigUint64(entryOff + 32, BigInt(shardStringTableLens[i]), true);
|
|
726
739
|
}
|
|
727
740
|
|
package/src/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { PreserveTokenizer, PreserveTokenizerError } from './PreserveTokenizer.j
|
|
|
17
17
|
import { PreserveWriter, PreserveWriterError } from './PreserveWriter.js';
|
|
18
18
|
import { PreserveReader, PreserveReaderError } from './PreserveReader.js';
|
|
19
19
|
import { checkOpts } from './Opts.js';
|
|
20
|
+
import { toContainerBuffer } from './Views.js';
|
|
20
21
|
|
|
21
22
|
export {
|
|
22
23
|
Tokenizer, TokenizerError,
|
|
@@ -27,7 +28,7 @@ export {
|
|
|
27
28
|
PreserveWriter, PreserveWriterError,
|
|
28
29
|
PreserveReader, PreserveReaderError,
|
|
29
30
|
};
|
|
30
|
-
export const VERSION = '1.
|
|
31
|
+
export const VERSION = '1.3.1';
|
|
31
32
|
|
|
32
33
|
const encoder = new TextEncoder();
|
|
33
34
|
|
|
@@ -188,20 +189,7 @@ async function _serializeAsyncIterablePreserve(iterable, opts) {
|
|
|
188
189
|
// Deserialize LBK1 bytes into the right Reader for the container's mode.
|
|
189
190
|
// Auto-detects preserve vs schema via header flag byte at offset 7 bit 0.
|
|
190
191
|
export function deserialize(bytes) {
|
|
191
|
-
|
|
192
|
-
if (bytes instanceof ArrayBuffer) {
|
|
193
|
-
buffer = bytes;
|
|
194
|
-
} else if (bytes instanceof Uint8Array) {
|
|
195
|
-
if (bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) {
|
|
196
|
-
buffer = bytes.buffer;
|
|
197
|
-
} else {
|
|
198
|
-
const copy = new Uint8Array(bytes.byteLength);
|
|
199
|
-
copy.set(bytes);
|
|
200
|
-
buffer = copy.buffer;
|
|
201
|
-
}
|
|
202
|
-
} else {
|
|
203
|
-
throw new TypeError('deserialize: expected Uint8Array or ArrayBuffer, got ' + typeof bytes);
|
|
204
|
-
}
|
|
192
|
+
const buffer = toContainerBuffer(bytes, 'deserialize');
|
|
205
193
|
if (buffer.byteLength < 8) {
|
|
206
194
|
throw new ReaderError('R_TRUNCATED', 'container too small to inspect header flags');
|
|
207
195
|
}
|