diff --git a/etc/benchmarks/main.mjs b/etc/benchmarks/main.mjs index 8fd6d858..e6530891 100644 --- a/etc/benchmarks/main.mjs +++ b/etc/benchmarks/main.mjs @@ -9,7 +9,7 @@ console.log(); //////////////////////////////////////////////////////////////////////////////////////////////////// await runner({ - skip: false, + skip: true, name: 'deserialize({ oid, string }, { validation: { utf8: false } })', iterations, setup(libs) { @@ -58,6 +58,46 @@ await runner({ } }); +await runner({ + skip: true, + name: 'Double Serialization', + iterations, + run(i, bson) { + bson.lib.serialize({ d: 2.3 }); + } +}); + +await runner({ + skip: false, + name: 'Double Deserialization', + iterations, + setup(libs) { + const bson = getCurrentLocalBSON(libs); + return bson.lib.serialize({ d: 2.3 }); + }, + run(i, bson, serialized_double) { + bson.lib.deserialize(serialized_double); + } +}); + +await runner({ + skip: false, + name: 'Many Doubles Deserialization', + iterations, + setup(libs) { + const bson = getCurrentLocalBSON(libs); + let doubles = Object.fromEntries( + Array.from({ length: 1000 }, i => { + return [`a_${i}`, 2.3]; + }) + ); + return bson.lib.serialize(doubles); + }, + run(i, bson, serialized_doubles) { + bson.lib.deserialize(serialized_doubles); + } +}); + // End console.log( 'Total time taken to benchmark:', diff --git a/src/float_parser.ts b/src/float_parser.ts deleted file mode 100644 index c881f4cf..00000000 --- a/src/float_parser.ts +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) 2008, Fair Oaks Labs, Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// * Neither the name of Fair Oaks Labs, Inc. nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// -// Modifications to writeIEEE754 to support negative zeroes made by Brian White - -type NumericalSequence = { [index: number]: number }; - -export function readIEEE754( - buffer: NumericalSequence, - offset: number, - endian: 'big' | 'little', - mLen: number, - nBytes: number -): number { - let e: number; - let m: number; - const bBE = endian === 'big'; - const eLen = nBytes * 8 - mLen - 1; - const eMax = (1 << eLen) - 1; - const eBias = eMax >> 1; - let nBits = -7; - let i = bBE ? 0 : nBytes - 1; - const d = bBE ? 1 : -1; - let s = buffer[offset + i]; - - i += d; - - e = s & ((1 << -nBits) - 1); - s >>= -nBits; - nBits += eLen; - for (; nBits > 0; e = e * 256 + buffer[offset + i], i += d, nBits -= 8); - - m = e & ((1 << -nBits) - 1); - e >>= -nBits; - nBits += mLen; - for (; nBits > 0; m = m * 256 + buffer[offset + i], i += d, nBits -= 8); - - if (e === 0) { - e = 1 - eBias; - } else if (e === eMax) { - return m ? NaN : (s ? -1 : 1) * Infinity; - } else { - m = m + Math.pow(2, mLen); - e = e - eBias; - } - return (s ? -1 : 1) * m * Math.pow(2, e - mLen); -} - -export function writeIEEE754( - buffer: NumericalSequence, - value: number, - offset: number, - endian: 'big' | 'little', - mLen: number, - nBytes: number -): void { - let e: number; - let m: number; - let c: number; - const bBE = endian === 'big'; - let eLen = nBytes * 8 - mLen - 1; - const eMax = (1 << eLen) - 1; - const eBias = eMax >> 1; - const rt = mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0; - let i = bBE ? nBytes - 1 : 0; - const d = bBE ? -1 : 1; - const s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0; - - value = Math.abs(value); - - if (isNaN(value) || value === Infinity) { - m = isNaN(value) ? 1 : 0; - e = eMax; - } else { - e = Math.floor(Math.log(value) / Math.LN2); - if (value * (c = Math.pow(2, -e)) < 1) { - e--; - c *= 2; - } - if (e + eBias >= 1) { - value += rt / c; - } else { - value += rt * Math.pow(2, 1 - eBias); - } - if (value * c >= 2) { - e++; - c /= 2; - } - - if (e + eBias >= eMax) { - m = 0; - e = eMax; - } else if (e + eBias >= 1) { - m = (value * c - 1) * Math.pow(2, mLen); - e = e + eBias; - } else { - m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen); - e = 0; - } - } - - if (isNaN(value)) m = 0; - - while (mLen >= 8) { - buffer[offset + i] = m & 0xff; - i += d; - m /= 256; - mLen -= 8; - } - - e = (e << mLen) | m; - - if (isNaN(value)) e += 8; - - eLen += mLen; - - while (eLen > 0) { - buffer[offset + i] = e & 0xff; - i += d; - e /= 256; - eLen -= 8; - } - - buffer[offset + i - d] |= s * 128; -} diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts index 6f3d567c..bd306c95 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -197,6 +197,7 @@ function deserializeObject( let isPossibleDBRef = isArray ? false : null; // While we have more left data left keep parsing + const dataview = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); while (!done) { // Read the type const elementType = buffer[index++]; @@ -263,10 +264,10 @@ function deserializeObject( (buffer[index++] << 16) | (buffer[index++] << 24); } else if (elementType === constants.BSON_DATA_NUMBER && promoteValues === false) { - value = new Double(buffer.readDoubleLE(index)); + value = new Double(dataview.getFloat64(index, true)); index = index + 8; } else if (elementType === constants.BSON_DATA_NUMBER) { - value = buffer.readDoubleLE(index); + value = dataview.getFloat64(index, true); index = index + 8; } else if (elementType === constants.BSON_DATA_DATE) { const lowBits = diff --git a/src/parser/serializer.ts b/src/parser/serializer.ts index 1fa810ec..e76402aa 100644 --- a/src/parser/serializer.ts +++ b/src/parser/serializer.ts @@ -9,7 +9,6 @@ import type { Double } from '../double'; import { ensureBuffer } from '../ensure_buffer'; import { BSONError, BSONTypeError } from '../error'; import { isBSONType } from '../extended_json'; -import { writeIEEE754 } from '../float_parser'; import type { Int32 } from '../int_32'; import { Long } from '../long'; import { Map } from '../map'; @@ -79,6 +78,12 @@ function serializeString( return index; } +const SPACE_FOR_FLOAT64 = new Uint8Array(8); +const DV_FOR_FLOAT64 = new DataView( + SPACE_FOR_FLOAT64.buffer, + SPACE_FOR_FLOAT64.byteOffset, + SPACE_FOR_FLOAT64.byteLength +); function serializeNumber( buffer: Buffer, key: string, @@ -119,7 +124,8 @@ function serializeNumber( index = index + numberOfWrittenBytes; buffer[index++] = 0; // Write float - writeIEEE754(buffer, value, index, 'little', 52, 8); + DV_FOR_FLOAT64.setFloat64(0, value, true); + buffer.set(SPACE_FOR_FLOAT64, index); // Adjust index index = index + 8; } @@ -487,7 +493,8 @@ function serializeDouble( buffer[index++] = 0; // Write float - writeIEEE754(buffer, value.value, index, 'little', 52, 8); + DV_FOR_FLOAT64.setFloat64(0, value.value, true); + buffer.set(SPACE_FOR_FLOAT64, index); // Adjust index index = index + 8; diff --git a/test/node/bson_corpus.spec.test.js b/test/node/bson_corpus.spec.test.js index b40d6c03..62dbf94b 100644 --- a/test/node/bson_corpus.spec.test.js +++ b/test/node/bson_corpus.spec.test.js @@ -121,11 +121,6 @@ describe('BSON Corpus', function () { describe('valid-bson', function () { for (const v of valid) { it(v.description, function () { - if (v.description === 'NaN with payload') { - // TODO(NODE-3630): remove custom float parser so we can handle the NaN payload data - this.skip(); - } - if ( v.description === 'All BSON types' && scenario._filename === 'multi-type-deprecated' diff --git a/test/node/double_tests.js b/test/node/double_tests.js index 9c690174..5346f6cf 100644 --- a/test/node/double_tests.js +++ b/test/node/double_tests.js @@ -3,36 +3,90 @@ const BSON = require('../register-bson'); const Double = BSON.Double; -describe('Double', function () { - describe('Constructor', function () { - var value = 42.3456; +describe('BSON Double Precision', function () { + context('class Double', function () { + describe('constructor()', function () { + const value = 42.3456; - it('Primitive number', function (done) { - expect(new Double(value).valueOf()).to.equal(value); - done(); + it('Primitive number', function () { + expect(new Double(value).valueOf()).to.equal(value); + }); + + it('Number object', function () { + expect(new Double(new Number(value)).valueOf()).to.equal(value); + }); }); - it('Number object', function (done) { - expect(new Double(new Number(value)).valueOf()).to.equal(value); - done(); + describe('#toString()', () => { + it('should serialize to a string', () => { + const testNumber = Math.random() * Number.MAX_VALUE; + const double = new Double(testNumber); + expect(double.toString()).to.equal(testNumber.toString()); + }); + + const testRadices = [2, 8, 10, 16, 22]; + + for (const radix of testRadices) { + it(`should support radix argument: ${radix}`, () => { + const testNumber = Math.random() * Number.MAX_VALUE; + const double = new Double(testNumber); + expect(double.toString(radix)).to.equal(testNumber.toString(radix)); + }); + } }); }); - describe('toString', () => { - it('should serialize to a string', () => { - const testNumber = Math.random() * Number.MAX_VALUE; - const double = new Double(testNumber); - expect(double.toString()).to.equal(testNumber.toString()); + function serializeThenDeserialize(value) { + const serializedDouble = BSON.serialize({ d: value }); + const deserializedDouble = BSON.deserialize(serializedDouble, { promoteValues: false }); + return deserializedDouble.d; + } + + const testCases = [ + { name: 'Infinity', doubleVal: new Double(Infinity), testVal: Infinity }, + { name: '-Infinity', doubleVal: new Double(-Infinity), testVal: -Infinity }, + { name: 'Number.EPSILON', doubleVal: new Double(Number.EPSILON), testVal: Number.EPSILON }, + { name: 'Zero', doubleVal: new Double(0), testVal: 0 }, + { name: 'Negative Zero', doubleVal: new Double(-0), testVal: -0 }, + { name: 'NaN', doubleVal: new Double(NaN), testVal: NaN } + ]; + + for (const { name, doubleVal, testVal } of testCases) { + it(`should preserve the input value ${name} in Double serialize-deserialize roundtrip`, () => { + const roundTrippedVal = serializeThenDeserialize(doubleVal); + expect(Object.is(doubleVal.value, testVal)).to.be.true; + expect(Object.is(roundTrippedVal.value, doubleVal.value)).to.be.true; }); + } - const testRadices = [2, 8, 10, 16, 22]; + context('NaN with Payload', function () { + const NanPayloadBuffer = Buffer.from('120000000000F87F', 'hex'); + const NanPayloadDV = new DataView( + NanPayloadBuffer.buffer, + NanPayloadBuffer.byteOffset, + NanPayloadBuffer.byteLength + ); + const NanPayloadDouble = NanPayloadDV.getFloat64(0, true); + // Using promoteValues: false (returning raw BSON) in order to be able to check that payload remains intact + const serializedNanPayloadDouble = BSON.serialize({ d: NanPayloadDouble }); - for (const radix of testRadices) { - it(`should support radix argument: ${radix}`, () => { - const testNumber = Math.random() * Number.MAX_VALUE; - const double = new Double(testNumber); - expect(double.toString(radix)).to.equal(testNumber.toString(radix)); - }); - } + it('should keep payload in serialize-deserialize roundtrip', function () { + expect(serializedNanPayloadDouble.subarray(7, 15)).to.deep.equal(NanPayloadBuffer); + }); + + it('should preserve NaN value in serialize-deserialize roundtrip', function () { + const { d: newVal } = BSON.deserialize(serializedNanPayloadDouble, { promoteValues: true }); + expect(newVal).to.be.NaN; + }); + }); + + it('NODE-4335: does not preserve -0 in serialize-deserialize roundtrip if JS number is used', function () { + // TODO (NODE-4335): -0 should be serialized as double + // This test is demonstrating the behavior of -0 being serialized as an int32 something we do NOT want to unintentionally change, but may want to change in the future, which the above ticket serves to track. + const value = -0; + const serializedDouble = BSON.serialize({ d: value }); + const type = serializedDouble[4]; + expect(type).to.not.equal(BSON.BSON_DATA_NUMBER); + expect(type).to.equal(BSON.BSON_DATA_INT); }); });