Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [3.4.0] - 2026-09-19

Pack `bigint` values outside the 64-bit integer range as MessagePack ext
type `0x42` (msgpackr `useBigIntExtension` two's-complement payload), and
unpack that type back to `bigint`. Payload is capped at 256 bytes. Values
that still fit int64/uint64 keep using integer wire.

### Added

- Out-of-range `bigint` packs as ext `0x42` and round-trips through
`unpack()`, including nested arrays/maps/objects, uint256, and the
64-bit boundaries `2^64` and `-2^63-1`.
- Unpack of ext `0x42` with a payload larger than 256 bytes throws
`cannot unpack BigInt: ext payload exceeds 256 bytes`. Other ext types
still throw `cannot unpack ext type`.

### Changed

- `cannot pack BigInt outside 64-bit range` is no longer thrown for
values that fit in a 256-byte ext payload. Larger values throw
`cannot pack BigInt: ext payload exceeds 256 bytes`.

## [3.3.0] - 2026-09-19

`Stream.send` queues packed messages when the underlying writable returns
Expand Down Expand Up @@ -152,7 +174,8 @@ GitHub Actions tests Node 18/20/22 on Ubuntu, macOS, and Windows 2022.
- Pack throw paths free or return pooled sbuffers on every exit.
- msgpack-c c-7.0.2 includes unpacker buffer-expansion overflow checks.

[Unreleased]: https://github.com/msgpack/msgpack-node/compare/v3.3.0...HEAD
[Unreleased]: https://github.com/msgpack/msgpack-node/compare/v3.4.0...HEAD
[3.4.0]: https://github.com/msgpack/msgpack-node/compare/v3.3.0...v3.4.0
[3.3.0]: https://github.com/msgpack/msgpack-node/compare/v3.2.0...v3.3.0
[3.2.0]: https://github.com/msgpack/msgpack-node/compare/v3.1.0...v3.2.0
[3.1.0]: https://github.com/msgpack/msgpack-node/compare/v3.0.0...v3.1.0
Expand Down
4 changes: 3 additions & 1 deletion COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ gcovr --root . --filter src/ --exclude deps/ --no-markers --txt-metric branch --

- `test/bigint.test.js` — `#37` Number-vs-BigInt rule: uint64 of 1 stays
Number, `MAX_SAFE_INTEGER` stays Number, reporter uint64 round-trips as
BigInt, out-of-range BigInt throws, rounded Number bits are not recovered.
BigInt, in-range BigInt still uses integer wire (not ext), out-of-range
BigInt packs as ext `0x42` (uint256, 64-bit boundaries, nested, 256-byte
cap), rounded Number bits are not recovered.
- `test/coverage-native.test.js` (57 tests) — hand-built wire buffers for
every MessagePack format family, including the ones `pack()` never emits
(float32, str8/16/32, bin16/32, array32, map16/32, all eight ext forms,
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ Packing:
* `undefined` / `null` → nil
* `boolean` → bool
* finite integers (`number` or `bigint` in the 64-bit range) → uint/int
* `bigint` outside uint64/int64 → throws
* `bigint` outside uint64/int64 → ext type 0x42 (msgpackr BigInt, two's-complement, ≤ 256 bytes)
* `bigint` whose two's-complement form exceeds 256 bytes → throws
* other numbers → float64
* `string` → str (UTF-8)
* `Date` → str (ISO 8601, `toISOString()`), at any nesting level
Expand All @@ -83,7 +84,8 @@ Unpacking:
* str → `string`
* bin → `Buffer`
* array / map → Array / Object
* ext → throws
* ext type 0x42 (msgpackr BigInt) → bigint (payload ≤ 256 bytes)
* other ext types → throws

So `unpack(pack(1n))` is Number `1`, and `unpack(pack(18446464814936021036n))`
is that same `bigint`.
Expand Down
15 changes: 9 additions & 6 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Type definitions for msgpack 3.3.0
// Type definitions for msgpack 3.4.0
// Project: https://github.com/msgpack/msgpack-node

/// <reference types="node" />
Expand Down Expand Up @@ -56,10 +56,12 @@ export interface PackOptions {
* and may also set `type` / `family`.
*
* `bigint` values in the int64/uint64 range pack as MessagePack integers
* (smallest family that fits). Values outside that range throw. A `number`
* that has already lost bits below 2^53 stays on the Number path; lost bits
* are not recovered. BigInt plus an integer `type`/`family` uses the same
* 64-bit path.
* (smallest family that fits). Larger values pack as ext type 0x42 (msgpackr
* BigInt extension) with a two's-complement payload of at most 256 bytes.
* Still larger values throw. A `number` that has already lost bits below
* 2^53 stays on the Number path; lost bits are not recovered. BigInt plus
* an integer `type`/`family` uses the same 64-bit path (hints do not
* truncate into ext).
*/
export function pack(value: any, options: PackOptions): Buffer;
export function pack(...values: any[]): Buffer;
Expand All @@ -69,7 +71,8 @@ export function pack(...values: any[]): Buffer;
*
* Integers whose magnitude is greater than `Number.MAX_SAFE_INTEGER` return
* as `bigint`. Values that fit stay `number`, even if the wire type is
* uint64 or int64 (a uint64 of 1 is Number 1).
* uint64 or int64 (a uint64 of 1 is Number 1). Ext type 0x42 unpacks as
* `bigint` (payload capped at 256 bytes). Other ext types throw.
*
* Returns `null` when the buffer holds an incomplete value, in which case
* `unpack.bytes_remaining` equals `buf.length`. Throws on malformed input or
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "msgpack",
"description": "A space-efficient object serialization library for Node.js",
"version": "3.3.0",
"version": "3.4.0",
"homepage": "https://github.com/msgpack/msgpack-node",
"author": "Peter Griess <pg@std.in>",
"contributors": [
Expand Down
167 changes: 160 additions & 7 deletions src/msgpack.cc
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ const double kInt64Min = -9223372036854775808.0;
const uint64_t kMaxSafeInteger = 9007199254740991ULL;
const int64_t kMinSafeInteger = -9007199254740991LL;
const size_t kSbufferPoolMax = 512;
/* msgpackr useBigIntExtension: two's-complement BigInt as ext type 0x42 ('B'). */
const int8_t kBigIntExtType = 0x42;
const uint32_t kMaxBigIntExtBytes = 256;
const int kMaxBigIntExtWords = 32;

enum ScanStatus {
kScanOk = 0,
Expand Down Expand Up @@ -332,6 +336,149 @@ static v8::Local<v8::Value> Error(const char* msg) {
return Nan::Error(msg);
}

/*
* Pack a BigInt that does not fit int64/uint64 as MessagePack ext 0x42.
* Payload is two's-complement big-endian bytes, minimal length, sign-extended
* so the high bit matches the sign (msgpackr useBigIntExtension algorithm).
* Fail closed at 256 payload bytes (2048-bit).
*/
static void PackBigIntExt(msgpack_packer* pk, v8::Local<v8::BigInt> bi) {
int word_count = bi->WordCount();
if (word_count > kMaxBigIntExtWords) {
throw MsgpackException(
Error("cannot pack BigInt: ext payload exceeds 256 bytes"));
}
uint64_t words[32];
memset(words, 0, sizeof(words));
int sign_bit = 0;
if (word_count > 0) {
int wc = word_count;
bi->ToWordsArray(&sign_bit, &wc, words);
word_count = wc;
}
unsigned char tmp[256];
memset(tmp, 0, sizeof(tmp));
size_t n = static_cast<size_t>(word_count) * 8u;
/* GCOVR_EXCL_START: WordCount is 0 only for 0n, which takes the int64 path. */
if (n == 0) {
tmp[0] = 0;
n = 1;
} else {
/* GCOVR_EXCL_STOP */
for (int i = 0; i < word_count; i++) {
uint64_t w = words[i];
for (int b = 0; b < 8; b++) {
tmp[static_cast<size_t>(i) * 8u + static_cast<size_t>(b)] =
static_cast<unsigned char>(w & 0xffu);
w >>= 8;
}
}
}
if (sign_bit) {
unsigned int carry = 1;
for (size_t i = 0; i < n; i++) {
unsigned int v =
static_cast<unsigned int>(static_cast<unsigned char>(~tmp[i])) + carry;
tmp[i] = static_cast<unsigned char>(v);
carry = v >> 8;
}
if ((tmp[n - 1] & 0x80u) == 0) {
if (n >= kMaxBigIntExtBytes) {
throw MsgpackException(
Error("cannot pack BigInt: ext payload exceeds 256 bytes"));
}
tmp[n] = 0xff;
n++;
}
} else if ((tmp[n - 1] & 0x80u) != 0) {
if (n >= kMaxBigIntExtBytes) {
throw MsgpackException(
Error("cannot pack BigInt: ext payload exceeds 256 bytes"));
}
tmp[n] = 0x00;
n++;
}
while (n > 1) {
if (tmp[n - 1] == 0x00 && (tmp[n - 2] & 0x80u) == 0) {
n--;
continue;
}
if (tmp[n - 1] == 0xff && (tmp[n - 2] & 0x80u) != 0) {
n--;
continue;
}
break;
}
/* GCOVR_EXCL_START: sign-extend already threw at 257 bytes; strip only shrinks. */
if (n > kMaxBigIntExtBytes) {
throw MsgpackException(
Error("cannot pack BigInt: ext payload exceeds 256 bytes"));
}
/* GCOVR_EXCL_STOP */
unsigned char be[256];
for (size_t i = 0; i < n; i++) {
be[i] = tmp[n - 1 - i];
}
int rc = msgpack_pack_ext(pk, n, kBigIntExtType);
if (rc == 0) { /* GCOVR_EXCL_BR_LINE: sbuffer write failure */
rc = msgpack_pack_ext_body(pk, be, n);
}
/* GCOVR_EXCL_START: sbuffer write failure */
if (rc != 0) {
throw MsgpackException(Error("Error serializing object"));
}
/* GCOVR_EXCL_STOP */
}

static v8::Local<v8::Value> ExtBigIntToJs(const char* ptr, uint32_t size) {
if (size == 0) {
throw MsgpackException(Error("cannot unpack BigInt"));
}
if (size > kMaxBigIntExtBytes) {
throw MsgpackException(
Error("cannot unpack BigInt: ext payload exceeds 256 bytes"));
}
const unsigned char* p = reinterpret_cast<const unsigned char*>(ptr);
unsigned char mag[256];
memcpy(mag, p, size);
const bool neg = (mag[0] & 0x80u) != 0;
if (neg) {
unsigned int carry = 1;
for (int i = static_cast<int>(size) - 1; i >= 0; i--) {
unsigned int v =
static_cast<unsigned int>(static_cast<unsigned char>(~mag[i])) + carry;
mag[i] = static_cast<unsigned char>(v);
carry = v >> 8;
}
}
uint32_t start = 0;
while (start + 1u < size && mag[start] == 0) {
start++;
}
const uint32_t nbytes = size - start;
int word_count = static_cast<int>((nbytes + 7u) / 8u);
if (word_count == 0) { /* GCOVR_EXCL_BR_LINE: nbytes is at least 1 after size==0 throw */
word_count = 1;
}
uint64_t words[32];
memset(words, 0, sizeof(words));
int byte_i = 0;
for (int i = static_cast<int>(size) - 1; i >= static_cast<int>(start); i--) {
const int wi = byte_i / 8;
const int sh = (byte_i % 8) * 8;
words[wi] |= static_cast<uint64_t>(mag[i]) << sh;
byte_i++;
}
v8::MaybeLocal<v8::BigInt> maybe = v8::BigInt::NewFromWords(
Nan::GetCurrentContext(), neg ? 1 : 0, word_count, words);
/* GCOVR_EXCL_START: NewFromWords fails only on OOM / isolate death. */
if (maybe.IsEmpty()) {
throw MsgpackException(Error("cannot unpack BigInt"));
}
/* GCOVR_EXCL_STOP */
return maybe.ToLocalChecked();
}

/* Persistent identity flag for cycle detection (not enumerable).
* thread_local because a v8::Persistent belongs to the isolate that created
* it: with a process-global handle, a worker's Init() would dispose the main
Expand Down Expand Up @@ -552,10 +699,12 @@ static void JsToMsgpack(msgpack_packer* pk, v8::Local<v8::Value> o, int depth) {
} else {
lossless = false;
const uint64_t u = bi->Uint64Value(&lossless);
if (!lossless) {
throw MsgpackException(Error("cannot pack BigInt outside 64-bit range"));
if (lossless) {
rc = msgpack_pack_uint64(pk, u);
} else {
PackBigIntExt(pk, bi);
return;
}
rc = msgpack_pack_uint64(pk, u);
}
} else if (o->IsString()) {
Nan::Utf8String bytes(o);
Expand Down Expand Up @@ -638,10 +787,14 @@ static v8::Local<v8::Value> MsgpackToJs(const msgpack_object* mo) {
return Nan::NewBuffer(0).ToLocalChecked();
}
return Nan::CopyBuffer(mo->via.bin.ptr, mo->via.bin.size).ToLocalChecked();
case MSGPACK_OBJECT_EXT:
/* Fail closed on extension types: callers expecting core JSON-like
* values should not silently receive opaque ext payloads. */
throw MsgpackException(Error("cannot unpack ext type"));
case MSGPACK_OBJECT_EXT: {
/* ext 0x42 is msgpackr BigInt. Every other ext type stays fail-closed. */
const msgpack_object_ext& ext = mo->via.ext;
if (ext.type != kBigIntExtType) {
throw MsgpackException(Error("cannot unpack ext type"));
}
return ExtBigIntToJs(ext.ptr, ext.size);
}
case MSGPACK_OBJECT_ARRAY: {
v8::Local<v8::Array> arr = Nan::New<v8::Array>(mo->via.array.size);
for (uint32_t i = 0; i < mo->via.array.size; i++) {
Expand Down
Loading
Loading