Skip to content
Closed
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
54 changes: 53 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [3.1.0] - 2026-09-19

Optional second-argument pack hints force a MessagePack wire type or family
without changing the default mapping. Two or more values still pack as an
array. See `#52`.

### Added

- `pack(value, { type })` writes a fixed MessagePack type (`fixint`,
`uint8`…`uint64`, `int8`…`int64`, `float32`/`float64`, `fixstr`/`str8`…
`str32`, `bin8`…`bin32`, `nil`/`true`/`false`). Out-of-range values throw
`cannot pack value as <type>`.
- `pack(value, { family })` picks a compact encoding in that family (`int`,
`float`, `str`, `bin`). `type` wins if both are set.
- `pack(array, { interpret })` maps each element through `interpret(item)`
which must return `{ data }` and may also set `type` / `family`.
- Detection is last-argument, two-arg only: the object must own-enumerate
only `type`, `family`, and/or `interpret`. Extra keys, one-arg objects, and
`pack(1, 2)` keep the old array packing.

## [3.0.0] - 2026-09-19

Integers whose magnitude is greater than `Number.MAX_SAFE_INTEGER` unpack as
`bigint` instead of a rounded `number`. Values that fit stay `number`
regardless of wire width. `pack()` accepts `bigint` in the signed/unsigned
64-bit range.

### Added

- `pack()` encodes `bigint` via `v8::BigInt` `Int64Value` / `Uint64Value` as
the smallest MessagePack integer family that fits.
- Unpack of uint64/int64 values outside `Number.MAX_SAFE_INTEGER` returns
`bigint` so 64-bit integers stay exact (`#37`).

### Changed

- A uint64 of `1` still unpacks as Number `1`. `Number.MAX_SAFE_INTEGER`
stays Number even when the wire type is uint64.
- A JS `number` that is already rounded (for example `18446464814936021000`)
still packs on the Number path; lost bits are not recovered.

### Breaking

- Unpacking a 64-bit integer larger than `Number.MAX_SAFE_INTEGER` now
returns `bigint` instead of the nearest double. Code that assumed
`typeof unpack(...) === 'number'` for every integer must accept `bigint`.
- `pack(10n)` no longer throws `cannot pack object`. BigInt outside
uint64/int64 (`2n ** 64n`, `-(2n ** 63n) - 1n`) throws
`cannot pack BigInt outside 64-bit range`.

## [2.0.0] - 2026-09-18

Security modernization. Requires **Node.js 18+**. Vendors **msgpack-c c-7.0.2**.
Expand Down Expand Up @@ -58,5 +108,7 @@ 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/e04c9b55f98d64512174d6e859b8294b729659a2...HEAD
[Unreleased]: https://github.com/msgpack/msgpack-node/compare/v3.1.0...HEAD
[3.1.0]: https://github.com/msgpack/msgpack-node/compare/v3.0.0...v3.1.0
[3.0.0]: https://github.com/msgpack/msgpack-node/compare/e04c9b55f98d64512174d6e859b8294b729659a2...HEAD
[2.0.0]: https://github.com/msgpack/msgpack-node/commit/e04c9b55f98d64512174d6e859b8294b729659a2
26 changes: 18 additions & 8 deletions COVERAGE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Coverage — msgpack 2.0.0
# Coverage — msgpack 3.1.0

`npm run coverage` runs both halves and fails the build under 95%.

Expand All @@ -8,9 +8,9 @@
| `lib/` + `bin/` (c8) | branches | **100%** | ≥ 95% |
| `lib/` + `bin/` (c8) | functions | **100%** | ≥ 95% |
| `lib/` + `bin/` (c8) | lines | **100%** | ≥ 95% |
| `src/msgpack.cc` (gcovr) | lines | **95.9%** (473/493) | ≥ 95% |
| `src/msgpack.cc` (gcovr) | branches | **99.5%** (400/402) | ≥ 95% |
| `src/msgpack.cc` (gcovr) | functions | 100% (36/36) | — |
| `src/` (gcovr) | lines | **95.2%** (902/947) | ≥ 95% |
| `src/` (gcovr) | branches | **95.4%** (836/876) | ≥ 95% |
| `src/` (gcovr) | functions | 100% (59/59) | — |

`deps/` is excluded from the native report; the vendored msgpack-c is not our
code. `build/` is rebuilt without instrumentation at the end of
Expand Down Expand Up @@ -56,12 +56,12 @@ Every one is an error arm that cannot be entered from JS without stubbing
| 735 | `throw` after `msgpack_pack_array` in `Pack` | Allocation failure only. |
| 809–813, 815 | `MSGPACK_UNPACK_CONTINUE` / parse-error tail of `Unpack` | `ScanOne` walks the same grammar first with limits at or below the vendored library's own (511 vs 512 nested containers, the same 1 000 000 element cap), so once it returns `kScanOk`, `msgpack_unpack_next` can only succeed. The arms stay so a future divergence fails closed instead of reading `result.data` uninitialised. |

## Remaining uncovered native branches (2 of 402)
## Remaining uncovered native branches in `msgpack.cc` (2)

| Line | Code | Why |
| --- | --- | --- |
| 148 | `switch (b)` in `ScanOne` | The `default:` edge — see lines 307–308 above. It cannot be excluded on its own without also dropping the 30 covered case edges on the same line, so it is left in and counted against us. |
| 575 | `switch (mo->type)` in `MsgpackToJs` | Same, for the `default:` edge covering the complete `msgpack_object_type` enum. |
| 154 | `switch (b)` in `ScanOne` | The `default:` edge — see lines 307–308 above. It cannot be excluded on its own without also dropping the 30 covered case edges on the same line, so it is left in and counted against us. |
| 611 | `switch (mo->type)` in `MsgpackToJs` | Same, for the `default:` edge covering the complete `msgpack_object_type` enum. |

## About the native branch number

Expand Down Expand Up @@ -126,16 +126,26 @@ gcovr --root . --filter src/ --exclude deps/ --no-markers --txt-metric branch --

## What the new tests cover

- `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.
- `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,
negative fixint); a truncation point for every header and payload;
`kMaxBytes` / `kMaxContainer` / `kMaxDepth` rejections; `0xc1`; the pack-side
type dispatch (Symbol, BigInt, non-finite numbers, integer edges, undefined,
type dispatch (Symbol, non-finite numbers, integer edges, undefined,
zero-argument and multi-argument `pack`); Date failure modes; `toJSON`
failure modes and mark cleanup; and a worker that nests 600 packs deep to
saturate the thread-local sbuffer pool and reach the "pool is full, free it"
arm of `~PackBuffer`.
- `test/pack-hints.test.js` — `pack(value, { type, family, interpret })` wire
types, last-arg options detection (host objects, Proxies, ownKeys throws),
and the reachable miss paths in `src/pack_hints.inc`. Unreachable arms
(empty `info[1]`, `kTypeNone` / `kFamilyNone` defaults, hinted
`kMaxPackDepth`) are marked `GCOVR_EXCL_*`, not deleted. Native overall
stays above the 95% gate (`pack_hints.inc` itself is 91% branches because
switch `default:` edges sit on the same line as covered cases).
- `test/cli.test.js` (12 tests) — the exit-1 paths of both CLIs: invalid JSON,
empty stdin, a pack rejection reachable from real JSON, an unparseable byte,
an oversized header, incomplete input both alone and after a good frame, and
Expand Down
71 changes: 61 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
and de-serializes JavaScript values with [MessagePack](https://msgpack.org).
Packed output is a `Buffer` and is typically much smaller than JSON.

Version 2.0 requires **Node.js 18+**, vendors **msgpack-c c-7.0.2**, and
rejects oversized unpack headers instead of allocating them. See
[`SECURITY.md`](SECURITY.md).
Version 3.1 requires **Node.js 18+**, vendors **msgpack-c c-7.0.2**, unpacks
64-bit integers outside `Number.MAX_SAFE_INTEGER` as `bigint`, and accepts
optional pack type/family hints. See [`SECURITY.md`](SECURITY.md).

### Usage

Expand All @@ -19,10 +19,10 @@ const oo = msgpack.unpack(b);
assert.deepEqual(oo, o);
```

`pack()` accepts any JSON-like value plus Node `Buffer`s and `Date`s.
`unpack()` consumes a `Buffer` and returns a JavaScript value, or `null` if
the buffer is a truncated (incomplete) MessagePack object. Oversized
array/map/string bombs throw.
`pack()` accepts any JSON-like value plus Node `Buffer`s, `Date`s, and
`bigint` values in the int64/uint64 range. `unpack()` consumes a `Buffer`
and returns a JavaScript value, or `null` if the buffer is a truncated
(incomplete) MessagePack object. Oversized array/map/string bombs throw.

A streaming helper wraps a readable socket and emits `msg`, plus `error` when
a packet cannot be unpacked (the offending buffer is dropped):
Expand All @@ -39,13 +39,14 @@ ms.on('error', (e) => {
ms.send({ hello: 'world' });
```

### Type mapping (2.0)
### Type mapping (3.0)

Packing:

* `undefined` / `null` → nil
* `boolean` → bool
* finite integers → uint/int
* finite integers (`number` or `bigint` in the 64-bit range) → uint/int
* `bigint` outside uint64/int64 → throws
* other numbers → float64
* `string` → str (UTF-8)
* `Date` → str (ISO 8601, `toISOString()`), at any nesting level
Expand All @@ -57,18 +58,68 @@ Packing:
integer keys, not dropped
* functions, circular refs, and nesting deeper than 512 throw

A `number` that is already rounded (for example `18446464814936021000`) packs
on the Number path. Lost bits are not recovered.

Unpacking:

* nil → `null`
* bool / int / float → JS boolean / number
* bool / float → JS boolean / number
* int whose magnitude ≤ `Number.MAX_SAFE_INTEGER` → `number` (a uint64 of
`1` is Number `1`; `Number.MAX_SAFE_INTEGER` stays Number)
* int whose magnitude > `Number.MAX_SAFE_INTEGER` → `bigint`
* str → `string`
* bin → `Buffer`
* array / map → Array / Object
* ext → throws

So `unpack(pack(1n))` is Number `1`, and `unpack(pack(18446464814936021036n))`
is that same `bigint`.

`unpack.bytes_remaining` is the number of unused trailing bytes after the last
successful (or attempted) unpack. Stream uses that to splice leftover data.

### Pack type hints (3.1)

`pack(value, options)` takes an optional last-argument options object when
there are exactly two arguments and that object own-enumerates only `type`,
`family`, and/or `interpret`. Extra keys, a one-argument `{ type: ... }`
value, and `pack(1, 2)` still pack as values / an array.

```javascript
msgpack.pack(123, { type: 'fixint' }); // 0x7b
msgpack.pack(123, { type: 'uint8' }); // 0xcc 0x7b
msgpack.pack(Math.PI, { type: 'float32' }); // 0xca + 4 bytes
msgpack.pack(buf, { family: 'bin' });
msgpack.pack(1.5, { family: 'int' }); // throws
msgpack.pack(500, { type: 'uint8' }); // throws

msgpack.pack(
[
{ data: Math.PI, type: 'float64' },
{ data: 3.14, type: 'float32' },
],
{
interpret(item) {
return { data: item.data, type: item.type };
},
},
);
```

`type` forces that MessagePack type (`fixint`, `uint8`…`uint64`, `int8`…
`int64`, `float32`/`float64`, `fixstr`/`str8`/`str16`/`str32`, `bin8`/`bin16`/
`bin32`, `nil`/`true`/`false`). `family` (`int`, `float`, `str`, `bin`) picks
a compact encoding in that family. If both are set, `type` wins. Out-of-range
values throw `cannot pack value as <type>`.

`interpret` is used when packing an Array. Each element is passed to
`interpret(item)`, which must return `{ data }` and may also set `type` /
`family` for that element. Nested `interpret` on the returned object is
ignored.

Default packing is unchanged when no recognized options object is passed.

### Limits

* array/map length ≤ 1,000,000
Expand Down
57 changes: 56 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,76 @@
// Type definitions for msgpack 2.0.0
// Type definitions for msgpack 3.1.0
// Project: https://github.com/msgpack/msgpack-node

/// <reference types="node" />

import { EventEmitter } from 'events';

export type PackType =
| 'fixint'
| 'uint8'
| 'uint16'
| 'uint32'
| 'uint64'
| 'int8'
| 'int16'
| 'int32'
| 'int64'
| 'float32'
| 'float64'
| 'fixstr'
| 'str8'
| 'str16'
| 'str32'
| 'bin8'
| 'bin16'
| 'bin32'
| 'nil'
| 'true'
| 'false';

export type PackFamily = 'int' | 'float' | 'str' | 'bin';

export interface PackInterpretResult {
data: any;
type?: PackType;
family?: PackFamily;
}

export interface PackOptions {
type?: PackType;
family?: PackFamily;
interpret?: (item: any) => PackInterpretResult;
}

/**
* Serialize values to MessagePack.
*
* A single argument is packed as itself; two or more are packed as an array
* of that many elements.
*
* When the second argument own-enumerates only `type`, `family`, and/or
* `interpret`, it is pack options rather than a second value. `type` forces
* a MessagePack wire type; `family` picks a compact encoding in that family
* (`type` wins if both are set). `interpret` is used when packing an Array:
* each element is replaced by `interpret(item)`, which must return `{ data }`
* 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.
*/
export function pack(value: any, options: PackOptions): Buffer;
export function pack(...values: any[]): Buffer;

/**
* Deserialize the first MessagePack value in `buf`.
*
* 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).
*
* Returns `null` when the buffer holds an incomplete value, in which case
* `unpack.bytes_remaining` equals `buf.length`. Throws on malformed input or
* when a container/string/bin header exceeds the decoder's limits.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions 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": "2.0.0",
"version": "3.1.0",
"homepage": "https://github.com/msgpack/msgpack-node",
"author": "Peter Griess <pg@std.in>",
"contributors": [
Expand Down Expand Up @@ -34,7 +34,7 @@
"nan": "^2.23.1"
},
"scripts": {
"test": "node --test test/cli.test.js test/coverage-native.test.js test/msgpack.test.js test/regression.test.js test/security.test.js test/worker.test.js",
"test": "node --test test/bigint.test.js test/cli.test.js test/coverage-native.test.js test/msgpack.test.js test/pack-hints.test.js test/regression.test.js test/security.test.js test/worker.test.js",
"bench": "node test/benchmark/benchmark.js",
"rebuild": "node-gyp rebuild",
"coverage": "npm run coverage:js && npm run coverage:native",
Expand Down
Loading
Loading