-
Notifications
You must be signed in to change notification settings - Fork 4
feat(native): wire memlimit through N-API decoder #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| --- | ||
| 'node-liblzma': minor | ||
| --- | ||
|
|
||
| Wire `memlimit` through the native N-API decoder, closing the WASM↔native asymmetry advertised in TSDoc since the v5.x line. | ||
|
|
||
| `Unxz`, `createUnxz`, and the `unxz`/`unxzAsync` Buffer APIs now all honor `LZMAOptions.memlimit`: | ||
|
|
||
| ```ts | ||
| const stream = new Unxz({ memlimit: 64 * 1024 * 1024 }); // 64 MiB | ||
| // → emits 'error' with LZMAMemoryLimitError on real .xz exceeding the limit | ||
| ``` | ||
|
|
||
| **Changes** | ||
| - N-API `InitializeDecoder` reads `opts.memlimit` and passes it to `lzma_stream_decoder` (was hardcoded to `UINT64_MAX`). | ||
| - JS-side `validateMemlimit` (now exported from `src/wasm/bindings.ts`) is reused at the `Xz`/`Unxz` constructor entry, so NaN/Infinity/fractional/negative/`> Number.MAX_SAFE_INTEGER`/`> UINT64_MAX` are caught synchronously with `LZMAOptionsError` regardless of whether the eventual decoder is native or WASM. | ||
| - New bigint upper-bound guard: `memlimit > 18446744073709551615n` (`UINT64_MAX`) now rejected, preventing silent BigInt→uint64 truncation. | ||
|
|
||
| **TSDoc** updated to remove the "WASM only" caveat — `memlimit` is now uniform across all decoder paths. | ||
|
|
||
| Default behavior is preserved: when `memlimit` is omitted, the native decoder uses `UINT64_MAX` (no limit), and the WASM Buffer API uses 256 MiB (its existing default). |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,6 +16,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| **/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #include <cmath> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #include <node_buffer.h> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #include <vector> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #include "node-liblzma.hpp" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -109,7 +110,7 @@ LZMA::LZMA(const Napi::CallbackInfo &info) : Napi::ObjectWrap<LZMA>(info), _stre | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch (mode) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case STREAM_DECODE: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success = InitializeDecoder(env); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success = InitializeDecoder(env, opts); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case STREAM_ENCODE: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success = InitializeEncoder(opts, preset, check); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -482,9 +483,66 @@ bool LZMA::InitializeEncoder(const Napi::Object &opts, uint32_t preset, lzma_che | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bool LZMA::InitializeDecoder(const Napi::Env &env) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bool LZMA::InitializeDecoder(const Napi::Env &env, const Napi::Object &opts) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lzma_ret ret = lzma_stream_decoder(&this->_stream, UINT64_MAX, LZMA_CONCATENATED); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Read optional memlimit from JS options object. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If absent or undefined, use UINT64_MAX (no limit — preserves pre-fix behaviour). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uint64_t memlimit = UINT64_MAX; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Napi::Value optsMemlimit = opts.Get("memlimit"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!optsMemlimit.IsUndefined() && !optsMemlimit.IsNull()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (optsMemlimit.IsBigInt()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // node-addon-api Napi::BigInt::Uint64Value: lossless=true when value fits uint64_t. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // JS-side validateMemlimit() already rejects values > UINT64_MAX, so lossless | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // should always be true here. Throw on precision loss (defense-in-depth: a future | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // direct-N-API consumer bypassing the JS guard must not silently get 'no limit'). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bool lossless = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uint64_t val = optsMemlimit.As<Napi::BigInt>().Uint64Value(&lossless); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!lossless) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Napi::TypeError::New(env, "memlimit out of range or precision lost").ThrowAsJavaScriptException(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| memlimit = val; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else if (optsMemlimit.IsNumber()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // JS-side validateMemlimit() already rejects non-integer, negative, NaN, Infinity, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // and values above Number.MAX_SAFE_INTEGER (2^53-1). Throw on any out-of-range | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // value (defense-in-depth: direct-N-API consumers bypassing the JS guard get an | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // error, not a silent UINT64_MAX 'no limit' coercion that inverts caller intent). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| double d = optsMemlimit.ToNumber().DoubleValue(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (d < 0.0 || !std::isfinite(d) || d != std::floor(d) || d > static_cast<double>(UINT64_MAX)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Napi::TypeError::New(env, "memlimit out of range or precision lost").ThrowAsJavaScriptException(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Defense-in-depth: validateMemlimit() in JS rejects this first; native enforces | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // independently for direct-N-API consumers. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Number.MAX_SAFE_INTEGER = 2^53 - 1 = 9007199254740991. Any double above this | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // threshold no longer represents an exact integer — caller intent is silently | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // corrupted. Reject to mirror the JS-side guard. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (d > 9007199254740991.0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Napi::TypeError::New(env, "memlimit number exceeds MAX_SAFE_INTEGER (use bigint for values >= 2^53)").ThrowAsJavaScriptException(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| memlimit = static_cast<uint64_t>(d); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+512
to
+531
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // and values above Number.MAX_SAFE_INTEGER (2^53-1). Throw on any out-of-range | |
| // value (defense-in-depth: direct-N-API consumers bypassing the JS guard get an | |
| // error, not a silent UINT64_MAX 'no limit' coercion that inverts caller intent). | |
| double d = optsMemlimit.ToNumber().DoubleValue(); | |
| if (d < 0.0 || !std::isfinite(d) || d != std::floor(d) || d > static_cast<double>(UINT64_MAX)) | |
| { | |
| Napi::TypeError::New(env, "memlimit out of range or precision lost").ThrowAsJavaScriptException(); | |
| return false; | |
| } | |
| memlimit = static_cast<uint64_t>(d); | |
| // and values above Number.MAX_SAFE_INTEGER (2^53-1). Enforce the same contract | |
| // here so Number -> uint64_t conversion is lossless for direct native callers too. | |
| double d = optsMemlimit.ToNumber().DoubleValue(); | |
| const double kMaxSafeInteger = 9007199254740991.0; // Number.MAX_SAFE_INTEGER | |
| if (d < 0.0 || !std::isfinite(d) || d != std::floor(d) || d > kMaxSafeInteger) | |
| { | |
| Napi::TypeError::New(env, "memlimit out of range or precision lost").ThrowAsJavaScriptException(); | |
| return false; | |
| } | |
| uint64_t val = static_cast<uint64_t>(d); | |
| if (static_cast<double>(val) != d) | |
| { | |
| Napi::TypeError::New(env, "memlimit out of range or precision lost").ThrowAsJavaScriptException(); | |
| return false; | |
| } | |
| memlimit = val; |
Copilot
AI
Apr 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If opts.memlimit is provided but is neither a BigInt nor a Number (e.g. a string), the current logic silently falls back to UINT64_MAX (no limit). Since other options are type-validated, it would be more consistent/safer to throw a TypeError when memlimit is present with an unsupported type, rather than ignoring caller intent.
| } | |
| } | |
| else | |
| { | |
| Napi::TypeError::New(env, "memlimit must be a Number or BigInt").ThrowAsJavaScriptException(); | |
| return false; | |
| } |
Copilot
AI
Apr 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Number branch assumes JS-side validation ensures integer / finite values, but the native binding is still a public boundary. Consider validating here too (finite, integer, non-negative, <= MAX_SAFE_INTEGER or otherwise explicitly documented), and throw on invalid input rather than silently ignoring (leaving memlimit as UINT64_MAX). This avoids surprising behavior if the binding is used directly or future JS validation paths change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the BigInt branch, if
Uint64Value()is not lossless (e.g. value > UINT64_MAX or negative), the code currently falls back toUINT64_MAX(no limit). That can silently disable the caller’s intended limit and reintroduce a potential memory-DoS vector if JS-side validation is bypassed. Prefer throwing a JS exception (RangeError/TypeError) when the value isn’t representable as uint64_t, instead of silently defaulting to no limit.