Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/parser/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,10 @@ function serializeObjectId(
// Write the objectId into the shared buffer
if (typeof value.id === 'string') {
buffer.write(value.id, index, undefined, 'binary');
} else if (value.id && value.id.copy) {
value.id.copy(buffer, index, 0, 12);
} else if (isUint8Array(value.id)) {
// Use the standard JS methods here because buffer.copy() is buggy with the
// browser polyfill
buffer.set(value.id.subarray(0, 12), index);
} else {
throw new TypeError('object [' + JSON.stringify(value) + '] is not a valid ObjectId');
}
Expand Down Expand Up @@ -406,7 +408,9 @@ function serializeDecimal128(
index = index + numberOfWrittenBytes;
buffer[index++] = 0;
// Write the data from the value
value.bytes.copy(buffer, index, 0, 16);
// Prefer the standard JS methods because their typechecking is not buggy,
// unlike the `buffer` polyfill's.
buffer.set(value.bytes.subarray(0, 16), index);
return index + 16;
}

Expand Down
175 changes: 160 additions & 15 deletions test/bson_older_versions_tests.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
'use strict';

const newBSON = require('./register-bson');
const currentNodeBSON = require('./register-bson');
const vm = require('vm');
const fs = require('fs');
const fetch = require('node-fetch').default;
const rimraf = require('rimraf');
const cp = require('child_process');

/*
* This file tests that previous versions of BSON
* serialize and deserialize correctly in the most recent version of BSON
* serialize and deserialize correctly in the most recent version of BSON,
* and that the different distributions (browser, Node.js, etc.) of the
* most recent version are mutually compatible as well.
*
* This is an unusual situation to run into as users should be using one BSON lib version
* but it does arise with sub deps etc. and we wish to protect against unexpected behavior
Expand Down Expand Up @@ -39,14 +42,14 @@ function downloadZip(version, done) {
});
}

describe('Current version', function () {
describe('Mutual version and distribution compatibility', function () {
OLD_VERSIONS.forEach(version => {
before(function (done) {
this.timeout(30000); // Downloading may take a few seconds.
if (Number(process.version.split('.')[0].substring(1)) < 8) {
// WHATWG fetch doesn't download correctly prior to node 8
// but we should be safe by testing on node 8 +
this.skip();
return done();
}
if (fs.existsSync(`bson-${version}.zip`)) {
fs.unlinkSync(`bson-${version}.zip`);
Expand All @@ -73,18 +76,160 @@ describe('Current version', function () {
done();
});
});
});

it(`serializes correctly against ${version} Binary class`, function () {
const oldBSON = require(getImportPath(version));
const binFromNew = {
binary: new newBSON.Binary('aaaa')
};
const binFromOld = {
binary: new oldBSON.Binary('aaaa')
};
expect(oldBSON.prototype.serialize(binFromNew).toString('hex')).to.equal(
newBSON.serialize(binFromOld).toString('hex')
// Node.js requires an .mjs filename extension for loading ES modules.
before(() => {
try {
fs.writeFileSync(
'./bson.browser.esm.mjs',
fs.readFileSync(__dirname + '/../dist/bson.browser.esm.js')
);
});
fs.writeFileSync('./bson.esm.mjs', fs.readFileSync(__dirname + '/../dist/bson.esm.js'));
} catch (e) {
// bundling fails in CI on Windows, no idea why, hence also the
// process.platform !== 'win32' check below
}
});

after(() => {
try {
fs.unlinkSync('./bson.browser.esm.mjs');
fs.unlinkSync('./bson.esm.mjs');
} catch (e) {
// ignore
}
});

const variants = OLD_VERSIONS.map(version => ({
name: `legacy ${version}`,
load: () => {
const bson = require(getImportPath(version));
bson.serialize = bson.prototype.serialize;
bson.deserialize = bson.prototype.deserialize;
return Promise.resolve(bson);
},
legacy: true
})).concat([
{
name: 'Node.js lib/bson',
load: () => Promise.resolve(currentNodeBSON)
},
{
name: 'Browser ESM',
// eval because import is a syntax error in earlier Node.js versions
// that are still supported in CI
load: () => eval(`import("${__dirname}/../bson.browser.esm.mjs")`),
usesBufferPolyfill: true
},
{
name: 'Browser UMD',
load: () => Promise.resolve(require('../dist/bson.browser.umd.js')),
usesBufferPolyfill: true
},
{
name: 'Generic bundle',
load: () => {
const source = fs.readFileSync(__dirname + '/../dist/bson.bundle.js', 'utf8');
return Promise.resolve(vm.runInNewContext(`${source}; BSON`, { global, process }));
},
usesBufferPolyfill: true
},
{
name: 'Node.js ESM',
load: () => eval(`import("${__dirname}/../bson.esm.mjs")`)
}
]);

const makeObjects = bson => [
new bson.ObjectId('5f16b8bebe434dc98cdfc9ca'),
new bson.DBRef('a', new bson.ObjectId('5f16b8bebe434dc98cdfc9cb'), 'db'),
new bson.MinKey(),
new bson.MaxKey(),
new bson.Timestamp(1, 100),
new bson.Code('abc'),
bson.Decimal128.fromString('1'),
bson.Long.fromString('1'),
new bson.Binary(Buffer.from('abcäbc🎉'), 128),
new Date('2021-05-04T15:49:33.000Z'),
/match/
];

for (const from of variants) {
for (const to of variants) {
describe(`serializing objects from ${from.name} using ${to.name}`, () => {
let fromObjects;
let fromBSON;
let toBSON;

before(function () {
// Load the from/to BSON versions asynchronously because e.g. ESM
// requires asynchronous loading.
return Promise.resolve()
.then(() => {
return from.load();
})
.then(loaded => {
fromBSON = loaded;
return to.load();
})
.then(loaded => {
toBSON = loaded;
})
.then(
() => {
fromObjects = makeObjects(fromBSON);
},
err => {
if (+process.version.slice(1).split('.')[0] >= 12 && process.platform !== 'win32') {
throw err; // On Node.js 12+, all loading is expected to work.
} else {
this.skip(); // Otherwise, e.g. ESM can't be loaded, so just skip.
}
}
);
});

it('serializes in a compatible way', function () {
for (const object of fromObjects) {
// If the object in question uses Buffers in its serialization, and
// its Buffer was created using the polyfill, and we're serializing
// using a legacy version that uses buf.copy(), then that fails
// because the Buffer polyfill's typechecking is buggy, so we
// skip these cases.
// This is tracked as https://jira.mongodb.org/browse/NODE-2848
// and would be addressed by https://github.com/feross/buffer/pull/285
// if that is merged at some point.
if (
from.usesBufferPolyfill &&
to.legacy &&
['ObjectId', 'Decimal128', 'DBRef', 'Binary'].includes(object.constructor.name)
) {
continue;
}

try {
// Check that both BSON versions serialize to equal Buffers
expect(toBSON.serialize({ object }).toString('hex')).to.equal(
fromBSON.serialize({ object }).toString('hex')
);
if (!from.legacy) {
// Check that serializing using one version and deserializing using
// the other gives back the original object.
const cloned = fromBSON.deserialize(toBSON.serialize({ object })).object;
expect(fromBSON.EJSON.serialize(cloned)).to.deep.equal(
fromBSON.EJSON.serialize(object)
);
}
} catch (err) {
// If something fails, note the object type in the error message
// for easier debugging.
err.message += ` (${object.constructor.name})`;
throw err;
}
}
});
});
}
}
});