zlib-streams 1.2.3 → 1.4.0

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/Makefile CHANGED
@@ -298,6 +298,9 @@ run_all_tests: dist/zlib-streams-dev.wasm
298
298
  @node src/wasm/tests/test_process_error_encoding.js dist/zlib-streams-dev.wasm
299
299
  # Buffers and zlib state released whichever end ends the stream, with no transformer hook
300
300
  @node src/wasm/tests/test_stream_abort_releases_memory.js dist/zlib-streams-dev.wasm
301
+ # Bytes after the end of the stream rejected, as the platform inflaters do
302
+ @node src/wasm/tests/test_trailing_data.js dist/zlib-streams-dev.wasm
303
+ @node src/wasm/tests/test_unknown_format.js dist/zlib-streams-dev.wasm
301
304
  @echo "Completed run_all_tests"
302
305
 
303
306
  dist/zlib-streams-dev.wasm: $(WASM_SRCS)
package/README.md CHANGED
@@ -42,5 +42,45 @@ Output: `dist/zip-module.wasm`
42
42
 
43
43
  The files in `dist/` are tracked, so a checkout leaves them with the same timestamp as the sources and `make dist/<file>` reports them up to date even when the sources changed. The three targets above always rebuild their file; the test targets depend on the file rules and rebuild only when a source is newer. Rebuild and commit `dist/` in the same commit as a change to `src/`.
44
44
 
45
+ ## API
46
+
47
+ The npm package exports the JavaScript driver as `zlib-streams/zlib-streams.js` and the modules as `zlib-streams/zlib-streams.wasm` and `zlib-streams/zip-module.wasm`. The module imports nothing, so it is instantiated bare, and its exports are handed to the driver before any stream is created.
48
+
49
+ ```js
50
+ import { setWasmExports, CompressionStreamZlib, DecompressionStreamZlib } from "zlib-streams/zlib-streams.js";
51
+
52
+ const wasmBytes = await (await fetch(import.meta.resolve("zlib-streams/zlib-streams.wasm"))).arrayBuffer();
53
+ const { instance } = await WebAssembly.instantiate(wasmBytes);
54
+ setWasmExports(instance.exports);
55
+
56
+ const compressed = new Blob(["Hello World"]).stream().pipeThrough(new CompressionStreamZlib("gzip", { level: 9 }));
57
+ const text = await new Response(compressed.pipeThrough(new DecompressionStreamZlib("gzip"))).text();
58
+ ```
59
+
60
+ ### Driver functions
61
+
62
+ - `setWasmExports(exports)`: registers the exports of an instantiated module (`malloc`, `free`, `memory` and the codec functions); throws `"Invalid WASM module"` when they are missing.
63
+ - `resetWasmExports()`: forgets the exports; the next stream throws `"WASM module not loaded"`.
64
+ - `setInitError(error)`: records why the module could not be loaded; the error is the `cause` of `"WASM module not loaded"`.
65
+
66
+ ### Streams
67
+
68
+ ```js
69
+ new CompressionStreamZlib(format = "deflate", { level, outBuffer, inBufferSize })
70
+ new DecompressionStreamZlib(format = "deflate", { outBuffer, inBufferSize })
71
+ ```
72
+
73
+ Both classes return a `{ readable, writable }` pair usable with `pipeThrough()`. The formats are `"deflate"` (the zlib wrapper, RFC 1950), `"deflate-raw"` (RFC 1951, no wrapper), `"gzip"` and, for decompression only, `"deflate64-raw"`. `level` is the zlib compression level, 0 to 9, -1 (the default) for the zlib default. `outBuffer` and `inBufferSize` are the sizes in bytes of the output and input buffers allocated in the module heap, 64 KB each by default; input chunks are processed in slices of at most 32 KB.
74
+
75
+ The static properties `supportedFormats` (an array) and `requiresModule` (`true`) let a host check the formats and know that the classes are unusable until `setWasmExports()` has run.
76
+
77
+ ### Errors
78
+
79
+ - Bytes following the end of a stream are rejected with `"trailing data after the end of the stream"`, whatever the format, as the `DecompressionStream` of the browsers does; concatenated gzip members are not decoded.
80
+ - A corrupted stream fails with `"process error:<code>"` carrying the zlib return code; a stream whose end is missing fails on close with `"end error:<code>"`.
81
+ - A failure to allocate memory, on construction or while processing, carries the property `code` set to `"Z_MEM_ERROR"`: `"allocation failed"` when the heap is exhausted, `"init failed:-4"` or `"process error:-4"` when zlib reports it.
82
+
83
+ The heap of the module is fixed, so the number of streams alive at the same time is bounded by their buffers; a stream releases its buffers when it closes, errors or is cancelled.
84
+
45
85
  ## License
46
86
  See LICENSE for details.
@@ -32,6 +32,10 @@ const FORMAT_DEFLATE = "deflate";
32
32
  const FORMAT_DEFLATE_RAW = "deflate-raw";
33
33
  const FORMAT_DEFLATE64_RAW = "deflate64-raw";
34
34
  const FORMAT_GZIP = "gzip";
35
+ const Z_MEM_ERROR = -4;
36
+ const Z_MEM_ERROR_CODE = "Z_MEM_ERROR";
37
+ const COMPRESSION_FORMATS = [FORMAT_DEFLATE, FORMAT_DEFLATE_RAW, FORMAT_GZIP];
38
+ const DECOMPRESSION_FORMATS = [FORMAT_DEFLATE, FORMAT_DEFLATE_RAW, FORMAT_GZIP, FORMAT_DEFLATE64_RAW];
35
39
 
36
40
  let wasm, malloc, free, memory, initError;
37
41
 
@@ -52,7 +56,17 @@ export function resetWasmExports() {
52
56
  wasm = malloc = free = memory = initError = null;
53
57
  }
54
58
 
59
+ function setZlibCode(error, result) {
60
+ if (result === Z_MEM_ERROR) {
61
+ error.code = Z_MEM_ERROR_CODE;
62
+ }
63
+ return error;
64
+ }
65
+
55
66
  function _make(isCompress, type, options = {}) {
67
+ if (!(isCompress ? COMPRESSION_FORMATS : DECOMPRESSION_FORMATS).includes(type)) {
68
+ throw new TypeError("Unsupported format: " + type);
69
+ }
56
70
  if (!wasm) {
57
71
  const error = new Error("WASM module not loaded");
58
72
  error.cause = initError;
@@ -61,7 +75,7 @@ function _make(isCompress, type, options = {}) {
61
75
  const level = (typeof options.level === "number") ? options.level : -1;
62
76
  const outBufferSize = (typeof options.outBuffer === "number") ? options.outBuffer : 64 * 1024;
63
77
  const inBufferSize = (typeof options.inBufferSize === "number") ? options.inBufferSize : 64 * 1024;
64
- const state = { out: 0, in: 0, inBufferSize: 0, streamHandle: 0 };
78
+ const state = { out: 0, in: 0, inBufferSize: 0, streamHandle: 0, streamEnded: false };
65
79
  let readableController, writableController, resolveWrite, cancelReason;
66
80
  let backpressure = true;
67
81
  let cancelled = false;
@@ -114,7 +128,7 @@ function _make(isCompress, type, options = {}) {
114
128
  }
115
129
  const result = disposeStream(state);
116
130
  if (result !== 0) {
117
- const error = new Error("end error:" + result);
131
+ const error = setZlibCode(new Error("end error:" + result), result);
118
132
  readableController.error(error);
119
133
  throw error;
120
134
  }
@@ -134,7 +148,7 @@ function _make(isCompress, type, options = {}) {
134
148
  state.in = malloc(inBufferSize);
135
149
  state.inBufferSize = inBufferSize;
136
150
  if (!state.out || !state.in) {
137
- throw new Error("allocation failed");
151
+ throw setZlibCode(new Error("allocation failed"), Z_MEM_ERROR);
138
152
  }
139
153
  if (isCompress) {
140
154
  state._process = wasm.deflate_process;
@@ -170,7 +184,7 @@ function _make(isCompress, type, options = {}) {
170
184
  }
171
185
  }
172
186
  if (result !== 0) {
173
- throw new Error("init failed:" + result);
187
+ throw setZlibCode(new Error("init failed:" + result), result);
174
188
  }
175
189
  } catch (error) {
176
190
  disposeStream(state);
@@ -185,6 +199,9 @@ function _make(isCompress, type, options = {}) {
185
199
  const out = state.out;
186
200
  let offset = 0;
187
201
  while (offset < chunk.length) {
202
+ if (state.streamEnded) {
203
+ throw new Error("trailing data after the end of the stream");
204
+ }
188
205
  const toRead = Math.min(chunk.length - offset, 32 * 1024);
189
206
  if (!state.in || state.inBufferSize < toRead) {
190
207
  if (state.in && free) {
@@ -194,7 +211,7 @@ function _make(isCompress, type, options = {}) {
194
211
  state.in = malloc(toRead);
195
212
  state.inBufferSize = toRead;
196
213
  if (!state.in) {
197
- throw new Error("allocation failed");
214
+ throw setZlibCode(new Error("allocation failed"), Z_MEM_ERROR);
198
215
  }
199
216
  }
200
217
  heap.set(chunk.subarray(offset, offset + toRead), state.in);
@@ -203,14 +220,16 @@ function _make(isCompress, type, options = {}) {
203
220
  const code = (result >> 24) & 0xff;
204
221
  const signedCode = (code & 0x80) ? code - 256 : code;
205
222
  if (signedCode < 0) {
206
- throw new Error("process error:" + signedCode);
223
+ throw setZlibCode(new Error("process error:" + signedCode), signedCode);
207
224
  }
208
225
  const prod = result & 0x00ffffff;
209
226
  if (prod) {
210
227
  enqueue(heap.slice(out, out + prod));
211
228
  }
212
229
  const consumed = last_consumed(state.streamHandle);
213
- if (consumed === 0 && prod === 0) {
230
+ if (code === 1) {
231
+ state.streamEnded = true;
232
+ } else if (consumed === 0 && prod === 0) {
214
233
  break;
215
234
  }
216
235
  offset += consumed;
@@ -226,7 +245,7 @@ function _make(isCompress, type, options = {}) {
226
245
  const code = (result >> 24) & 0xff;
227
246
  const signedCode = (code & 0x80) ? code - 256 : code;
228
247
  if (signedCode < 0) {
229
- throw new Error("process error:" + signedCode);
248
+ throw setZlibCode(new Error("process error:" + signedCode), signedCode);
230
249
  }
231
250
  const produced = result & 0x00ffffff;
232
251
  if (produced) {
@@ -285,7 +304,7 @@ export class DecompressionStreamZlib {
285
304
  CompressionStreamZlib.requiresModule = true;
286
305
  DecompressionStreamZlib.requiresModule = true;
287
306
  // Constructing these classes before the module is loaded throws, so capability probes cannot rely
288
- // on trying the constructor; the formats are declared instead, next to the branches implementing
289
- // them in _make().
290
- CompressionStreamZlib.supportedFormats = [FORMAT_DEFLATE, FORMAT_DEFLATE_RAW, FORMAT_GZIP];
291
- DecompressionStreamZlib.supportedFormats = [FORMAT_DEFLATE, FORMAT_DEFLATE_RAW, FORMAT_GZIP, FORMAT_DEFLATE64_RAW];
307
+ // on trying the constructor; the formats are declared instead, the lists _make() checks a format
308
+ // against before anything else.
309
+ CompressionStreamZlib.supportedFormats = COMPRESSION_FORMATS;
310
+ DecompressionStreamZlib.supportedFormats = DECOMPRESSION_FORMATS;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "WASM-based Compression Streams API implementation using zlib, with support for deflate64 decompression.",
4
4
  "author": "Gildas Lormeau",
5
5
  "license": "BSD-3-Clause",
6
- "version": "1.2.3",
6
+ "version": "1.4.0",
7
7
  "type": "module",
8
8
  "keywords": [
9
9
  "deflate",
@@ -32,6 +32,10 @@ const FORMAT_DEFLATE = "deflate";
32
32
  const FORMAT_DEFLATE_RAW = "deflate-raw";
33
33
  const FORMAT_DEFLATE64_RAW = "deflate64-raw";
34
34
  const FORMAT_GZIP = "gzip";
35
+ const Z_MEM_ERROR = -4;
36
+ const Z_MEM_ERROR_CODE = "Z_MEM_ERROR";
37
+ const COMPRESSION_FORMATS = [FORMAT_DEFLATE, FORMAT_DEFLATE_RAW, FORMAT_GZIP];
38
+ const DECOMPRESSION_FORMATS = [FORMAT_DEFLATE, FORMAT_DEFLATE_RAW, FORMAT_GZIP, FORMAT_DEFLATE64_RAW];
35
39
 
36
40
  let wasm, malloc, free, memory, initError;
37
41
 
@@ -52,7 +56,17 @@ export function resetWasmExports() {
52
56
  wasm = malloc = free = memory = initError = null;
53
57
  }
54
58
 
59
+ function setZlibCode(error, result) {
60
+ if (result === Z_MEM_ERROR) {
61
+ error.code = Z_MEM_ERROR_CODE;
62
+ }
63
+ return error;
64
+ }
65
+
55
66
  function _make(isCompress, type, options = {}) {
67
+ if (!(isCompress ? COMPRESSION_FORMATS : DECOMPRESSION_FORMATS).includes(type)) {
68
+ throw new TypeError("Unsupported format: " + type);
69
+ }
56
70
  if (!wasm) {
57
71
  const error = new Error("WASM module not loaded");
58
72
  error.cause = initError;
@@ -61,7 +75,7 @@ function _make(isCompress, type, options = {}) {
61
75
  const level = (typeof options.level === "number") ? options.level : -1;
62
76
  const outBufferSize = (typeof options.outBuffer === "number") ? options.outBuffer : 64 * 1024;
63
77
  const inBufferSize = (typeof options.inBufferSize === "number") ? options.inBufferSize : 64 * 1024;
64
- const state = { out: 0, in: 0, inBufferSize: 0, streamHandle: 0 };
78
+ const state = { out: 0, in: 0, inBufferSize: 0, streamHandle: 0, streamEnded: false };
65
79
  let readableController, writableController, resolveWrite, cancelReason;
66
80
  let backpressure = true;
67
81
  let cancelled = false;
@@ -114,7 +128,7 @@ function _make(isCompress, type, options = {}) {
114
128
  }
115
129
  const result = disposeStream(state);
116
130
  if (result !== 0) {
117
- const error = new Error("end error:" + result);
131
+ const error = setZlibCode(new Error("end error:" + result), result);
118
132
  readableController.error(error);
119
133
  throw error;
120
134
  }
@@ -134,7 +148,7 @@ function _make(isCompress, type, options = {}) {
134
148
  state.in = malloc(inBufferSize);
135
149
  state.inBufferSize = inBufferSize;
136
150
  if (!state.out || !state.in) {
137
- throw new Error("allocation failed");
151
+ throw setZlibCode(new Error("allocation failed"), Z_MEM_ERROR);
138
152
  }
139
153
  if (isCompress) {
140
154
  state._process = wasm.deflate_process;
@@ -170,7 +184,7 @@ function _make(isCompress, type, options = {}) {
170
184
  }
171
185
  }
172
186
  if (result !== 0) {
173
- throw new Error("init failed:" + result);
187
+ throw setZlibCode(new Error("init failed:" + result), result);
174
188
  }
175
189
  } catch (error) {
176
190
  disposeStream(state);
@@ -185,6 +199,9 @@ function _make(isCompress, type, options = {}) {
185
199
  const out = state.out;
186
200
  let offset = 0;
187
201
  while (offset < chunk.length) {
202
+ if (state.streamEnded) {
203
+ throw new Error("trailing data after the end of the stream");
204
+ }
188
205
  const toRead = Math.min(chunk.length - offset, 32 * 1024);
189
206
  if (!state.in || state.inBufferSize < toRead) {
190
207
  if (state.in && free) {
@@ -194,7 +211,7 @@ function _make(isCompress, type, options = {}) {
194
211
  state.in = malloc(toRead);
195
212
  state.inBufferSize = toRead;
196
213
  if (!state.in) {
197
- throw new Error("allocation failed");
214
+ throw setZlibCode(new Error("allocation failed"), Z_MEM_ERROR);
198
215
  }
199
216
  }
200
217
  heap.set(chunk.subarray(offset, offset + toRead), state.in);
@@ -203,14 +220,16 @@ function _make(isCompress, type, options = {}) {
203
220
  const code = (result >> 24) & 0xff;
204
221
  const signedCode = (code & 0x80) ? code - 256 : code;
205
222
  if (signedCode < 0) {
206
- throw new Error("process error:" + signedCode);
223
+ throw setZlibCode(new Error("process error:" + signedCode), signedCode);
207
224
  }
208
225
  const prod = result & 0x00ffffff;
209
226
  if (prod) {
210
227
  enqueue(heap.slice(out, out + prod));
211
228
  }
212
229
  const consumed = last_consumed(state.streamHandle);
213
- if (consumed === 0 && prod === 0) {
230
+ if (code === 1) {
231
+ state.streamEnded = true;
232
+ } else if (consumed === 0 && prod === 0) {
214
233
  break;
215
234
  }
216
235
  offset += consumed;
@@ -226,7 +245,7 @@ function _make(isCompress, type, options = {}) {
226
245
  const code = (result >> 24) & 0xff;
227
246
  const signedCode = (code & 0x80) ? code - 256 : code;
228
247
  if (signedCode < 0) {
229
- throw new Error("process error:" + signedCode);
248
+ throw setZlibCode(new Error("process error:" + signedCode), signedCode);
230
249
  }
231
250
  const produced = result & 0x00ffffff;
232
251
  if (produced) {
@@ -285,7 +304,7 @@ export class DecompressionStreamZlib {
285
304
  CompressionStreamZlib.requiresModule = true;
286
305
  DecompressionStreamZlib.requiresModule = true;
287
306
  // Constructing these classes before the module is loaded throws, so capability probes cannot rely
288
- // on trying the constructor; the formats are declared instead, next to the branches implementing
289
- // them in _make().
290
- CompressionStreamZlib.supportedFormats = [FORMAT_DEFLATE, FORMAT_DEFLATE_RAW, FORMAT_GZIP];
291
- DecompressionStreamZlib.supportedFormats = [FORMAT_DEFLATE, FORMAT_DEFLATE_RAW, FORMAT_GZIP, FORMAT_DEFLATE64_RAW];
307
+ // on trying the constructor; the formats are declared instead, the lists _make() checks a format
308
+ // against before anything else.
309
+ CompressionStreamZlib.supportedFormats = COMPRESSION_FORMATS;
310
+ DecompressionStreamZlib.supportedFormats = DECOMPRESSION_FORMATS;