@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 +100 -0
- package/LICENSE +28 -3
- package/README.md +114 -48
- package/index.d.ts +44 -16
- package/package.json +9 -4
- package/prebuilds/darwin-arm64/@trkyshorty+node-lzf.node +0 -0
- package/prebuilds/darwin-x64/@trkyshorty+node-lzf.node +0 -0
- package/prebuilds/linux-arm64/@trkyshorty+node-lzf.glibc.node +0 -0
- package/prebuilds/linux-x64/@trkyshorty+node-lzf.glibc.node +0 -0
- package/prebuilds/linux-x64/@trkyshorty+node-lzf.musl.node +0 -0
- package/prebuilds/win32-arm64/@trkyshorty+node-lzf.node +0 -0
- package/prebuilds/win32-x64/@trkyshorty+node-lzf.node +0 -0
- package/src/lzf/lzfP.h +18 -2
- package/src/lzf/lzf_c.cc +13 -4
- package/src/lzf.cc +221 -125
- package/prebuilds/linux-arm64/@trkyshorty+node-lzf.node +0 -0
- package/prebuilds/linux-x64/@trkyshorty+node-lzf.node +0 -0
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
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
1
|
+
# node-lzf
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+
[](https://www.npmjs.com/package/@trkyshorty/node-lzf)
|
|
4
|
+
[](https://github.com/trkyshorty/node-lzf/actions/workflows/prebuild.yml)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](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
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
+
### `compress(data): Buffer`
|
|
78
99
|
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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.
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
+
## License
|
|
151
217
|
|
|
152
|
-
BSD-2-Clause, see [LICENSE](LICENSE). Bundles [liblzf](
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
|
13
|
-
*
|
|
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:
|
|
43
|
+
function compressAsync(data: Input): Promise<Buffer>;
|
|
17
44
|
|
|
18
45
|
/**
|
|
19
|
-
* Decompresses
|
|
20
|
-
* (or an upper bound of the) decompressed size,
|
|
21
|
-
* Throws TypeError/RangeError on invalid arguments
|
|
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:
|
|
51
|
+
function decompress(data: Input, expectedLength: number): Buffer;
|
|
25
52
|
|
|
26
53
|
/**
|
|
27
|
-
* Decompresses on the libuv thread pool without blocking the event
|
|
28
|
-
*
|
|
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:
|
|
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": "
|
|
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": ">=
|
|
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
|
-
"
|
|
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"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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 <
|
|
8
|
+
#include <cmath>
|
|
9
|
+
#include <cstdint>
|
|
8
10
|
#include <cstdlib>
|
|
9
|
-
#include <
|
|
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
|
-
*
|
|
33
|
-
bool DoCompress(const char* input, size_t inputLength, RawResult* out,
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
58
|
-
* actual result size. Distinguishes "expected length too small"
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
*
|
|
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>(
|
|
155
|
+
buffer, static_cast<unsigned int>(capacity));
|
|
72
156
|
|
|
73
157
|
if (decompressedLength == 0) {
|
|
74
158
|
int cause = errno;
|
|
75
159
|
free(buffer);
|
|
76
|
-
|
|
77
|
-
|
|
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 <
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
*
|
|
224
|
+
*failure = kInvalidLengthType;
|
|
124
225
|
return false;
|
|
125
226
|
}
|
|
126
227
|
double raw = info[1].As<Napi::Number>().DoubleValue();
|
|
127
|
-
if (raw
|
|
128
|
-
*
|
|
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
|
-
|
|
133
|
-
|
|
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>(
|
|
236
|
+
*expectedLength = static_cast<size_t>(raw);
|
|
139
237
|
return true;
|
|
140
238
|
}
|
|
141
239
|
|
|
142
240
|
/* ---- async workers ------------------------------------------------------ */
|
|
143
241
|
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
296
|
+
class CompressWorker : public CodecWorker {
|
|
174
297
|
public:
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
306
|
+
class DecompressWorker : public CodecWorker {
|
|
307
|
+
public:
|
|
308
|
+
DecompressWorker(Napi::Env env, size_t expectedLength)
|
|
309
|
+
: CodecWorker(env), expectedLength_(expectedLength) {}
|
|
192
310
|
|
|
193
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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.
|
|
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
|
-
|
|
228
|
-
bool rangeError = false;
|
|
350
|
+
Input input;
|
|
229
351
|
size_t expectedLength = 0;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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.
|
|
243
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
368
|
+
Input input;
|
|
369
|
+
Failure failure;
|
|
370
|
+
if (!GetInput(info, &input, &failure)) return RejectedPromise(env, failure);
|
|
264
371
|
|
|
265
|
-
|
|
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
|
-
|
|
275
|
-
bool rangeError = false;
|
|
378
|
+
Input input;
|
|
276
379
|
size_t expectedLength = 0;
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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) {
|
|
Binary file
|
|
Binary file
|