Skip to content
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

Add BigInt support #211

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
63 changes: 22 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ deepStrictEqual(decode(encoded), object);
- [`EncodeOptions`](#encodeoptions)
- [`decode(buffer: ArrayLike<number> | BufferSource, options?: DecodeOptions): unknown`](#decodebuffer-arraylikenumber--buffersource-options-decodeoptions-unknown)
- [`DecodeOptions`](#decodeoptions)
- [`IntMode`](#intmode)
- [`decodeMulti(buffer: ArrayLike<number> | BufferSource, options?: DecodeOptions): Generator<unknown, void, unknown>`](#decodemultibuffer-arraylikenumber--buffersource-options-decodeoptions-generatorunknown-void-unknown)
- [`decodeAsync(stream: ReadableStreamLike<ArrayLike<number> | BufferSource>, options?: DecodeAsyncOptions): Promise<unknown>`](#decodeasyncstream-readablestreamlikearraylikenumber--buffersource-options-decodeasyncoptions-promiseunknown)
- [`decodeArrayStream(stream: ReadableStreamLike<ArrayLike<number> | BufferSource>, options?: DecodeAsyncOptions): AsyncIterable<unknown>`](#decodearraystreamstream-readablestreamlikearraylikenumber--buffersource-options-decodeasyncoptions-asynciterableunknown)
- [`decodeMultiStream(stream: ReadableStreamLike<ArrayLike<number> | BufferSource>, options?: DecodeAsyncOptions): AsyncIterable<unknown>`](#decodemultistreamstream-readablestreamlikearraylikenumber--buffersource-options-decodeasyncoptions-asynciterableunknown)
- [Reusing Encoder and Decoder instances](#reusing-encoder-and-decoder-instances)
- [Extension Types](#extension-types)
- [ExtensionCodec context](#extensioncodec-context)
- [Handling BigInt with ExtensionCodec](#handling-bigint-with-extensioncodec)
- [The temporal module as timestamp extensions](#the-temporal-module-as-timestamp-extensions)
- [Decoding a Blob](#decoding-a-blob)
- [MessagePack Specification](#messagepack-specification)
Expand Down Expand Up @@ -148,10 +148,22 @@ maxBinLength | number | `4_294_967_295` (UINT32_MAX)
maxArrayLength | number | `4_294_967_295` (UINT32_MAX)
maxMapLength | number | `4_294_967_295` (UINT32_MAX)
maxExtLength | number | `4_294_967_295` (UINT32_MAX)
intMode | `IntMode` | `IntMode.UNSAFE_NUMBER`
context | user-defined | -

You can use `max${Type}Length` to limit the length of each type decoded.

`intMode` determines whether decoded integers should be returned as numbers or bigints. The possible values are described below.

##### `IntMode`

The `IntMode` enum defines different options for decoding integers. They are described below:

- `IntMode.UNSAFE_NUMBER`: Always returns the value as a number. Be aware that there will be a loss of precision if the value is outside the range of `Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`.
- `IntMode.SAFE_NUMBER`: Always returns the value as a number, but throws an error if the value is outside of the range of `Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`.
- `IntMode.MIXED`: Returns all values inside the range of `Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER` as numbers and all values outside that range as bigints.
- `IntMode.BIGINT`: Always returns the value as a bigint, even if it is small enough to safely fit in a number.

### `decodeMulti(buffer: ArrayLike<number> | BufferSource, options?: DecodeOptions): Generator<unknown, void, unknown>`

It decodes `buffer` that includes multiple MessagePack-encoded objects, and returns decoded objects as a generator. See also `decodeMultiStream()`, which is an asynchronous variant of this function.
Expand Down Expand Up @@ -346,39 +358,6 @@ const encoded = = encode({myType: new MyType<any>()}, { extensionCodec, context
const decoded = decode(encoded, { extensionCodec, context });
```

#### Handling BigInt with ExtensionCodec

This library does not handle BigInt by default, but you can handle it with `ExtensionCodec` like this:

```typescript
import { deepStrictEqual } from "assert";
import { encode, decode, ExtensionCodec } from "@msgpack/msgpack";

const BIGINT_EXT_TYPE = 0; // Any in 0-127
const extensionCodec = new ExtensionCodec();
extensionCodec.register({
type: BIGINT_EXT_TYPE,
encode: (input: unknown) => {
if (typeof input === "bigint") {
if (input <= Number.MAX_SAFE_INTEGER && input >= Number.MIN_SAFE_INTEGER) {
return encode(parseInt(input.toString(), 10));
} else {
return encode(input.toString());
}
} else {
return null;
}
},
decode: (data: Uint8Array) => {
return BigInt(decode(data));
},
});

const value = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1);
const encoded: = encode(value, { extensionCodec });
deepStrictEqual(decode(encoded, { extensionCodec }), value);
```

#### The temporal module as timestamp extensions

There is a proposal for a new date/time representations in JavaScript:
Expand Down Expand Up @@ -468,18 +447,20 @@ Source Value|MessagePack Format|Value Decoded
----|----|----
null, undefined|nil|null (*1)
boolean (true, false)|bool family|boolean (true, false)
number (53-bit int)|int family|number (53-bit int)
number (53-bit int)|int family|number or bigint (*2)
number (64-bit float)|float family|number (64-bit float)
bigint|int family|number or bigint (*2)
string|str family|string
ArrayBufferView |bin family|Uint8Array (*2)
ArrayBufferView |bin family|Uint8Array (*3)
Array|array family|Array
Object|map family|Object (*3)
Date|timestamp ext family|Date (*4)
Object|map family|Object (*4)
Date|timestamp ext family|Date (*5)

* *1 Both `null` and `undefined` are mapped to `nil` (`0xC0`) type, and are decoded into `null`
* *2 Any `ArrayBufferView`s including NodeJS's `Buffer` are mapped to `bin` family, and are decoded into `Uint8Array`
* *3 In handling `Object`, it is regarded as `Record<string, unknown>` in terms of TypeScript
* *4 MessagePack timestamps may have nanoseconds, which will lost when it is decoded into JavaScript `Date`. This behavior can be overridden by registering `-1` for the extension codec.
* *2 MessagePack ints are decoded as either numbers or bigints depending on the [IntMode](#intmode) used during decoding.
* *3 Any `ArrayBufferView`s including NodeJS's `Buffer` are mapped to `bin` family, and are decoded into `Uint8Array`
* *4 In handling `Object`, it is regarded as `Record<string, unknown>` in terms of TypeScript
* *5 MessagePack timestamps may have nanoseconds, which will lost when it is decoded into JavaScript `Date`. This behavior can be overridden by registering `-1` for the extension codec.

## Prerequisites

Expand Down
27 changes: 16 additions & 11 deletions src/Decoder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { prettyByte } from "./utils/prettyByte";
import { ExtensionCodec, ExtensionCodecType } from "./ExtensionCodec";
import { getInt64, getUint64, UINT32_MAX } from "./utils/int";
import { IntMode, getInt64, getUint64, convertSafeIntegerToMode, UINT32_MAX } from "./utils/int";
import { utf8DecodeJs, TEXT_DECODER_THRESHOLD, utf8DecodeTD } from "./utils/utf8";
import { createDataView, ensureUint8Array } from "./utils/typedArrays";
import { CachedKeyDecoder, KeyDecoder } from "./CachedKeyDecoder";
Expand Down Expand Up @@ -74,6 +74,7 @@ export class Decoder<ContextType = undefined> {
private readonly maxArrayLength = UINT32_MAX,
private readonly maxMapLength = UINT32_MAX,
private readonly maxExtLength = UINT32_MAX,
private readonly intMode = IntMode.UNSAFE_NUMBER,
private readonly keyDecoder: KeyDecoder | null = sharedCachedKeyDecoder,
) {}

Expand Down Expand Up @@ -272,25 +273,25 @@ export class Decoder<ContextType = undefined> {
object = this.readF64();
} else if (headByte === 0xcc) {
// uint 8
object = this.readU8();
object = this.convertNumber(this.readU8());
} else if (headByte === 0xcd) {
// uint 16
object = this.readU16();
object = this.convertNumber(this.readU16());
} else if (headByte === 0xce) {
// uint 32
object = this.readU32();
object = this.convertNumber(this.readU32());
} else if (headByte === 0xcf) {
// uint 64
object = this.readU64();
} else if (headByte === 0xd0) {
// int 8
object = this.readI8();
object = this.convertNumber(this.readI8());
} else if (headByte === 0xd1) {
// int 16
object = this.readI16();
object = this.convertNumber(this.readI16());
} else if (headByte === 0xd2) {
// int 32
object = this.readI32();
object = this.convertNumber(this.readI32());
} else if (headByte === 0xd3) {
// int 64
object = this.readI64();
Expand Down Expand Up @@ -551,6 +552,10 @@ export class Decoder<ContextType = undefined> {
return this.extensionCodec.decode(data, extType, this.context);
}

private convertNumber(value: number): number | bigint {
return convertSafeIntegerToMode(value, this.intMode);
}

private lookU8() {
return this.view.getUint8(this.pos);
}
Expand Down Expand Up @@ -599,14 +604,14 @@ export class Decoder<ContextType = undefined> {
return value;
}

private readU64(): number {
const value = getUint64(this.view, this.pos);
private readU64(): number | bigint {
const value = getUint64(this.view, this.pos, this.intMode);
this.pos += 8;
return value;
}

private readI64(): number {
const value = getInt64(this.view, this.pos);
private readI64(): number | bigint {
const value = getInt64(this.view, this.pos, this.intMode);
this.pos += 8;
return value;
}
Expand Down
44 changes: 44 additions & 0 deletions src/Encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,32 @@ export class Encoder<ContextType = undefined> {
}
}

private encodeBigint(object: bigint) {
if (object >= 0) {
if (object < 0x100000000 || this.forceIntegerToFloat) {
// uint 32 or lower, or force to float
this.encodeNumber(Number(object))
} else if (object < BigInt("0x10000000000000000")) {
// uint 64
this.writeU8(0xcf);
this.writeUBig(object);
} else {
throw new Error(`Bigint is too large for uint64: ${object}`);
}
} else {
if (object >= -0x80000000 || this.forceIntegerToFloat) {
// int 32 or lower, or force to float
this.encodeNumber(Number(object));
} else if (object >= BigInt(-1) * BigInt("0x8000000000000000")) {
// int 64
this.writeU8(0xd3);
this.writeIBig(object);
} else {
throw new Error(`Bigint is too small for int64: ${object}`);
}
}
}

private writeStringHeader(byteLength: number) {
if (byteLength < 32) {
// fixstr
Expand Down Expand Up @@ -199,6 +225,10 @@ export class Encoder<ContextType = undefined> {
const ext = this.extensionCodec.tryToEncode(object, this.context);
if (ext != null) {
this.encodeExtension(ext);
} else if (typeof object === "bigint") {
// this is here instead of in doEncode so that we can try encoding with an extension first,
// otherwise we would break existing extensions for bigints
this.encodeBigint(object);
} else if (Array.isArray(object)) {
this.encodeArray(object, depth);
} else if (ArrayBuffer.isView(object)) {
Expand Down Expand Up @@ -409,4 +439,18 @@ export class Encoder<ContextType = undefined> {
setInt64(this.view, this.pos, value);
this.pos += 8;
}

private writeIBig(value: bigint) {
this.ensureBufferSizeToWrite(8);

this.view.setBigInt64(this.pos, value);
this.pos += 8;
}

private writeUBig(value: bigint) {
this.ensureBufferSizeToWrite(8);

this.view.setBigUint64(this.pos, value);
this.pos += 8;
}
}
10 changes: 10 additions & 0 deletions src/decode.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Decoder } from "./Decoder";
import type { IntMode } from "./utils/int";
import type { ExtensionCodecType } from "./ExtensionCodec";
import type { ContextOf, SplitUndefined } from "./context";

Expand Down Expand Up @@ -36,6 +37,13 @@ export type DecodeOptions<ContextType = undefined> = Readonly<
* Defaults to 4_294_967_295 (UINT32_MAX).
*/
maxExtLength: number;
/**
* Determines whether decoded integers should be returned as numbers or bigints.
*
* Defaults to IntMode.UNSAFE_NUMBER, which always returns the value as a number, even when it
* is outside the range of Number.MIN_SAFE_INTEGER to Number.MAX_SAFE_INTEGER.
*/
intMode: IntMode;
}>
> &
ContextOf<ContextType>;
Expand Down Expand Up @@ -63,6 +71,7 @@ export function decode<ContextType = undefined>(
options.maxArrayLength,
options.maxMapLength,
options.maxExtLength,
options.intMode,
);
return decoder.decode(buffer);
}
Expand All @@ -86,6 +95,7 @@ export function decodeMulti<ContextType = undefined>(
options.maxArrayLength,
options.maxMapLength,
options.maxExtLength,
options.intMode,
);
return decoder.decodeMulti(buffer);
}
3 changes: 3 additions & 0 deletions src/decodeAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { SplitUndefined } from "./context";
options.maxArrayLength,
options.maxMapLength,
options.maxExtLength,
options.intMode,
);
return decoder.decodeAsync(stream);
}
Expand All @@ -45,6 +46,7 @@ import type { SplitUndefined } from "./context";
options.maxArrayLength,
options.maxMapLength,
options.maxExtLength,
options.intMode,
);

return decoder.decodeArrayStream(stream);
Expand All @@ -68,6 +70,7 @@ export function decodeMultiStream<ContextType>(
options.maxArrayLength,
options.maxMapLength,
options.maxExtLength,
options.intMode,
);

return decoder.decodeStream(stream);
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { decode, decodeMulti } from "./decode";
export { decode, decodeMulti };
import type { DecodeOptions } from "./decode";
export { DecodeOptions };
import { IntMode } from './utils/int';
export { IntMode };

import { decodeAsync, decodeArrayStream, decodeMultiStream, decodeStream } from "./decodeAsync";
export { decodeAsync, decodeArrayStream, decodeMultiStream, decodeStream };
Expand Down
4 changes: 2 additions & 2 deletions src/timestamp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type
import { DecodeError } from "./DecodeError";
import { getInt64, setInt64 } from "./utils/int";
import { IntMode, getInt64, setInt64 } from "./utils/int";

export const EXT_TIMESTAMP = -1;

Expand Down Expand Up @@ -87,7 +87,7 @@ export function decodeTimestampToTimeSpec(data: Uint8Array): TimeSpec {
case 12: {
// timestamp 96 = { nsec32 (unsigned), sec64 (signed) }

const sec = getInt64(view, 4);
const sec = getInt64(view, 4, IntMode.UNSAFE_NUMBER);
const nsec = view.getUint32(0);
return { sec, nsec };
}
Expand Down
Loading