Skip to content

Commit 1007830

Browse files
Expanded BigInt support (#2)
Co-authored-by: Jason Paulos <[email protected]>
1 parent da6d10a commit 1007830

19 files changed

+1531
-240
lines changed

.vscode/extensions.json

+4-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
{
2-
// List of extensions which should be recommended for users of this workspace.
3-
"recommendations": [
4-
"dbaeumer.vscode-eslint",
5-
"yzhang.markdown-all-in-one"
6-
],
7-
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
8-
"unwantedRecommendations": [
9-
]
2+
// List of extensions which should be recommended for users of this workspace.
3+
"recommendations": ["dbaeumer.vscode-eslint", "yzhang.markdown-all-in-one", "hbenl.vscode-mocha-test-adapter"],
4+
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
5+
"unwantedRecommendations": []
106
}

.vscode/settings.json

+7-7
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
"typescript.tsdk": "node_modules/typescript/lib",
33
"files.eol": "\n",
44
"editor.tabSize": 2,
5+
"editor.defaultFormatter": "esbenp.prettier-vscode",
6+
"editor.formatOnSave": true,
57
"editor.codeActionsOnSave": {
6-
"source.fixAll.eslint": true
8+
"source.fixAll.eslint": true,
9+
"source.fixAll": "always"
710
},
8-
"cSpell.words": [
9-
"instanceof",
10-
"tsdoc",
11-
"typeof",
12-
"whatwg"
13-
]
11+
"cSpell.words": ["instanceof", "tsdoc", "typeof", "whatwg"],
12+
"mochaExplorer.files": "test/**/*.test.{ts,js}",
13+
"mochaExplorer.require": ["ts-node/register", "tsconfig-paths/register"]
1414
}

README.md

+126-104
Large diffs are not rendered by default.

package-lock.json

+3-19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@
7474
"karma-webpack": "latest",
7575
"lodash": "latest",
7676
"mocha": "latest",
77-
"msgpack-test-js": "latest",
77+
"msg-ext": "^1.0.1",
78+
"msg-int64": "^0.1.1",
79+
"msg-timestamp": "^1.0.1",
7880
"prettier": "latest",
7981
"rimraf": "latest",
8082
"ts-loader": "latest",

src/Decoder.ts

+29-38
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { prettyByte } from "./utils/prettyByte";
22
import { ExtensionCodec, ExtensionCodecType } from "./ExtensionCodec";
3-
import { getInt64, getUint64, UINT32_MAX } from "./utils/int";
3+
import { IntMode, getInt64, getUint64, convertSafeIntegerToMode, UINT32_MAX } from "./utils/int";
44
import { utf8Decode } from "./utils/utf8";
55
import { createDataView, ensureUint8Array } from "./utils/typedArrays";
66
import { CachedKeyDecoder, KeyDecoder } from "./CachedKeyDecoder";
@@ -16,10 +16,17 @@ export type DecoderOptions<ContextType = undefined> = Readonly<
1616
* Depends on ES2020's {@link DataView#getBigInt64} and
1717
* {@link DataView#getBigUint64}.
1818
*
19-
* Defaults to false.
19+
* Defaults to false. If true, equivalent to intMode: IntMode.AS_ENCODED.
2020
*/
2121
useBigInt64: boolean;
2222

23+
/**
24+
* Allows for more fine-grained control of BigInt handling, overrides useBigInt64.
25+
*
26+
* Defaults to IntMode.AS_ENCODED if useBigInt64 is true or IntMode.UNSAFE_NUMBER otherwise.
27+
*/
28+
intMode?: IntMode;
29+
2330
/**
2431
* Maximum string length.
2532
*
@@ -194,7 +201,7 @@ const sharedCachedKeyDecoder = new CachedKeyDecoder();
194201
export class Decoder<ContextType = undefined> {
195202
private readonly extensionCodec: ExtensionCodecType<ContextType>;
196203
private readonly context: ContextType;
197-
private readonly useBigInt64: boolean;
204+
private readonly intMode: IntMode;
198205
private readonly maxStrLength: number;
199206
private readonly maxBinLength: number;
200207
private readonly maxArrayLength: number;
@@ -214,7 +221,7 @@ export class Decoder<ContextType = undefined> {
214221
this.extensionCodec = options?.extensionCodec ?? (ExtensionCodec.defaultCodec as ExtensionCodecType<ContextType>);
215222
this.context = (options as { context: ContextType } | undefined)?.context as ContextType; // needs a type assertion because EncoderOptions has no context property when ContextType is undefined
216223

217-
this.useBigInt64 = options?.useBigInt64 ?? false;
224+
this.intMode = options?.intMode ?? (options?.useBigInt64 ? IntMode.AS_ENCODED : IntMode.UNSAFE_NUMBER);
218225
this.maxStrLength = options?.maxStrLength ?? UINT32_MAX;
219226
this.maxBinLength = options?.maxBinLength ?? UINT32_MAX;
220227
this.maxArrayLength = options?.maxArrayLength ?? UINT32_MAX;
@@ -371,11 +378,11 @@ export class Decoder<ContextType = undefined> {
371378

372379
if (headByte >= 0xe0) {
373380
// negative fixint (111x xxxx) 0xe0 - 0xff
374-
object = headByte - 0x100;
381+
object = this.convertNumber(headByte - 0x100);
375382
} else if (headByte < 0xc0) {
376383
if (headByte < 0x80) {
377384
// positive fixint (0xxx xxxx) 0x00 - 0x7f
378-
object = headByte;
385+
object = this.convertNumber(headByte);
379386
} else if (headByte < 0x90) {
380387
// fixmap (1000 xxxx) 0x80 - 0x8f
381388
const size = headByte - 0x80;
@@ -418,36 +425,28 @@ export class Decoder<ContextType = undefined> {
418425
object = this.readF64();
419426
} else if (headByte === 0xcc) {
420427
// uint 8
421-
object = this.readU8();
428+
object = this.convertNumber(this.readU8());
422429
} else if (headByte === 0xcd) {
423430
// uint 16
424-
object = this.readU16();
431+
object = this.convertNumber(this.readU16());
425432
} else if (headByte === 0xce) {
426433
// uint 32
427-
object = this.readU32();
434+
object = this.convertNumber(this.readU32());
428435
} else if (headByte === 0xcf) {
429436
// uint 64
430-
if (this.useBigInt64) {
431-
object = this.readU64AsBigInt();
432-
} else {
433-
object = this.readU64();
434-
}
437+
object = this.readU64();
435438
} else if (headByte === 0xd0) {
436439
// int 8
437-
object = this.readI8();
440+
object = this.convertNumber(this.readI8());
438441
} else if (headByte === 0xd1) {
439442
// int 16
440-
object = this.readI16();
443+
object = this.convertNumber(this.readI16());
441444
} else if (headByte === 0xd2) {
442445
// int 32
443-
object = this.readI32();
446+
object = this.convertNumber(this.readI32());
444447
} else if (headByte === 0xd3) {
445448
// int 64
446-
if (this.useBigInt64) {
447-
object = this.readI64AsBigInt();
448-
} else {
449-
object = this.readI64();
450-
}
449+
object = this.readI64();
451450
} else if (headByte === 0xd9) {
452451
// str 8
453452
const byteLength = this.lookU8();
@@ -692,6 +691,10 @@ export class Decoder<ContextType = undefined> {
692691
return this.extensionCodec.decode(data, extType, this.context);
693692
}
694693

694+
private convertNumber(value: number): number | bigint {
695+
return convertSafeIntegerToMode(value, this.intMode);
696+
}
697+
695698
private lookU8() {
696699
return this.view.getUint8(this.pos);
697700
}
@@ -740,26 +743,14 @@ export class Decoder<ContextType = undefined> {
740743
return value;
741744
}
742745

743-
private readU64(): number {
744-
const value = getUint64(this.view, this.pos);
745-
this.pos += 8;
746-
return value;
747-
}
748-
749-
private readI64(): number {
750-
const value = getInt64(this.view, this.pos);
751-
this.pos += 8;
752-
return value;
753-
}
754-
755-
private readU64AsBigInt(): bigint {
756-
const value = this.view.getBigUint64(this.pos);
746+
private readU64(): number | bigint {
747+
const value = getUint64(this.view, this.pos, this.intMode);
757748
this.pos += 8;
758749
return value;
759750
}
760751

761-
private readI64AsBigInt(): bigint {
762-
const value = this.view.getBigInt64(this.pos);
752+
private readI64(): number | bigint {
753+
const value = getInt64(this.view, this.pos, this.intMode);
763754
this.pos += 8;
764755
return value;
765756
}

src/Encoder.ts

+40-19
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ export type EncoderOptions<ContextType = undefined> = Partial<
1313
extensionCodec: ExtensionCodecType<ContextType>;
1414

1515
/**
16-
* Encodes bigint as Int64 or Uint64 if it's set to true.
17-
* {@link forceIntegerToFloat} does not affect bigint.
16+
* Encodes bigint as Int64 or Uint64 if it's set to true, regardless of the size of bigint number.
17+
* {@link forceIntegerToFloat} does not affect bigint if this is enabled.
1818
* Depends on ES2020's {@link DataView#setBigInt64} and
1919
* {@link DataView#setBigUint64}.
2020
*
2121
* Defaults to false.
2222
*/
23-
useBigInt64: boolean;
23+
forceBigIntToInt64: boolean;
2424

2525
/**
2626
* The maximum depth in nested objects and arrays.
@@ -43,6 +43,7 @@ export type EncoderOptions<ContextType = undefined> = Partial<
4343
* Defaults to `false`. If enabled, it spends more time in encoding objects.
4444
*/
4545
sortKeys: boolean;
46+
4647
/**
4748
* If `true`, non-integer numbers are encoded in float32, not in float64 (the default).
4849
*
@@ -74,7 +75,7 @@ export type EncoderOptions<ContextType = undefined> = Partial<
7475
export class Encoder<ContextType = undefined> {
7576
private readonly extensionCodec: ExtensionCodecType<ContextType>;
7677
private readonly context: ContextType;
77-
private readonly useBigInt64: boolean;
78+
private readonly forceBigIntToInt64: boolean;
7879
private readonly maxDepth: number;
7980
private readonly initialBufferSize: number;
8081
private readonly sortKeys: boolean;
@@ -90,7 +91,7 @@ export class Encoder<ContextType = undefined> {
9091
this.extensionCodec = options?.extensionCodec ?? (ExtensionCodec.defaultCodec as ExtensionCodecType<ContextType>);
9192
this.context = (options as { context: ContextType } | undefined)?.context as ContextType; // needs a type assertion because EncoderOptions has no context property when ContextType is undefined
9293

93-
this.useBigInt64 = options?.useBigInt64 ?? false;
94+
this.forceBigIntToInt64 = options?.forceBigIntToInt64 ?? false;
9495
this.maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
9596
this.initialBufferSize = options?.initialBufferSize ?? DEFAULT_INITIAL_BUFFER_SIZE;
9697
this.sortKeys = options?.sortKeys ?? false;
@@ -137,15 +138,9 @@ export class Encoder<ContextType = undefined> {
137138
} else if (typeof object === "boolean") {
138139
this.encodeBoolean(object);
139140
} else if (typeof object === "number") {
140-
if (!this.forceIntegerToFloat) {
141-
this.encodeNumber(object);
142-
} else {
143-
this.encodeNumberAsFloat(object);
144-
}
141+
this.encodeNumber(object);
145142
} else if (typeof object === "string") {
146143
this.encodeString(object);
147-
} else if (this.useBigInt64 && typeof object === "bigint") {
148-
this.encodeBigInt64(object);
149144
} else {
150145
this.encodeObject(object, depth);
151146
}
@@ -200,12 +195,10 @@ export class Encoder<ContextType = undefined> {
200195
// uint 32
201196
this.writeU8(0xce);
202197
this.writeU32(object);
203-
} else if (!this.useBigInt64) {
198+
} else {
204199
// uint 64
205200
this.writeU8(0xcf);
206201
this.writeU64(object);
207-
} else {
208-
this.encodeNumberAsFloat(object);
209202
}
210203
} else {
211204
if (object >= -0x20) {
@@ -223,12 +216,10 @@ export class Encoder<ContextType = undefined> {
223216
// int 32
224217
this.writeU8(0xd2);
225218
this.writeI32(object);
226-
} else if (!this.useBigInt64) {
219+
} else {
227220
// int 64
228221
this.writeU8(0xd3);
229222
this.writeI64(object);
230-
} else {
231-
this.encodeNumberAsFloat(object);
232223
}
233224
}
234225
} else {
@@ -248,7 +239,33 @@ export class Encoder<ContextType = undefined> {
248239
}
249240
}
250241

251-
private encodeBigInt64(object: bigint): void {
242+
private encodeBigInt(object: bigint) {
243+
if (this.forceBigIntToInt64) {
244+
this.encodeBigIntAsInt64(object);
245+
} else if (object >= 0) {
246+
if (object < 0x100000000 || this.forceIntegerToFloat) {
247+
// uint 32 or lower, or force to float
248+
this.encodeNumber(Number(object));
249+
} else if (object < BigInt("0x10000000000000000")) {
250+
// uint 64
251+
this.encodeBigIntAsInt64(object);
252+
} else {
253+
throw new Error(`Bigint is too large for uint64: ${object}`);
254+
}
255+
} else {
256+
if (object >= -0x80000000 || this.forceIntegerToFloat) {
257+
// int 32 or lower, or force to float
258+
this.encodeNumber(Number(object));
259+
} else if (object >= BigInt(-1) * BigInt("0x8000000000000000")) {
260+
// int 64
261+
this.encodeBigIntAsInt64(object);
262+
} else {
263+
throw new Error(`Bigint is too small for int64: ${object}`);
264+
}
265+
}
266+
}
267+
268+
private encodeBigIntAsInt64(object: bigint): void {
252269
if (object >= BigInt(0)) {
253270
// uint 64
254271
this.writeU8(0xcf);
@@ -300,6 +317,10 @@ export class Encoder<ContextType = undefined> {
300317
this.encodeArray(object, depth);
301318
} else if (ArrayBuffer.isView(object)) {
302319
this.encodeBinary(object);
320+
} else if (typeof object === "bigint") {
321+
// this is here instead of in doEncode so that we can try encoding with an extension first,
322+
// otherwise we would break existing extensions for bigints
323+
this.encodeBigInt(object);
303324
} else if (typeof object === "object") {
304325
this.encodeMap(object as Record<string, unknown>, depth);
305326
} else {

src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { decode, decodeMulti } from "./decode";
99
export { decode, decodeMulti };
1010
import type { DecodeOptions } from "./decode";
1111
export type { DecodeOptions };
12+
import { IntMode } from './utils/int';
13+
export { IntMode };
1214

1315
import { decodeAsync, decodeArrayStream, decodeMultiStream, decodeStream } from "./decodeAsync";
1416
export { decodeAsync, decodeArrayStream, decodeMultiStream, decodeStream };

src/timestamp.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type
22
import { DecodeError } from "./DecodeError";
3-
import { getInt64, setInt64 } from "./utils/int";
3+
import { IntMode, getInt64, setInt64 } from "./utils/int";
44

55
export const EXT_TIMESTAMP = -1;
66

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

90-
const sec = getInt64(view, 4);
90+
const sec = getInt64(view, 4, IntMode.UNSAFE_NUMBER);
9191
const nsec = view.getUint32(0);
9292
return { sec, nsec };
9393
}

0 commit comments

Comments
 (0)