Skip to content

Commit

Permalink
feat!(NODE-4850): serialize negative zero to double (#529)
Browse files Browse the repository at this point in the history
  • Loading branch information
nbbeeken authored Dec 5, 2022
1 parent a469e91 commit be74b30
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 45 deletions.
18 changes: 18 additions & 0 deletions docs/upgrade-to-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
```
71 changes: 31 additions & 40 deletions src/parser/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down
14 changes: 9 additions & 5 deletions test/node/double.test.ts
Original file line number Diff line number Diff line change
@@ -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 () {
Expand Down Expand Up @@ -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', () => {
Expand Down

0 comments on commit be74b30

Please sign in to comment.