@trkyshorty/node-lzf 1.3.0 → 2.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,100 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@trkyshorty/node-lzf` are listed here. Versions follow
4
+ [Semantic Versioning](https://semver.org/).
5
+
6
+ ## 2.0.0
7
+
8
+ ### Breaking
9
+
10
+ - Requires Node.js 20 or later (Node.js 18 is end-of-life).
11
+ - Empty input no longer throws: `compress` returns an empty Buffer and an empty
12
+ stream decompresses to an empty Buffer. Code that relied on the
13
+ `Input buffer must not be empty` `TypeError` to reject empty data has to
14
+ check the length itself.
15
+ - Input larger than 1 GiB throws `RangeError` (`ERR_OUT_OF_RANGE`) instead of
16
+ `TypeError`.
17
+ - Argument error messages were reworded; match on the new `code` property
18
+ instead of the message. The decompression messages still contain
19
+ `corrupted input` and `expected length too small`.
20
+
21
+ ### Fixed
22
+
23
+ - `compressAsync`/`decompressAsync` no longer read the caller's memory from the
24
+ worker thread. Detaching the input's `ArrayBuffer` during the call
25
+ (`transfer()`, `postMessage`) returned wrong data or crashed the process with
26
+ a use-after-free, and changing the input before the promise settled leaked
27
+ into the result. The input is now copied before the call returns.
28
+ - A `DataView` input threw an uncatchable error and aborted the process; it is
29
+ now rejected with a `TypeError`.
30
+ - `decompress` with `expectedLength` set to `Infinity` or a huge number relied
31
+ on undefined behavior in C++; it is now range-checked before conversion.
32
+ - The compressor could read out of bounds on Windows ARM64 (32-bit offset
33
+ type), and its unaligned 16-bit load was undefined behavior on x86 with
34
+ gcc/clang.
35
+ - Compressing a 1-byte input read one byte past the end of the input (an
36
+ upstream liblzf bug found by AddressSanitizer).
37
+
38
+ ### Added
39
+
40
+ - Every error has a stable `code` property (`ERR_INVALID_ARG_TYPE`,
41
+ `ERR_OUT_OF_RANGE`, `ERR_LZF_OUTPUT_TOO_SMALL`, `ERR_LZF_CORRUPTED_INPUT`,
42
+ `ERR_LZF_ALLOCATION_FAILED`, `ERR_LZF_COMPRESSION_FAILED`). The type
43
+ definitions export `ErrorCode` and `LzfError`.
44
+ - `expectedLength` may be `0`, for an empty stream.
45
+ - Any TypedArray (`Float64Array`, `Int16Array`, `Float16Array`, ...) is
46
+ accepted and read as its raw bytes; the type definitions accept
47
+ `NodeJS.TypedArray`.
48
+ - Prebuilt binaries for Windows arm64, macOS x64 and Alpine Linux (musl) x64.
49
+ Linux prebuilds are tagged with their libc.
50
+
51
+ ### Changed
52
+
53
+ - `decompress` allocates at most 88× the input size, the largest expansion an
54
+ LZF stream can encode, so an untrusted `expectedLength` cannot make a small
55
+ input allocate up to 1 GiB. A corrupted stream that would exceed this cap
56
+ now reports `ERR_LZF_CORRUPTED_INPUT` rather than `ERR_LZF_OUTPUT_TOO_SMALL`.
57
+ - `compressAsync`/`decompressAsync` copy their input (see Fixed), so each call
58
+ in flight holds an extra copy of the input until it settles.
59
+
60
+ ## 1.3.1 - 2026-09-11
61
+
62
+ ### Changed
63
+
64
+ - A local `npm publish` is refused; releases are published by the CI workflow
65
+ only, so stale local prebuilds cannot reach the registry.
66
+
67
+ ## 1.3.0 - 2026-09-11
68
+
69
+ ### Changed
70
+
71
+ - Releases are published to npm automatically by CI when the package version
72
+ changes, using npm trusted publishing with provenance.
73
+
74
+ ## 1.2.0 - 2026-09-11
75
+
76
+ ### Added
77
+
78
+ - TypeScript usage example in the README.
79
+
80
+ ### Changed
81
+
82
+ - Vendored liblzf builds without compiler warnings.
83
+
84
+ ## 1.1.0 - 2026-07-22
85
+
86
+ First release as `@trkyshorty/node-lzf`.
87
+
88
+ ### Added
89
+
90
+ - N-API addon with prebuilt binaries for linux-x64, linux-arm64, win32-x64 and
91
+ darwin-arm64.
92
+ - `compressAsync` and `decompressAsync`, running on the libuv thread pool.
93
+ - Type definitions, a complete test suite and a benchmark against zlib.
94
+
95
+ ### Changed
96
+
97
+ - **Breaking:** `decompress` requires `expectedLength` (1 byte to 1 GiB).
98
+ Omitting it used to allocate a 999 MB buffer on every call. This should have
99
+ been a major version.
100
+ - Arguments are validated strictly; corrupted input throws instead of crashing.
package/LICENSE CHANGED
@@ -27,6 +27,31 @@ POSSIBILITY OF SUCH DAMAGE.
27
27
 
28
28
  ----------------------------------------------------------------------------
29
29
 
30
- This package bundles liblzf by Marc Alexander Lehmann (src/lzf/), which is
31
- distributed under the BSD 2-Clause license (or alternatively the GPL v2 or
32
- later). See the license headers in src/lzf/*.h and src/lzf/*.cc for details.
30
+ This package bundles a modified copy of liblzf (src/lzf/), which is compiled
31
+ into the native addon and the prebuilt binaries. node-lzf uses liblzf under
32
+ the BSD 2-Clause terms below; the original files also offer the GPL v2 or
33
+ later as an alternative. Changes made by node-lzf are marked with
34
+ "node-lzf:" comments in the source files.
35
+
36
+ Copyright (c) 2000-2010 Marc Alexander Lehmann <schmorp@schmorp.de>
37
+
38
+ Redistribution and use in source and binary forms, with or without
39
+ modification, are permitted provided that the following conditions are met:
40
+
41
+ 1. Redistributions of source code must retain the above copyright notice,
42
+ this list of conditions and the following disclaimer.
43
+
44
+ 2. Redistributions in binary form must reproduce the above copyright notice,
45
+ this list of conditions and the following disclaimer in the documentation
46
+ and/or other materials provided with the distribution.
47
+
48
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
49
+ WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
50
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
51
+ EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
52
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
53
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
54
+ OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
55
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
56
+ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
57
+ OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md CHANGED
@@ -1,38 +1,41 @@
1
- ## node-lzf
1
+ # node-lzf
2
2
 
3
- [LZF](http://oldhome.schmorp.de/marc/liblzf.html) compression library for nodejs.
3
+ [![npm version](https://img.shields.io/npm/v/@trkyshorty/node-lzf.svg)](https://www.npmjs.com/package/@trkyshorty/node-lzf)
4
+ [![prebuild](https://github.com/trkyshorty/node-lzf/actions/workflows/prebuild.yml/badge.svg?branch=master)](https://github.com/trkyshorty/node-lzf/actions/workflows/prebuild.yml)
5
+ [![node](https://img.shields.io/node/v/@trkyshorty/node-lzf.svg)](https://nodejs.org)
6
+ [![license](https://img.shields.io/npm/l/@trkyshorty/node-lzf.svg)](LICENSE)
7
+
8
+ [LZF](https://software.schmorp.de/pkg/liblzf.html) compression library for Node.js.
4
9
 
5
10
  LZF advantages:
6
11
 
7
- - Small code size (less then 500 lines including header files and docs).
8
- - Very fast compression speeds, rivaling a straight copy loop, especially for decompression which is basically at (unoptimized) memcpy-speed. Compression speed can be increased by 20% by sacrificing a few percent of compression ratio.
9
- - Mediocre compression ratios - you can usually expect about 40-50% compression for typical binary data
10
- - Easy to use (just two functions, no state attached)
11
- - Highly portable (written in C)
12
- - Tunable, see the file lzfP.h in the distribution, to tailor liblzf to your needs. The generated compressed blocks can be decompressed by any liblzf version regardless of the options used to compress.
13
- - Freely usable (BSD-type-license)
12
+ - Very fast compression speeds, rivaling a straight copy loop, especially for decompression which is basically at (unoptimized) memcpy-speed.
13
+ - Mediocre compression ratios - you can usually expect about 40-50% compression for typical binary data.
14
+ - Easy to use (just compress and decompress, no state attached).
15
+ - Any liblzf build decodes the output, whatever options it was compressed with.
16
+ - Freely usable (BSD-type license).
14
17
 
15
- ### Install
18
+ ## Install
16
19
 
17
20
  ```bash
18
21
  npm install @trkyshorty/node-lzf
19
22
  ```
20
23
 
21
- The module is N-API based and ships prebuilt binaries (`prebuilds/`) for
22
- linux-x64, linux-arm64, win32-x64 and darwin-arm64 — no compiler toolchain
23
- is needed on those platforms. On other platforms it falls back to compiling
24
- from source via `node-gyp-build` (requires a C++ toolchain).
24
+ Requires Node.js 20 or later. The module is N-API based and ships prebuilt
25
+ binaries (`prebuilds/`) for these platforms, so no compiler toolchain is
26
+ needed on them:
27
+
28
+ | OS | Architectures |
29
+ | ------- | ------------------------------------- |
30
+ | Linux | x64, arm64 (glibc); x64 (musl/Alpine) |
31
+ | Windows | x64, arm64 |
32
+ | macOS | arm64, x64 |
25
33
 
26
- Prebuilds are produced by the `prebuild` GitHub Actions workflow
27
- (`npm run prebuild` runs `prebuildify --napi --strip` for the current
28
- platform). Releases are fully automatic: bump `version` in `package.json`
29
- and push to `master`. If that version is not on npm yet, the workflow builds
30
- and tests every platform, publishes the package with the merged `prebuilds/`
31
- via npm trusted publishing (with provenance) and tags the commit
32
- `v<version>`. Pushes that keep the version publish nothing; a manual run
33
- always builds and tests.
34
+ On any other platform (Alpine on arm64, for example) it falls back to
35
+ compiling from source via `node-gyp-build`, which requires a C++ toolchain
36
+ and Python.
34
37
 
35
- ### Usage
38
+ ## Usage
36
39
 
37
40
  ```javascript
38
41
  const lzf = require('@trkyshorty/node-lzf');
@@ -48,7 +51,11 @@ const compressed2 = await lzf.compressAsync(data);
48
51
  const restored2 = await lzf.decompressAsync(compressed2, data.length);
49
52
  ```
50
53
 
51
- #### TypeScript
54
+ LZF streams do not store the original size, so store `data.length` next to
55
+ the compressed bytes (a length prefix in your framing, for example) and pass
56
+ it back to `decompress`.
57
+
58
+ ### TypeScript
52
59
 
53
60
  Type definitions are bundled, no `@types` package is needed. The module is
54
61
  CommonJS (`export =`), so import it as a namespace — or as a default import
@@ -70,46 +77,77 @@ async function roundtrip(input: Buffer): Promise<Buffer> {
70
77
  const packed = await lzf.compressAsync(input);
71
78
  return lzf.decompressAsync(packed, input.length);
72
79
  }
80
+
81
+ // errors carry a typed code
82
+ try {
83
+ lzf.decompress(compressed, 1);
84
+ } catch (err) {
85
+ if ((err as lzf.LzfError).code === 'ERR_LZF_OUTPUT_TOO_SMALL') {
86
+ // ...
87
+ }
88
+ }
73
89
  ```
74
90
 
75
- ### API
91
+ ## API
92
+
93
+ Every function accepts a `Buffer` or any other TypedArray (`Uint8Array`,
94
+ `Float64Array`, ...), read as its raw bytes, and always returns a `Buffer`.
95
+ `DataView` and `ArrayBuffer` are rejected; wrap an `ArrayBuffer` in a
96
+ `Uint8Array` first. Inputs are limited to 1 GiB.
76
97
 
77
- TypeScript definitions are bundled (`index.d.ts`).
98
+ ### `compress(data): Buffer`
78
99
 
79
- #### `compress(data: Buffer): Buffer`
100
+ Returns an LZF-compressed Buffer sized exactly to the result. An empty input
101
+ returns an empty Buffer. Incompressible input can grow slightly (up to
102
+ ~104%).
80
103
 
81
- Returns an LZF-compressed Buffer sized exactly to the result. Throws
82
- `TypeError` if `data` is not a Buffer, is empty, or exceeds 1 GiB.
83
- Note: incompressible input can grow slightly (up to ~104%).
104
+ ### `decompress(data, expectedLength): Buffer`
84
105
 
85
- #### `decompress(data: Buffer, expectedLength: number): Buffer`
106
+ Returns the decompressed Buffer. `expectedLength` is **required**: pass the
107
+ exact decompressed size, or an upper bound — the result is shrunk to the
108
+ actual size. It must be an integer between 0 and 1 GiB. An empty input
109
+ returns an empty Buffer.
86
110
 
87
- Returns the decompressed Buffer. `expectedLength` is **required** — pass the
88
- exact decompressed size (or an upper bound); the result is shrunk to the
89
- actual size. Throws `TypeError`/`RangeError` for invalid arguments
90
- (`expectedLength` must be an integer between 1 and 1 GiB) and `Error` with
91
- a descriptive message when the input is corrupted (`corrupted input`) or
92
- `expectedLength` is too small (`expected length too small`). The
93
- decompressor is safe on untrusted input: corrupt streams throw instead of
94
- reading or writing out of bounds.
111
+ The decompressor is safe on untrusted input: a corrupted stream throws
112
+ instead of reading or writing out of bounds. The output allocation never
113
+ exceeds 88× the input size (the largest expansion an LZF stream can encode),
114
+ however large `expectedLength` is. Still check `expectedLength` against a
115
+ limit that fits your application when it comes from an untrusted source.
95
116
 
96
117
  > **v1.1.0 note:** older versions allowed omitting `expectedLength` and
97
118
  > silently allocated a 999 MB scratch buffer per call — that default has
98
119
  > been removed.
99
120
 
100
- #### `compressAsync(data: Buffer): Promise<Buffer>` / `decompressAsync(data: Buffer, expectedLength: number): Promise<Buffer>`
121
+ ### `compressAsync(data): Promise<Buffer>` / `decompressAsync(data, expectedLength): Promise<Buffer>`
101
122
 
102
123
  Same semantics as the sync variants, but the (de)compression runs on the
103
124
  libuv thread pool and the returned promise rejects with the errors the sync
104
- variants would throw. Prefer these for payloads larger than a few hundred
105
- KB on latency-sensitive servers.
125
+ variants would throw. The input is copied before the call returns, so it may
126
+ be modified, reused or transferred right away. Prefer these for payloads
127
+ larger than a few hundred KB on latency-sensitive servers.
106
128
 
107
- Compressed output is a valid LZF stream decodable by any liblzf build.
108
- The exact compressed bytes are not guaranteed to be identical across calls
129
+ ### Errors
130
+
131
+ Every error has a stable `code` property; match on it rather than on the
132
+ message.
133
+
134
+ | `code` | Class | Cause |
135
+ | ---------------------------- | ------------ | ----------------------------------------------------------------------- |
136
+ | `ERR_INVALID_ARG_TYPE` | `TypeError` | `data` is not a TypedArray, or `expectedLength` is not a number |
137
+ | `ERR_OUT_OF_RANGE` | `RangeError` | `data` exceeds 1 GiB, or `expectedLength` is not an integer in 0..1 GiB |
138
+ | `ERR_LZF_OUTPUT_TOO_SMALL` | `Error` | the decompressed data does not fit in `expectedLength` bytes |
139
+ | `ERR_LZF_CORRUPTED_INPUT` | `Error` | the input is not a valid LZF stream |
140
+ | `ERR_LZF_ALLOCATION_FAILED` | `Error` | the output, or the async input copy, could not be allocated |
141
+ | `ERR_LZF_COMPRESSION_FAILED` | `Error` | liblzf reported a compression failure (not expected in practice) |
142
+
143
+ ### Output stability
144
+
145
+ Compressed output is a valid LZF stream decodable by any liblzf build. The
146
+ exact compressed bytes are not guaranteed to be identical across calls
109
147
  (liblzf's hash table is intentionally left uninitialized for speed) — only
110
148
  the roundtrip contract holds.
111
149
 
112
- ### Benchmarks
150
+ ## Benchmarks
113
151
 
114
152
  `npm run bench` compares against node's built-in zlib (`deflateRaw`,
115
153
  levels 1 and 6) on deterministic datasets. Numbers below from a Windows
@@ -140,14 +178,42 @@ In short: LZF compresses 2–10× faster than zlib at its fastest level, at
140
178
  the cost of a worse ratio. Pick LZF when compression latency matters more
141
179
  than size (hot network paths); pick zlib/brotli for cold storage.
142
180
 
181
+ ## Development
182
+
183
+ `npm test` runs the suite against whichever binary `node-gyp-build` resolves:
184
+ `build/Release` when it exists, otherwise the local `prebuilds/` — which are
185
+ git-ignored and may predate your latest `src/` change. After changing the C++
186
+ sources (`src/`, `binding.gyp`) run `npm run test:local` instead: it rebuilds
187
+ `build/Release` (a C++ toolchain is required) and runs the tests against that
188
+ fresh build. Delete `build/` to test the prebuilds again. Builds use the
189
+ `node-gyp` devDependency rather than the copy bundled with npm, which is too
190
+ old to find Visual Studio 2026 on Node 20.
191
+
192
+ The `test` GitHub Actions workflow runs on every push and pull request: it
193
+ builds from source and runs the suite on Linux, Windows and macOS with
194
+ Node 20, 22 and 24, and once more on Linux under AddressSanitizer and
195
+ UndefinedBehaviorSanitizer.
196
+
197
+ ### Releasing
198
+
199
+ Releases are fully automatic: add the changes to `CHANGELOG.md`, bump
200
+ `version` in `package.json` and push to `master`. If that version is not on
201
+ npm yet, the `prebuild` workflow builds and tests every platform
202
+ (`npm run build:prebuilds` runs `prebuildify --napi --strip` for the current
203
+ platform), publishes the package with the merged `prebuilds/` via npm trusted
204
+ publishing (with provenance) and tags the commit `v<version>`. Pushes that
205
+ keep the version publish nothing; a manual run always builds and tests. A
206
+ local `npm publish` is refused (`prepublishOnly`), so stale local
207
+ `prebuilds/` can never reach the registry.
208
+
143
209
  ---
144
210
 
145
- ### Authors
211
+ ## Authors
146
212
 
147
213
  - Ian Babrou (`ibobrik@gmail.com`) — original author
148
214
  - Türkay Tanrikulu (`trky.shorty@gmail.com`) — fork maintainer
149
215
 
150
- ### License
216
+ ## License
151
217
 
152
- BSD-2-Clause, see [LICENSE](LICENSE). Bundles [liblzf](http://oldhome.schmorp.de/marc/liblzf.html)
218
+ BSD-2-Clause, see [LICENSE](LICENSE). Bundles [liblzf](https://software.schmorp.de/pkg/liblzf.html)
153
219
  by Marc Alexander Lehmann (BSD-2-Clause / GPL dual-licensed).
package/index.d.ts CHANGED
@@ -2,32 +2,60 @@
2
2
 
3
3
  declare namespace lzf {
4
4
  /**
5
- * Compresses a Buffer with LZF. Throws TypeError on invalid input
6
- * (non-Buffer, empty, or larger than 1 GiB). Runs synchronously on
7
- * the calling thread.
5
+ * Input accepted by every function: a Buffer or any other TypedArray,
6
+ * read as its raw bytes. DataView and ArrayBuffer are rejected.
8
7
  */
9
- function compress(data: Buffer): Buffer;
8
+ type Input = NodeJS.TypedArray;
9
+
10
+ /** Value of the `code` property on every error the module throws. */
11
+ type ErrorCode =
12
+ /** TypeError: `data` is not a TypedArray or `expectedLength` is not a number. */
13
+ | 'ERR_INVALID_ARG_TYPE'
14
+ /** RangeError: `data` exceeds 1 GiB or `expectedLength` is not an integer in 0..1 GiB. */
15
+ | 'ERR_OUT_OF_RANGE'
16
+ /** Error: the decompressed data does not fit in `expectedLength` bytes. */
17
+ | 'ERR_LZF_OUTPUT_TOO_SMALL'
18
+ /** Error: the input is not a valid LZF stream. */
19
+ | 'ERR_LZF_CORRUPTED_INPUT'
20
+ /** Error: the output (or the async input copy) could not be allocated. */
21
+ | 'ERR_LZF_ALLOCATION_FAILED'
22
+ /** Error: liblzf reported a compression failure (not expected in practice). */
23
+ | 'ERR_LZF_COMPRESSION_FAILED';
24
+
25
+ /** Shape of the errors thrown (sync) or rejected (async) by this module. */
26
+ interface LzfError extends Error {
27
+ code: ErrorCode;
28
+ }
29
+
30
+ /**
31
+ * Compresses the input with LZF and returns a new Buffer. An empty input
32
+ * returns an empty Buffer. Throws TypeError for input that is not a
33
+ * TypedArray and RangeError for input larger than 1 GiB. Runs
34
+ * synchronously on the calling thread.
35
+ */
36
+ function compress(data: Input): Buffer;
10
37
 
11
38
  /**
12
- * Compresses a Buffer with LZF on the libuv thread pool without
13
- * blocking the event loop. Rejects with the same errors compress()
14
- * throws.
39
+ * Compresses on the libuv thread pool without blocking the event loop.
40
+ * The input is copied before the call returns, so it may be reused right
41
+ * away. Rejects with the same errors compress() throws.
15
42
  */
16
- function compressAsync(data: Buffer): Promise<Buffer>;
43
+ function compressAsync(data: Input): Promise<Buffer>;
17
44
 
18
45
  /**
19
- * Decompresses an LZF-compressed Buffer. expectedLength is the exact
20
- * (or an upper bound of the) decompressed size, between 1 and 1 GiB.
21
- * Throws TypeError/RangeError on invalid arguments and Error when the
22
- * input is corrupted or expectedLength is too small.
46
+ * Decompresses LZF-compressed data into a new Buffer. expectedLength is
47
+ * the exact (or an upper bound of the) decompressed size, an integer
48
+ * between 0 and 1 GiB. Throws TypeError/RangeError on invalid arguments
49
+ * and Error when the input is corrupted or expectedLength is too small.
23
50
  */
24
- function decompress(data: Buffer, expectedLength: number): Buffer;
51
+ function decompress(data: Input, expectedLength: number): Buffer;
25
52
 
26
53
  /**
27
- * Decompresses on the libuv thread pool without blocking the event
28
- * loop. Rejects with the same errors decompress() throws.
54
+ * Decompresses on the libuv thread pool without blocking the event loop.
55
+ * The input is copied before the call returns, so it may be reused right
56
+ * away. Rejects with the same errors decompress() throws.
29
57
  */
30
- function decompressAsync(data: Buffer, expectedLength: number): Promise<Buffer>;
58
+ function decompressAsync(data: Input, expectedLength: number): Promise<Buffer>;
31
59
  }
32
60
 
33
61
  export = lzf;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trkyshorty/node-lzf",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "description": "lzf compression module for nodejs (Topface/node-lzf fork)",
5
5
  "keywords": [
6
6
  "lzf",
@@ -14,7 +14,7 @@
14
14
  "Ian Babrou <ibobrik@gmail.com>"
15
15
  ],
16
16
  "engines": {
17
- "node": ">=18"
17
+ "node": ">=20"
18
18
  },
19
19
  "repository": {
20
20
  "type": "git",
@@ -25,6 +25,7 @@
25
25
  "node-gyp-build": "^4.8.4"
26
26
  },
27
27
  "devDependencies": {
28
+ "node-gyp": "^12.4.0",
28
29
  "prebuildify": "^6.0.1"
29
30
  },
30
31
  "main": "./index.js",
@@ -36,12 +37,16 @@
36
37
  "binding.gyp",
37
38
  "src/",
38
39
  "prebuilds/",
39
- "README.md"
40
+ "README.md",
41
+ "CHANGELOG.md"
40
42
  ],
41
43
  "scripts": {
42
44
  "install": "node-gyp-build",
43
- "prebuild": "prebuildify --napi --strip",
45
+ "prepublishOnly": "node -e \"if (!process.env.GITHUB_ACTIONS) { console.error('Refusing to publish locally: the prebuild workflow publishes releases. Bump the version and push to master.'); process.exit(1); }\"",
46
+ "build:prebuilds": "prebuildify --napi --strip",
47
+ "build:source": "node-gyp rebuild",
44
48
  "test": "node --test test/test.js",
49
+ "test:local": "node-gyp rebuild && node --test test/test.js",
45
50
  "bench": "node benchmark/benchmark.js"
46
51
  },
47
52
  "license": "BSD-2-Clause"
package/src/lzf/lzfP.h CHANGED
@@ -82,6 +82,9 @@
82
82
  * node-lzf: upstream used `# define STRICT_ALIGN !(defined(__i386) || defined (__amd64))`;
83
83
  * a `defined` operator produced by macro expansion is undefined behavior in the standard
84
84
  * (gcc/clang -Wexpansion-to-defined). We select the same value directly with #if.
85
+ * MSVC x64 is deliberately left on the aligned path: enabling the unaligned one there
86
+ * was measured (npm run bench) at a few percent faster on most data but ~70% slower
87
+ * on highly repetitive input.
85
88
  */
86
89
  #ifndef STRICT_ALIGN
87
90
  # if defined(__i386) || defined (__amd64)
@@ -151,8 +154,9 @@ using namespace std;
151
154
 
152
155
  #ifndef LZF_USE_OFFSETS
153
156
  # if defined (WIN32)
154
- /* node-lzf: replaces upstream's macro-expanded `defined(_M_X64)`; same rationale as STRICT_ALIGN. */
155
- # if defined(_M_X64)
157
+ /* node-lzf: replaces upstream's macro-expanded `defined(_M_X64)`; same rationale as STRICT_ALIGN.
158
+ * _WIN64 covers both x64 and ARM64 Windows. */
159
+ # if defined(_WIN64)
156
160
  # define LZF_USE_OFFSETS 1
157
161
  # else
158
162
  # define LZF_USE_OFFSETS 0
@@ -191,6 +195,18 @@ typedef LZF_HSLOT LZF_STATE[1 << (HLOG)];
191
195
  # endif
192
196
  #endif
193
197
 
198
+ #if !STRICT_ALIGN
199
+ /* node-lzf: upstream dereferenced `*(u16 *)p`, an unaligned load that is undefined
200
+ * behavior (UBSan -fsanitize=alignment). memcpy is well-defined and compiles to the
201
+ * same single load on the targets that enable this path. */
202
+ static inline u16 lzf_load16 (const u8 *p)
203
+ {
204
+ u16 v;
205
+ memcpy (&v, p, sizeof (v));
206
+ return v;
207
+ }
208
+ #endif
209
+
194
210
  #if ULTRA_FAST
195
211
  # undef VERY_FAST
196
212
  #endif
package/src/lzf/lzf_c.cc CHANGED
@@ -118,8 +118,12 @@ lzf_compress (const void *const in_data, unsigned int in_len,
118
118
  * no bit pattern traps. Since the only platform that is both non-POSIX
119
119
  * and fails to support both assumptions is windows 64 bit, we make a
120
120
  * special workaround for it.
121
+ * node-lzf: upstream only checked _M_X64, leaving a 32-bit `unsigned long` on
122
+ * Windows ARM64. With the uninitialized hash table a garbage `ref` could then
123
+ * truncate to a small offset and be dereferenced out of bounds. _WIN64 covers
124
+ * every 64-bit Windows target.
121
125
  */
122
- #if defined (WIN32) && defined (_M_X64)
126
+ #if defined (_WIN64)
123
127
  unsigned _int64 off; /* workaround for missing POSIX compliance */
124
128
  #else
125
129
  unsigned long off;
@@ -136,8 +140,12 @@ lzf_compress (const void *const in_data, unsigned int in_len,
136
140
 
137
141
  lit = 0; op++; /* start run */
138
142
 
139
- hval = FRST (ip);
140
- while (ip < in_end - 2)
143
+ /* node-lzf: upstream read FRST (ip) unconditionally, i.e. ip[1] one byte past
144
+ * a 1-byte input (AddressSanitizer heap-buffer-overflow), and formed in_end - 2
145
+ * before the start of the buffer. Inputs shorter than 3 bytes never enter the
146
+ * match loop and are emitted as literals below. */
147
+ hval = in_len > 1 ? FRST (ip) : 0;
148
+ while (in_len > 2 && ip < in_end - 2)
141
149
  {
142
150
  LZF_HSLOT *hslot;
143
151
 
@@ -155,7 +163,8 @@ lzf_compress (const void *const in_data, unsigned int in_len,
155
163
  #if STRICT_ALIGN
156
164
  && ((ref[1] << 8) | ref[0]) == ((ip[1] << 8) | ip[0])
157
165
  #else
158
- && *(u16 *)ref == *(u16 *)ip
166
+ /* node-lzf: upstream compared `*(u16 *)ref == *(u16 *)ip`; see lzf_load16 in lzfP.h. */
167
+ && lzf_load16 (ref) == lzf_load16 (ip)
159
168
  #endif
160
169
  )
161
170
  {
package/src/lzf.cc CHANGED
@@ -3,10 +3,12 @@
3
3
 
4
4
  #include <napi.h>
5
5
 
6
+ #include <algorithm>
6
7
  #include <cerrno>
7
- #include <climits>
8
+ #include <cmath>
9
+ #include <cstdint>
8
10
  #include <cstdlib>
9
- #include <string>
11
+ #include <cstring>
10
12
 
11
13
  #include "lzf/lzf.h"
12
14
 
@@ -23,18 +25,91 @@ inline size_t CompressBound(size_t n) {
23
25
  return n + (n / 16) + 64 + 3;
24
26
  }
25
27
 
28
+ /* Largest output any LZF stream of n bytes can decode to. The densest token
29
+ * is a long back reference: 3 input bytes (control, length, offset) that
30
+ * expand to 7 + 255 + 2 = 264 output bytes, i.e. 88x. Literal runs and short
31
+ * back references expand less, so no valid or corrupted stream can outgrow
32
+ * this. Capping the allocation with it means an untrusted expectedLength
33
+ * cannot make a tiny input allocate up to 1 GiB. */
34
+ constexpr size_t kMaxExpansion = 88;
35
+
36
+ inline size_t DecompressBound(size_t n) {
37
+ return n > SIZE_MAX / kMaxExpansion ? SIZE_MAX : n * kMaxExpansion;
38
+ }
39
+
40
+ /* ---- errors ------------------------------------------------------------- */
41
+
42
+ enum class ErrorKind { kError, kTypeError, kRangeError };
43
+
44
+ /* Every error carries a stable `code` so callers never have to match on the
45
+ * message. The strings are static, so a Failure can be filled on a worker
46
+ * thread and turned into a JS error later on the main thread. */
47
+ struct Failure {
48
+ ErrorKind kind = ErrorKind::kError;
49
+ const char* code = nullptr;
50
+ const char* message = nullptr;
51
+ };
52
+
53
+ constexpr Failure kInvalidData{ErrorKind::kTypeError, "ERR_INVALID_ARG_TYPE",
54
+ "The \"data\" argument must be a Buffer or TypedArray"};
55
+ constexpr Failure kDataTooLarge{ErrorKind::kRangeError, "ERR_OUT_OF_RANGE",
56
+ "The \"data\" argument exceeds the 1 GiB limit"};
57
+ constexpr Failure kInvalidLengthType{ErrorKind::kTypeError, "ERR_INVALID_ARG_TYPE",
58
+ "The \"expectedLength\" argument must be a number"};
59
+ constexpr Failure kLengthNotInteger{ErrorKind::kRangeError, "ERR_OUT_OF_RANGE",
60
+ "The \"expectedLength\" argument must be an integer"};
61
+ constexpr Failure kLengthOutOfRange{ErrorKind::kRangeError, "ERR_OUT_OF_RANGE",
62
+ "The \"expectedLength\" argument must be between 0 and 1 GiB"};
63
+ constexpr Failure kAllocationFailed{ErrorKind::kError, "ERR_LZF_ALLOCATION_FAILED",
64
+ "Failed to allocate memory"};
65
+ constexpr Failure kCompressionFailed{ErrorKind::kError, "ERR_LZF_COMPRESSION_FAILED",
66
+ "Compression failed"};
67
+ constexpr Failure kOutputTooSmall{ErrorKind::kError, "ERR_LZF_OUTPUT_TOO_SMALL",
68
+ "Decompression failed: expected length too small"};
69
+ constexpr Failure kCorruptedInput{ErrorKind::kError, "ERR_LZF_CORRUPTED_INPUT",
70
+ "Decompression failed: corrupted input"};
71
+
72
+ Napi::Value MakeError(Napi::Env env, const Failure& failure) {
73
+ napi_value code;
74
+ napi_value message;
75
+ napi_value error = nullptr;
76
+ napi_create_string_utf8(env, failure.code, NAPI_AUTO_LENGTH, &code);
77
+ napi_create_string_utf8(env, failure.message, NAPI_AUTO_LENGTH, &message);
78
+ switch (failure.kind) {
79
+ case ErrorKind::kTypeError: napi_create_type_error(env, code, message, &error); break;
80
+ case ErrorKind::kRangeError: napi_create_range_error(env, code, message, &error); break;
81
+ case ErrorKind::kError: napi_create_error(env, code, message, &error); break;
82
+ }
83
+ return Napi::Value(env, error);
84
+ }
85
+
86
+ Napi::Value Throw(Napi::Env env, const Failure& failure) {
87
+ napi_throw(env, MakeError(env, failure));
88
+ return env.Undefined();
89
+ }
90
+
91
+ Napi::Value RejectedPromise(Napi::Env env, const Failure& failure) {
92
+ auto deferred = Napi::Promise::Deferred::New(env);
93
+ deferred.Reject(MakeError(env, failure));
94
+ return deferred.Promise();
95
+ }
96
+
97
+ /* ---- codec -------------------------------------------------------------- */
98
+
26
99
  struct RawResult {
27
100
  char* data = nullptr;
28
101
  size_t length = 0;
29
102
  };
30
103
 
31
- /* Compresses into a malloc'd buffer shrunk to the exact result size.
32
- * Returns false and fills `error` on failure (buffer already freed). */
33
- bool DoCompress(const char* input, size_t inputLength, RawResult* out, std::string* error) {
104
+ /* Compresses into a malloc'd buffer shrunk to the exact result size. An
105
+ * empty input yields an empty result (lzf_compress itself rejects it). */
106
+ bool DoCompress(const char* input, size_t inputLength, RawResult* out, Failure* failure) {
107
+ if (inputLength == 0) return true;
108
+
34
109
  size_t bound = CompressBound(inputLength);
35
110
  char* buffer = static_cast<char*>(malloc(bound));
36
111
  if (buffer == nullptr) {
37
- *error = "Failed to allocate output buffer";
112
+ *failure = kAllocationFailed;
38
113
  return false;
39
114
  }
40
115
 
@@ -44,7 +119,7 @@ bool DoCompress(const char* input, size_t inputLength, RawResult* out, std::stri
44
119
 
45
120
  if (compressedLength == 0) {
46
121
  free(buffer);
47
- *error = "Compression failed";
122
+ *failure = kCompressionFailed;
48
123
  return false;
49
124
  }
50
125
 
@@ -54,31 +129,41 @@ bool DoCompress(const char* input, size_t inputLength, RawResult* out, std::stri
54
129
  return true;
55
130
  }
56
131
 
57
- /* Decompresses into a malloc'd buffer of expectedLength, shrunk to the
58
- * actual result size. Distinguishes "expected length too small" (E2BIG)
59
- * from corrupted input via errno set by lzf_decompress. */
132
+ /* Decompresses into a malloc'd buffer of at most expectedLength bytes, shrunk
133
+ * to the actual result size. Distinguishes "expected length too small"
134
+ * (E2BIG) from corrupted input via errno set by lzf_decompress. An empty
135
+ * input decodes to an empty result, the inverse of DoCompress. */
60
136
  bool DoDecompress(const char* input, size_t inputLength, size_t expectedLength, RawResult* out,
61
- std::string* error) {
62
- char* buffer = static_cast<char*>(malloc(expectedLength));
137
+ Failure* failure) {
138
+ if (inputLength == 0) return true;
139
+ if (expectedLength == 0) {
140
+ /* every LZF token produces at least one byte */
141
+ *failure = kOutputTooSmall;
142
+ return false;
143
+ }
144
+
145
+ size_t capacity = std::min(expectedLength, DecompressBound(inputLength));
146
+ char* buffer = static_cast<char*>(malloc(capacity));
63
147
  if (buffer == nullptr) {
64
- *error = "Failed to allocate output buffer";
148
+ *failure = kAllocationFailed;
65
149
  return false;
66
150
  }
67
151
 
68
152
  errno = 0;
69
153
  unsigned int decompressedLength = lzf_decompress(
70
154
  input, static_cast<unsigned int>(inputLength),
71
- buffer, static_cast<unsigned int>(expectedLength));
155
+ buffer, static_cast<unsigned int>(capacity));
72
156
 
73
157
  if (decompressedLength == 0) {
74
158
  int cause = errno;
75
159
  free(buffer);
76
- *error = cause == E2BIG ? "Decompression failed: expected length too small"
77
- : "Decompression failed: corrupted input";
160
+ /* E2BIG below the expansion bound cannot come from a real stream, but
161
+ * report it as corruption rather than blaming expectedLength. */
162
+ *failure = cause == E2BIG && capacity == expectedLength ? kOutputTooSmall : kCorruptedInput;
78
163
  return false;
79
164
  }
80
165
 
81
- if (decompressedLength < expectedLength) {
166
+ if (decompressedLength < capacity) {
82
167
  char* shrunk = static_cast<char*>(realloc(buffer, decompressedLength));
83
168
  if (shrunk != nullptr) buffer = shrunk;
84
169
  }
@@ -90,6 +175,10 @@ bool DoDecompress(const char* input, size_t inputLength, size_t expectedLength,
90
175
  /* Wraps a malloc'd result as a Buffer without copying when the runtime
91
176
  * supports external buffers (plain Node does); falls back to copy+free. */
92
177
  Napi::Value WrapResult(Napi::Env env, RawResult* result) {
178
+ if (result->length == 0) {
179
+ free(result->data);
180
+ return Napi::Buffer<char>::New(env, 0);
181
+ }
93
182
  return Napi::Buffer<char>::NewOrCopy(
94
183
  env, result->data, result->length,
95
184
  [](Napi::Env, char* data) { free(data); });
@@ -97,195 +186,202 @@ Napi::Value WrapResult(Napi::Env env, RawResult* result) {
97
186
 
98
187
  /* ---- argument validation ------------------------------------------------ */
99
188
 
100
- bool ValidateInputBuffer(const Napi::CallbackInfo& info, std::string* error) {
101
- if (info.Length() < 1 || !info[0].IsBuffer()) {
102
- *error = "First argument must be a Buffer";
189
+ struct Input {
190
+ const char* data = nullptr;
191
+ size_t length = 0;
192
+ };
193
+
194
+ /* Accepts any TypedArray (Buffer, Uint8Array, Float64Array, ...) and reads its
195
+ * bytes. DataView and ArrayBuffer are rejected. napi_get_buffer_info reports
196
+ * the view's byte length straight from V8, so element types node-addon-api
197
+ * does not know (Float16Array) are measured correctly too. */
198
+ bool GetInput(const Napi::CallbackInfo& info, Input* input, Failure* failure) {
199
+ if (info.Length() < 1 || !info[0].IsTypedArray()) {
200
+ *failure = kInvalidData;
103
201
  return false;
104
202
  }
105
- size_t length = info[0].As<Napi::Buffer<char>>().Length();
106
- if (length == 0) {
107
- *error = "Input buffer must not be empty";
203
+ void* data = nullptr;
204
+ size_t length = 0;
205
+ if (napi_get_buffer_info(info.Env(), info[0], &data, &length) != napi_ok) {
206
+ *failure = kInvalidData;
108
207
  return false;
109
208
  }
110
209
  if (length > kMaxInputLength) {
111
- *error = "Input buffer exceeds 1 GiB limit";
210
+ *failure = kDataTooLarge;
112
211
  return false;
113
212
  }
213
+ input->data = static_cast<const char*>(data);
214
+ input->length = length;
114
215
  return true;
115
216
  }
116
217
 
117
218
  /* expectedLength is required: the legacy default silently allocated a
118
- * 999 MB buffer per call, which is a footgun rather than a feature. */
119
- bool ValidateExpectedLength(const Napi::CallbackInfo& info, size_t* expectedLength,
120
- std::string* error, bool* rangeError) {
121
- *rangeError = false;
219
+ * 999 MB buffer per call, which is a footgun rather than a feature. The
220
+ * range is checked on the double before any integer conversion, because
221
+ * converting an out-of-range double (Infinity, 1e300) is undefined. */
222
+ bool GetExpectedLength(const Napi::CallbackInfo& info, size_t* expectedLength, Failure* failure) {
122
223
  if (info.Length() < 2 || !info[1].IsNumber()) {
123
- *error = "Second argument (expectedLength) must be a number";
224
+ *failure = kInvalidLengthType;
124
225
  return false;
125
226
  }
126
227
  double raw = info[1].As<Napi::Number>().DoubleValue();
127
- if (raw != raw /* NaN */ || raw != static_cast<double>(static_cast<int64_t>(raw))) {
128
- *rangeError = true;
129
- *error = "expectedLength must be an integer";
228
+ if (!std::isfinite(raw) || std::trunc(raw) != raw) {
229
+ *failure = kLengthNotInteger;
130
230
  return false;
131
231
  }
132
- int64_t value = static_cast<int64_t>(raw);
133
- if (value <= 0 || static_cast<uint64_t>(value) > kMaxOutputLength) {
134
- *rangeError = true;
135
- *error = "expectedLength must be between 1 and 1 GiB";
232
+ if (raw < 0 || raw > static_cast<double>(kMaxOutputLength)) {
233
+ *failure = kLengthOutOfRange;
136
234
  return false;
137
235
  }
138
- *expectedLength = static_cast<size_t>(value);
236
+ *expectedLength = static_cast<size_t>(raw);
139
237
  return true;
140
238
  }
141
239
 
142
240
  /* ---- async workers ------------------------------------------------------ */
143
241
 
144
- class CompressWorker : public Napi::AsyncWorker {
242
+ /* The async API copies the input on the calling thread before queueing. A
243
+ * pointer into the caller's memory is not safe to keep: the ArrayBuffer can
244
+ * be detached or shrunk (transfer(), postMessage, resize()) while the worker
245
+ * runs, which freed the memory under it. The copy also means the caller may
246
+ * reuse or mutate the input as soon as the call returns. */
247
+ class OwnedInput {
145
248
  public:
146
- CompressWorker(Napi::Env env, Napi::Buffer<char> input)
147
- : Napi::AsyncWorker(env),
148
- deferred_(Napi::Promise::Deferred::New(env)),
149
- inputRef_(Napi::Persistent(input.As<Napi::Object>())),
150
- data_(input.Data()),
151
- length_(input.Length()) {}
249
+ OwnedInput() = default;
250
+ OwnedInput(const OwnedInput&) = delete;
251
+ OwnedInput& operator=(const OwnedInput&) = delete;
252
+ ~OwnedInput() { free(data_); }
253
+
254
+ bool CopyFrom(const Input& input) {
255
+ if (input.length == 0) return true;
256
+ data_ = static_cast<char*>(malloc(input.length));
257
+ if (data_ == nullptr) return false;
258
+ memcpy(data_, input.data, input.length);
259
+ length_ = input.length;
260
+ return true;
261
+ }
152
262
 
263
+ const char* data() const { return data_; }
264
+ size_t length() const { return length_; }
265
+
266
+ private:
267
+ char* data_ = nullptr;
268
+ size_t length_ = 0;
269
+ };
270
+
271
+ class CodecWorker : public Napi::AsyncWorker {
272
+ public:
273
+ explicit CodecWorker(Napi::Env env)
274
+ : Napi::AsyncWorker(env), deferred_(Napi::Promise::Deferred::New(env)) {}
275
+
276
+ OwnedInput& input() { return input_; }
153
277
  Napi::Promise Promise() { return deferred_.Promise(); }
154
278
 
155
279
  protected:
156
- void Execute() override {
157
- std::string error;
158
- if (!DoCompress(data_, length_, &result_, &error)) SetError(error);
280
+ /* Failures are reported through failure_ rather than SetError so the
281
+ * rejection keeps its error class and code. */
282
+ void OnOK() override {
283
+ if (failed_) deferred_.Reject(MakeError(Env(), failure_));
284
+ else deferred_.Resolve(WrapResult(Env(), &result_));
159
285
  }
160
286
 
161
- void OnOK() override { deferred_.Resolve(WrapResult(Env(), &result_)); }
162
-
163
- void OnError(const Napi::Error& e) override { deferred_.Reject(e.Value()); }
287
+ OwnedInput input_;
288
+ RawResult result_;
289
+ Failure failure_;
290
+ bool failed_ = false;
164
291
 
165
292
  private:
166
293
  Napi::Promise::Deferred deferred_;
167
- Napi::ObjectReference inputRef_; // keeps the input buffer alive while the worker runs
168
- const char* data_;
169
- size_t length_;
170
- RawResult result_;
171
294
  };
172
295
 
173
- class DecompressWorker : public Napi::AsyncWorker {
296
+ class CompressWorker : public CodecWorker {
174
297
  public:
175
- DecompressWorker(Napi::Env env, Napi::Buffer<char> input, size_t expectedLength)
176
- : Napi::AsyncWorker(env),
177
- deferred_(Napi::Promise::Deferred::New(env)),
178
- inputRef_(Napi::Persistent(input.As<Napi::Object>())),
179
- data_(input.Data()),
180
- length_(input.Length()),
181
- expectedLength_(expectedLength) {}
182
-
183
- Napi::Promise Promise() { return deferred_.Promise(); }
298
+ using CodecWorker::CodecWorker;
184
299
 
185
300
  protected:
186
301
  void Execute() override {
187
- std::string error;
188
- if (!DoDecompress(data_, length_, expectedLength_, &result_, &error)) SetError(error);
302
+ failed_ = !DoCompress(input_.data(), input_.length(), &result_, &failure_);
189
303
  }
304
+ };
190
305
 
191
- void OnOK() override { deferred_.Resolve(WrapResult(Env(), &result_)); }
306
+ class DecompressWorker : public CodecWorker {
307
+ public:
308
+ DecompressWorker(Napi::Env env, size_t expectedLength)
309
+ : CodecWorker(env), expectedLength_(expectedLength) {}
192
310
 
193
- void OnError(const Napi::Error& e) override { deferred_.Reject(e.Value()); }
311
+ protected:
312
+ void Execute() override {
313
+ failed_ = !DoDecompress(input_.data(), input_.length(), expectedLength_, &result_,
314
+ &failure_);
315
+ }
194
316
 
195
317
  private:
196
- Napi::Promise::Deferred deferred_;
197
- Napi::ObjectReference inputRef_;
198
- const char* data_;
199
- size_t length_;
200
318
  size_t expectedLength_;
201
- RawResult result_;
202
319
  };
203
320
 
321
+ /* Copies the input into the worker and queues it, or deletes the worker and
322
+ * returns a rejected promise when the copy cannot be allocated. */
323
+ Napi::Value QueueWorker(Napi::Env env, CodecWorker* worker, const Input& input) {
324
+ if (!worker->input().CopyFrom(input)) {
325
+ delete worker;
326
+ return RejectedPromise(env, kAllocationFailed);
327
+ }
328
+ Napi::Promise promise = worker->Promise();
329
+ worker->Queue();
330
+ return promise;
331
+ }
332
+
204
333
  /* ---- sync API ----------------------------------------------------------- */
205
334
 
206
335
  Napi::Value Compress(const Napi::CallbackInfo& info) {
207
336
  Napi::Env env = info.Env();
208
337
 
209
- std::string error;
210
- if (!ValidateInputBuffer(info, &error)) {
211
- Napi::TypeError::New(env, error).ThrowAsJavaScriptException();
212
- return env.Undefined();
213
- }
338
+ Input input;
339
+ Failure failure;
340
+ if (!GetInput(info, &input, &failure)) return Throw(env, failure);
214
341
 
215
- Napi::Buffer<char> input = info[0].As<Napi::Buffer<char>>();
216
342
  RawResult result;
217
- if (!DoCompress(input.Data(), input.Length(), &result, &error)) {
218
- Napi::Error::New(env, error).ThrowAsJavaScriptException();
219
- return env.Undefined();
220
- }
343
+ if (!DoCompress(input.data, input.length, &result, &failure)) return Throw(env, failure);
221
344
  return WrapResult(env, &result);
222
345
  }
223
346
 
224
347
  Napi::Value Decompress(const Napi::CallbackInfo& info) {
225
348
  Napi::Env env = info.Env();
226
349
 
227
- std::string error;
228
- bool rangeError = false;
350
+ Input input;
229
351
  size_t expectedLength = 0;
230
- if (!ValidateInputBuffer(info, &error)) {
231
- Napi::TypeError::New(env, error).ThrowAsJavaScriptException();
232
- return env.Undefined();
233
- }
234
- if (!ValidateExpectedLength(info, &expectedLength, &error, &rangeError)) {
235
- if (rangeError) Napi::RangeError::New(env, error).ThrowAsJavaScriptException();
236
- else Napi::TypeError::New(env, error).ThrowAsJavaScriptException();
237
- return env.Undefined();
238
- }
352
+ Failure failure;
353
+ if (!GetInput(info, &input, &failure)) return Throw(env, failure);
354
+ if (!GetExpectedLength(info, &expectedLength, &failure)) return Throw(env, failure);
239
355
 
240
- Napi::Buffer<char> input = info[0].As<Napi::Buffer<char>>();
241
356
  RawResult result;
242
- if (!DoDecompress(input.Data(), input.Length(), expectedLength, &result, &error)) {
243
- Napi::Error::New(env, error).ThrowAsJavaScriptException();
244
- return env.Undefined();
357
+ if (!DoDecompress(input.data, input.length, expectedLength, &result, &failure)) {
358
+ return Throw(env, failure);
245
359
  }
246
360
  return WrapResult(env, &result);
247
361
  }
248
362
 
249
363
  /* ---- async API (libuv thread pool; never blocks the event loop) --------- */
250
364
 
251
- Napi::Value RejectedPromise(Napi::Env env, const Napi::Error& error) {
252
- auto deferred = Napi::Promise::Deferred::New(env);
253
- deferred.Reject(error.Value());
254
- return deferred.Promise();
255
- }
256
-
257
365
  Napi::Value CompressAsync(const Napi::CallbackInfo& info) {
258
366
  Napi::Env env = info.Env();
259
367
 
260
- std::string error;
261
- if (!ValidateInputBuffer(info, &error)) {
262
- return RejectedPromise(env, Napi::TypeError::New(env, error));
263
- }
368
+ Input input;
369
+ Failure failure;
370
+ if (!GetInput(info, &input, &failure)) return RejectedPromise(env, failure);
264
371
 
265
- auto* worker = new CompressWorker(env, info[0].As<Napi::Buffer<char>>());
266
- Napi::Promise promise = worker->Promise();
267
- worker->Queue();
268
- return promise;
372
+ return QueueWorker(env, new CompressWorker(env), input);
269
373
  }
270
374
 
271
375
  Napi::Value DecompressAsync(const Napi::CallbackInfo& info) {
272
376
  Napi::Env env = info.Env();
273
377
 
274
- std::string error;
275
- bool rangeError = false;
378
+ Input input;
276
379
  size_t expectedLength = 0;
277
- if (!ValidateInputBuffer(info, &error)) {
278
- return RejectedPromise(env, Napi::TypeError::New(env, error));
279
- }
280
- if (!ValidateExpectedLength(info, &expectedLength, &error, &rangeError)) {
281
- if (rangeError) return RejectedPromise(env, Napi::RangeError::New(env, error));
282
- return RejectedPromise(env, Napi::TypeError::New(env, error));
283
- }
380
+ Failure failure;
381
+ if (!GetInput(info, &input, &failure)) return RejectedPromise(env, failure);
382
+ if (!GetExpectedLength(info, &expectedLength, &failure)) return RejectedPromise(env, failure);
284
383
 
285
- auto* worker = new DecompressWorker(env, info[0].As<Napi::Buffer<char>>(), expectedLength);
286
- Napi::Promise promise = worker->Promise();
287
- worker->Queue();
288
- return promise;
384
+ return QueueWorker(env, new DecompressWorker(env, expectedLength), input);
289
385
  }
290
386
 
291
387
  Napi::Object Init(Napi::Env env, Napi::Object exports) {