Skip to content

Commit 54ca603

Browse files
fix(NODE-3630): remove float parser and test edge cases for Double (#502)
1 parent 4bda57d commit 54ca603

File tree

6 files changed

+130
-185
lines changed

6 files changed

+130
-185
lines changed

etc/benchmarks/main.mjs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ console.log();
99

1010
////////////////////////////////////////////////////////////////////////////////////////////////////
1111
await runner({
12-
skip: false,
12+
skip: true,
1313
name: 'deserialize({ oid, string }, { validation: { utf8: false } })',
1414
iterations,
1515
setup(libs) {
@@ -58,6 +58,46 @@ await runner({
5858
}
5959
});
6060

61+
await runner({
62+
skip: true,
63+
name: 'Double Serialization',
64+
iterations,
65+
run(i, bson) {
66+
bson.lib.serialize({ d: 2.3 });
67+
}
68+
});
69+
70+
await runner({
71+
skip: false,
72+
name: 'Double Deserialization',
73+
iterations,
74+
setup(libs) {
75+
const bson = getCurrentLocalBSON(libs);
76+
return bson.lib.serialize({ d: 2.3 });
77+
},
78+
run(i, bson, serialized_double) {
79+
bson.lib.deserialize(serialized_double);
80+
}
81+
});
82+
83+
await runner({
84+
skip: false,
85+
name: 'Many Doubles Deserialization',
86+
iterations,
87+
setup(libs) {
88+
const bson = getCurrentLocalBSON(libs);
89+
let doubles = Object.fromEntries(
90+
Array.from({ length: 1000 }, i => {
91+
return [`a_${i}`, 2.3];
92+
})
93+
);
94+
return bson.lib.serialize(doubles);
95+
},
96+
run(i, bson, serialized_doubles) {
97+
bson.lib.deserialize(serialized_doubles);
98+
}
99+
});
100+
61101
// End
62102
console.log(
63103
'Total time taken to benchmark:',

src/float_parser.ts

Lines changed: 0 additions & 152 deletions
This file was deleted.

src/parser/deserializer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ function deserializeObject(
197197
let isPossibleDBRef = isArray ? false : null;
198198

199199
// While we have more left data left keep parsing
200+
const dataview = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
200201
while (!done) {
201202
// Read the type
202203
const elementType = buffer[index++];
@@ -263,10 +264,10 @@ function deserializeObject(
263264
(buffer[index++] << 16) |
264265
(buffer[index++] << 24);
265266
} else if (elementType === constants.BSON_DATA_NUMBER && promoteValues === false) {
266-
value = new Double(buffer.readDoubleLE(index));
267+
value = new Double(dataview.getFloat64(index, true));
267268
index = index + 8;
268269
} else if (elementType === constants.BSON_DATA_NUMBER) {
269-
value = buffer.readDoubleLE(index);
270+
value = dataview.getFloat64(index, true);
270271
index = index + 8;
271272
} else if (elementType === constants.BSON_DATA_DATE) {
272273
const lowBits =

src/parser/serializer.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import type { Double } from '../double';
99
import { ensureBuffer } from '../ensure_buffer';
1010
import { BSONError, BSONTypeError } from '../error';
1111
import { isBSONType } from '../extended_json';
12-
import { writeIEEE754 } from '../float_parser';
1312
import type { Int32 } from '../int_32';
1413
import { Long } from '../long';
1514
import { Map } from '../map';
@@ -79,6 +78,12 @@ function serializeString(
7978
return index;
8079
}
8180

81+
const SPACE_FOR_FLOAT64 = new Uint8Array(8);
82+
const DV_FOR_FLOAT64 = new DataView(
83+
SPACE_FOR_FLOAT64.buffer,
84+
SPACE_FOR_FLOAT64.byteOffset,
85+
SPACE_FOR_FLOAT64.byteLength
86+
);
8287
function serializeNumber(
8388
buffer: Buffer,
8489
key: string,
@@ -119,7 +124,8 @@ function serializeNumber(
119124
index = index + numberOfWrittenBytes;
120125
buffer[index++] = 0;
121126
// Write float
122-
writeIEEE754(buffer, value, index, 'little', 52, 8);
127+
DV_FOR_FLOAT64.setFloat64(0, value, true);
128+
buffer.set(SPACE_FOR_FLOAT64, index);
123129
// Adjust index
124130
index = index + 8;
125131
}
@@ -487,7 +493,8 @@ function serializeDouble(
487493
buffer[index++] = 0;
488494

489495
// Write float
490-
writeIEEE754(buffer, value.value, index, 'little', 52, 8);
496+
DV_FOR_FLOAT64.setFloat64(0, value.value, true);
497+
buffer.set(SPACE_FOR_FLOAT64, index);
491498

492499
// Adjust index
493500
index = index + 8;

test/node/bson_corpus.spec.test.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,6 @@ describe('BSON Corpus', function () {
121121
describe('valid-bson', function () {
122122
for (const v of valid) {
123123
it(v.description, function () {
124-
if (v.description === 'NaN with payload') {
125-
// TODO(NODE-3630): remove custom float parser so we can handle the NaN payload data
126-
this.skip();
127-
}
128-
129124
if (
130125
v.description === 'All BSON types' &&
131126
scenario._filename === 'multi-type-deprecated'

test/node/double_tests.js

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,90 @@
33
const BSON = require('../register-bson');
44
const Double = BSON.Double;
55

6-
describe('Double', function () {
7-
describe('Constructor', function () {
8-
var value = 42.3456;
6+
describe('BSON Double Precision', function () {
7+
context('class Double', function () {
8+
describe('constructor()', function () {
9+
const value = 42.3456;
910

10-
it('Primitive number', function (done) {
11-
expect(new Double(value).valueOf()).to.equal(value);
12-
done();
11+
it('Primitive number', function () {
12+
expect(new Double(value).valueOf()).to.equal(value);
13+
});
14+
15+
it('Number object', function () {
16+
expect(new Double(new Number(value)).valueOf()).to.equal(value);
17+
});
1318
});
1419

15-
it('Number object', function (done) {
16-
expect(new Double(new Number(value)).valueOf()).to.equal(value);
17-
done();
20+
describe('#toString()', () => {
21+
it('should serialize to a string', () => {
22+
const testNumber = Math.random() * Number.MAX_VALUE;
23+
const double = new Double(testNumber);
24+
expect(double.toString()).to.equal(testNumber.toString());
25+
});
26+
27+
const testRadices = [2, 8, 10, 16, 22];
28+
29+
for (const radix of testRadices) {
30+
it(`should support radix argument: ${radix}`, () => {
31+
const testNumber = Math.random() * Number.MAX_VALUE;
32+
const double = new Double(testNumber);
33+
expect(double.toString(radix)).to.equal(testNumber.toString(radix));
34+
});
35+
}
1836
});
1937
});
2038

21-
describe('toString', () => {
22-
it('should serialize to a string', () => {
23-
const testNumber = Math.random() * Number.MAX_VALUE;
24-
const double = new Double(testNumber);
25-
expect(double.toString()).to.equal(testNumber.toString());
39+
function serializeThenDeserialize(value) {
40+
const serializedDouble = BSON.serialize({ d: value });
41+
const deserializedDouble = BSON.deserialize(serializedDouble, { promoteValues: false });
42+
return deserializedDouble.d;
43+
}
44+
45+
const testCases = [
46+
{ name: 'Infinity', doubleVal: new Double(Infinity), testVal: Infinity },
47+
{ name: '-Infinity', doubleVal: new Double(-Infinity), testVal: -Infinity },
48+
{ name: 'Number.EPSILON', doubleVal: new Double(Number.EPSILON), testVal: Number.EPSILON },
49+
{ name: 'Zero', doubleVal: new Double(0), testVal: 0 },
50+
{ name: 'Negative Zero', doubleVal: new Double(-0), testVal: -0 },
51+
{ name: 'NaN', doubleVal: new Double(NaN), testVal: NaN }
52+
];
53+
54+
for (const { name, doubleVal, testVal } of testCases) {
55+
it(`should preserve the input value ${name} in Double serialize-deserialize roundtrip`, () => {
56+
const roundTrippedVal = serializeThenDeserialize(doubleVal);
57+
expect(Object.is(doubleVal.value, testVal)).to.be.true;
58+
expect(Object.is(roundTrippedVal.value, doubleVal.value)).to.be.true;
2659
});
60+
}
2761

28-
const testRadices = [2, 8, 10, 16, 22];
62+
context('NaN with Payload', function () {
63+
const NanPayloadBuffer = Buffer.from('120000000000F87F', 'hex');
64+
const NanPayloadDV = new DataView(
65+
NanPayloadBuffer.buffer,
66+
NanPayloadBuffer.byteOffset,
67+
NanPayloadBuffer.byteLength
68+
);
69+
const NanPayloadDouble = NanPayloadDV.getFloat64(0, true);
70+
// Using promoteValues: false (returning raw BSON) in order to be able to check that payload remains intact
71+
const serializedNanPayloadDouble = BSON.serialize({ d: NanPayloadDouble });
2972

30-
for (const radix of testRadices) {
31-
it(`should support radix argument: ${radix}`, () => {
32-
const testNumber = Math.random() * Number.MAX_VALUE;
33-
const double = new Double(testNumber);
34-
expect(double.toString(radix)).to.equal(testNumber.toString(radix));
35-
});
36-
}
73+
it('should keep payload in serialize-deserialize roundtrip', function () {
74+
expect(serializedNanPayloadDouble.subarray(7, 15)).to.deep.equal(NanPayloadBuffer);
75+
});
76+
77+
it('should preserve NaN value in serialize-deserialize roundtrip', function () {
78+
const { d: newVal } = BSON.deserialize(serializedNanPayloadDouble, { promoteValues: true });
79+
expect(newVal).to.be.NaN;
80+
});
81+
});
82+
83+
it('NODE-4335: does not preserve -0 in serialize-deserialize roundtrip if JS number is used', function () {
84+
// TODO (NODE-4335): -0 should be serialized as double
85+
// 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.
86+
const value = -0;
87+
const serializedDouble = BSON.serialize({ d: value });
88+
const type = serializedDouble[4];
89+
expect(type).to.not.equal(BSON.BSON_DATA_NUMBER);
90+
expect(type).to.equal(BSON.BSON_DATA_INT);
3791
});
3892
});

0 commit comments

Comments
 (0)