Navigation Menu

Skip to content

Commit

Permalink
Fix Long bugs, and represent Longs as Numbers when possible.
Browse files Browse the repository at this point in the history
Before, any JS Number which was an integer would be serialized as an INT
if it fit in 32 bits, and as a LONG otherwise.  This broke when integral
Numbers were larger than 2^63.  Now such numbers are represented as
DOUBLEs.

Before, any Long was deserialized as a Long, which was awkward because
you'd serialize a Number and get back a Long, which is awkward to deal
with.  Now, any Long which is in [-2^53, 2^53] (a range that can be
precisely represented by a JS Number) is deserialized as a Number.
  • Loading branch information
sethml committed Jul 29, 2011
1 parent 8068cfc commit ba88962
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 26 deletions.
63 changes: 39 additions & 24 deletions lib/mongodb/bson/bson.js
Expand Up @@ -19,8 +19,12 @@ var BinaryParser = require('./binary_parser').BinaryParser
function BSON () {};

// BSON MAX VALUES
BSON.BSON_INT32_MAX = 2147483648;
BSON.BSON_INT32_MIN = -2147483648;
BSON.BSON_INT32_MAX = 0x80000000;
BSON.BSON_INT32_MIN = -0x80000000;

// JS MAX PRECISE VALUES
BSON.JS_INT_MAX = 0x20000000000000; // Any integer up to 2^53 can be precisely represented by a double.
BSON.JS_INT_MIN = -0x20000000000000; // Any integer down to -2^53 can be precisely represented by a double.

// BSON DATA TYPES
BSON.BSON_DATA_NUMBER = 1;
Expand Down Expand Up @@ -85,14 +89,14 @@ BSON.calculateObjectSize = function(object) {
stack.push({keys:keys, object:currentObject});
currentObject = value;
keys = Object.keys(value)
} else if(typeof value == 'number' && value === parseInt(value, 10)) {
if(value >= BSON.BSON_INT32_MAX || value < BSON.BSON_INT32_MIN) {
} else if(typeof value == 'number' || toString.call(value) === '[object Number]') {
if(value >= BSON.BSON_INT32_MAX || value < BSON.BSON_INT32_MIN ||
value !== parseInt(value, 10)) {
// Long and Number take same number of bytes.
totalLength += (name != null ? (Buffer.byteLength(name) + 1) : 0) + (8 + 1);
} else {
totalLength += (name != null ? (Buffer.byteLength(name) + 1) : 0) + (4 + 1);
}
} else if(typeof value == 'number' || toString.call(value) === '[object Number]') {
totalLength += (name != null ? (Buffer.byteLength(name) + 1) : 0) + (8 + 1);
} else if(typeof value == 'boolean' || toString.call(value) === '[object Boolean]') {
totalLength += (name != null ? (Buffer.byteLength(name) + 1) : 0) + (1 + 1);
} else if(value instanceof Date) {
Expand Down Expand Up @@ -246,28 +250,27 @@ BSON.serializeWithBufferAndIndex = function serializeWithBufferAndIndex(object,
index = index + size - 1;
// Write zero
buffer[index++] = 0;
} else if(typeof value == 'number' && value === parseInt(value, 10)) {
} else if((typeof value == 'number' || toString.call(value) === '[object Number]') &&
value === parseInt(value, 10) &&
value >= BSON.JS_INT_MIN && value <= BSON.JS_INT_MAX) {
// Write the type
buffer[index++] = value >= BSON.BSON_INT32_MAX || value < BSON.BSON_INT32_MIN ? BSON.BSON_DATA_LONG : BSON.BSON_DATA_INT;
var int64 = value >= BSON.BSON_INT32_MAX || value < BSON.BSON_INT32_MIN;
buffer[index++] = int64 ? BSON.BSON_DATA_LONG : BSON.BSON_DATA_INT;
// Write the name
if(name != null) {
index = index + buffer.write(name, index, 'utf8') + 1;
buffer[index - 1] = 0;
}

if(value >= BSON.BSON_INT32_MAX || value < BSON.BSON_INT32_MIN) {
if(int64) {
// Write the number
var long = Long.fromNumber(value);
binaryutils.encodeIntInPlace(long.getLowBits(), buffer, index);
binaryutils.encodeIntInPlace(long.getHighBits(), buffer, index + 4);
index += 8;
} else {
// Write the int value to the buffer
buffer[index + 3] = (value >> 24) & 0xff;
buffer[index + 2] = (value >> 16) & 0xff;
buffer[index + 1] = (value >> 8) & 0xff;
buffer[index] = value & 0xff;
// Ajust the index
binaryutils.encodeIntInPlace(value, buffer, index);
index = index + 4;
}
} else if(typeof value == 'number' || toString.call(value) === '[object Number]') {
Expand Down Expand Up @@ -636,7 +639,9 @@ BSON.serialize = function serialize(object, checkKeys, asBuffer) {
stack.push({keys:keys, object:currentObject});
currentObject = value;
keys = Object.keys(value)
} else if(typeof value == 'number' && value === parseInt(value, 10)) {
} else if((typeof value == 'number' || toString.call(value) === '[object Number]') &&
value === parseInt(value, 10) &&
value >= BSON.JS_INT_MIN && value <= BSON.JS_INT_MAX) {
if(value >= BSON.BSON_INT32_MAX || value < BSON.BSON_INT32_MIN) {
totalLength += (name != null ? (Buffer.byteLength(name) + 1) : 0) + (8 + 1);
} else {
Expand Down Expand Up @@ -796,28 +801,27 @@ BSON.serialize = function serialize(object, checkKeys, asBuffer) {
index = index + size - 1;
// Write zero
buffer[index++] = 0;
} else if(typeof value == 'number' && value === parseInt(value, 10)) {
} else if((typeof value == 'number' || toString.call(value) === '[object Number]') &&
value === parseInt(value, 10) &&
value >= BSON.JS_INT_MIN && value <= BSON.JS_INT_MAX) {
// Write the type
buffer[index++] = value >= BSON.BSON_INT32_MAX || value < BSON.BSON_INT32_MIN ? BSON.BSON_DATA_LONG : BSON.BSON_DATA_INT;
var int64 = value >= BSON.BSON_INT32_MAX || value < BSON.BSON_INT32_MIN;
buffer[index++] = int64 ? BSON.BSON_DATA_LONG : BSON.BSON_DATA_INT;
// Write the name
if(name != null) {
index = index + buffer.write(name, index, 'utf8') + 1;
buffer[index - 1] = 0;
}

if(value >= BSON.BSON_INT32_MAX || value < BSON.BSON_INT32_MIN) {
if(int64) {
// Write the number
var long = Long.fromNumber(value);
binaryutils.encodeIntInPlace(long.getLowBits(), buffer, index);
binaryutils.encodeIntInPlace(long.getHighBits(), buffer, index + 4);
index += 8;
} else {
// Write the int value to the buffer
buffer[index + 3] = (value >> 24) & 0xff;
buffer[index + 2] = (value >> 16) & 0xff;
buffer[index + 1] = (value >> 8) & 0xff;
buffer[index] = value & 0xff;
// Ajust the index
binaryutils.encodeIntInPlace(value, buffer, index);
index = index + 4;
}
} else if(typeof value == 'number' || toString.call(value) === '[object Number]') {
Expand Down Expand Up @@ -1423,7 +1427,18 @@ BSON.deserialize = function(data) {
var high_bits = data[index] | data[index + 1] << 8 | data[index + 2] << 16 | data[index + 3] << 24;
// Adjust index
index = index + 4;
var value = type === BSON.BSON_DATA_LONG ? new Long(low_bits, high_bits) : new Timestamp(low_bits, high_bits);
var value;
if (type === BSON.BSON_DATA_LONG) {
value = new Long(low_bits, high_bits);
// Tricky: if this value is in [-2^53, 2^53], then it's perfectly
// representable as a Number.
if ((high_bits < 0x200000 || (high_bits === 0x200000 && low_bits === 0)) &&
high_bits >= -0x200000) {
value = value.toNumber();
}
} else {
value = new Timestamp(low_bits, high_bits);
}

// Set object property
currentObject[Array.isArray(currentObject) ? parseInt(string_name, 10) : string_name] = value;
Expand Down
38 changes: 36 additions & 2 deletions test/bson/bson_test.js
Expand Up @@ -307,7 +307,7 @@ var tests = testCase({
var doc = {doc:2147483648};
var serialized_data = BSONSE.BSON.serialize(doc, false, true);
var doc2 = BSONDE.BSON.deserialize(serialized_data);
test.deepEqual(doc.doc, doc2.doc.toNumber())
test.deepEqual(doc.doc, doc2.doc)
test.done();
},

Expand All @@ -328,6 +328,40 @@ var tests = testCase({
test.deepEqual(doc.doc, deserialized_data.doc);
test.done();
},

'Should Deserialize Large Integers as Number not Long' : function(test) {
function roundTrip(val) {
var doc = {doc: val};
var serialized_data = BSONSE.BSON.serialize(doc, false, true);
var deserialized_data = BSONDE.BSON.deserialize(serialized_data);
test.deepEqual(doc.doc, deserialized_data.doc);
};

roundTrip(Math.pow(2,52));
roundTrip(Math.pow(2,53) - 1);
roundTrip(Math.pow(2,53));
roundTrip(-Math.pow(2,52));
roundTrip(-Math.pow(2,53) + 1);
roundTrip(-Math.pow(2,53));
roundTrip(Math.pow(2,65)); // Too big for Long.
roundTrip(-Math.pow(2,65));
roundTrip(1234567890123456800); // Bigger than 2^53, stays a double.
roundTrip(-1234567890123456800);
test.done();
},

'Should Deserialize Larger Integers as Long not Number' : function(test) {
function roundTrip(val) {
var doc = {doc: val};
var serialized_data = BSONSE.BSON.serialize(doc, false, true);
var deserialized_data = BSONDE.BSON.deserialize(serialized_data);
test.deepEqual(doc.doc, deserialized_data.doc);
};

roundTrip(Long.fromNumber(Math.pow(2,53)).add(Long.ONE));
roundTrip(Long.fromNumber(-Math.pow(2,53)).subtract(Long.ONE));
test.done();
},

'Should Correctly Serialize and Deserialize Long Integer and Timestamp as different types' : function(test) {
var long = Long.fromNumber(9223372036854775807);
Expand Down Expand Up @@ -594,4 +628,4 @@ var tests = testCase({
});

// Assign out tests
module.exports = tests;
module.exports = tests;

0 comments on commit ba88962

Please sign in to comment.