diff --git a/lib/internal/util/comparisons.js b/lib/internal/util/comparisons.js index 0e58ea2cb4e024..9f636f31a3d629 100644 --- a/lib/internal/util/comparisons.js +++ b/lib/internal/util/comparisons.js @@ -5,32 +5,52 @@ const { isArrayBufferView } = require('internal/util/types'); const { internalBinding } = require('internal/bootstrap/loaders'); const { isDate, isMap, isRegExp, isSet } = internalBinding('types'); -function objectToString(o) { - return Object.prototype.toString.call(o); +const ReflectApply = Reflect.apply; + +function uncurryThis(func) { + return (thisArg, ...args) => ReflectApply(func, thisArg, args); } +const kStrict = true; +const kLoose = false; + +const kNoIterator = 0; +const kIsArray = 1; +const kIsSet = 2; +const kIsMap = 3; + +const objectToString = uncurryThis(Object.prototype.toString); +const hasOwnProperty = uncurryThis(Object.prototype.hasOwnProperty); +const propertyIsEnumerable = uncurryThis(Object.prototype.propertyIsEnumerable); + +const objectKeys = Object.keys; +const getPrototypeOf = Object.getPrototypeOf; +const getOwnPropertySymbols = Object.getOwnPropertySymbols; +const objectIs = Object.is; +const numberIsNaN = Number.isNaN; + // Check if they have the same source and flags function areSimilarRegExps(a, b) { return a.source === b.source && a.flags === b.flags; } -// For small buffers it's faster to compare the buffer in a loop. The c++ -// barrier including the Uint8Array operation takes the advantage of the faster -// binary compare otherwise. The break even point was at about 300 characters. -function areSimilarTypedArrays(a, b, max) { - const len = a.byteLength; - if (len !== b.byteLength) { +function areSimilarFloatArrays(a, b) { + if (a.byteLength !== b.byteLength) { return false; } - if (len < max) { - for (var offset = 0; offset < len; offset++) { - if (a[offset] !== b[offset]) { - return false; - } + for (var offset = 0; offset < a.byteLength; offset++) { + if (a[offset] !== b[offset]) { + return false; } - return true; } - return compare(new Uint8Array(a.buffer, a.byteOffset, len), + return true; +} + +function areSimilarTypedArrays(a, b) { + if (a.byteLength !== b.byteLength) { + return false; + } + return compare(new Uint8Array(a.buffer, a.byteOffset, a.byteLength), new Uint8Array(b.buffer, b.byteOffset, b.byteLength)) === 0; } @@ -66,8 +86,8 @@ function isObjectOrArrayTag(tag) { // b) The same prototypes. function strictDeepEqual(val1, val2, memos) { if (typeof val1 !== 'object') { - return typeof val1 === 'number' && Number.isNaN(val1) && - Number.isNaN(val2); + return typeof val1 === 'number' && numberIsNaN(val1) && + numberIsNaN(val2); } if (typeof val2 !== 'object' || val1 === null || val2 === null) { return false; @@ -78,21 +98,38 @@ function strictDeepEqual(val1, val2, memos) { if (val1Tag !== val2Tag) { return false; } - if (Object.getPrototypeOf(val1) !== Object.getPrototypeOf(val2)) { + if (getPrototypeOf(val1) !== getPrototypeOf(val2)) { return false; } if (val1Tag === '[object Array]') { // Check for sparse arrays and general fast path - if (val1.length !== val2.length) + if (val1.length !== val2.length) { + return false; + } + const keys = objectKeys(val1); + if (keys.length !== objectKeys(val2).length) { return false; - // Skip testing the part below and continue with the keyCheck. - return keyCheck(val1, val2, true, memos); + } + // Fast path for non sparse arrays (no key comparison for indices + // properties). + // See https://tc39.github.io/ecma262/#sec-ordinaryownpropertykeys + if (val1.length === keys.length) { + if (keys.length === 0 || keys[val1.length - 1] === `${val1.length - 1}`) { + return keyCheck(val1, val2, kStrict, memos, kIsArray, []); + } + } else if (keys.length > val1.length && + keys[val1.length - 1] === `${val1.length - 1}`) { + const minimalKeys = keys.slice(val1.length); + return keyCheck(val1, val2, kStrict, memos, kIsArray, minimalKeys); + } + // Only set this to kIsArray in case the array is not sparse! + return keyCheck(val1, val2, kStrict, memos, kNoIterator, keys); } if (val1Tag === '[object Object]') { - // Skip testing the part below and continue with the keyCheck. - return keyCheck(val1, val2, true, memos); + return keyCheck(val1, val2, kStrict, memos, kNoIterator); } if (isDate(val1)) { + // TODO: Make these safe. if (val1.getTime() !== val2.getTime()) { return false; } @@ -108,35 +145,52 @@ function strictDeepEqual(val1, val2, memos) { return false; } } else if (isArrayBufferView(val1)) { - if (!areSimilarTypedArrays(val1, val2, - isFloatTypedArrayTag(val1Tag) ? 0 : 300)) { + if (!areSimilarTypedArrays(val1, val2)) { return false; } // Buffer.compare returns true, so val1.length === val2.length - // if they both only contain numeric keys, we don't need to exam further - return keyCheck(val1, val2, true, memos, val1.length, - val2.length); + // if they both only contain numeric keys, we don't need to exam further. + const keys = objectKeys(val1); + if (keys.length !== objectKeys(val2).length) { + return false; + } + if (keys.length === val1.length) { + return keyCheck(val1, val2, kStrict, memos, kNoIterator, []); + } + // Only compare the special keys. + const minimalKeys = keys.slice(val1.length); + return keyCheck(val1, val2, kStrict, memos, kNoIterator, minimalKeys); + } else if (isSet(val1)) { + if (!isSet(val2) || val1.size !== val2.size) { + return false; + } + return keyCheck(val1, val2, kStrict, memos, kIsSet); + } else if (isMap(val1)) { + if (!isMap(val2) || val1.size !== val2.size) { + return false; + } + return keyCheck(val1, val2, kStrict, memos, kIsMap); + // TODO: Make the valueOf checks safe. } else if (typeof val1.valueOf === 'function') { const val1Value = val1.valueOf(); - // Note: Boxed string keys are going to be compared again by Object.keys if (val1Value !== val1) { if (typeof val2.valueOf !== 'function') { return false; } if (!innerDeepEqual(val1Value, val2.valueOf(), true)) return false; - // Fast path for boxed primitives - var lengthval1 = 0; - var lengthval2 = 0; + // Fast path for boxed primitive strings. if (typeof val1Value === 'string') { - lengthval1 = val1.length; - lengthval2 = val2.length; + const keys = objectKeys(val1); + if (keys.length !== objectKeys(val2).length) { + return false; + } + const minimalKeys = keys.slice(val1.length); + return keyCheck(val1, val2, kStrict, memos, kNoIterator, minimalKeys); } - return keyCheck(val1, val2, true, memos, lengthval1, - lengthval2); } } - return keyCheck(val1, val2, true, memos); + return keyCheck(val1, val2, kStrict, memos, kNoIterator); } function looseDeepEqual(val1, val2, memos) { @@ -150,33 +204,54 @@ function looseDeepEqual(val1, val2, memos) { if (val2 === null || typeof val2 !== 'object') { return false; } - if (isDate(val1) && isDate(val2)) { - return val1.getTime() === val2.getTime(); - } - if (isRegExp(val1) && isRegExp(val2)) { - return areSimilarRegExps(val1, val2); - } - if (val1 instanceof Error && val2 instanceof Error) { - if (val1.message !== val2.message || val1.name !== val2.name) - return false; - } const val1Tag = objectToString(val1); const val2Tag = objectToString(val2); if (val1Tag === val2Tag) { - if (!isObjectOrArrayTag(val1Tag) && isArrayBufferView(val1)) { - return areSimilarTypedArrays(val1, val2, - isFloatTypedArrayTag(val1Tag) ? - Infinity : 300); + if (isObjectOrArrayTag(val1Tag)) { + return keyCheck(val1, val2, kLoose, memos, kNoIterator); + } + if (isArrayBufferView(val1)) { + if (isFloatTypedArrayTag(val1Tag)) { + return areSimilarFloatArrays(val1, val2); + } + return areSimilarTypedArrays(val1, val2); + } + if (isDate(val1) && isDate(val2)) { + return val1.getTime() === val2.getTime(); + } + if (isRegExp(val1) && isRegExp(val2)) { + return areSimilarRegExps(val1, val2); + } + if (val1 instanceof Error && val2 instanceof Error) { + if (val1.message !== val2.message || val1.name !== val2.name) + return false; } // Ensure reflexivity of deepEqual with `arguments` objects. // See https://github.com/nodejs/node-v0.x-archive/pull/7178 } else if (isArguments(val1Tag) || isArguments(val2Tag)) { return false; } - return keyCheck(val1, val2, false, memos); + if (isSet(val1)) { + if (!isSet(val2) || val1.size !== val2.size) { + return false; + } + return keyCheck(val1, val2, kLoose, memos, kIsSet); + } else if (isMap(val1)) { + if (!isMap(val2) || val1.size !== val2.size) { + return false; + } + return keyCheck(val1, val2, kLoose, memos, kIsMap); + } else if (isSet(val2) || isMap(val2)) { + return false; + } + return keyCheck(val1, val2, kLoose, memos, kNoIterator); +} + +function getEnumerables(val, keys) { + return keys.filter((k) => propertyIsEnumerable(val, k)); } -function keyCheck(val1, val2, strict, memos, lengthA, lengthB) { +function keyCheck(val1, val2, strict, memos, iterationType, aKeys) { // For all remaining Object pairs, including Array, objects and Maps, // equivalence is determined by having: // a) The same number of owned enumerable properties @@ -184,50 +259,59 @@ function keyCheck(val1, val2, strict, memos, lengthA, lengthB) { // c) Equivalent values for every corresponding key/index // d) For Sets and Maps, equal contents // Note: this accounts for both named and indexed properties on Arrays. - var aKeys = Object.keys(val1); - var bKeys = Object.keys(val2); - var i; + if (arguments.length === 5) { + aKeys = objectKeys(val1); + const bKeys = objectKeys(val2); - // The pair must have the same number of owned properties. - if (aKeys.length !== bKeys.length) - return false; + // The pair must have the same number of owned properties. + if (aKeys.length !== bKeys.length) { + return false; + } + } + + // Cheap key test + let i = 0; + for (; i < aKeys.length; i++) { + if (!hasOwnProperty(val2, aKeys[i])) { + return false; + } + } if (strict) { - var symbolKeysA = Object.getOwnPropertySymbols(val1); - var symbolKeysB = Object.getOwnPropertySymbols(val2); + const symbolKeysA = getOwnPropertySymbols(val1); if (symbolKeysA.length !== 0) { - symbolKeysA = symbolKeysA.filter((k) => - propertyIsEnumerable.call(val1, k)); - symbolKeysB = symbolKeysB.filter((k) => - propertyIsEnumerable.call(val2, k)); - if (symbolKeysA.length !== symbolKeysB.length) + let count = 0; + for (i = 0; i < symbolKeysA.length; i++) { + const key = symbolKeysA[i]; + if (propertyIsEnumerable(val1, key)) { + if (!propertyIsEnumerable(val2, key)) { + return false; + } + aKeys.push(key); + count++; + } else if (propertyIsEnumerable(val2, key)) { + return false; + } + } + const symbolKeysB = getOwnPropertySymbols(val2); + if (symbolKeysA.length !== symbolKeysB.length && + getEnumerables(val2, symbolKeysB).length !== count) { return false; - } else if (symbolKeysB.length !== 0 && symbolKeysB.filter((k) => - propertyIsEnumerable.call(val2, k)).length !== 0) { - return false; - } - if (lengthA !== undefined) { - if (aKeys.length !== lengthA || bKeys.length !== lengthB) + } + } else { + const symbolKeysB = getOwnPropertySymbols(val2); + if (symbolKeysB.length !== 0 && + getEnumerables(val2, symbolKeysB).length !== 0) { return false; - if (symbolKeysA.length === 0) - return true; - aKeys = []; - bKeys = []; - } - if (symbolKeysA.length !== 0) { - aKeys.push(...symbolKeysA); - bKeys.push(...symbolKeysB); + } } } - // Cheap key test: - const keys = {}; - for (i = 0; i < aKeys.length; i++) { - keys[aKeys[i]] = true; - } - for (i = 0; i < aKeys.length; i++) { - if (keys[bKeys[i]] === undefined) - return false; + if (aKeys.length === 0 && + (iterationType === kNoIterator || + iterationType === kIsArray && val1.length === 0 || + val1.size === 0)) { + return true; } // Use memos to handle cycles. @@ -254,7 +338,7 @@ function keyCheck(val1, val2, strict, memos, lengthA, lengthB) { memos.val1.set(val1, memos.position); memos.val2.set(val2, memos.position); - const areEq = objEquiv(val1, val2, strict, aKeys, memos); + const areEq = objEquiv(val1, val2, strict, aKeys, memos, iterationType); memos.val1.delete(val1); memos.val2.delete(val2); @@ -267,7 +351,7 @@ function innerDeepEqual(val1, val2, strict, memos) { if (val1 === val2) { if (val1 !== 0) return true; - return strict ? Object.is(val1, val2) : true; + return strict ? objectIs(val1, val2) : true; } // Check more closely if val1 and val2 are equal. @@ -297,7 +381,7 @@ function setHasLoosePrim(a, b, val) { if (altValues === undefined) return false; - var matches = 1; + let matches = 1; for (var i = 0; i < altValues.length; i++) { if (b.has(altValues[i])) { matches--; @@ -310,19 +394,9 @@ function setHasLoosePrim(a, b, val) { } function setEquiv(a, b, strict, memo) { - // This code currently returns false for this pair of sets: - // assert.deepEqual(new Set(['1', 1]), new Set([1])) - // - // In theory, all the items in the first set have a corresponding == value in - // the second set, but the sets have different sizes. Its a silly case, - // and more evidence that deepStrictEqual should always be preferred over - // deepEqual. - if (a.size !== b.size) - return false; - // This is a lazily initiated Set of entries which have to be compared // pairwise. - var set = null; + let set = null; for (const val of a) { // Note: Checking for the objects first improves the performance for object // heavy sets but it is a minor slow down for primitives. As they are fast @@ -405,7 +479,7 @@ function mapHasLoosePrim(a, b, key1, memo, item1, item2) { const setA = new Set(); const setB = new Set(); - var keyCount = 1; + let keyCount = 1; setA.add(item1); if (b.has(key1)) { @@ -454,10 +528,7 @@ function mapHasEqualEntry(set, map, key1, item1, strict, memo) { } function mapEquiv(a, b, strict, memo) { - if (a.size !== b.size) - return false; - - var set = null; + let set = null; for (const [key, item1] of a) { if (typeof key === 'object' && key !== null) { @@ -492,35 +563,44 @@ function mapEquiv(a, b, strict, memo) { return true; } -function objEquiv(a, b, strict, keys, memos) { +function objEquiv(a, b, strict, keys, memos, iterationType) { // Sets and maps don't have their entries accessible via normal object // properties. - if (isSet(a)) { - if (!isSet(b) || !setEquiv(a, b, strict, memos)) + let i = 0; + + if (iterationType === kIsSet) { + if (!setEquiv(a, b, strict, memos)) { return false; - } else if (isMap(a)) { - if (!isMap(b) || !mapEquiv(a, b, strict, memos)) + } + } else if (iterationType === kIsMap) { + if (!mapEquiv(a, b, strict, memos)) { return false; - } else if (isSet(b) || isMap(b)) { - return false; + } + } else if (iterationType === kIsArray) { + for (; i < a.length; i++) { + if (!innerDeepEqual(a[i], b[i], strict, memos)) { + return false; + } + } } // The pair must have equivalent values for every corresponding key. // Possibly expensive deep test: - for (var i = 0; i < keys.length; i++) { + for (i = 0; i < keys.length; i++) { const key = keys[i]; - if (!innerDeepEqual(a[key], b[key], strict, memos)) + if (!innerDeepEqual(a[key], b[key], strict, memos)) { return false; + } } return true; } function isDeepEqual(val1, val2) { - return innerDeepEqual(val1, val2, false); + return innerDeepEqual(val1, val2, kLoose); } function isDeepStrictEqual(val1, val2) { - return innerDeepEqual(val1, val2, true); + return innerDeepEqual(val1, val2, kStrict); } module.exports = { diff --git a/test/parallel/test-assert-deep.js b/test/parallel/test-assert-deep.js index 84fe7f50c32046..04587f3e40b654 100644 --- a/test/parallel/test-assert-deep.js +++ b/test/parallel/test-assert-deep.js @@ -942,3 +942,10 @@ assert.throws(() => assert.deepStrictEqual(new Boolean(true), {}), a.valueOf = undefined; assertNotDeepOrStrict(a, new String(1)); } + +// Basic array out of bounds check. +{ + const arr = [1, 2, 3]; + arr[2 ** 32] = true; + assertNotDeepOrStrict(arr, [1, 2, 3]); +}