Skip to content

Commit 8305bdf

Browse files
authored
fix(NODE-3015): ObjectId.equals should use Buffer.equals for better performance (#478)
1 parent 1e705f6 commit 8305bdf

File tree

3 files changed

+115
-3
lines changed

3 files changed

+115
-3
lines changed

src/objectid.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ export class ObjectId {
212212
}
213213

214214
if (otherId instanceof ObjectId) {
215-
return this.toString() === otherId.toString();
215+
return this[kId][11] === otherId[kId][11] && this[kId].equals(otherId[kId]);
216216
}
217217

218218
if (
@@ -237,7 +237,9 @@ export class ObjectId {
237237
'toHexString' in otherId &&
238238
typeof otherId.toHexString === 'function'
239239
) {
240-
return otherId.toHexString() === this.toHexString();
240+
const otherIdString = otherId.toHexString();
241+
const thisIdString = this.toHexString().toLowerCase();
242+
return typeof otherIdString === 'string' && otherIdString.toLowerCase() === thisIdString;
241243
}
242244

243245
return false;

test/node/object_id_tests.js

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
const Buffer = require('buffer').Buffer;
44
const BSON = require('../register-bson');
55
const BSONTypeError = BSON.BSONTypeError;
6-
const util = require('util');
76
const ObjectId = BSON.ObjectId;
7+
const util = require('util');
8+
const getSymbolFrom = require('./tools/utils').getSymbolFrom;
89

910
describe('ObjectId', function () {
1011
it('should correctly handle objectId timestamps', function (done) {
@@ -289,4 +290,97 @@ describe('ObjectId', function () {
289290
new BSON.ObjectId(BSON.ObjectId.generate(farFuture / 1000)).getTimestamp().getTime()
290291
).to.equal(farFuture);
291292
});
293+
294+
describe('.equals(otherId)', () => {
295+
/*
296+
* ObjectId.equals() covers many varieties of cases passed into it In an attempt to handle ObjectId-like objects
297+
* Each test covers a corresponding if stmt in the equals method.
298+
*/
299+
const oidBytesInAString = 'kaffeeklatch';
300+
const oidString = '6b61666665656b6c61746368';
301+
const oid = new ObjectId(oidString);
302+
const oidKId = getSymbolFrom(oid, 'id');
303+
it('should return false for an undefined otherId', () => {
304+
// otherId === undefined || otherId === null
305+
expect(oid.equals(null)).to.be.false;
306+
expect(oid.equals(undefined)).to.be.false;
307+
expect(oid.equals()).to.be.false;
308+
});
309+
310+
it('should return true for another ObjectId with the same bytes', () => {
311+
// otherId instanceof ObjectId
312+
const equalOid = new ObjectId(oid.id);
313+
expect(oid.equals(equalOid)).to.be.true;
314+
});
315+
316+
it('should return true if otherId is a valid 24 char hex string', () => {
317+
// typeof otherId === 'string' && ObjectId.isValid(otherId) && otherId.length === 24
318+
const equalOid = oidString;
319+
expect(oid.equals(equalOid)).to.be.true;
320+
});
321+
322+
it('should return true if otherId is a valid 12 byte string', () => {
323+
/*
324+
typeof otherId === 'string' &&
325+
ObjectId.isValid(otherId) &&
326+
otherId.length === 12 &&
327+
isUint8Array(this.id)
328+
*/
329+
const equalOid = oidBytesInAString;
330+
expect(oid.equals(equalOid)).to.be.true;
331+
});
332+
333+
it.skip('should return true if otherId is a valid 12 byte string and oid.id is not Uint8Array', () => {
334+
// typeof otherId === 'string' && ObjectId.isValid(otherId) && otherId.length === 12
335+
// Skipped because the check inside of this if statement is incorrect, it will never return true
336+
// But it is also unreachable because of the other 12 len case checked before it
337+
const equalOid = oidBytesInAString;
338+
Object.defineProperty(oid, 'id', { value: oid.toHexString() });
339+
expect(oid.equals(equalOid)).to.be.true;
340+
});
341+
342+
it('should return true if otherId is an object with a toHexString function, regardless of casing', () => {
343+
/*
344+
typeof otherId === 'object' &&
345+
'toHexString' in otherId &&
346+
typeof otherId.toHexString === 'function'
347+
*/
348+
expect(oid.equals({ toHexString: () => oidString.toLowerCase() })).to.be.true;
349+
expect(oid.equals({ toHexString: () => oidString.toUpperCase() })).to.be.true;
350+
});
351+
352+
it('should return false if toHexString does not return a string', () => {
353+
// typeof otherIdString === 'string'
354+
355+
// Now that we call toLowerCase() make sure we guard the call with a type check
356+
expect(() => oid.equals({ toHexString: () => 100 })).to.not.throw(TypeError);
357+
expect(oid.equals({ toHexString: () => 100 })).to.be.false;
358+
});
359+
360+
it('should not rely on toString for otherIds that are instanceof ObjectId', () => {
361+
// Note: the method access the symbol prop directly instead of the getter
362+
const equalId = { toString: () => oidString + 'wrong', [oidKId]: oid.id };
363+
Object.setPrototypeOf(equalId, ObjectId.prototype);
364+
expect(oid.toString()).to.not.equal(equalId.toString());
365+
expect(oid.equals(equalId)).to.be.true;
366+
});
367+
368+
it('should use otherId[kId] Buffer for equality when otherId is instanceof ObjectId', () => {
369+
let equalId = { [oidKId]: oid.id };
370+
Object.setPrototypeOf(equalId, ObjectId.prototype);
371+
372+
const propAccessRecord = [];
373+
equalId = new Proxy(equalId, {
374+
get(target, prop, recv) {
375+
propAccessRecord.push(prop);
376+
return Reflect.get(target, prop, recv);
377+
}
378+
});
379+
380+
expect(oid.equals(equalId)).to.be.true;
381+
// once for the 11th byte shortcut
382+
// once for the total equality
383+
expect(propAccessRecord).to.deep.equal([oidKId, oidKId]);
384+
});
385+
});
292386
});

test/node/tools/utils.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,19 @@ exports.isNode6 = function () {
156156
// eslint-disable-next-line no-undef
157157
return process.version.split('.')[0] === 'v6';
158158
};
159+
160+
const getSymbolFrom = function (target, symbolName, assertExists) {
161+
if (assertExists == null) assertExists = true;
162+
163+
const symbol = Object.getOwnPropertySymbols(target).filter(
164+
s => s.toString() === `Symbol(${symbolName})`
165+
)[0];
166+
167+
if (assertExists && !symbol) {
168+
throw new Error(`Did not find Symbol(${symbolName}) on ${target}`);
169+
}
170+
171+
return symbol;
172+
};
173+
174+
exports.getSymbolFrom = getSymbolFrom;

0 commit comments

Comments
 (0)