@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 CHANGED
@@ -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` / `decodeAsync` compression | `CompressionStream` / `DecompressionStream` where the runtime supports the format, else the synchronous codecs | the same |
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
- `DecompressionStream('deflate')` is the RFC 1950 zlib format the spec requires. Other
102
- formats depend on the runtime: at the time of writing, Brotli streams exist in Node.js 22+
103
- and Bun, and Zstandard streams in Bun.
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.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; the
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
@@ -1,5 +1,5 @@
1
1
  /** Version of this library. */
2
- export declare const VERSION: "0.1.1";
2
+ export declare const VERSION: "0.1.2";
3
3
  /** Version of the BTOON specification this library implements. */
4
4
  export declare const SPEC_VERSION: "0.1";
5
5
  export declare const EXT_TIMESTAMP = -1;
@@ -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.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).
@@ -1,8 +1,13 @@
1
1
  import { type CompressionAlgorithm, type DecodeOptions, type EncodeOptions } from './options.js';
2
- /** Synchronous building blocks of one environment. */
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.webFormat)(alg, 'DecompressionStream') !== undefined);
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
- const format = (0, web_js_1.webFormat)(alg, 'DecompressionStream');
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
+ }
@@ -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 a = new Array(n);
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 = new Array(cols);
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[c] = name;
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
- const rows = new Array(rowCount);
478
- for (let r = 0; r < rowCount; r++)
479
- rows[r] = {};
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 (name === '__proto__') {
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;
@@ -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 decompresses with `DecompressionStream` (when the runtime
64
- * supports the algorithm) and verifies signatures with `crypto.subtle.verify`, falling back
65
- * to the synchronous implementations. The input is copied first, so the caller may reuse
66
- * its buffer immediately. Rejects with a {@link BtoonError}.
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 decompresses with `DecompressionStream` (when the runtime
80
- * supports the algorithm) and verifies signatures with `crypto.subtle.verify`, falling back
81
- * to the synchronous implementations. The input is copied first, so the caller may reuse
82
- * its buffer immediately. Rejects with a {@link BtoonError}.
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);
@@ -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 decompresses with `DecompressionStream` (when the runtime
60
- * supports the algorithm) and verifies signatures with `crypto.subtle.verify`, falling back
61
- * to the synchronous implementations. The input is copied first, so the caller may reuse
62
- * its buffer immediately. Rejects with a {@link BtoonError}.
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 decompresses with `DecompressionStream` (when the runtime
76
- * supports the algorithm) and verifies signatures with `crypto.subtle.verify`, falling back
77
- * to the synchronous implementations. The input is copied first, so the caller may reuse
78
- * its buffer immediately. Rejects with a {@link BtoonError}.
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
- * Wraps a node:zlib `*Sync` decompressor: output is capped at the declared size
50
- * (`maxOutputLength`), and input left over after the end of the stream is rejected.
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, { ...extra?.(size), info: true, maxOutputLength: Math.max(1, size) });
88
+ r = fn(data, options(size, extra));
59
89
  }
60
90
  catch (e) {
61
- if (e.code === 'ERR_BUFFER_TOO_LARGE') {
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
- else if (alg === 'zlib') {
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
- function compressor(fn, options) {
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, options));
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
- // by rejecting frames whose window exceeds max(8 MiB, min(uncompressed_size, 128 MiB)).
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
- decompress.zstd = decompressor('zstd', nodeFn('zstdDecompressSync'), (size) => zstdWindowLogMax === undefined
108
- ? {}
109
- : { params: { [zstdWindowLogMax]: Math.ceil(Math.log2(Math.max(8 << 20, Math.min(size, 128 << 20)))) } });
110
- compress.zstd = compressor(nodeFn('zstdCompressSync'), {});
111
- for (const alg of Object.keys(decompress)) {
112
- if (decompress[alg] === undefined)
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
- for (const alg of Object.keys(compress)) {
116
- if (compress[alg] === undefined)
117
- delete compress[alg];
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
- * `limit` (the declared size). For zlib it also checks that the payload ends with the
8
- * Adler-32 of the output: older runtimes silently ignore bytes after the end of the
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 = {