From 1a740aa286a56525be4192929d23f7db214fefcb Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Wed, 2 Oct 2019 18:04:36 -0400 Subject: [PATCH] feat: use `jsbi` for reading and writing `int64` and `uint64` values --- src/token/done-token-parser.ts | 14 +- src/token/stream-parser.ts | 38 ++++ src/tracking-buffer/bigint.ts | 83 --------- .../writable-tracking-buffer.ts | 40 +++- src/value-parser.ts | 6 +- test/integration/datatypes-in-results-test.js | 4 + test/integration/rpc-test.js | 12 ++ test/unit/tracking-buffer/bigint-test.js | 171 ------------------ .../writable-tracking-buffer-test.js | 17 ++ 9 files changed, 120 insertions(+), 265 deletions(-) delete mode 100644 src/tracking-buffer/bigint.ts delete mode 100644 test/unit/tracking-buffer/bigint-test.js diff --git a/src/token/done-token-parser.ts b/src/token/done-token-parser.ts index 9ae96a34c..d1630cb65 100644 --- a/src/token/done-token-parser.ts +++ b/src/token/done-token-parser.ts @@ -1,3 +1,5 @@ +import JSBI from 'jsbi'; + import Parser from './stream-parser'; import { ColumnMetadata } from './colmetadata-token-parser'; import { InternalConnectionOptions } from '../connection'; @@ -33,7 +35,7 @@ function parseToken(parser: Parser, options: InternalConnectionOptions, callback const serverError = !!(status & STATUS.SRVERROR); parser.readUInt16LE((curCmd) => { - (options.tdsVersion < '7_2' ? parser.readUInt32LE : parser.readUInt64LE).call(parser, (rowCount) => { + const next = (rowCount: number) => { callback({ more: more, sqlError: sqlError, @@ -42,7 +44,15 @@ function parseToken(parser: Parser, options: InternalConnectionOptions, callback rowCount: rowCountValid ? rowCount : undefined, curCmd: curCmd }); - }); + }; + + if (options.tdsVersion < '7_2') { + parser.readUInt32LE(next); + } else { + parser.readBigUInt64LE((rowCount) => { + next(JSBI.toNumber(rowCount)); + }); + } }); }); } diff --git a/src/token/stream-parser.ts b/src/token/stream-parser.ts index 9f0e46ae9..cd0577c97 100644 --- a/src/token/stream-parser.ts +++ b/src/token/stream-parser.ts @@ -1,5 +1,6 @@ import Debug from '../debug'; import { InternalConnectionOptions } from '../connection'; +import JSBI from 'jsbi'; const Transform = require('readable-stream').Transform; import { TYPE, Token, EndOfMessageToken, ColMetadataToken } from './token'; @@ -212,6 +213,32 @@ class Parser extends Transform { }); } + readBigInt64LE(callback: (data: JSBI) => void) { + this.awaitData(8, () => { + const result = JSBI.add( + JSBI.leftShift( + JSBI.BigInt( + this.buffer[this.position + 4] + + this.buffer[this.position + 5] * 2 ** 8 + + this.buffer[this.position + 6] * 2 ** 16 + + (this.buffer[this.position + 7] << 24) // Overflow + ), + JSBI.BigInt(32) + ), + JSBI.BigInt( + this.buffer[this.position] + + this.buffer[this.position + 1] * 2 ** 8 + + this.buffer[this.position + 2] * 2 ** 16 + + this.buffer[this.position + 3] * 2 ** 24 + ) + ); + + this.position += 8; + + callback(result); + }); + } + readInt64LE(callback: (data: number) => void) { this.awaitData(8, () => { const data = Math.pow(2, 32) * this.buffer.readInt32LE(this.position + 4) + ((this.buffer[this.position + 4] & 0x80) === 0x80 ? 1 : -1) * this.buffer.readUInt32LE(this.position); @@ -228,6 +255,17 @@ class Parser extends Transform { }); } + readBigUInt64LE(callback: (data: JSBI) => void) { + this.awaitData(8, () => { + const low = JSBI.BigInt(this.buffer.readUInt32LE(this.position)); + const high = JSBI.BigInt(this.buffer.readUInt32LE(this.position + 4)); + + this.position += 8; + + callback(JSBI.add(low, JSBI.leftShift(high, JSBI.BigInt(32)))); + }); + } + readUInt64LE(callback: (data: number) => void) { this.awaitData(8, () => { const data = Math.pow(2, 32) * this.buffer.readUInt32LE(this.position + 4) + this.buffer.readUInt32LE(this.position); diff --git a/src/tracking-buffer/bigint.ts b/src/tracking-buffer/bigint.ts deleted file mode 100644 index 01b401e70..000000000 --- a/src/tracking-buffer/bigint.ts +++ /dev/null @@ -1,83 +0,0 @@ -function isZero(array: number[]) { - for (let j = 0, len = array.length; j < len; j++) { - const byte = array[j]; - if (byte !== 0) { - return false; - } - } - return true; -} - -function getNextRemainder(array: number[]) { - let remainder = 0; - - for (let i = array.length - 1; i >= 0; i--) { - const s = (remainder * 256) + array[i]; - array[i] = Math.floor(s / 10); - remainder = s % 10; - } - - return remainder; -} - -function invert(array: number[]) { - // Invert bits - const len = array.length; - - for (let i = 0; i < len; i++) { - array[i] = array[i] ^ 0xFF; - } - - for (let i = 0; i < len; i++) { - array[i] = array[i] + 1; - - if (array[i] > 255) { - array[i] = 0; - } else { - break; - } - } -} - -export function convertLEBytesToString(buffer: Buffer) { - const array = Array.prototype.slice.call(buffer, 0, buffer.length); - if (isZero(array)) { - return '0'; - } else { - let sign; - if (array[array.length - 1] & 0x80) { - sign = '-'; - invert(array); - } else { - sign = ''; - } - let result = ''; - while (!isZero(array)) { - const t = getNextRemainder(array); - result = t + result; - } - return sign + result; - } -} - -export function numberToInt64LE(num: number) { - // adapted from https://github.com/broofa/node-int64 - const negate = num < 0; - let hi = Math.abs(num); - let lo = hi % 0x100000000; - hi = (hi / 0x100000000) | 0; - const buf = Buffer.alloc(8, 0); - for (let i = 0; i <= 7; i++) { - buf[i] = lo & 0xff; - lo = i === 3 ? hi : lo >>> 8; - } - if (negate) { - let carry = 1; - for (let i = 0; i <= 7; i++) { - const v = (buf[i] ^ 0xff) + carry; - buf[i] = v & 0xff; - carry = v >> 8; - } - } - return buf; -} diff --git a/src/tracking-buffer/writable-tracking-buffer.ts b/src/tracking-buffer/writable-tracking-buffer.ts index 50f646a08..c5d9139ac 100644 --- a/src/tracking-buffer/writable-tracking-buffer.ts +++ b/src/tracking-buffer/writable-tracking-buffer.ts @@ -1,4 +1,4 @@ -import { numberToInt64LE } from './bigint'; +import JSBI from 'jsbi'; const SHIFT_LEFT_32 = (1 << 16) * (1 << 16); const SHIFT_RIGHT_32 = 1 / SHIFT_LEFT_32; @@ -106,9 +106,36 @@ class WritableTrackingBuffer { this.position += length; } + writeBigInt64LE(value: JSBI) { + this.writeBigU_Int64LE(value); + } + + private writeBigU_Int64LE(value: JSBI) { + this.makeRoomFor(8); + + let lo = JSBI.toNumber(JSBI.bitwiseAnd(value, JSBI.BigInt(0xffffffff))); + + this.buffer[this.position++] = lo; + lo = lo >> 8; + this.buffer[this.position++] = lo; + lo = lo >> 8; + this.buffer[this.position++] = lo; + lo = lo >> 8; + this.buffer[this.position++] = lo; + + let hi = JSBI.toNumber(JSBI.bitwiseAnd(JSBI.signedRightShift(value, JSBI.BigInt(32)), JSBI.BigInt(0xffffffff))); + + this.buffer[this.position++] = hi; + hi = hi >> 8; + this.buffer[this.position++] = hi; + hi = hi >> 8; + this.buffer[this.position++] = hi; + hi = hi >> 8; + this.buffer[this.position++] = hi; + } + writeInt64LE(value: number) { - const buf = numberToInt64LE(value); - this.copyFrom(buf); + this.writeBigInt64LE(JSBI.BigInt(value)); } writeUInt32BE(value: number) { @@ -125,8 +152,11 @@ class WritableTrackingBuffer { } writeUInt64LE(value: number) { - this.writeInt32LE(value & -1); - this.writeUInt32LE(Math.floor(value * SHIFT_RIGHT_32)); + this.writeBigUInt64LE(JSBI.BigInt(value)); + } + + writeBigUInt64LE(value: JSBI) { + this.writeBigU_Int64LE(value); } writeInt8(value: number) { diff --git a/src/value-parser.ts b/src/value-parser.ts index 5809b188c..68a5e3292 100644 --- a/src/value-parser.ts +++ b/src/value-parser.ts @@ -7,8 +7,6 @@ const iconv = require('iconv-lite'); const sprintf = require('sprintf-js').sprintf; import { bufferToLowerCaseGuid, bufferToUpperCaseGuid } from './guid-parser'; -const convertLEBytesToString = require('./tracking-buffer/bigint').convertLEBytesToString; - const NULL = (1 << 16) - 1; const MAX = (1 << 16) - 1; const THREE_AND_A_THIRD = 3 + (1 / 3); @@ -30,8 +28,8 @@ function readInt(parser: Parser, callback: (value: unknown) => void) { } function readBigInt(parser: Parser, callback: (value: unknown) => void) { - parser.readBuffer(8, (buffer) => { - callback(convertLEBytesToString(buffer)); + parser.readBigInt64LE((value) => { + callback(value.toString()); }); } diff --git a/test/integration/datatypes-in-results-test.js b/test/integration/datatypes-in-results-test.js index 620139636..46cd31954 100644 --- a/test/integration/datatypes-in-results-test.js +++ b/test/integration/datatypes-in-results-test.js @@ -127,6 +127,10 @@ describe('Datatypes in results test', function() { execSql(done, 'select cast(8 as bigint)', '8'); }); + it('should test negative big int', function(done) { + execSql(done, 'select cast(-8 as bigint)', '-8'); + }); + it('should test big int null', function(done) { execSql(done, 'select cast(null as bigint)', null); }); diff --git a/test/integration/rpc-test.js b/test/integration/rpc-test.js index 4212ab063..e55efad3f 100644 --- a/test/integration/rpc-test.js +++ b/test/integration/rpc-test.js @@ -201,6 +201,18 @@ describe('RPC test', function() { testProc(done, TYPES.SmallInt, 'smallint', null); }); + it('should exec proc bigint', function(done) { + testProc(done, TYPES.BigInt, 'bigint', '3'); + }); + + it('should exec proc negative bigint', function(done) { + testProc(done, TYPES.BigInt, 'bigint', '-3'); + }); + + it('should exec proc bigint null', function(done) { + testProc(done, TYPES.BigInt, 'bigint', null); + }); + it('should exec proc int', function(done) { testProc(done, TYPES.Int, 'int', 3); }); diff --git a/test/unit/tracking-buffer/bigint-test.js b/test/unit/tracking-buffer/bigint-test.js deleted file mode 100644 index 3dab31601..000000000 --- a/test/unit/tracking-buffer/bigint-test.js +++ /dev/null @@ -1,171 +0,0 @@ -const convertLEBytesToString = require('../../../src/tracking-buffer/bigint').convertLEBytesToString; -const numberToInt64LE = require('../../../src/tracking-buffer/bigint').numberToInt64LE; -const assert = require('chai').assert; - - -function assertBuffer(actual, expected) { - for (var i = 0, end = actual.length; i < end; i++) { - if (actual[i] !== expected[i]) { - console.log('actual ', actual); - console.log('expected', Buffer.from(expected)); - assert.isOk(false); - } - } -} - -describe('Bigint Test', () => { - it('should be zero', () => { - assert.strictEqual( - '0', - convertLEBytesToString(Buffer.from([0, 0, 0, 0, 0, 0, 0, 0])) - ); - }); - - it('should be small positive', () => { - assert.strictEqual( - '1', - convertLEBytesToString(Buffer.from([1, 0, 0, 0, 0, 0, 0, 0])) - ); - assert.strictEqual( - '2', - convertLEBytesToString(Buffer.from([2, 0, 0, 0, 0, 0, 0, 0])) - ); - }); - - it('should be small negative', () => { - assert.strictEqual( - '-1', - convertLEBytesToString(Buffer.from([255, 255, 255, 255, 255, 255, 255, 255])) - ); - assert.strictEqual( - '-2', - convertLEBytesToString(Buffer.from([254, 255, 255, 255, 255, 255, 255, 255])) - ); - }); - - it('should be big positive', () => { - assert.strictEqual( - '9223372036854775807', - convertLEBytesToString(Buffer.from([255, 255, 255, 255, 255, 255, 255, 127])) - ); - }); - - it('should be big negative', () => { - assert.strictEqual( - '-9223372036854775808', - convertLEBytesToString(Buffer.from([0, 0, 0, 0, 0, 0, 0, 128])) - ); - }); - - it('should be powersOf10', () => { - assert.strictEqual( - '10', - convertLEBytesToString(Buffer.from([10, 0, 0, 0, 0, 0, 0, 0])) - ); - assert.strictEqual( - '100', - convertLEBytesToString(Buffer.from([100, 0, 0, 0, 0, 0, 0, 0])) - ); - assert.strictEqual( - '1000', - convertLEBytesToString(Buffer.from([232, 3, 0, 0, 0, 0, 0, 0])) - ); - assert.strictEqual( - '10000', - convertLEBytesToString(Buffer.from([16, 39, 0, 0, 0, 0, 0, 0])) - ); - }); - - it('should be toInt64LE', () => { - assertBuffer(numberToInt64LE(-3500000000), [ - 0x00, - 0x3d, - 0x62, - 0x2f, - 0xff, - 0xff, - 0xff, - 0xff - ]); - assertBuffer(numberToInt64LE(3500000000), [ - 0x00, - 0xc3, - 0x9d, - 0xd0, - 0x00, - 0x00, - 0x00, - 0x00 - ]); - assertBuffer(numberToInt64LE(-2), [ - 0xfe, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff, - 0xff - ]); - assertBuffer(numberToInt64LE(2), [ - 0x02, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00 - ]); - assertBuffer(numberToInt64LE(0), [ - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00 - ]); - assertBuffer(numberToInt64LE(-5000000000), [ - 0x00, - 0x0e, - 0xfa, - 0xd5, - 0xfe, - 0xff, - 0xff, - 0xff - ]); - assertBuffer(numberToInt64LE(5000000000), [ - 0x00, - 0xf2, - 0x05, - 0x2a, - 0x01, - 0x00, - 0x00, - 0x00 - ]); - assertBuffer(numberToInt64LE(5201683247893), [ - 0x15, - 0x73, - 0x7b, - 0x1c, - 0xbb, - 0x04, - 0x00, - 0x00 - ]); - assertBuffer(numberToInt64LE(-5201683247893), [ - 0xeb, - 0x8c, - 0x84, - 0xe3, - 0x44, - 0xfb, - 0xff, - 0xff - ]); - }); -}); diff --git a/test/unit/tracking-buffer/writable-tracking-buffer-test.js b/test/unit/tracking-buffer/writable-tracking-buffer-test.js index 221bc9010..0b758db59 100644 --- a/test/unit/tracking-buffer/writable-tracking-buffer-test.js +++ b/test/unit/tracking-buffer/writable-tracking-buffer-test.js @@ -1,5 +1,6 @@ const TrackingBuffer = require('../../../src/tracking-buffer/writable-tracking-buffer'); const assert = require('chai').assert; +const JSBI = require('jsbi'); function assertBuffer(actual, expected) { actual = actual.data; @@ -115,6 +116,22 @@ describe('Wrtiable Tracking Buffer', () => { assertBuffer(buffer, [0x03, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63, 0x00]); }); + it('should write 64-bit signed JSBIs', () => { + const buffer = new TrackingBuffer(8); + + buffer.writeBigInt64LE(JSBI.BigInt('0x0807060504030201')); + + assertBuffer(buffer, [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); + }); + + it('should write 64-bit unsigned JSBIs', () => { + const buffer = new TrackingBuffer(8); + + buffer.writeBigUInt64LE(JSBI.BigInt('0xdecafafecacefade')); + + assertBuffer(buffer, [0xde, 0xfa, 0xce, 0xca, 0xfe, 0xfa, 0xca, 0xde]); + }); + it('should copyFrom', () => { const buffer = new TrackingBuffer(10); const source = Buffer.from([0x01, 0x02, 0x03, 0x04]);