@btoon/js 0.1.1 → 0.1.2
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/README.md +14 -6
- package/dist/cjs/constants.d.ts +1 -1
- package/dist/cjs/constants.js +1 -1
- package/dist/cjs/core.d.ts +6 -1
- package/dist/cjs/core.js +24 -11
- package/dist/cjs/decode.js +41 -8
- package/dist/cjs/index.d.ts +7 -4
- package/dist/cjs/index.js +7 -4
- package/dist/cjs/node.d.ts +7 -4
- package/dist/cjs/node.js +7 -4
- package/dist/cjs/platform-node.js +80 -45
- package/dist/cjs/web.d.ts +11 -4
- package/dist/cjs/web.js +106 -21
- package/dist/esm/constants.d.ts +1 -1
- package/dist/esm/constants.js +1 -1
- package/dist/esm/core.d.ts +6 -1
- package/dist/esm/core.js +25 -12
- package/dist/esm/decode.js +41 -8
- package/dist/esm/index.d.ts +7 -4
- package/dist/esm/index.js +7 -4
- package/dist/esm/node.d.ts +7 -4
- package/dist/esm/node.js +7 -4
- package/dist/esm/platform-node.js +81 -46
- package/dist/esm/web.d.ts +11 -4
- package/dist/esm/web.js +104 -21
- package/package.json +1 -1
package/dist/cjs/web.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.webFormat = webFormat;
|
|
4
|
+
exports.trustedDecompressionFormat = trustedDecompressionFormat;
|
|
5
|
+
exports.mayDecompressAsync = mayDecompressAsync;
|
|
4
6
|
exports.webCompress = webCompress;
|
|
5
7
|
exports.webDecompress = webDecompress;
|
|
6
8
|
exports.subtle = subtle;
|
|
@@ -9,8 +11,10 @@ exports.webHmacVerify = webHmacVerify;
|
|
|
9
11
|
// Asynchronous implementations on top of web platform APIs: Compression Streams and
|
|
10
12
|
// WebCrypto. Used by encodeAsync/decodeAsync wherever the runtime provides them.
|
|
11
13
|
const errors_js_1 = require("./errors.js");
|
|
12
|
-
const inflate_js_1 = require("./inflate.js");
|
|
13
14
|
const FORMATS = { zlib: 'deflate', brotli: 'brotli', zstd: 'zstd' };
|
|
15
|
+
// DecompressionStream has no way to bound a Zstandard window (SPEC §6), so it is never used
|
|
16
|
+
// for zstd.
|
|
17
|
+
const DECOMPRESSION_FORMATS = { zlib: 'deflate', brotli: 'brotli' };
|
|
14
18
|
const CHUNK = 64 * 1024;
|
|
15
19
|
const supported = new Map();
|
|
16
20
|
function streamCtor(kind) {
|
|
@@ -48,6 +52,76 @@ function concat(chunks, total) {
|
|
|
48
52
|
}
|
|
49
53
|
return out;
|
|
50
54
|
}
|
|
55
|
+
function fromHex(h) {
|
|
56
|
+
const out = new Uint8Array(h.length / 2);
|
|
57
|
+
for (let i = 0; i < out.length; i++)
|
|
58
|
+
out[i] = parseInt(h.slice(2 * i, 2 * i + 2), 16);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
// Streams of the five bytes "BTOON" (zlib level 9, Brotli quality 11) for the trust probe.
|
|
62
|
+
const PROBE_TEXT = 'BTOON';
|
|
63
|
+
const PROBE_STREAMS = { deflate: '78da730af1f7f7030004780183', brotli: '0b028042544f4f4e03' };
|
|
64
|
+
const trust = new Map();
|
|
65
|
+
/**
|
|
66
|
+
* Whether this runtime's DecompressionStream can be trusted with `format`: it must decode a
|
|
67
|
+
* valid stream and reject one followed by a byte (in the same write or in the next), or by a
|
|
68
|
+
* second stream. Node.js 18 to 23 (some later 22.x releases excepted) ignore bytes after a
|
|
69
|
+
* zlib stream, and Node.js 22 and 24 after a Brotli stream, while SPEC §6 requires
|
|
70
|
+
* `invalid_envelope`. Checked once per format.
|
|
71
|
+
*/
|
|
72
|
+
function trusted(format) {
|
|
73
|
+
let p = trust.get(format);
|
|
74
|
+
if (p === undefined) {
|
|
75
|
+
p = probe(format);
|
|
76
|
+
trust.set(format, p);
|
|
77
|
+
}
|
|
78
|
+
return p;
|
|
79
|
+
}
|
|
80
|
+
async function probe(format) {
|
|
81
|
+
const hex = PROBE_STREAMS[format];
|
|
82
|
+
if (hex === undefined)
|
|
83
|
+
return false;
|
|
84
|
+
const valid = fromHex(hex);
|
|
85
|
+
const next = (extra) => {
|
|
86
|
+
const out = new Uint8Array(valid.length + extra.length);
|
|
87
|
+
out.set(valid);
|
|
88
|
+
out.set(extra, valid.length);
|
|
89
|
+
return out;
|
|
90
|
+
};
|
|
91
|
+
try {
|
|
92
|
+
const out = await decompressChunks(format, [valid], 64, format);
|
|
93
|
+
if (String.fromCharCode(...out) !== PROBE_TEXT)
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
for (const chunks of [[next(Uint8Array.of(0))], [valid, Uint8Array.of(0)], [next(valid)]]) {
|
|
100
|
+
try {
|
|
101
|
+
await decompressChunks(format, chunks, 64, format);
|
|
102
|
+
return false; // trailing bytes accepted
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// rejected, as it should be
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The DecompressionStream format that decodeAsync may use for `alg` when no other
|
|
112
|
+
* decompressor is available, or undefined: never for zstd, and only once the runtime has
|
|
113
|
+
* shown that it rejects trailing bytes.
|
|
114
|
+
*/
|
|
115
|
+
async function trustedDecompressionFormat(alg) {
|
|
116
|
+
const format = DECOMPRESSION_FORMATS[alg];
|
|
117
|
+
if (format === undefined || webFormat(alg, 'DecompressionStream') === undefined)
|
|
118
|
+
return undefined;
|
|
119
|
+
return (await trusted(format)) ? format : undefined;
|
|
120
|
+
}
|
|
121
|
+
/** Whether decodeAsync might decompress `alg` with a DecompressionStream here (for error messages). */
|
|
122
|
+
function mayDecompressAsync(alg) {
|
|
123
|
+
return DECOMPRESSION_FORMATS[alg] !== undefined && webFormat(alg, 'DecompressionStream') !== undefined;
|
|
124
|
+
}
|
|
51
125
|
async function readAll(reader, limit, alg) {
|
|
52
126
|
const chunks = [];
|
|
53
127
|
let total = 0;
|
|
@@ -64,11 +138,18 @@ async function readAll(reader, limit, alg) {
|
|
|
64
138
|
}
|
|
65
139
|
return concat(chunks, total);
|
|
66
140
|
}
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
141
|
+
/** Splits `data` into the chunks written to a stream. */
|
|
142
|
+
function chunked(data) {
|
|
143
|
+
const chunks = [];
|
|
144
|
+
for (let i = 0; i < data.length; i += CHUNK)
|
|
145
|
+
chunks.push(data.subarray(i, Math.min(i + CHUNK, data.length)));
|
|
146
|
+
return chunks;
|
|
147
|
+
}
|
|
148
|
+
/** Writes `chunks`, honouring backpressure, then closes the writer. */
|
|
149
|
+
async function pump(writer, chunks) {
|
|
150
|
+
for (const chunk of chunks) {
|
|
70
151
|
await writer.ready;
|
|
71
|
-
await writer.write(
|
|
152
|
+
await writer.write(chunk);
|
|
72
153
|
}
|
|
73
154
|
await writer.close();
|
|
74
155
|
}
|
|
@@ -78,41 +159,45 @@ function messageOf(e) {
|
|
|
78
159
|
async function webCompress(format, data) {
|
|
79
160
|
const stream = new (streamCtor('CompressionStream'))(format);
|
|
80
161
|
const writer = stream.writable.getWriter();
|
|
81
|
-
const writing = pump(writer, data);
|
|
162
|
+
const writing = pump(writer, chunked(data));
|
|
82
163
|
writing.catch(() => undefined);
|
|
83
164
|
const out = await readAll(stream.readable.getReader(), Infinity, format);
|
|
84
165
|
await writing;
|
|
85
166
|
return out;
|
|
86
167
|
}
|
|
87
168
|
/**
|
|
88
|
-
*
|
|
89
|
-
* `
|
|
90
|
-
* Adler-32 of the output: older runtimes silently ignore bytes after the end of the
|
|
91
|
-
* stream, which a strict decoder must reject.
|
|
169
|
+
* Runs `chunks` through a DecompressionStream. Output beyond `limit` bytes fails with
|
|
170
|
+
* `invalid_envelope` ("<label> payload decompresses to more than the declared size").
|
|
92
171
|
*/
|
|
93
|
-
async function
|
|
172
|
+
async function decompressChunks(format, chunks, limit, label) {
|
|
94
173
|
const stream = new (streamCtor('DecompressionStream'))(format);
|
|
95
174
|
const writer = stream.writable.getWriter();
|
|
96
|
-
const writing = pump(writer,
|
|
175
|
+
const writing = pump(writer, chunks);
|
|
97
176
|
writing.catch(() => undefined);
|
|
98
|
-
let out;
|
|
99
177
|
try {
|
|
100
|
-
out = await readAll(stream.readable.getReader(), limit,
|
|
178
|
+
const out = await readAll(stream.readable.getReader(), limit, label);
|
|
101
179
|
await writing;
|
|
180
|
+
return out;
|
|
102
181
|
}
|
|
103
182
|
catch (e) {
|
|
104
183
|
writer.abort(e).catch(() => undefined);
|
|
184
|
+
throw e;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Decompresses with a DecompressionStream, stopping as soon as the output exceeds `limit`
|
|
189
|
+
* (the declared size). Only for a format that trustedDecompressionFormat() returned, which
|
|
190
|
+
* rejects bytes after the end of the stream.
|
|
191
|
+
*/
|
|
192
|
+
async function webDecompress(alg, format, data, limit) {
|
|
193
|
+
try {
|
|
194
|
+
return await decompressChunks(format, chunked(data), limit, alg);
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
105
197
|
if (e instanceof errors_js_1.BtoonError)
|
|
106
198
|
throw e;
|
|
107
199
|
throw (0, errors_js_1.fail)('invalid_envelope', `${alg} payload is corrupt: ${messageOf(e)}`, e);
|
|
108
200
|
}
|
|
109
|
-
if (alg === 'zlib') {
|
|
110
|
-
const n = data.length;
|
|
111
|
-
const trailer = n >= 4 ? ((data[n - 4] << 24) | (data[n - 3] << 16) | (data[n - 2] << 8) | data[n - 1]) >>> 0 : -1;
|
|
112
|
-
if (trailer !== (0, inflate_js_1.adler32)(out))
|
|
113
|
-
throw (0, errors_js_1.fail)('invalid_envelope', 'zlib payload has trailing bytes');
|
|
114
|
-
}
|
|
115
|
-
return out;
|
|
116
201
|
}
|
|
117
202
|
/** WebCrypto's SubtleCrypto, if available (absent in Node 18 and on insecure origins). */
|
|
118
203
|
function subtle() {
|
package/dist/esm/constants.d.ts
CHANGED
package/dist/esm/constants.js
CHANGED
package/dist/esm/core.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { type CompressionAlgorithm, type DecodeOptions, type EncodeOptions } from './options.js';
|
|
2
|
-
/**
|
|
2
|
+
/** Building blocks of one environment: its synchronous codecs and HMAC, and non-blocking decompressors. */
|
|
3
3
|
export interface Platform {
|
|
4
4
|
/** Decompressors; `size` is the declared uncompressed size (already checked against the limit). */
|
|
5
5
|
readonly decompress: Readonly<Partial<Record<CompressionAlgorithm, (data: Uint8Array, size: number) => Uint8Array>>>;
|
|
6
|
+
/**
|
|
7
|
+
* Non-blocking forms of `decompress` that reach the same verdict (node:zlib on the thread
|
|
8
|
+
* pool). decodeAsync prefers them.
|
|
9
|
+
*/
|
|
10
|
+
readonly decompressAsync?: Readonly<Partial<Record<CompressionAlgorithm, (data: Uint8Array, size: number) => Promise<Uint8Array>>>>;
|
|
6
11
|
readonly compress: Readonly<Partial<Record<CompressionAlgorithm, (data: Uint8Array) => Uint8Array>>>;
|
|
7
12
|
hmac(key: Uint8Array, parts: readonly Uint8Array[]): Uint8Array;
|
|
8
13
|
equal(a: Uint8Array, b: Uint8Array): boolean;
|
package/dist/esm/core.js
CHANGED
|
@@ -7,7 +7,7 @@ import { encodeDocument } from './encode.js';
|
|
|
7
7
|
import { BtoonError, fail } from './errors.js';
|
|
8
8
|
import { decodeConfig, encodeConfig, } from './options.js';
|
|
9
9
|
import { isUint8Array } from './values.js';
|
|
10
|
-
import { subtle, webCompress, webDecompress, webFormat, webHmacSign, webHmacVerify } from './web.js';
|
|
10
|
+
import { mayDecompressAsync, subtle, trustedDecompressionFormat, webCompress, webDecompress, webFormat, webHmacSign, webHmacVerify, } from './web.js';
|
|
11
11
|
const COMPRESSED_MAGIC = [0x42, 0x54, 0x4f, 0x4e]; // "BTON"
|
|
12
12
|
const SIGNED_MAGIC = [0x42, 0x54, 0x53, 0x47]; // "BTSG"
|
|
13
13
|
const SIGNED_HEADER = new Uint8Array([0x42, 0x54, 0x53, 0x47, 1, 1, 0, 0]);
|
|
@@ -198,7 +198,7 @@ export function decodeWith(data, options, platform) {
|
|
|
198
198
|
if (out === undefined) {
|
|
199
199
|
const builtin = platform.decompress[alg];
|
|
200
200
|
if (builtin === undefined)
|
|
201
|
-
throw unavailable(alg, 'decompression',
|
|
201
|
+
throw unavailable(alg, 'decompression', mayDecompressAsync(alg));
|
|
202
202
|
out = builtin(payload, size);
|
|
203
203
|
}
|
|
204
204
|
bytes = checkSize(out, size, alg);
|
|
@@ -253,17 +253,30 @@ export async function decodeAsyncWith(data, options, platform) {
|
|
|
253
253
|
if (env !== null) {
|
|
254
254
|
const { alg, payload, size } = env;
|
|
255
255
|
let out = await callCodecAsync(cfg.codecs, alg, 'decompress', [payload, size]);
|
|
256
|
-
if (out === undefined)
|
|
257
|
-
|
|
258
|
-
const builtin = platform.decompress[alg];
|
|
259
|
-
if (format !== undefined)
|
|
260
|
-
out = await webDecompress(alg, format, payload, size);
|
|
261
|
-
else if (builtin !== undefined)
|
|
262
|
-
out = builtin(payload, size);
|
|
263
|
-
else
|
|
264
|
-
throw unavailable(alg, 'decompression', false);
|
|
265
|
-
}
|
|
256
|
+
if (out === undefined)
|
|
257
|
+
out = await decompressAsync(platform, alg, payload, size);
|
|
266
258
|
bytes = checkSize(out, size, alg);
|
|
267
259
|
}
|
|
268
260
|
return decodeDocument(bytes, cfg);
|
|
269
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* decodeAsync's decompressor. It reaches the same verdict as decode's: first the same
|
|
264
|
+
* decompressor, in its non-blocking form where there is one (node:zlib), then the synchronous
|
|
265
|
+
* one (the pure-JS inflate). A DecompressionStream is the last resort, only for a format that
|
|
266
|
+
* the runtime has shown it handles strictly, and never for zstd.
|
|
267
|
+
*/
|
|
268
|
+
async function decompressAsync(platform, alg, payload, size) {
|
|
269
|
+
const nonBlocking = platform.decompressAsync?.[alg];
|
|
270
|
+
if (nonBlocking !== undefined)
|
|
271
|
+
return nonBlocking(payload, size);
|
|
272
|
+
const builtin = platform.decompress[alg];
|
|
273
|
+
if (builtin !== undefined)
|
|
274
|
+
return builtin(payload, size);
|
|
275
|
+
const format = await trustedDecompressionFormat(alg);
|
|
276
|
+
if (format !== undefined)
|
|
277
|
+
return webDecompress(alg, format, payload, size);
|
|
278
|
+
if (mayDecompressAsync(alg)) {
|
|
279
|
+
throw fail('unsupported_compression', `${alg} decompression is not available in this environment: its DecompressionStream accepts bytes after the end of a stream; pass options.codecs.${alg}`);
|
|
280
|
+
}
|
|
281
|
+
throw unavailable(alg, 'decompression', false);
|
|
282
|
+
}
|
package/dist/esm/decode.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Hardened BTOON decoder (SPEC §7). Invariants:
|
|
2
2
|
// * every read is bounds-checked against `end` (no reads past the input);
|
|
3
3
|
// * containers are depth-limited (max_depth) — no unbounded recursion;
|
|
4
|
-
// * nothing is allocated beyond what the remaining input could encode
|
|
4
|
+
// * nothing is allocated beyond what the remaining input could encode, and what containers
|
|
5
|
+
// allocate before their elements are decoded is bounded for the whole document.
|
|
5
6
|
import { DEFAULT_MAX_DEPTH, EXT_BIGINT, EXT_DATE, EXT_DATETIME, EXT_F32_VECTOR, EXT_F64_VECTOR, EXT_TABULAR, EXT_TIMESTAMP, MAX_DATE_MS, MAX_OFFSET_MINUTES, NANOS_PER_SECOND, TABULAR_VERSION, } from './constants.js';
|
|
6
7
|
import { BtoonError, fail, isStackOverflow } from './errors.js';
|
|
7
8
|
import { decodeUtf8 } from './utf8.js';
|
|
@@ -17,6 +18,15 @@ const fromCharCode = String.fromCharCode;
|
|
|
17
18
|
const KEY_CACHE_SIZE = 4096;
|
|
18
19
|
const KEY_CACHE_MAX_LEN = 16;
|
|
19
20
|
const keyCache = new Array(KEY_CACHE_SIZE).fill(undefined);
|
|
21
|
+
// Limits on what containers allocate before their elements are decoded. A header may announce
|
|
22
|
+
// as many elements as there are bytes left, but nested containers announce the same bytes
|
|
23
|
+
// again at every level, so trusting the counts let a 1 MiB document of 64 nested tabulars
|
|
24
|
+
// build 4 GB of rows and exhaust the heap (review 2026-11-13, X1). An array, or the row list
|
|
25
|
+
// of a table, reserves at most MAX_RESERVE slots, and those open at any time at most
|
|
26
|
+
// RESERVE_BUDGET together; beyond that they grow as elements are decoded. Each tabular row is
|
|
27
|
+
// created once its first cell has been decoded.
|
|
28
|
+
const MAX_RESERVE = 1024;
|
|
29
|
+
const RESERVE_BUDGET = 64 * MAX_RESERVE;
|
|
20
30
|
const HEX = [];
|
|
21
31
|
for (let i = 0; i < 256; i++)
|
|
22
32
|
HEX.push((i < 16 ? '0' : '') + i.toString(16));
|
|
@@ -45,6 +55,8 @@ export function plainBytes(b) {
|
|
|
45
55
|
class Decoder {
|
|
46
56
|
constructor(bytes, strict, maxDepth, tsObjects, bigAlways) {
|
|
47
57
|
this.pos = 0;
|
|
58
|
+
/** Slots reserved by the arrays and row lists being decoded (see reserve). */
|
|
59
|
+
this.reserved = 0;
|
|
48
60
|
this.bytes = plainBytes(bytes);
|
|
49
61
|
this.view = new DataView(this.bytes.buffer, this.bytes.byteOffset, this.bytes.byteLength);
|
|
50
62
|
this.end = this.bytes.length;
|
|
@@ -209,15 +221,26 @@ class Decoder {
|
|
|
209
221
|
this.pos += len;
|
|
210
222
|
return b;
|
|
211
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* How many of the `n` elements that an array, or a table's row list, announces it may
|
|
226
|
+
* allocate up front. They are charged to the document's budget until it is complete.
|
|
227
|
+
*/
|
|
228
|
+
reserve(n) {
|
|
229
|
+
const r = Math.min(n, MAX_RESERVE, RESERVE_BUDGET - this.reserved);
|
|
230
|
+
this.reserved += r;
|
|
231
|
+
return r;
|
|
232
|
+
}
|
|
212
233
|
array(n, depth) {
|
|
213
234
|
if (depth >= this.maxDepth)
|
|
214
235
|
throw this.err('max_depth', 'maximum nesting depth exceeded');
|
|
215
236
|
// Every element takes at least one byte.
|
|
216
237
|
if (n > this.end - this.pos)
|
|
217
238
|
throw this.err('truncated', 'array length exceeds the remaining input');
|
|
218
|
-
const
|
|
239
|
+
const r = this.reserve(n);
|
|
240
|
+
const a = new Array(r); // grows as elements beyond the reservation arrive
|
|
219
241
|
for (let i = 0; i < n; i++)
|
|
220
242
|
a[i] = this.value(depth + 1);
|
|
243
|
+
this.reserved -= r;
|
|
221
244
|
return a;
|
|
222
245
|
}
|
|
223
246
|
key() {
|
|
@@ -447,7 +470,7 @@ class Decoder {
|
|
|
447
470
|
// Each column needs at least 9 bytes: name length, type code, column length.
|
|
448
471
|
if (cols > (end - p) / 9)
|
|
449
472
|
throw bad('tabular column count exceeds the payload');
|
|
450
|
-
const names =
|
|
473
|
+
const names = [];
|
|
451
474
|
for (let c = 0; c < cols; c++) {
|
|
452
475
|
if (end - p < 4)
|
|
453
476
|
throw bad('tabular payload truncated');
|
|
@@ -458,7 +481,7 @@ class Decoder {
|
|
|
458
481
|
const name = decodeUtf8(this.bytes, p, n, this.strict);
|
|
459
482
|
if (name === undefined)
|
|
460
483
|
throw this.err('invalid_utf8', 'column name is not valid UTF-8', p);
|
|
461
|
-
names
|
|
484
|
+
names.push(name);
|
|
462
485
|
p += n;
|
|
463
486
|
}
|
|
464
487
|
if (new Set(names).size !== cols)
|
|
@@ -469,9 +492,10 @@ class Decoder {
|
|
|
469
492
|
// Every cell is at least one byte.
|
|
470
493
|
if (rowCount * cols > end - p)
|
|
471
494
|
throw bad('tabular row count exceeds the payload');
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
495
|
+
// Each row is created once its first cell has been decoded, so the rows are backed by
|
|
496
|
+
// input actually consumed, however many the header announces.
|
|
497
|
+
const reserved = this.reserve(rowCount);
|
|
498
|
+
const rows = new Array(reserved);
|
|
475
499
|
const savedEnd = this.end;
|
|
476
500
|
for (let c = 0; c < cols; c++) {
|
|
477
501
|
if (end - p < 4)
|
|
@@ -485,7 +509,15 @@ class Decoder {
|
|
|
485
509
|
this.pos = p;
|
|
486
510
|
this.end = colEnd;
|
|
487
511
|
try {
|
|
488
|
-
if (
|
|
512
|
+
if (c === 0) {
|
|
513
|
+
for (let r = 0; r < rowCount; r++) {
|
|
514
|
+
const v = this.value(depth + 2);
|
|
515
|
+
const row = {};
|
|
516
|
+
setProperty(row, name, v);
|
|
517
|
+
rows[r] = row;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
else if (name === '__proto__') {
|
|
489
521
|
for (let r = 0; r < rowCount; r++)
|
|
490
522
|
setProperty(rows[r], name, this.value(depth + 2));
|
|
491
523
|
}
|
|
@@ -505,6 +537,7 @@ class Decoder {
|
|
|
505
537
|
p = colEnd;
|
|
506
538
|
}
|
|
507
539
|
this.end = savedEnd;
|
|
540
|
+
this.reserved -= reserved;
|
|
508
541
|
if (p !== end)
|
|
509
542
|
throw bad('trailing bytes in tabular payload');
|
|
510
543
|
this.pos = end;
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -60,9 +60,12 @@ export declare function decode(data: BinaryInput, options?: DecodeOptions): unkn
|
|
|
60
60
|
*/
|
|
61
61
|
export declare function encodeAsync(value: unknown, options?: EncodeOptions): Promise<Uint8Array<ArrayBuffer>>;
|
|
62
62
|
/**
|
|
63
|
-
* Like {@link decode}, but
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
63
|
+
* Like {@link decode}, but asynchronous. In the Node.js entry, decompression runs on the
|
|
64
|
+
* `node:zlib` thread pool. In the portable entry, zlib uses the same pure-JS inflate as
|
|
65
|
+
* `decode`, and Brotli can also be decompressed with a `DecompressionStream` where the runtime
|
|
66
|
+
* has shown that it rejects bytes after the end of a stream (never Zstandard, whose window
|
|
67
|
+
* size it can't bound). Signatures are verified with `crypto.subtle.verify` where available.
|
|
68
|
+
* `decodeAsync` never accepts a document that `decode` rejects. The input is copied first, so
|
|
69
|
+
* the caller may reuse its buffer immediately. Rejects with a {@link BtoonError}.
|
|
67
70
|
*/
|
|
68
71
|
export declare function decodeAsync(data: BinaryInput, options?: DecodeOptions): Promise<unknown>;
|
package/dist/esm/index.js
CHANGED
|
@@ -64,10 +64,13 @@ export function encodeAsync(value, options) {
|
|
|
64
64
|
return encodeAsyncWith(value, options, portablePlatform);
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
|
-
* Like {@link decode}, but
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
67
|
+
* Like {@link decode}, but asynchronous. In the Node.js entry, decompression runs on the
|
|
68
|
+
* `node:zlib` thread pool. In the portable entry, zlib uses the same pure-JS inflate as
|
|
69
|
+
* `decode`, and Brotli can also be decompressed with a `DecompressionStream` where the runtime
|
|
70
|
+
* has shown that it rejects bytes after the end of a stream (never Zstandard, whose window
|
|
71
|
+
* size it can't bound). Signatures are verified with `crypto.subtle.verify` where available.
|
|
72
|
+
* `decodeAsync` never accepts a document that `decode` rejects. The input is copied first, so
|
|
73
|
+
* the caller may reuse its buffer immediately. Rejects with a {@link BtoonError}.
|
|
71
74
|
*/
|
|
72
75
|
export function decodeAsync(data, options) {
|
|
73
76
|
return decodeAsyncWith(data, options, portablePlatform);
|
package/dist/esm/node.d.ts
CHANGED
|
@@ -56,9 +56,12 @@ export declare function decode(data: BinaryInput, options?: DecodeOptions): unkn
|
|
|
56
56
|
*/
|
|
57
57
|
export declare function encodeAsync(value: unknown, options?: EncodeOptions): Promise<Uint8Array<ArrayBuffer>>;
|
|
58
58
|
/**
|
|
59
|
-
* Like {@link decode}, but
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
59
|
+
* Like {@link decode}, but asynchronous. In the Node.js entry, decompression runs on the
|
|
60
|
+
* `node:zlib` thread pool. In the portable entry, zlib uses the same pure-JS inflate as
|
|
61
|
+
* `decode`, and Brotli can also be decompressed with a `DecompressionStream` where the runtime
|
|
62
|
+
* has shown that it rejects bytes after the end of a stream (never Zstandard, whose window
|
|
63
|
+
* size it can't bound). Signatures are verified with `crypto.subtle.verify` where available.
|
|
64
|
+
* `decodeAsync` never accepts a document that `decode` rejects. The input is copied first, so
|
|
65
|
+
* the caller may reuse its buffer immediately. Rejects with a {@link BtoonError}.
|
|
63
66
|
*/
|
|
64
67
|
export declare function decodeAsync(data: BinaryInput, options?: DecodeOptions): Promise<unknown>;
|
package/dist/esm/node.js
CHANGED
|
@@ -60,10 +60,13 @@ export function encodeAsync(value, options) {
|
|
|
60
60
|
return encodeAsyncWith(value, options, nodePlatform);
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
|
-
* Like {@link decode}, but
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
63
|
+
* Like {@link decode}, but asynchronous. In the Node.js entry, decompression runs on the
|
|
64
|
+
* `node:zlib` thread pool. In the portable entry, zlib uses the same pure-JS inflate as
|
|
65
|
+
* `decode`, and Brotli can also be decompressed with a `DecompressionStream` where the runtime
|
|
66
|
+
* has shown that it rejects bytes after the end of a stream (never Zstandard, whose window
|
|
67
|
+
* size it can't bound). Signatures are verified with `crypto.subtle.verify` where available.
|
|
68
|
+
* `decodeAsync` never accepts a document that `decode` rejects. The input is copied first, so
|
|
69
|
+
* the caller may reuse its buffer immediately. Rejects with a {@link BtoonError}.
|
|
67
70
|
*/
|
|
68
71
|
export function decodeAsync(data, options) {
|
|
69
72
|
return decodeAsyncWith(data, options, nodePlatform);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import * as zlib from 'node:zlib';
|
|
3
3
|
import { fail } from './errors.js';
|
|
4
|
-
import {
|
|
4
|
+
import { inflateZlib } from './inflate.js';
|
|
5
5
|
function nodeFn(name) {
|
|
6
6
|
const f = zlib[name];
|
|
7
7
|
return typeof f === 'function' ? f : undefined;
|
|
@@ -9,80 +9,115 @@ function nodeFn(name) {
|
|
|
9
9
|
function plain(b) {
|
|
10
10
|
return new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
/** node:zlib options: output capped at the declared size, and `info` to learn how much input was consumed. */
|
|
13
|
+
function options(size, extra) {
|
|
14
|
+
return { ...extra?.(size), info: true, maxOutputLength: Math.max(1, size) };
|
|
15
|
+
}
|
|
16
|
+
function failure(alg, e) {
|
|
17
|
+
if (e.code === 'ERR_BUFFER_TOO_LARGE') {
|
|
18
|
+
return fail('invalid_envelope', `${alg} payload decompresses to more than the declared size`, e);
|
|
19
|
+
}
|
|
20
|
+
return fail('invalid_envelope', `${alg} payload is corrupt: ${e.message}`, e);
|
|
21
|
+
}
|
|
22
|
+
/** The output of a node:zlib decompressor, once it is clear that the stream used the whole payload. */
|
|
23
|
+
function output(alg, data, size, r) {
|
|
24
|
+
let out;
|
|
25
|
+
let consumed;
|
|
26
|
+
if (r instanceof Uint8Array) {
|
|
27
|
+
out = r;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const info = r;
|
|
31
|
+
out = info.buffer;
|
|
32
|
+
consumed = info.engine?.bytesWritten;
|
|
33
|
+
}
|
|
34
|
+
if (typeof consumed !== 'number') {
|
|
35
|
+
// A runtime that doesn't report the input it consumed can't show that no bytes follow the
|
|
36
|
+
// stream (SPEC §6). zlib has an exact pure-JS decoder; the other algorithms don't.
|
|
37
|
+
if (alg === 'zlib')
|
|
38
|
+
return inflateZlib(data, size);
|
|
39
|
+
throw fail('unsupported_compression', `${alg} decompression in this runtime cannot detect trailing bytes; pass options.codecs.${alg}`);
|
|
40
|
+
}
|
|
41
|
+
if (consumed !== data.length)
|
|
42
|
+
throw fail('invalid_envelope', `${alg} payload has trailing bytes`);
|
|
43
|
+
return plain(out);
|
|
44
|
+
}
|
|
45
|
+
/** Wraps a node:zlib `*Sync` decompressor, for decode. */
|
|
16
46
|
function decompressor(alg, fn, extra) {
|
|
17
47
|
if (fn === undefined)
|
|
18
48
|
return undefined;
|
|
19
49
|
return (data, size) => {
|
|
20
50
|
let r;
|
|
21
51
|
try {
|
|
22
|
-
r = fn(data,
|
|
52
|
+
r = fn(data, options(size, extra));
|
|
23
53
|
}
|
|
24
54
|
catch (e) {
|
|
25
|
-
|
|
26
|
-
throw fail('invalid_envelope', `${alg} payload decompresses to more than the declared size`, e);
|
|
27
|
-
}
|
|
28
|
-
throw fail('invalid_envelope', `${alg} payload is corrupt: ${e.message}`, e);
|
|
29
|
-
}
|
|
30
|
-
let out;
|
|
31
|
-
let consumed;
|
|
32
|
-
if (r instanceof Uint8Array) {
|
|
33
|
-
out = r;
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
const info = r;
|
|
37
|
-
out = info.buffer;
|
|
38
|
-
consumed = info.engine?.bytesWritten;
|
|
39
|
-
}
|
|
40
|
-
if (typeof consumed === 'number') {
|
|
41
|
-
if (consumed !== data.length)
|
|
42
|
-
throw fail('invalid_envelope', `${alg} payload has trailing bytes`);
|
|
43
|
-
}
|
|
44
|
-
else if (alg === 'zlib') {
|
|
45
|
-
// A runtime that doesn't report consumed input: the stream must end in its Adler-32.
|
|
46
|
-
const n = data.length;
|
|
47
|
-
const trailer = n >= 4 ? ((data[n - 4] << 24) | (data[n - 3] << 16) | (data[n - 2] << 8) | data[n - 1]) >>> 0 : -1;
|
|
48
|
-
if (trailer !== adler32(out))
|
|
49
|
-
throw fail('invalid_envelope', 'zlib payload has trailing bytes');
|
|
55
|
+
throw failure(alg, e);
|
|
50
56
|
}
|
|
51
|
-
return
|
|
57
|
+
return output(alg, data, size, r);
|
|
52
58
|
};
|
|
53
59
|
}
|
|
54
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Wraps the callback form of the same node:zlib decompressor, for decodeAsync: it runs on the
|
|
62
|
+
* thread pool, with the same limits and checks, so both give the same verdict.
|
|
63
|
+
*/
|
|
64
|
+
function asyncDecompressor(alg, fn, extra) {
|
|
65
|
+
if (fn === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
return (data, size) => new Promise((resolve, reject) => {
|
|
68
|
+
fn(data, options(size, extra), (e, r) => {
|
|
69
|
+
if (e) {
|
|
70
|
+
reject(failure(alg, e));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
resolve(output(alg, data, size, r));
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
reject(err);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function compressor(fn, opts) {
|
|
55
83
|
if (fn === undefined)
|
|
56
84
|
return undefined;
|
|
57
|
-
return (data) => plain(fn(data,
|
|
85
|
+
return (data) => plain(fn(data, opts));
|
|
58
86
|
}
|
|
59
87
|
const decompress = {};
|
|
88
|
+
const decompressAsync = {};
|
|
60
89
|
const compress = {};
|
|
61
90
|
decompress.zlib = decompressor('zlib', nodeFn('inflateSync'));
|
|
91
|
+
decompressAsync.zlib = asyncDecompressor('zlib', nodeFn('inflate'));
|
|
62
92
|
compress.zlib = compressor(nodeFn('deflateSync'), { level: 6 });
|
|
63
93
|
const brotliQuality = zlib.constants?.BROTLI_PARAM_QUALITY;
|
|
64
94
|
if (brotliQuality !== undefined) {
|
|
65
95
|
decompress.brotli = decompressor('brotli', nodeFn('brotliDecompressSync'));
|
|
96
|
+
decompressAsync.brotli = asyncDecompressor('brotli', nodeFn('brotliDecompress'));
|
|
66
97
|
compress.brotli = compressor(nodeFn('brotliCompressSync'), { params: { [brotliQuality]: 6 } });
|
|
67
98
|
}
|
|
68
|
-
// node:zlib has Zstandard from Node.js 22.15 / 23.8. SPEC §6: bound the decoder's memory
|
|
69
|
-
//
|
|
99
|
+
// node:zlib has Zstandard from Node.js 22.15 / 23.8. SPEC §6: bound the decoder's memory by
|
|
100
|
+
// rejecting frames whose window exceeds max(8 MiB, min(uncompressed_size, 128 MiB)). Without
|
|
101
|
+
// the windowLogMax parameter there is no bound, so zstd decompression is not offered at all.
|
|
70
102
|
const zstdWindowLogMax = zlib.constants?.ZSTD_d_windowLogMax;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
delete decompress[alg];
|
|
103
|
+
if (zstdWindowLogMax !== undefined) {
|
|
104
|
+
const zstdWindow = (size) => ({
|
|
105
|
+
params: { [zstdWindowLogMax]: Math.ceil(Math.log2(Math.max(8 << 20, Math.min(size, 128 << 20)))) },
|
|
106
|
+
});
|
|
107
|
+
decompress.zstd = decompressor('zstd', nodeFn('zstdDecompressSync'), zstdWindow);
|
|
108
|
+
decompressAsync.zstd = asyncDecompressor('zstd', nodeFn('zstdDecompress'), zstdWindow);
|
|
78
109
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
110
|
+
compress.zstd = compressor(nodeFn('zstdCompressSync'), {});
|
|
111
|
+
for (const table of [decompress, decompressAsync, compress]) {
|
|
112
|
+
for (const alg of Object.keys(table)) {
|
|
113
|
+
if (table[alg] === undefined)
|
|
114
|
+
delete table[alg];
|
|
115
|
+
}
|
|
82
116
|
}
|
|
83
117
|
/** Node.js (and Bun): node:zlib for zlib, Brotli and Zstandard; node:crypto for HMAC. */
|
|
84
118
|
export const nodePlatform = {
|
|
85
119
|
decompress,
|
|
120
|
+
decompressAsync,
|
|
86
121
|
compress,
|
|
87
122
|
hmac(key, parts) {
|
|
88
123
|
const h = createHmac('sha256', key);
|