diff --git a/docs/upgrade-to-v5.md b/docs/upgrade-to-v5.md index 56fd43a8..dec24d3a 100644 --- a/docs/upgrade-to-v5.md +++ b/docs/upgrade-to-v5.md @@ -100,3 +100,21 @@ The following deprecated methods have been removed: - `ObjectId.prototype.get_inc` - `ObjectId.get_inc` - The `static getInc()` is private since invoking it increments the next `ObjectId` index, so invoking would impact the creation of subsequent ObjectIds. + +### Negative Zero is now serialized to Double + +BSON serialize will now preserve negative zero values as a floating point number. + +Previously it was required to use the `Double` type class to preserve `-0`: +```ts +BSON.deserialize(BSON.serialize({ d: -0 })) +// no type preservation, returns { d: 0 } +BSON.deserialize(BSON.serialize({ d: new Double(-0) })) +// type preservation, returns { d: -0 } +``` + +Now `-0` can be used directly +```ts +BSON.deserialize(BSON.serialize({ d: -0 })) +// type preservation, returns { d: -0 } +``` diff --git a/src/parser/serializer.ts b/src/parser/serializer.ts index 4dd6885c..3cf35e05 100644 --- a/src/parser/serializer.ts +++ b/src/parser/serializer.ts @@ -68,48 +68,39 @@ function serializeString(buffer: Uint8Array, key: string, value: string, index: 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 -); +const NUMBER_SPACE = new DataView(new ArrayBuffer(8), 0, 8); +const FOUR_BYTE_VIEW_ON_NUMBER = new Uint8Array(NUMBER_SPACE.buffer, 0, 4); +const EIGHT_BYTE_VIEW_ON_NUMBER = new Uint8Array(NUMBER_SPACE.buffer, 0, 8); + function serializeNumber(buffer: Uint8Array, key: string, value: number, index: number) { - // We have an integer value - // TODO(NODE-2529): Add support for big int - if ( - Number.isInteger(value) && - value >= constants.BSON_INT32_MIN && - value <= constants.BSON_INT32_MAX - ) { - // If the value fits in 32 bits encode as int32 - // Set int type 32 bits or less - buffer[index++] = constants.BSON_DATA_INT; - // Number of written bytes - const numberOfWrittenBytes = ByteUtils.encodeUTF8Into(buffer, key, index); - // Encode the name - index = index + numberOfWrittenBytes; - buffer[index++] = 0; - // Write the int value - buffer[index++] = value & 0xff; - buffer[index++] = (value >> 8) & 0xff; - buffer[index++] = (value >> 16) & 0xff; - buffer[index++] = (value >> 24) & 0xff; + const isNegativeZero = Object.is(value, -0); + + const type = + !isNegativeZero && + Number.isSafeInteger(value) && + value <= constants.BSON_INT32_MAX && + value >= constants.BSON_INT32_MIN + ? constants.BSON_DATA_INT + : constants.BSON_DATA_NUMBER; + + if (type === constants.BSON_DATA_INT) { + NUMBER_SPACE.setInt32(0, value, true); } else { - // Encode as double - buffer[index++] = constants.BSON_DATA_NUMBER; - // Number of written bytes - const numberOfWrittenBytes = ByteUtils.encodeUTF8Into(buffer, key, index); - // Encode the name - index = index + numberOfWrittenBytes; - buffer[index++] = 0; - // Write float - DV_FOR_FLOAT64.setFloat64(0, value, true); - buffer.set(SPACE_FOR_FLOAT64, index); - // Adjust index - index = index + 8; + NUMBER_SPACE.setFloat64(0, value, true); } + const bytes = + type === constants.BSON_DATA_INT ? FOUR_BYTE_VIEW_ON_NUMBER : EIGHT_BYTE_VIEW_ON_NUMBER; + + buffer[index++] = type; + + const numberOfWrittenBytes = ByteUtils.encodeUTF8Into(buffer, key, index); + index = index + numberOfWrittenBytes; + buffer[index++] = 0x00; + + buffer.set(bytes, index); + index += bytes.byteLength; + return index; } @@ -387,8 +378,8 @@ function serializeDouble(buffer: Uint8Array, key: string, value: Double, index: buffer[index++] = 0; // Write float - DV_FOR_FLOAT64.setFloat64(0, value.value, true); - buffer.set(SPACE_FOR_FLOAT64, index); + NUMBER_SPACE.setFloat64(0, value.value, true); + buffer.set(EIGHT_BYTE_VIEW_ON_NUMBER, index); // Adjust index index = index + 8; diff --git a/test/node/double.test.ts b/test/node/double.test.ts index b6fcb37b..0c751828 100644 --- a/test/node/double.test.ts +++ b/test/node/double.test.ts @@ -1,6 +1,9 @@ import * as BSON from '../register-bson'; const Double = BSON.Double; +const BSON_DOUBLE_TYPE_INDICATOR = 0x01; +const BSON_INT_TYPE_INDICATOR = 0x10; + describe('BSON Double Precision', function () { context('class Double', function () { describe('constructor()', function () { @@ -78,14 +81,15 @@ describe('BSON Double Precision', function () { }); }); - 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. + it('does preserve -0 in serialize as a double', function () { 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); + expect(type).to.equal(BSON_DOUBLE_TYPE_INDICATOR); + expect(type).to.not.equal(BSON_INT_TYPE_INDICATOR); + expect(serializedDouble.subarray(7, 15)).to.deep.equal( + new Uint8Array(new Float64Array([-0]).buffer) + ); }); describe('extended JSON', () => {