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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@msgpack/msgpack",
"name": "@gathertown/msgpack",
"version": "3.1.3",
"description": "MessagePack for ECMA-262/JavaScript/TypeScript",
"author": "The MessagePack community",
Expand Down
14 changes: 13 additions & 1 deletion src/Encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ export type EncoderOptions<ContextType = undefined> = Partial<
*/
ignoreUndefined: boolean;

/**
* If `true`, undefineds are not handled by the library and are instead
* made available to extension codecs
*
* Defaults to `false`
*/
allowUndefinedCustomEncoding: boolean;

/**
* If `true`, integer numbers are encoded as floating point numbers,
* with the `forceFloat32` option taken into account.
Expand All @@ -81,6 +89,7 @@ export class Encoder<ContextType = undefined> {
private readonly sortKeys: boolean;
private readonly forceFloat32: boolean;
private readonly ignoreUndefined: boolean;
private readonly allowUndefinedCustomEncoding: boolean;
private readonly forceIntegerToFloat: boolean;

private pos: number;
Expand All @@ -99,6 +108,7 @@ export class Encoder<ContextType = undefined> {
this.sortKeys = options?.sortKeys ?? false;
this.forceFloat32 = options?.forceFloat32 ?? false;
this.ignoreUndefined = options?.ignoreUndefined ?? false;
this.allowUndefinedCustomEncoding = options?.allowUndefinedCustomEncoding ?? false;
this.forceIntegerToFloat = options?.forceIntegerToFloat ?? false;

this.pos = 0;
Expand All @@ -119,6 +129,7 @@ export class Encoder<ContextType = undefined> {
sortKeys: this.sortKeys,
forceFloat32: this.forceFloat32,
ignoreUndefined: this.ignoreUndefined,
allowUndefinedCustomEncoding: this.allowUndefinedCustomEncoding,
forceIntegerToFloat: this.forceIntegerToFloat,
} as any);
}
Expand Down Expand Up @@ -174,7 +185,8 @@ export class Encoder<ContextType = undefined> {
throw new Error(`Too deep objects in depth ${depth}`);
}

if (object == null) {
const objectIsNil = this.allowUndefinedCustomEncoding ? object === null : object == null;
if (objectIsNil) {
this.encodeNil();
} else if (typeof object === "boolean") {
this.encodeBoolean(object);
Expand Down
74 changes: 73 additions & 1 deletion test/ExtensionCodec.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from "assert";
import util from "util";
import { encode, decode, ExtensionCodec, decodeAsync } from "../src/index.ts";
import { encode, decode, Encoder, ExtensionCodec, decodeAsync } from "../src/index.ts";

describe("ExtensionCodec", () => {
context("timestamp", () => {
Expand Down Expand Up @@ -202,6 +202,78 @@ describe("ExtensionCodec", () => {
});
});

context("allowUndefinedCustomEncoding", () => {
const extensionCodec = new ExtensionCodec();

extensionCodec.register({
type: 0x1,
encode: (object: unknown): Uint8Array | null => {
if (object === undefined) {
return new Uint8Array(0);
}
return null;
},
decode: (data: Uint8Array) => {
if (data.length === 0) {
return undefined;
}
throw new Error("invalid data");
},
});

it("encodes and decodes undefined (synchronously)", () => {
const encoded = encode([undefined], { extensionCodec, allowUndefinedCustomEncoding: true });
assert.deepStrictEqual(decode(encoded, { extensionCodec }), [undefined]);
});
});

context("allowUndefinedCustomEncoding with clone() propagation (reentrancy)", () => {
// Box is a wrapper type whose extension codec calls encoder.encode() recursively,
// forcing the encoder's reentrancy guard to invoke clone().
class Box {
constructor(public readonly value: unknown) {}
}

const extensionCodec = new ExtensionCodec();

// Undefined handler (type 0x1)
extensionCodec.register({
type: 0x1,
encode: (object: unknown): Uint8Array | null => {
if (object === undefined) {
return new Uint8Array(0);
}
return null;
},
decode: (_data: Uint8Array) => undefined,
});

const encoder = new Encoder({ extensionCodec, allowUndefinedCustomEncoding: true });

// Box handler (type 0x2): calls encoder.encode() recursively to trigger clone()
extensionCodec.register({
type: 0x2,
encode: (object: unknown): Uint8Array | null => {
if (object instanceof Box) {
return encoder.encode(object.value);
}
return null;
},
decode: (data: Uint8Array) => new Box(decode(data, { extensionCodec })),
});

it("propagates allowUndefinedCustomEncoding through clone()", () => {
// Encoding Box(undefined):
// outer encode() handles Box via type 0x2, which calls encoder.encode(undefined)
// encoder is already entered, so clone() fires — the clone must carry
// allowUndefinedCustomEncoding or undefined would become nil instead of
// reaching the type 0x1 codec.
const encoded = encoder.encode(new Box(undefined));
const decoded = decode(encoded, { extensionCodec }) as Box;
assert.strictEqual(decoded.value, undefined);
});
});

context("custom extensions with alignment", () => {
const extensionCodec = new ExtensionCodec();

Expand Down
Loading