@btoon/js 0.1.0 → 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 +15 -7
- 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/README.md
CHANGED
|
@@ -17,7 +17,7 @@ compression envelope and an optional HMAC-signed envelope.
|
|
|
17
17
|
the input justifies, strict UTF-8, duplicate-key rejection, capped decompression, and
|
|
18
18
|
signatures verified before anything is parsed.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Implements spec 0.1. In Node.js it is also the fastest BTOON package: on
|
|
21
21
|
10,000 records it encodes in 1.1 ms and decodes in 1.6 ms, against 7.8 ms and 7.1 ms for
|
|
22
22
|
the native [`btoon`](https://github.com/BTOON-project/btoon-nodejs) addon, with
|
|
23
23
|
byte-identical output.
|
|
@@ -94,13 +94,20 @@ API; they differ only in which synchronous codecs are built in.
|
|
|
94
94
|
| `decode` of Brotli / Zstandard | `node:zlib` (Zstandard from Node.js 22.15) | `options.codecs` |
|
|
95
95
|
| `encode` with `compress` | zlib, Brotli, Zstandard via `node:zlib` | `options.codecs`, otherwise `unsupported_compression` (use `encodeAsync`) |
|
|
96
96
|
| HMAC in `encode`/`decode` | `node:crypto` | built-in pure-JS SHA-256 |
|
|
97
|
-
| `encodeAsync`
|
|
97
|
+
| `encodeAsync` compression | `CompressionStream` where the runtime supports the format, else `node:zlib` | `CompressionStream` where the runtime supports the format |
|
|
98
|
+
| `decodeAsync` decompression | `node:zlib` on its thread pool | the pure-JS inflate for zlib; Brotli through `DecompressionStream` where it proves strict (see below) |
|
|
98
99
|
| `encodeAsync` / `decodeAsync` HMAC | WebCrypto (`crypto.subtle`), else `node:crypto` | WebCrypto, else pure JS (for example on `http://` pages) |
|
|
99
100
|
| LZ4, Snappy | `options.codecs` | `options.codecs` |
|
|
100
101
|
|
|
101
|
-
`
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
`decodeAsync` accepts and rejects exactly what `decode` does, because it uses the same
|
|
103
|
+
decompressors: `node:zlib`'s asynchronous functions in the Node entry, and the pure-JS
|
|
104
|
+
inflate in the portable entry. Only where the portable entry has no decompressor of its
|
|
105
|
+
own (Brotli) does it use a `DecompressionStream`, and only after checking once that the
|
|
106
|
+
runtime rejects bytes after the end of a stream: Node.js 18 to 23 accept them after a zlib
|
|
107
|
+
stream (some later 22.x releases excepted), and Node.js 22 and 24 after a Brotli stream. Zstandard never goes through a
|
|
108
|
+
`DecompressionStream`, which can't limit the window size (SPEC §6). Which formats
|
|
109
|
+
`CompressionStream` supports depends on the runtime: at the time of writing, Brotli streams
|
|
110
|
+
exist in Node.js 22+ and Bun, and Zstandard streams in Bun.
|
|
104
111
|
|
|
105
112
|
Why a pure-JS inflate and SHA-256 in the portable entry: zlib decoding is mandatory in
|
|
106
113
|
the spec and signatures are a security feature, so synchronous `decode` handles both in
|
|
@@ -183,7 +190,7 @@ Notes:
|
|
|
183
190
|
## API
|
|
184
191
|
|
|
185
192
|
```ts
|
|
186
|
-
const VERSION: '0.1.
|
|
193
|
+
const VERSION: '0.1.2';
|
|
187
194
|
const SPEC_VERSION: '0.1';
|
|
188
195
|
|
|
189
196
|
function encode(value: unknown, options?: EncodeOptions): Uint8Array;
|
|
@@ -265,7 +272,8 @@ Exceptions thrown by your own `toJSON()` methods or getters propagate unchanged.
|
|
|
265
272
|
## Security
|
|
266
273
|
|
|
267
274
|
Decoding is designed for hostile input (SPEC §7, §9): every read is bounds-checked; arrays,
|
|
268
|
-
maps and tables are checked against the remaining input before anything is allocated
|
|
275
|
+
maps and tables are checked against the remaining input before anything is allocated, and
|
|
276
|
+
grow as their elements are decoded instead of reserving what their headers announce; the
|
|
269
277
|
nesting limit applies to input and output; strings are validated with a fatal
|
|
270
278
|
`TextDecoder`; compressed payloads are checked against `maxDecompressedSize` before
|
|
271
279
|
decompressing and stopped at their declared size while decompressing; Zstandard frames
|
package/dist/cjs/constants.d.ts
CHANGED
package/dist/cjs/constants.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MAX_DATE_MS = exports.TIMESTAMP_BRAND = exports.EXT_BRAND = exports.ALGORITHM_NAMES = exports.ALGORITHM_IDS = exports.SIGNED_HEADER_SIZE = exports.COMPRESSION_HEADER_SIZE = exports.MIN_COMPRESSION_SIZE = exports.DEFAULT_MAX_DECOMPRESSED_SIZE = exports.DEFAULT_MAX_DEPTH = exports.NANOS_PER_SECOND = exports.MAX_OFFSET_MINUTES = exports.TABULAR_VERSION = exports.EXT_TABULAR = exports.EXT_F64_VECTOR = exports.EXT_F32_VECTOR = exports.EXT_BIGINT = exports.EXT_DATETIME = exports.EXT_DATE = exports.EXT_TIMESTAMP = exports.SPEC_VERSION = exports.VERSION = void 0;
|
|
4
4
|
/** Version of this library. */
|
|
5
|
-
exports.VERSION = '0.1.
|
|
5
|
+
exports.VERSION = '0.1.2';
|
|
6
6
|
/** Version of the BTOON specification this library implements. */
|
|
7
7
|
exports.SPEC_VERSION = '0.1';
|
|
8
8
|
// Extension type ids (SPEC §5).
|
package/dist/cjs/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/cjs/core.js
CHANGED
|
@@ -204,7 +204,7 @@ function decodeWith(data, options, platform) {
|
|
|
204
204
|
if (out === undefined) {
|
|
205
205
|
const builtin = platform.decompress[alg];
|
|
206
206
|
if (builtin === undefined)
|
|
207
|
-
throw unavailable(alg, 'decompression', (0, web_js_1.
|
|
207
|
+
throw unavailable(alg, 'decompression', (0, web_js_1.mayDecompressAsync)(alg));
|
|
208
208
|
out = builtin(payload, size);
|
|
209
209
|
}
|
|
210
210
|
bytes = checkSize(out, size, alg);
|
|
@@ -259,17 +259,30 @@ async function decodeAsyncWith(data, options, platform) {
|
|
|
259
259
|
if (env !== null) {
|
|
260
260
|
const { alg, payload, size } = env;
|
|
261
261
|
let out = await callCodecAsync(cfg.codecs, alg, 'decompress', [payload, size]);
|
|
262
|
-
if (out === undefined)
|
|
263
|
-
|
|
264
|
-
const builtin = platform.decompress[alg];
|
|
265
|
-
if (format !== undefined)
|
|
266
|
-
out = await (0, web_js_1.webDecompress)(alg, format, payload, size);
|
|
267
|
-
else if (builtin !== undefined)
|
|
268
|
-
out = builtin(payload, size);
|
|
269
|
-
else
|
|
270
|
-
throw unavailable(alg, 'decompression', false);
|
|
271
|
-
}
|
|
262
|
+
if (out === undefined)
|
|
263
|
+
out = await decompressAsync(platform, alg, payload, size);
|
|
272
264
|
bytes = checkSize(out, size, alg);
|
|
273
265
|
}
|
|
274
266
|
return (0, decode_js_1.decodeDocument)(bytes, cfg);
|
|
275
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* decodeAsync's decompressor. It reaches the same verdict as decode's: first the same
|
|
270
|
+
* decompressor, in its non-blocking form where there is one (node:zlib), then the synchronous
|
|
271
|
+
* one (the pure-JS inflate). A DecompressionStream is the last resort, only for a format that
|
|
272
|
+
* the runtime has shown it handles strictly, and never for zstd.
|
|
273
|
+
*/
|
|
274
|
+
async function decompressAsync(platform, alg, payload, size) {
|
|
275
|
+
const nonBlocking = platform.decompressAsync?.[alg];
|
|
276
|
+
if (nonBlocking !== undefined)
|
|
277
|
+
return nonBlocking(payload, size);
|
|
278
|
+
const builtin = platform.decompress[alg];
|
|
279
|
+
if (builtin !== undefined)
|
|
280
|
+
return builtin(payload, size);
|
|
281
|
+
const format = await (0, web_js_1.trustedDecompressionFormat)(alg);
|
|
282
|
+
if (format !== undefined)
|
|
283
|
+
return (0, web_js_1.webDecompress)(alg, format, payload, size);
|
|
284
|
+
if ((0, web_js_1.mayDecompressAsync)(alg)) {
|
|
285
|
+
throw (0, errors_js_1.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}`);
|
|
286
|
+
}
|
|
287
|
+
throw unavailable(alg, 'decompression', false);
|
|
288
|
+
}
|
package/dist/cjs/decode.js
CHANGED
|
@@ -6,7 +6,8 @@ exports.validateBtoonExt = validateBtoonExt;
|
|
|
6
6
|
// Hardened BTOON decoder (SPEC §7). Invariants:
|
|
7
7
|
// * every read is bounds-checked against `end` (no reads past the input);
|
|
8
8
|
// * containers are depth-limited (max_depth) — no unbounded recursion;
|
|
9
|
-
// * nothing is allocated beyond what the remaining input could encode
|
|
9
|
+
// * nothing is allocated beyond what the remaining input could encode, and what containers
|
|
10
|
+
// allocate before their elements are decoded is bounded for the whole document.
|
|
10
11
|
const constants_js_1 = require("./constants.js");
|
|
11
12
|
const errors_js_1 = require("./errors.js");
|
|
12
13
|
const utf8_js_1 = require("./utf8.js");
|
|
@@ -22,6 +23,15 @@ const fromCharCode = String.fromCharCode;
|
|
|
22
23
|
const KEY_CACHE_SIZE = 4096;
|
|
23
24
|
const KEY_CACHE_MAX_LEN = 16;
|
|
24
25
|
const keyCache = new Array(KEY_CACHE_SIZE).fill(undefined);
|
|
26
|
+
// Limits on what containers allocate before their elements are decoded. A header may announce
|
|
27
|
+
// as many elements as there are bytes left, but nested containers announce the same bytes
|
|
28
|
+
// again at every level, so trusting the counts let a 1 MiB document of 64 nested tabulars
|
|
29
|
+
// build 4 GB of rows and exhaust the heap (review 2026-11-13, X1). An array, or the row list
|
|
30
|
+
// of a table, reserves at most MAX_RESERVE slots, and those open at any time at most
|
|
31
|
+
// RESERVE_BUDGET together; beyond that they grow as elements are decoded. Each tabular row is
|
|
32
|
+
// created once its first cell has been decoded.
|
|
33
|
+
const MAX_RESERVE = 1024;
|
|
34
|
+
const RESERVE_BUDGET = 64 * MAX_RESERVE;
|
|
25
35
|
const HEX = [];
|
|
26
36
|
for (let i = 0; i < 256; i++)
|
|
27
37
|
HEX.push((i < 16 ? '0' : '') + i.toString(16));
|
|
@@ -50,6 +60,8 @@ function plainBytes(b) {
|
|
|
50
60
|
class Decoder {
|
|
51
61
|
constructor(bytes, strict, maxDepth, tsObjects, bigAlways) {
|
|
52
62
|
this.pos = 0;
|
|
63
|
+
/** Slots reserved by the arrays and row lists being decoded (see reserve). */
|
|
64
|
+
this.reserved = 0;
|
|
53
65
|
this.bytes = plainBytes(bytes);
|
|
54
66
|
this.view = new DataView(this.bytes.buffer, this.bytes.byteOffset, this.bytes.byteLength);
|
|
55
67
|
this.end = this.bytes.length;
|
|
@@ -214,15 +226,26 @@ class Decoder {
|
|
|
214
226
|
this.pos += len;
|
|
215
227
|
return b;
|
|
216
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* How many of the `n` elements that an array, or a table's row list, announces it may
|
|
231
|
+
* allocate up front. They are charged to the document's budget until it is complete.
|
|
232
|
+
*/
|
|
233
|
+
reserve(n) {
|
|
234
|
+
const r = Math.min(n, MAX_RESERVE, RESERVE_BUDGET - this.reserved);
|
|
235
|
+
this.reserved += r;
|
|
236
|
+
return r;
|
|
237
|
+
}
|
|
217
238
|
array(n, depth) {
|
|
218
239
|
if (depth >= this.maxDepth)
|
|
219
240
|
throw this.err('max_depth', 'maximum nesting depth exceeded');
|
|
220
241
|
// Every element takes at least one byte.
|
|
221
242
|
if (n > this.end - this.pos)
|
|
222
243
|
throw this.err('truncated', 'array length exceeds the remaining input');
|
|
223
|
-
const
|
|
244
|
+
const r = this.reserve(n);
|
|
245
|
+
const a = new Array(r); // grows as elements beyond the reservation arrive
|
|
224
246
|
for (let i = 0; i < n; i++)
|
|
225
247
|
a[i] = this.value(depth + 1);
|
|
248
|
+
this.reserved -= r;
|
|
226
249
|
return a;
|
|
227
250
|
}
|
|
228
251
|
key() {
|
|
@@ -452,7 +475,7 @@ class Decoder {
|
|
|
452
475
|
// Each column needs at least 9 bytes: name length, type code, column length.
|
|
453
476
|
if (cols > (end - p) / 9)
|
|
454
477
|
throw bad('tabular column count exceeds the payload');
|
|
455
|
-
const names =
|
|
478
|
+
const names = [];
|
|
456
479
|
for (let c = 0; c < cols; c++) {
|
|
457
480
|
if (end - p < 4)
|
|
458
481
|
throw bad('tabular payload truncated');
|
|
@@ -463,7 +486,7 @@ class Decoder {
|
|
|
463
486
|
const name = (0, utf8_js_1.decodeUtf8)(this.bytes, p, n, this.strict);
|
|
464
487
|
if (name === undefined)
|
|
465
488
|
throw this.err('invalid_utf8', 'column name is not valid UTF-8', p);
|
|
466
|
-
names
|
|
489
|
+
names.push(name);
|
|
467
490
|
p += n;
|
|
468
491
|
}
|
|
469
492
|
if (new Set(names).size !== cols)
|
|
@@ -474,9 +497,10 @@ class Decoder {
|
|
|
474
497
|
// Every cell is at least one byte.
|
|
475
498
|
if (rowCount * cols > end - p)
|
|
476
499
|
throw bad('tabular row count exceeds the payload');
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
500
|
+
// Each row is created once its first cell has been decoded, so the rows are backed by
|
|
501
|
+
// input actually consumed, however many the header announces.
|
|
502
|
+
const reserved = this.reserve(rowCount);
|
|
503
|
+
const rows = new Array(reserved);
|
|
480
504
|
const savedEnd = this.end;
|
|
481
505
|
for (let c = 0; c < cols; c++) {
|
|
482
506
|
if (end - p < 4)
|
|
@@ -490,7 +514,15 @@ class Decoder {
|
|
|
490
514
|
this.pos = p;
|
|
491
515
|
this.end = colEnd;
|
|
492
516
|
try {
|
|
493
|
-
if (
|
|
517
|
+
if (c === 0) {
|
|
518
|
+
for (let r = 0; r < rowCount; r++) {
|
|
519
|
+
const v = this.value(depth + 2);
|
|
520
|
+
const row = {};
|
|
521
|
+
setProperty(row, name, v);
|
|
522
|
+
rows[r] = row;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else if (name === '__proto__') {
|
|
494
526
|
for (let r = 0; r < rowCount; r++)
|
|
495
527
|
setProperty(rows[r], name, this.value(depth + 2));
|
|
496
528
|
}
|
|
@@ -510,6 +542,7 @@ class Decoder {
|
|
|
510
542
|
p = colEnd;
|
|
511
543
|
}
|
|
512
544
|
this.end = savedEnd;
|
|
545
|
+
this.reserved -= reserved;
|
|
513
546
|
if (p !== end)
|
|
514
547
|
throw bad('trailing bytes in tabular payload');
|
|
515
548
|
this.pos = end;
|
package/dist/cjs/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/cjs/index.js
CHANGED
|
@@ -76,10 +76,13 @@ function encodeAsync(value, options) {
|
|
|
76
76
|
return (0, core_js_1.encodeAsyncWith)(value, options, platform_portable_js_1.portablePlatform);
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
|
-
* Like {@link decode}, but
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
79
|
+
* Like {@link decode}, but asynchronous. In the Node.js entry, decompression runs on the
|
|
80
|
+
* `node:zlib` thread pool. In the portable entry, zlib uses the same pure-JS inflate as
|
|
81
|
+
* `decode`, and Brotli can also be decompressed with a `DecompressionStream` where the runtime
|
|
82
|
+
* has shown that it rejects bytes after the end of a stream (never Zstandard, whose window
|
|
83
|
+
* size it can't bound). Signatures are verified with `crypto.subtle.verify` where available.
|
|
84
|
+
* `decodeAsync` never accepts a document that `decode` rejects. The input is copied first, so
|
|
85
|
+
* the caller may reuse its buffer immediately. Rejects with a {@link BtoonError}.
|
|
83
86
|
*/
|
|
84
87
|
function decodeAsync(data, options) {
|
|
85
88
|
return (0, core_js_1.decodeAsyncWith)(data, options, platform_portable_js_1.portablePlatform);
|
package/dist/cjs/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/cjs/node.js
CHANGED
|
@@ -72,10 +72,13 @@ function encodeAsync(value, options) {
|
|
|
72
72
|
return (0, core_js_1.encodeAsyncWith)(value, options, platform_node_js_1.nodePlatform);
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
|
-
* Like {@link decode}, but
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
75
|
+
* Like {@link decode}, but asynchronous. In the Node.js entry, decompression runs on the
|
|
76
|
+
* `node:zlib` thread pool. In the portable entry, zlib uses the same pure-JS inflate as
|
|
77
|
+
* `decode`, and Brotli can also be decompressed with a `DecompressionStream` where the runtime
|
|
78
|
+
* has shown that it rejects bytes after the end of a stream (never Zstandard, whose window
|
|
79
|
+
* size it can't bound). Signatures are verified with `crypto.subtle.verify` where available.
|
|
80
|
+
* `decodeAsync` never accepts a document that `decode` rejects. The input is copied first, so
|
|
81
|
+
* the caller may reuse its buffer immediately. Rejects with a {@link BtoonError}.
|
|
79
82
|
*/
|
|
80
83
|
function decodeAsync(data, options) {
|
|
81
84
|
return (0, core_js_1.decodeAsyncWith)(data, options, platform_node_js_1.nodePlatform);
|
|
@@ -45,80 +45,115 @@ function nodeFn(name) {
|
|
|
45
45
|
function plain(b) {
|
|
46
46
|
return new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
/** node:zlib options: output capped at the declared size, and `info` to learn how much input was consumed. */
|
|
49
|
+
function options(size, extra) {
|
|
50
|
+
return { ...extra?.(size), info: true, maxOutputLength: Math.max(1, size) };
|
|
51
|
+
}
|
|
52
|
+
function failure(alg, e) {
|
|
53
|
+
if (e.code === 'ERR_BUFFER_TOO_LARGE') {
|
|
54
|
+
return (0, errors_js_1.fail)('invalid_envelope', `${alg} payload decompresses to more than the declared size`, e);
|
|
55
|
+
}
|
|
56
|
+
return (0, errors_js_1.fail)('invalid_envelope', `${alg} payload is corrupt: ${e.message}`, e);
|
|
57
|
+
}
|
|
58
|
+
/** The output of a node:zlib decompressor, once it is clear that the stream used the whole payload. */
|
|
59
|
+
function output(alg, data, size, r) {
|
|
60
|
+
let out;
|
|
61
|
+
let consumed;
|
|
62
|
+
if (r instanceof Uint8Array) {
|
|
63
|
+
out = r;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const info = r;
|
|
67
|
+
out = info.buffer;
|
|
68
|
+
consumed = info.engine?.bytesWritten;
|
|
69
|
+
}
|
|
70
|
+
if (typeof consumed !== 'number') {
|
|
71
|
+
// A runtime that doesn't report the input it consumed can't show that no bytes follow the
|
|
72
|
+
// stream (SPEC §6). zlib has an exact pure-JS decoder; the other algorithms don't.
|
|
73
|
+
if (alg === 'zlib')
|
|
74
|
+
return (0, inflate_js_1.inflateZlib)(data, size);
|
|
75
|
+
throw (0, errors_js_1.fail)('unsupported_compression', `${alg} decompression in this runtime cannot detect trailing bytes; pass options.codecs.${alg}`);
|
|
76
|
+
}
|
|
77
|
+
if (consumed !== data.length)
|
|
78
|
+
throw (0, errors_js_1.fail)('invalid_envelope', `${alg} payload has trailing bytes`);
|
|
79
|
+
return plain(out);
|
|
80
|
+
}
|
|
81
|
+
/** Wraps a node:zlib `*Sync` decompressor, for decode. */
|
|
52
82
|
function decompressor(alg, fn, extra) {
|
|
53
83
|
if (fn === undefined)
|
|
54
84
|
return undefined;
|
|
55
85
|
return (data, size) => {
|
|
56
86
|
let r;
|
|
57
87
|
try {
|
|
58
|
-
r = fn(data,
|
|
88
|
+
r = fn(data, options(size, extra));
|
|
59
89
|
}
|
|
60
90
|
catch (e) {
|
|
61
|
-
|
|
62
|
-
throw (0, errors_js_1.fail)('invalid_envelope', `${alg} payload decompresses to more than the declared size`, e);
|
|
63
|
-
}
|
|
64
|
-
throw (0, errors_js_1.fail)('invalid_envelope', `${alg} payload is corrupt: ${e.message}`, e);
|
|
65
|
-
}
|
|
66
|
-
let out;
|
|
67
|
-
let consumed;
|
|
68
|
-
if (r instanceof Uint8Array) {
|
|
69
|
-
out = r;
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
const info = r;
|
|
73
|
-
out = info.buffer;
|
|
74
|
-
consumed = info.engine?.bytesWritten;
|
|
75
|
-
}
|
|
76
|
-
if (typeof consumed === 'number') {
|
|
77
|
-
if (consumed !== data.length)
|
|
78
|
-
throw (0, errors_js_1.fail)('invalid_envelope', `${alg} payload has trailing bytes`);
|
|
91
|
+
throw failure(alg, e);
|
|
79
92
|
}
|
|
80
|
-
|
|
81
|
-
// A runtime that doesn't report consumed input: the stream must end in its Adler-32.
|
|
82
|
-
const n = data.length;
|
|
83
|
-
const trailer = n >= 4 ? ((data[n - 4] << 24) | (data[n - 3] << 16) | (data[n - 2] << 8) | data[n - 1]) >>> 0 : -1;
|
|
84
|
-
if (trailer !== (0, inflate_js_1.adler32)(out))
|
|
85
|
-
throw (0, errors_js_1.fail)('invalid_envelope', 'zlib payload has trailing bytes');
|
|
86
|
-
}
|
|
87
|
-
return plain(out);
|
|
93
|
+
return output(alg, data, size, r);
|
|
88
94
|
};
|
|
89
95
|
}
|
|
90
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Wraps the callback form of the same node:zlib decompressor, for decodeAsync: it runs on the
|
|
98
|
+
* thread pool, with the same limits and checks, so both give the same verdict.
|
|
99
|
+
*/
|
|
100
|
+
function asyncDecompressor(alg, fn, extra) {
|
|
101
|
+
if (fn === undefined)
|
|
102
|
+
return undefined;
|
|
103
|
+
return (data, size) => new Promise((resolve, reject) => {
|
|
104
|
+
fn(data, options(size, extra), (e, r) => {
|
|
105
|
+
if (e) {
|
|
106
|
+
reject(failure(alg, e));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
resolve(output(alg, data, size, r));
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
reject(err);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function compressor(fn, opts) {
|
|
91
119
|
if (fn === undefined)
|
|
92
120
|
return undefined;
|
|
93
|
-
return (data) => plain(fn(data,
|
|
121
|
+
return (data) => plain(fn(data, opts));
|
|
94
122
|
}
|
|
95
123
|
const decompress = {};
|
|
124
|
+
const decompressAsync = {};
|
|
96
125
|
const compress = {};
|
|
97
126
|
decompress.zlib = decompressor('zlib', nodeFn('inflateSync'));
|
|
127
|
+
decompressAsync.zlib = asyncDecompressor('zlib', nodeFn('inflate'));
|
|
98
128
|
compress.zlib = compressor(nodeFn('deflateSync'), { level: 6 });
|
|
99
129
|
const brotliQuality = zlib.constants?.BROTLI_PARAM_QUALITY;
|
|
100
130
|
if (brotliQuality !== undefined) {
|
|
101
131
|
decompress.brotli = decompressor('brotli', nodeFn('brotliDecompressSync'));
|
|
132
|
+
decompressAsync.brotli = asyncDecompressor('brotli', nodeFn('brotliDecompress'));
|
|
102
133
|
compress.brotli = compressor(nodeFn('brotliCompressSync'), { params: { [brotliQuality]: 6 } });
|
|
103
134
|
}
|
|
104
|
-
// node:zlib has Zstandard from Node.js 22.15 / 23.8. SPEC §6: bound the decoder's memory
|
|
105
|
-
//
|
|
135
|
+
// node:zlib has Zstandard from Node.js 22.15 / 23.8. SPEC §6: bound the decoder's memory by
|
|
136
|
+
// rejecting frames whose window exceeds max(8 MiB, min(uncompressed_size, 128 MiB)). Without
|
|
137
|
+
// the windowLogMax parameter there is no bound, so zstd decompression is not offered at all.
|
|
106
138
|
const zstdWindowLogMax = zlib.constants?.ZSTD_d_windowLogMax;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
delete decompress[alg];
|
|
139
|
+
if (zstdWindowLogMax !== undefined) {
|
|
140
|
+
const zstdWindow = (size) => ({
|
|
141
|
+
params: { [zstdWindowLogMax]: Math.ceil(Math.log2(Math.max(8 << 20, Math.min(size, 128 << 20)))) },
|
|
142
|
+
});
|
|
143
|
+
decompress.zstd = decompressor('zstd', nodeFn('zstdDecompressSync'), zstdWindow);
|
|
144
|
+
decompressAsync.zstd = asyncDecompressor('zstd', nodeFn('zstdDecompress'), zstdWindow);
|
|
114
145
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
146
|
+
compress.zstd = compressor(nodeFn('zstdCompressSync'), {});
|
|
147
|
+
for (const table of [decompress, decompressAsync, compress]) {
|
|
148
|
+
for (const alg of Object.keys(table)) {
|
|
149
|
+
if (table[alg] === undefined)
|
|
150
|
+
delete table[alg];
|
|
151
|
+
}
|
|
118
152
|
}
|
|
119
153
|
/** Node.js (and Bun): node:zlib for zlib, Brotli and Zstandard; node:crypto for HMAC. */
|
|
120
154
|
exports.nodePlatform = {
|
|
121
155
|
decompress,
|
|
156
|
+
decompressAsync,
|
|
122
157
|
compress,
|
|
123
158
|
hmac(key, parts) {
|
|
124
159
|
const h = (0, node_crypto_1.createHmac)('sha256', key);
|
package/dist/cjs/web.d.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { CompressionAlgorithm } from './options.js';
|
|
2
2
|
/** The Compression Streams format name for `alg`, if this runtime supports it. */
|
|
3
3
|
export declare function webFormat(alg: CompressionAlgorithm, kind: 'CompressionStream' | 'DecompressionStream'): string | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* The DecompressionStream format that decodeAsync may use for `alg` when no other
|
|
6
|
+
* decompressor is available, or undefined: never for zstd, and only once the runtime has
|
|
7
|
+
* shown that it rejects trailing bytes.
|
|
8
|
+
*/
|
|
9
|
+
export declare function trustedDecompressionFormat(alg: CompressionAlgorithm): Promise<string | undefined>;
|
|
10
|
+
/** Whether decodeAsync might decompress `alg` with a DecompressionStream here (for error messages). */
|
|
11
|
+
export declare function mayDecompressAsync(alg: CompressionAlgorithm): boolean;
|
|
4
12
|
export declare function webCompress(format: string, data: Uint8Array): Promise<Uint8Array>;
|
|
5
13
|
/**
|
|
6
|
-
* Decompresses with a DecompressionStream, stopping as soon as the output exceeds
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* stream, which a strict decoder must reject.
|
|
14
|
+
* Decompresses with a DecompressionStream, stopping as soon as the output exceeds `limit`
|
|
15
|
+
* (the declared size). Only for a format that trustedDecompressionFormat() returned, which
|
|
16
|
+
* rejects bytes after the end of the stream.
|
|
10
17
|
*/
|
|
11
18
|
export declare function webDecompress(alg: CompressionAlgorithm, format: string, data: Uint8Array, limit: number): Promise<Uint8Array>;
|
|
12
19
|
type Subtle = {
|