Skip to content

Commit

Permalink
fix(NODE-3376): use standard JS methods for copying Buffers (#444)
Browse files Browse the repository at this point in the history
  • Loading branch information
addaleax authored Jun 25, 2021
1 parent f5d984d commit 804050d
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 18 deletions.
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:/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;
}
}
});
});
}
}
});

0 comments on commit 804050d

Please sign in to comment.