@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/esm/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 = {
|
package/dist/esm/web.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Asynchronous implementations on top of web platform APIs: Compression Streams and
|
|
2
2
|
// WebCrypto. Used by encodeAsync/decodeAsync wherever the runtime provides them.
|
|
3
3
|
import { BtoonError, fail } from './errors.js';
|
|
4
|
-
import { adler32 } from './inflate.js';
|
|
5
4
|
const FORMATS = { zlib: 'deflate', brotli: 'brotli', zstd: 'zstd' };
|
|
5
|
+
// DecompressionStream has no way to bound a Zstandard window (SPEC §6), so it is never used
|
|
6
|
+
// for zstd.
|
|
7
|
+
const DECOMPRESSION_FORMATS = { zlib: 'deflate', brotli: 'brotli' };
|
|
6
8
|
const CHUNK = 64 * 1024;
|
|
7
9
|
const supported = new Map();
|
|
8
10
|
function streamCtor(kind) {
|
|
@@ -40,6 +42,76 @@ function concat(chunks, total) {
|
|
|
40
42
|
}
|
|
41
43
|
return out;
|
|
42
44
|
}
|
|
45
|
+
function fromHex(h) {
|
|
46
|
+
const out = new Uint8Array(h.length / 2);
|
|
47
|
+
for (let i = 0; i < out.length; i++)
|
|
48
|
+
out[i] = parseInt(h.slice(2 * i, 2 * i + 2), 16);
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
// Streams of the five bytes "BTOON" (zlib level 9, Brotli quality 11) for the trust probe.
|
|
52
|
+
const PROBE_TEXT = 'BTOON';
|
|
53
|
+
const PROBE_STREAMS = { deflate: '78da730af1f7f7030004780183', brotli: '0b028042544f4f4e03' };
|
|
54
|
+
const trust = new Map();
|
|
55
|
+
/**
|
|
56
|
+
* Whether this runtime's DecompressionStream can be trusted with `format`: it must decode a
|
|
57
|
+
* valid stream and reject one followed by a byte (in the same write or in the next), or by a
|
|
58
|
+
* second stream. Node.js 18 to 23 (some later 22.x releases excepted) ignore bytes after a
|
|
59
|
+
* zlib stream, and Node.js 22 and 24 after a Brotli stream, while SPEC §6 requires
|
|
60
|
+
* `invalid_envelope`. Checked once per format.
|
|
61
|
+
*/
|
|
62
|
+
function trusted(format) {
|
|
63
|
+
let p = trust.get(format);
|
|
64
|
+
if (p === undefined) {
|
|
65
|
+
p = probe(format);
|
|
66
|
+
trust.set(format, p);
|
|
67
|
+
}
|
|
68
|
+
return p;
|
|
69
|
+
}
|
|
70
|
+
async function probe(format) {
|
|
71
|
+
const hex = PROBE_STREAMS[format];
|
|
72
|
+
if (hex === undefined)
|
|
73
|
+
return false;
|
|
74
|
+
const valid = fromHex(hex);
|
|
75
|
+
const next = (extra) => {
|
|
76
|
+
const out = new Uint8Array(valid.length + extra.length);
|
|
77
|
+
out.set(valid);
|
|
78
|
+
out.set(extra, valid.length);
|
|
79
|
+
return out;
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
const out = await decompressChunks(format, [valid], 64, format);
|
|
83
|
+
if (String.fromCharCode(...out) !== PROBE_TEXT)
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
for (const chunks of [[next(Uint8Array.of(0))], [valid, Uint8Array.of(0)], [next(valid)]]) {
|
|
90
|
+
try {
|
|
91
|
+
await decompressChunks(format, chunks, 64, format);
|
|
92
|
+
return false; // trailing bytes accepted
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// rejected, as it should be
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The DecompressionStream format that decodeAsync may use for `alg` when no other
|
|
102
|
+
* decompressor is available, or undefined: never for zstd, and only once the runtime has
|
|
103
|
+
* shown that it rejects trailing bytes.
|
|
104
|
+
*/
|
|
105
|
+
export async function trustedDecompressionFormat(alg) {
|
|
106
|
+
const format = DECOMPRESSION_FORMATS[alg];
|
|
107
|
+
if (format === undefined || webFormat(alg, 'DecompressionStream') === undefined)
|
|
108
|
+
return undefined;
|
|
109
|
+
return (await trusted(format)) ? format : undefined;
|
|
110
|
+
}
|
|
111
|
+
/** Whether decodeAsync might decompress `alg` with a DecompressionStream here (for error messages). */
|
|
112
|
+
export function mayDecompressAsync(alg) {
|
|
113
|
+
return DECOMPRESSION_FORMATS[alg] !== undefined && webFormat(alg, 'DecompressionStream') !== undefined;
|
|
114
|
+
}
|
|
43
115
|
async function readAll(reader, limit, alg) {
|
|
44
116
|
const chunks = [];
|
|
45
117
|
let total = 0;
|
|
@@ -56,11 +128,18 @@ async function readAll(reader, limit, alg) {
|
|
|
56
128
|
}
|
|
57
129
|
return concat(chunks, total);
|
|
58
130
|
}
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
|
|
131
|
+
/** Splits `data` into the chunks written to a stream. */
|
|
132
|
+
function chunked(data) {
|
|
133
|
+
const chunks = [];
|
|
134
|
+
for (let i = 0; i < data.length; i += CHUNK)
|
|
135
|
+
chunks.push(data.subarray(i, Math.min(i + CHUNK, data.length)));
|
|
136
|
+
return chunks;
|
|
137
|
+
}
|
|
138
|
+
/** Writes `chunks`, honouring backpressure, then closes the writer. */
|
|
139
|
+
async function pump(writer, chunks) {
|
|
140
|
+
for (const chunk of chunks) {
|
|
62
141
|
await writer.ready;
|
|
63
|
-
await writer.write(
|
|
142
|
+
await writer.write(chunk);
|
|
64
143
|
}
|
|
65
144
|
await writer.close();
|
|
66
145
|
}
|
|
@@ -70,41 +149,45 @@ function messageOf(e) {
|
|
|
70
149
|
export async function webCompress(format, data) {
|
|
71
150
|
const stream = new (streamCtor('CompressionStream'))(format);
|
|
72
151
|
const writer = stream.writable.getWriter();
|
|
73
|
-
const writing = pump(writer, data);
|
|
152
|
+
const writing = pump(writer, chunked(data));
|
|
74
153
|
writing.catch(() => undefined);
|
|
75
154
|
const out = await readAll(stream.readable.getReader(), Infinity, format);
|
|
76
155
|
await writing;
|
|
77
156
|
return out;
|
|
78
157
|
}
|
|
79
158
|
/**
|
|
80
|
-
*
|
|
81
|
-
* `
|
|
82
|
-
* Adler-32 of the output: older runtimes silently ignore bytes after the end of the
|
|
83
|
-
* stream, which a strict decoder must reject.
|
|
159
|
+
* Runs `chunks` through a DecompressionStream. Output beyond `limit` bytes fails with
|
|
160
|
+
* `invalid_envelope` ("<label> payload decompresses to more than the declared size").
|
|
84
161
|
*/
|
|
85
|
-
|
|
162
|
+
async function decompressChunks(format, chunks, limit, label) {
|
|
86
163
|
const stream = new (streamCtor('DecompressionStream'))(format);
|
|
87
164
|
const writer = stream.writable.getWriter();
|
|
88
|
-
const writing = pump(writer,
|
|
165
|
+
const writing = pump(writer, chunks);
|
|
89
166
|
writing.catch(() => undefined);
|
|
90
|
-
let out;
|
|
91
167
|
try {
|
|
92
|
-
out = await readAll(stream.readable.getReader(), limit,
|
|
168
|
+
const out = await readAll(stream.readable.getReader(), limit, label);
|
|
93
169
|
await writing;
|
|
170
|
+
return out;
|
|
94
171
|
}
|
|
95
172
|
catch (e) {
|
|
96
173
|
writer.abort(e).catch(() => undefined);
|
|
174
|
+
throw e;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Decompresses with a DecompressionStream, stopping as soon as the output exceeds `limit`
|
|
179
|
+
* (the declared size). Only for a format that trustedDecompressionFormat() returned, which
|
|
180
|
+
* rejects bytes after the end of the stream.
|
|
181
|
+
*/
|
|
182
|
+
export async function webDecompress(alg, format, data, limit) {
|
|
183
|
+
try {
|
|
184
|
+
return await decompressChunks(format, chunked(data), limit, alg);
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
97
187
|
if (e instanceof BtoonError)
|
|
98
188
|
throw e;
|
|
99
189
|
throw fail('invalid_envelope', `${alg} payload is corrupt: ${messageOf(e)}`, e);
|
|
100
190
|
}
|
|
101
|
-
if (alg === 'zlib') {
|
|
102
|
-
const n = data.length;
|
|
103
|
-
const trailer = n >= 4 ? ((data[n - 4] << 24) | (data[n - 3] << 16) | (data[n - 2] << 8) | data[n - 1]) >>> 0 : -1;
|
|
104
|
-
if (trailer !== adler32(out))
|
|
105
|
-
throw fail('invalid_envelope', 'zlib payload has trailing bytes');
|
|
106
|
-
}
|
|
107
|
-
return out;
|
|
108
191
|
}
|
|
109
192
|
/** WebCrypto's SubtleCrypto, if available (absent in Node 18 and on insecure origins). */
|
|
110
193
|
export function subtle() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btoon/js",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "BTOON (Binary TOON) for JavaScript and TypeScript: canonical, hardened MessagePack with a columnar encoding for tables. Pure TypeScript, zero dependencies; runs in browsers, Node.js, Deno, Bun and edge runtimes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"btoon",
|