diff --git a/src/Decoder.ts b/src/Decoder.ts index 173142c7..356a8310 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -65,6 +65,7 @@ export class Decoder { public constructor( private readonly extensionCodec: ExtensionCodecType = ExtensionCodec.defaultCodec as any, private readonly context: ContextType = undefined as any, + private readonly useBigInt64 = false, private readonly maxStrLength = UINT32_MAX, private readonly maxBinLength = UINT32_MAX, private readonly maxArrayLength = UINT32_MAX, @@ -277,7 +278,11 @@ export class Decoder { object = this.readU32(); } else if (headByte === 0xcf) { // uint 64 - object = this.readU64(); + if (this.useBigInt64) { + object = this.readU64AsBigInt(); + } else { + object = this.readU64(); + } } else if (headByte === 0xd0) { // int 8 object = this.readI8(); @@ -289,7 +294,11 @@ export class Decoder { object = this.readI32(); } else if (headByte === 0xd3) { // int 64 - object = this.readI64(); + if (this.useBigInt64) { + object = this.readI64AsBigInt(); + } else { + object = this.readI64(); + } } else if (headByte === 0xd9) { // str 8 const byteLength = this.lookU8(); @@ -605,6 +614,18 @@ export class Decoder { return value; } + private readU64AsBigInt(): bigint { + const value = this.view.getBigUint64(this.pos); + this.pos += 8; + return value; + } + + private readI64AsBigInt(): bigint { + const value = this.view.getBigInt64(this.pos); + this.pos += 8; + return value; + } + private readF32() { const value = this.view.getFloat32(this.pos); this.pos += 4; diff --git a/src/Encoder.ts b/src/Encoder.ts index c2fdb4ea..7312471d 100644 --- a/src/Encoder.ts +++ b/src/Encoder.ts @@ -15,6 +15,7 @@ export class Encoder { public constructor( private readonly extensionCodec: ExtensionCodecType = ExtensionCodec.defaultCodec as any, private readonly context: ContextType = undefined as any, + private readonly useBigInt64 = false, private readonly maxDepth = DEFAULT_MAX_DEPTH, private readonly initialBufferSize = DEFAULT_INITIAL_BUFFER_SIZE, private readonly sortKeys = false, @@ -57,9 +58,15 @@ export class Encoder { } else if (typeof object === "boolean") { this.encodeBoolean(object); } else if (typeof object === "number") { - this.encodeNumber(object); + if (!this.forceIntegerToFloat) { + this.encodeNumber(object); + } else { + this.encodeNumberAsFloat(object); + } } else if (typeof object === "string") { this.encodeString(object); + } else if (this.useBigInt64 && typeof object === "bigint") { + this.encodeBigInt64(object); } else { this.encodeObject(object, depth); } @@ -95,8 +102,9 @@ export class Encoder { this.writeU8(0xc3); } } - private encodeNumber(object: number) { - if (Number.isSafeInteger(object) && !this.forceIntegerToFloat) { + + private encodeNumber(object: number): void { + if (!this.forceIntegerToFloat && Number.isSafeInteger(object)) { if (object >= 0) { if (object < 0x80) { // positive fixint @@ -113,10 +121,12 @@ export class Encoder { // uint 32 this.writeU8(0xce); this.writeU32(object); - } else { + } else if (!this.useBigInt64) { // uint 64 this.writeU8(0xcf); this.writeU64(object); + } else { + this.encodeNumberAsFloat(object); } } else { if (object >= -0x20) { @@ -134,23 +144,40 @@ export class Encoder { // int 32 this.writeU8(0xd2); this.writeI32(object); - } else { + } else if (!this.useBigInt64) { // int 64 this.writeU8(0xd3); this.writeI64(object); + } else { + this.encodeNumberAsFloat(object); } } } else { - // non-integer numbers - if (this.forceFloat32) { - // float 32 - this.writeU8(0xca); - this.writeF32(object); - } else { - // float 64 - this.writeU8(0xcb); - this.writeF64(object); - } + this.encodeNumberAsFloat(object); + } + } + + private encodeNumberAsFloat(object: number): void { + if (this.forceFloat32) { + // float 32 + this.writeU8(0xca); + this.writeF32(object); + } else { + // float 64 + this.writeU8(0xcb); + this.writeF64(object); + } + } + + private encodeBigInt64(object: bigint): void { + if (object >= BigInt(0)) { + // uint 64 + this.writeU8(0xcf); + this.writeBigUint64(object); + } else { + // int 64 + this.writeU8(0xd3); + this.writeBigInt64(object); } } @@ -377,12 +404,14 @@ export class Encoder { private writeF32(value: number) { this.ensureBufferSizeToWrite(4); + this.view.setFloat32(this.pos, value); this.pos += 4; } private writeF64(value: number) { this.ensureBufferSizeToWrite(8); + this.view.setFloat64(this.pos, value); this.pos += 8; } @@ -400,4 +429,18 @@ export class Encoder { setInt64(this.view, this.pos, value); this.pos += 8; } + + private writeBigUint64(value: bigint) { + this.ensureBufferSizeToWrite(8); + + this.view.setBigUint64(this.pos, value); + this.pos += 8; + } + + private writeBigInt64(value: bigint) { + this.ensureBufferSizeToWrite(8); + + this.view.setBigInt64(this.pos, value); + this.pos += 8; + } } diff --git a/src/decode.ts b/src/decode.ts index 30a88ab3..03c78f6b 100644 --- a/src/decode.ts +++ b/src/decode.ts @@ -6,6 +6,15 @@ export type DecodeOptions = Readonly< Partial<{ extensionCodec: ExtensionCodecType; + /** + * Decodes Int64 and Uint64 as bigint if it's set to true. + * Depends on ES2020's {@link DataView#getBigInt64} and + * {@link DataView#getBigUint64}. + * + * Defaults to false. + */ + useBigInt64: boolean; + /** * Maximum string length. * @@ -58,6 +67,7 @@ export function decode( const decoder = new Decoder( options.extensionCodec, (options as typeof options & { context: any }).context, + options.useBigInt64, options.maxStrLength, options.maxBinLength, options.maxArrayLength, @@ -81,6 +91,7 @@ export function decodeMulti( const decoder = new Decoder( options.extensionCodec, (options as typeof options & { context: any }).context, + options.useBigInt64, options.maxStrLength, options.maxBinLength, options.maxArrayLength, diff --git a/src/decodeAsync.ts b/src/decodeAsync.ts index ee9922fa..9ec8f2c9 100644 --- a/src/decodeAsync.ts +++ b/src/decodeAsync.ts @@ -18,6 +18,7 @@ import type { SplitUndefined } from "./context"; const decoder = new Decoder( options.extensionCodec, (options as typeof options & { context: any }).context, + options.useBigInt64, options.maxStrLength, options.maxBinLength, options.maxArrayLength, @@ -40,6 +41,7 @@ import type { SplitUndefined } from "./context"; const decoder = new Decoder( options.extensionCodec, (options as typeof options & { context: any }).context, + options.useBigInt64, options.maxStrLength, options.maxBinLength, options.maxArrayLength, @@ -63,6 +65,7 @@ export function decodeMultiStream( const decoder = new Decoder( options.extensionCodec, (options as typeof options & { context: any }).context, + options.useBigInt64, options.maxStrLength, options.maxBinLength, options.maxArrayLength, diff --git a/src/encode.ts b/src/encode.ts index 7e6a602e..898dd840 100644 --- a/src/encode.ts +++ b/src/encode.ts @@ -6,6 +6,16 @@ export type EncodeOptions = Partial< Readonly<{ extensionCodec: ExtensionCodecType; + /** + * Encodes bigint as Int64 or Uint64 if it's set to true. + * {@link forceIntegerToFloat} does not affect bigint. + * Depends on ES2020's {@link DataView#setBigInt64} and + * {@link DataView#setBigUint64}. + * + * Defaults to false. + */ + useBigInt64: boolean; + /** * The maximum depth in nested objects and arrays. * @@ -70,6 +80,7 @@ export function encode( const encoder = new Encoder( options.extensionCodec, (options as typeof options & { context: any }).context, + options.useBigInt64, options.maxDepth, options.initialBufferSize, options.sortKeys, diff --git a/test/bigint64.test.ts b/test/bigint64.test.ts new file mode 100644 index 00000000..fabf0f54 --- /dev/null +++ b/test/bigint64.test.ts @@ -0,0 +1,38 @@ +import assert from "assert"; +import { encode, decode } from "../src"; + +describe("useBigInt64: true", () => { + before(function () { + if (typeof BigInt === "undefined") { + this.skip(); + } + }); + + it("encodes and decodes 0n", () => { + const value = BigInt(0); + const encoded = encode(value, { useBigInt64: true }); + assert.deepStrictEqual(decode(encoded, { useBigInt64: true }), value); + }); + + it("encodes and decodes MAX_SAFE_INTEGER+1", () => { + const value = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1); + const encoded = encode(value, { useBigInt64: true }); + assert.deepStrictEqual(decode(encoded, { useBigInt64: true }), value); + }); + + it("encodes and decodes MIN_SAFE_INTEGER-1", () => { + const value = BigInt(Number.MIN_SAFE_INTEGER) - BigInt(1); + const encoded = encode(value, { useBigInt64: true }); + assert.deepStrictEqual(decode(encoded, { useBigInt64: true }), value); + }); + + it("encodes and decodes values with numbers and bigints", () => { + const value = { + ints: [0, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER], + nums: [Number.NaN, Math.PI, Math.E, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY], + bigints: [BigInt(0), BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1), BigInt(Number.MIN_SAFE_INTEGER) - BigInt(1)], + }; + const encoded = encode(value, { useBigInt64: true }); + assert.deepStrictEqual(decode(encoded, { useBigInt64: true }), value); + }); +}); diff --git a/test/codec-bigint.test.ts b/test/codec-bigint.test.ts index fc649a83..1725b46f 100644 --- a/test/codec-bigint.test.ts +++ b/test/codec-bigint.test.ts @@ -1,6 +1,9 @@ import assert from "assert"; import { encode, decode, ExtensionCodec, DecodeError } from "../src"; +// This test is provided for backward compatibility since this library now has +// native bigint support with `useBigInt64: true` option. + const extensionCodec = new ExtensionCodec(); extensionCodec.register({ type: 0,