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
23 changes: 22 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ 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
Expand Down Expand Up @@ -88,6 +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/v3.0.0...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
21 changes: 14 additions & 7 deletions COVERAGE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Coverage — msgpack 3.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 @@ -139,6 +139,13 @@ gcovr --root . --filter src/ --exclude deps/ --no-markers --txt-metric branch --
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
47 changes: 44 additions & 3 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 3.0 requires **Node.js 18+**, vendors **msgpack-c c-7.0.2**, and
unpacks 64-bit integers outside `Number.MAX_SAFE_INTEGER` as `bigint`. 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 Down Expand Up @@ -79,6 +79,47 @@ 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
50 changes: 48 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,67 @@
// Type definitions for msgpack 3.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.
* 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;

/**
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": "3.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/bigint.test.js 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
19 changes: 19 additions & 0 deletions src/msgpack.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>

#include <nan.h>
#include <msgpack.h>
Expand Down Expand Up @@ -425,6 +427,18 @@ static v8::Local<v8::Array> CheckedOwnNames(v8::Local<v8::Object> obj) {
return r.ToLocalChecked();
}

static v8::Local<v8::Value> CallOneArg(v8::Local<v8::Object> recv,
v8::Local<v8::Function> fn,
v8::Local<v8::Value> arg) {
Nan::TryCatch try_catch;
v8::Local<v8::Value> argv[1] = {arg};
Nan::MaybeLocal<v8::Value> r = Nan::Call(fn, recv, 1, argv);
if (r.IsEmpty()) {
ThrowCaught(try_catch); /* GCOVR_EXCL_BR_LINE: never returns */
}
return r.ToLocalChecked();
}

static void PackArray(msgpack_packer* pk, v8::Local<v8::Array> arr, int depth) {
if (IsMarked(arr)) {
throw MsgpackException(Error("Cowardly refusing to pack circular reference"));
Expand Down Expand Up @@ -589,6 +603,8 @@ static void JsToMsgpack(msgpack_packer* pk, v8::Local<v8::Value> o, int depth) {
/* GCOVR_EXCL_BR_STOP */
}

#include "pack_hints.inc"

static v8::Local<v8::Value> MsgpackToJs(const msgpack_object* mo);

static v8::Local<v8::Value> MsgpackToJs(const msgpack_object* mo) {
Expand Down Expand Up @@ -755,6 +771,9 @@ NAN_METHOD(Pack) {

if (info.Length() == 1) {
JsToMsgpack(&pk, info[0], 0);
} else if (info.Length() == 2 && IsPackOptionsObject(info[1])) {
PackHint hint = ParsePackOptions(info[1].As<v8::Object>());
JsToMsgpackHinted(&pk, info[0], 0, hint);
} else {
/* GCOVR_EXCL_BR_START: allocation failure only. */
if (msgpack_pack_array(&pk, info.Length())) {
Expand Down
Loading
Loading