Skip to content

Commit ec05e2e

Browse files
authored
Merge pull request #223 from msgpack/gfx/bigint
optional BigInt support: map BigInt to int64/uint64 when `useBigInt64` is set to true
2 parents daae51c + df46c60 commit ec05e2e

File tree

7 files changed

+147
-17
lines changed

7 files changed

+147
-17
lines changed

src/Decoder.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export class Decoder<ContextType = undefined> {
6565
public constructor(
6666
private readonly extensionCodec: ExtensionCodecType<ContextType> = ExtensionCodec.defaultCodec as any,
6767
private readonly context: ContextType = undefined as any,
68+
private readonly useBigInt64 = false,
6869
private readonly maxStrLength = UINT32_MAX,
6970
private readonly maxBinLength = UINT32_MAX,
7071
private readonly maxArrayLength = UINT32_MAX,
@@ -277,7 +278,11 @@ export class Decoder<ContextType = undefined> {
277278
object = this.readU32();
278279
} else if (headByte === 0xcf) {
279280
// uint 64
280-
object = this.readU64();
281+
if (this.useBigInt64) {
282+
object = this.readU64AsBigInt();
283+
} else {
284+
object = this.readU64();
285+
}
281286
} else if (headByte === 0xd0) {
282287
// int 8
283288
object = this.readI8();
@@ -289,7 +294,11 @@ export class Decoder<ContextType = undefined> {
289294
object = this.readI32();
290295
} else if (headByte === 0xd3) {
291296
// int 64
292-
object = this.readI64();
297+
if (this.useBigInt64) {
298+
object = this.readI64AsBigInt();
299+
} else {
300+
object = this.readI64();
301+
}
293302
} else if (headByte === 0xd9) {
294303
// str 8
295304
const byteLength = this.lookU8();
@@ -605,6 +614,18 @@ export class Decoder<ContextType = undefined> {
605614
return value;
606615
}
607616

617+
private readU64AsBigInt(): bigint {
618+
const value = this.view.getBigUint64(this.pos);
619+
this.pos += 8;
620+
return value;
621+
}
622+
623+
private readI64AsBigInt(): bigint {
624+
const value = this.view.getBigInt64(this.pos);
625+
this.pos += 8;
626+
return value;
627+
}
628+
608629
private readF32() {
609630
const value = this.view.getFloat32(this.pos);
610631
this.pos += 4;

src/Encoder.ts

+58-15
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export class Encoder<ContextType = undefined> {
1515
public constructor(
1616
private readonly extensionCodec: ExtensionCodecType<ContextType> = ExtensionCodec.defaultCodec as any,
1717
private readonly context: ContextType = undefined as any,
18+
private readonly useBigInt64 = false,
1819
private readonly maxDepth = DEFAULT_MAX_DEPTH,
1920
private readonly initialBufferSize = DEFAULT_INITIAL_BUFFER_SIZE,
2021
private readonly sortKeys = false,
@@ -57,9 +58,15 @@ export class Encoder<ContextType = undefined> {
5758
} else if (typeof object === "boolean") {
5859
this.encodeBoolean(object);
5960
} else if (typeof object === "number") {
60-
this.encodeNumber(object);
61+
if (!this.forceIntegerToFloat) {
62+
this.encodeNumber(object);
63+
} else {
64+
this.encodeNumberAsFloat(object);
65+
}
6166
} else if (typeof object === "string") {
6267
this.encodeString(object);
68+
} else if (this.useBigInt64 && typeof object === "bigint") {
69+
this.encodeBigInt64(object);
6370
} else {
6471
this.encodeObject(object, depth);
6572
}
@@ -95,8 +102,9 @@ export class Encoder<ContextType = undefined> {
95102
this.writeU8(0xc3);
96103
}
97104
}
98-
private encodeNumber(object: number) {
99-
if (Number.isSafeInteger(object) && !this.forceIntegerToFloat) {
105+
106+
private encodeNumber(object: number): void {
107+
if (!this.forceIntegerToFloat && Number.isSafeInteger(object)) {
100108
if (object >= 0) {
101109
if (object < 0x80) {
102110
// positive fixint
@@ -113,10 +121,12 @@ export class Encoder<ContextType = undefined> {
113121
// uint 32
114122
this.writeU8(0xce);
115123
this.writeU32(object);
116-
} else {
124+
} else if (!this.useBigInt64) {
117125
// uint 64
118126
this.writeU8(0xcf);
119127
this.writeU64(object);
128+
} else {
129+
this.encodeNumberAsFloat(object);
120130
}
121131
} else {
122132
if (object >= -0x20) {
@@ -134,23 +144,40 @@ export class Encoder<ContextType = undefined> {
134144
// int 32
135145
this.writeU8(0xd2);
136146
this.writeI32(object);
137-
} else {
147+
} else if (!this.useBigInt64) {
138148
// int 64
139149
this.writeU8(0xd3);
140150
this.writeI64(object);
151+
} else {
152+
this.encodeNumberAsFloat(object);
141153
}
142154
}
143155
} else {
144-
// non-integer numbers
145-
if (this.forceFloat32) {
146-
// float 32
147-
this.writeU8(0xca);
148-
this.writeF32(object);
149-
} else {
150-
// float 64
151-
this.writeU8(0xcb);
152-
this.writeF64(object);
153-
}
156+
this.encodeNumberAsFloat(object);
157+
}
158+
}
159+
160+
private encodeNumberAsFloat(object: number): void {
161+
if (this.forceFloat32) {
162+
// float 32
163+
this.writeU8(0xca);
164+
this.writeF32(object);
165+
} else {
166+
// float 64
167+
this.writeU8(0xcb);
168+
this.writeF64(object);
169+
}
170+
}
171+
172+
private encodeBigInt64(object: bigint): void {
173+
if (object >= BigInt(0)) {
174+
// uint 64
175+
this.writeU8(0xcf);
176+
this.writeBigUint64(object);
177+
} else {
178+
// int 64
179+
this.writeU8(0xd3);
180+
this.writeBigInt64(object);
154181
}
155182
}
156183

@@ -377,12 +404,14 @@ export class Encoder<ContextType = undefined> {
377404

378405
private writeF32(value: number) {
379406
this.ensureBufferSizeToWrite(4);
407+
380408
this.view.setFloat32(this.pos, value);
381409
this.pos += 4;
382410
}
383411

384412
private writeF64(value: number) {
385413
this.ensureBufferSizeToWrite(8);
414+
386415
this.view.setFloat64(this.pos, value);
387416
this.pos += 8;
388417
}
@@ -400,4 +429,18 @@ export class Encoder<ContextType = undefined> {
400429
setInt64(this.view, this.pos, value);
401430
this.pos += 8;
402431
}
432+
433+
private writeBigUint64(value: bigint) {
434+
this.ensureBufferSizeToWrite(8);
435+
436+
this.view.setBigUint64(this.pos, value);
437+
this.pos += 8;
438+
}
439+
440+
private writeBigInt64(value: bigint) {
441+
this.ensureBufferSizeToWrite(8);
442+
443+
this.view.setBigInt64(this.pos, value);
444+
this.pos += 8;
445+
}
403446
}

src/decode.ts

+11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ export type DecodeOptions<ContextType = undefined> = Readonly<
66
Partial<{
77
extensionCodec: ExtensionCodecType<ContextType>;
88

9+
/**
10+
* Decodes Int64 and Uint64 as bigint if it's set to true.
11+
* Depends on ES2020's {@link DataView#getBigInt64} and
12+
* {@link DataView#getBigUint64}.
13+
*
14+
* Defaults to false.
15+
*/
16+
useBigInt64: boolean;
17+
918
/**
1019
* Maximum string length.
1120
*
@@ -58,6 +67,7 @@ export function decode<ContextType = undefined>(
5867
const decoder = new Decoder(
5968
options.extensionCodec,
6069
(options as typeof options & { context: any }).context,
70+
options.useBigInt64,
6171
options.maxStrLength,
6272
options.maxBinLength,
6373
options.maxArrayLength,
@@ -81,6 +91,7 @@ export function decodeMulti<ContextType = undefined>(
8191
const decoder = new Decoder(
8292
options.extensionCodec,
8393
(options as typeof options & { context: any }).context,
94+
options.useBigInt64,
8495
options.maxStrLength,
8596
options.maxBinLength,
8697
options.maxArrayLength,

src/decodeAsync.ts

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { SplitUndefined } from "./context";
1818
const decoder = new Decoder(
1919
options.extensionCodec,
2020
(options as typeof options & { context: any }).context,
21+
options.useBigInt64,
2122
options.maxStrLength,
2223
options.maxBinLength,
2324
options.maxArrayLength,
@@ -40,6 +41,7 @@ import type { SplitUndefined } from "./context";
4041
const decoder = new Decoder(
4142
options.extensionCodec,
4243
(options as typeof options & { context: any }).context,
44+
options.useBigInt64,
4345
options.maxStrLength,
4446
options.maxBinLength,
4547
options.maxArrayLength,
@@ -63,6 +65,7 @@ export function decodeMultiStream<ContextType>(
6365
const decoder = new Decoder(
6466
options.extensionCodec,
6567
(options as typeof options & { context: any }).context,
68+
options.useBigInt64,
6669
options.maxStrLength,
6770
options.maxBinLength,
6871
options.maxArrayLength,

src/encode.ts

+11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ export type EncodeOptions<ContextType = undefined> = Partial<
66
Readonly<{
77
extensionCodec: ExtensionCodecType<ContextType>;
88

9+
/**
10+
* Encodes bigint as Int64 or Uint64 if it's set to true.
11+
* {@link forceIntegerToFloat} does not affect bigint.
12+
* Depends on ES2020's {@link DataView#setBigInt64} and
13+
* {@link DataView#setBigUint64}.
14+
*
15+
* Defaults to false.
16+
*/
17+
useBigInt64: boolean;
18+
919
/**
1020
* The maximum depth in nested objects and arrays.
1121
*
@@ -70,6 +80,7 @@ export function encode<ContextType = undefined>(
7080
const encoder = new Encoder(
7181
options.extensionCodec,
7282
(options as typeof options & { context: any }).context,
83+
options.useBigInt64,
7384
options.maxDepth,
7485
options.initialBufferSize,
7586
options.sortKeys,

test/bigint64.test.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import assert from "assert";
2+
import { encode, decode } from "../src";
3+
4+
describe("useBigInt64: true", () => {
5+
before(function () {
6+
if (typeof BigInt === "undefined") {
7+
this.skip();
8+
}
9+
});
10+
11+
it("encodes and decodes 0n", () => {
12+
const value = BigInt(0);
13+
const encoded = encode(value, { useBigInt64: true });
14+
assert.deepStrictEqual(decode(encoded, { useBigInt64: true }), value);
15+
});
16+
17+
it("encodes and decodes MAX_SAFE_INTEGER+1", () => {
18+
const value = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1);
19+
const encoded = encode(value, { useBigInt64: true });
20+
assert.deepStrictEqual(decode(encoded, { useBigInt64: true }), value);
21+
});
22+
23+
it("encodes and decodes MIN_SAFE_INTEGER-1", () => {
24+
const value = BigInt(Number.MIN_SAFE_INTEGER) - BigInt(1);
25+
const encoded = encode(value, { useBigInt64: true });
26+
assert.deepStrictEqual(decode(encoded, { useBigInt64: true }), value);
27+
});
28+
29+
it("encodes and decodes values with numbers and bigints", () => {
30+
const value = {
31+
ints: [0, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER],
32+
nums: [Number.NaN, Math.PI, Math.E, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY],
33+
bigints: [BigInt(0), BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1), BigInt(Number.MIN_SAFE_INTEGER) - BigInt(1)],
34+
};
35+
const encoded = encode(value, { useBigInt64: true });
36+
assert.deepStrictEqual(decode(encoded, { useBigInt64: true }), value);
37+
});
38+
});

test/codec-bigint.test.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import assert from "assert";
22
import { encode, decode, ExtensionCodec, DecodeError } from "../src";
33

4+
// This test is provided for backward compatibility since this library now has
5+
// native bigint support with `useBigInt64: true` option.
6+
47
const extensionCodec = new ExtensionCodec();
58
extensionCodec.register({
69
type: 0,

0 commit comments

Comments
 (0)