1'use strict'; 2 3const { 4 ArrayIsArray, 5 ArrayPrototypeFilter, 6 ArrayPrototypePush, 7 BigIntPrototypeValueOf, 8 BooleanPrototypeValueOf, 9 DatePrototypeGetTime, 10 Error, 11 NumberIsNaN, 12 NumberPrototypeValueOf, 13 ObjectGetOwnPropertySymbols, 14 ObjectGetPrototypeOf, 15 ObjectIs, 16 ObjectKeys, 17 ObjectPrototypeHasOwnProperty, 18 ObjectPrototypePropertyIsEnumerable, 19 ObjectPrototypeToString, 20 SafeMap, 21 SafeSet, 22 StringPrototypeValueOf, 23 SymbolPrototypeValueOf, 24 TypedArrayPrototypeGetSymbolToStringTag, 25 Uint8Array, 26} = primordials; 27 28const { compare } = internalBinding('buffer'); 29const assert = require('internal/assert'); 30const types = require('internal/util/types'); 31const { 32 isAnyArrayBuffer, 33 isArrayBufferView, 34 isDate, 35 isMap, 36 isRegExp, 37 isSet, 38 isNativeError, 39 isBoxedPrimitive, 40 isNumberObject, 41 isStringObject, 42 isBooleanObject, 43 isBigIntObject, 44 isSymbolObject, 45 isFloat32Array, 46 isFloat64Array, 47} = types; 48const { 49 constants: { 50 ONLY_ENUMERABLE, 51 SKIP_SYMBOLS, 52 }, 53 getOwnNonIndexProperties, 54} = internalBinding('util'); 55 56const kStrict = true; 57const kLoose = false; 58 59const kNoIterator = 0; 60const kIsArray = 1; 61const kIsSet = 2; 62const kIsMap = 3; 63 64// Check if they have the same source and flags 65function areSimilarRegExps(a, b) { 66 return a.source === b.source && 67 a.flags === b.flags && 68 a.lastIndex === b.lastIndex; 69} 70 71function areSimilarFloatArrays(a, b) { 72 if (a.byteLength !== b.byteLength) { 73 return false; 74 } 75 for (let offset = 0; offset < a.byteLength; offset++) { 76 if (a[offset] !== b[offset]) { 77 return false; 78 } 79 } 80 return true; 81} 82 83function areSimilarTypedArrays(a, b) { 84 if (a.byteLength !== b.byteLength) { 85 return false; 86 } 87 return compare(new Uint8Array(a.buffer, a.byteOffset, a.byteLength), 88 new Uint8Array(b.buffer, b.byteOffset, b.byteLength)) === 0; 89} 90 91function areEqualArrayBuffers(buf1, buf2) { 92 return buf1.byteLength === buf2.byteLength && 93 compare(new Uint8Array(buf1), new Uint8Array(buf2)) === 0; 94} 95 96function isEqualBoxedPrimitive(val1, val2) { 97 if (isNumberObject(val1)) { 98 return isNumberObject(val2) && 99 ObjectIs(NumberPrototypeValueOf(val1), 100 NumberPrototypeValueOf(val2)); 101 } 102 if (isStringObject(val1)) { 103 return isStringObject(val2) && 104 StringPrototypeValueOf(val1) === StringPrototypeValueOf(val2); 105 } 106 if (isBooleanObject(val1)) { 107 return isBooleanObject(val2) && 108 BooleanPrototypeValueOf(val1) === BooleanPrototypeValueOf(val2); 109 } 110 if (isBigIntObject(val1)) { 111 return isBigIntObject(val2) && 112 BigIntPrototypeValueOf(val1) === BigIntPrototypeValueOf(val2); 113 } 114 if (isSymbolObject(val1)) { 115 return isSymbolObject(val2) && 116 SymbolPrototypeValueOf(val1) === SymbolPrototypeValueOf(val2); 117 } 118 /* c8 ignore next */ 119 assert.fail(`Unknown boxed type ${val1}`); 120} 121 122// Notes: Type tags are historical [[Class]] properties that can be set by 123// FunctionTemplate::SetClassName() in C++ or Symbol.toStringTag in JS 124// and retrieved using Object.prototype.toString.call(obj) in JS 125// See https://tc39.github.io/ecma262/#sec-object.prototype.tostring 126// for a list of tags pre-defined in the spec. 127// There are some unspecified tags in the wild too (e.g. typed array tags). 128// Since tags can be altered, they only serve fast failures 129// 130// For strict comparison, objects should have 131// a) The same built-in type tag. 132// b) The same prototypes. 133 134function innerDeepEqual(val1, val2, strict, memos) { 135 // All identical values are equivalent, as determined by ===. 136 if (val1 === val2) { 137 if (val1 !== 0) 138 return true; 139 return strict ? ObjectIs(val1, val2) : true; 140 } 141 142 // Check more closely if val1 and val2 are equal. 143 if (strict) { 144 if (typeof val1 !== 'object') { 145 return typeof val1 === 'number' && NumberIsNaN(val1) && 146 NumberIsNaN(val2); 147 } 148 if (typeof val2 !== 'object' || val1 === null || val2 === null) { 149 return false; 150 } 151 if (ObjectGetPrototypeOf(val1) !== ObjectGetPrototypeOf(val2)) { 152 return false; 153 } 154 } else { 155 if (val1 === null || typeof val1 !== 'object') { 156 if (val2 === null || typeof val2 !== 'object') { 157 // eslint-disable-next-line eqeqeq 158 return val1 == val2 || (NumberIsNaN(val1) && NumberIsNaN(val2)); 159 } 160 return false; 161 } 162 if (val2 === null || typeof val2 !== 'object') { 163 return false; 164 } 165 } 166 const val1Tag = ObjectPrototypeToString(val1); 167 const val2Tag = ObjectPrototypeToString(val2); 168 169 if (val1Tag !== val2Tag) { 170 return false; 171 } 172 173 if (ArrayIsArray(val1)) { 174 // Check for sparse arrays and general fast path 175 if (!ArrayIsArray(val2) || val1.length !== val2.length) { 176 return false; 177 } 178 const filter = strict ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS; 179 const keys1 = getOwnNonIndexProperties(val1, filter); 180 const keys2 = getOwnNonIndexProperties(val2, filter); 181 if (keys1.length !== keys2.length) { 182 return false; 183 } 184 return keyCheck(val1, val2, strict, memos, kIsArray, keys1); 185 } else if (val1Tag === '[object Object]') { 186 return keyCheck(val1, val2, strict, memos, kNoIterator); 187 } else if (isDate(val1)) { 188 if (!isDate(val2) || 189 DatePrototypeGetTime(val1) !== DatePrototypeGetTime(val2)) { 190 return false; 191 } 192 } else if (isRegExp(val1)) { 193 if (!isRegExp(val2) || !areSimilarRegExps(val1, val2)) { 194 return false; 195 } 196 } else if (isNativeError(val1) || val1 instanceof Error) { 197 // Do not compare the stack as it might differ even though the error itself 198 // is otherwise identical. 199 if ((!isNativeError(val2) && !(val2 instanceof Error)) || 200 val1.message !== val2.message || 201 val1.name !== val2.name) { 202 return false; 203 } 204 } else if (isArrayBufferView(val1)) { 205 if (TypedArrayPrototypeGetSymbolToStringTag(val1) !== 206 TypedArrayPrototypeGetSymbolToStringTag(val2)) { 207 return false; 208 } 209 if (!strict && (isFloat32Array(val1) || isFloat64Array(val1))) { 210 if (!areSimilarFloatArrays(val1, val2)) { 211 return false; 212 } 213 } else if (!areSimilarTypedArrays(val1, val2)) { 214 return false; 215 } 216 // Buffer.compare returns true, so val1.length === val2.length. If they both 217 // only contain numeric keys, we don't need to exam further than checking 218 // the symbols. 219 const filter = strict ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS; 220 const keys1 = getOwnNonIndexProperties(val1, filter); 221 const keys2 = getOwnNonIndexProperties(val2, filter); 222 if (keys1.length !== keys2.length) { 223 return false; 224 } 225 return keyCheck(val1, val2, strict, memos, kNoIterator, keys1); 226 } else if (isSet(val1)) { 227 if (!isSet(val2) || val1.size !== val2.size) { 228 return false; 229 } 230 return keyCheck(val1, val2, strict, memos, kIsSet); 231 } else if (isMap(val1)) { 232 if (!isMap(val2) || val1.size !== val2.size) { 233 return false; 234 } 235 return keyCheck(val1, val2, strict, memos, kIsMap); 236 } else if (isAnyArrayBuffer(val1)) { 237 if (!isAnyArrayBuffer(val2) || !areEqualArrayBuffers(val1, val2)) { 238 return false; 239 } 240 } else if (isBoxedPrimitive(val1)) { 241 if (!isEqualBoxedPrimitive(val1, val2)) { 242 return false; 243 } 244 } else if (ArrayIsArray(val2) || 245 isArrayBufferView(val2) || 246 isSet(val2) || 247 isMap(val2) || 248 isDate(val2) || 249 isRegExp(val2) || 250 isAnyArrayBuffer(val2) || 251 isBoxedPrimitive(val2) || 252 isNativeError(val2) || 253 val2 instanceof Error) { 254 return false; 255 } 256 return keyCheck(val1, val2, strict, memos, kNoIterator); 257} 258 259function getEnumerables(val, keys) { 260 return ArrayPrototypeFilter( 261 keys, 262 (k) => ObjectPrototypePropertyIsEnumerable(val, k), 263 ); 264} 265 266function keyCheck(val1, val2, strict, memos, iterationType, aKeys) { 267 // For all remaining Object pairs, including Array, objects and Maps, 268 // equivalence is determined by having: 269 // a) The same number of owned enumerable properties 270 // b) The same set of keys/indexes (although not necessarily the same order) 271 // c) Equivalent values for every corresponding key/index 272 // d) For Sets and Maps, equal contents 273 // Note: this accounts for both named and indexed properties on Arrays. 274 if (arguments.length === 5) { 275 aKeys = ObjectKeys(val1); 276 const bKeys = ObjectKeys(val2); 277 278 // The pair must have the same number of owned properties. 279 if (aKeys.length !== bKeys.length) { 280 return false; 281 } 282 } 283 284 // Cheap key test 285 let i = 0; 286 for (; i < aKeys.length; i++) { 287 if (!ObjectPrototypePropertyIsEnumerable(val2, aKeys[i])) { 288 return false; 289 } 290 } 291 292 if (strict && arguments.length === 5) { 293 const symbolKeysA = ObjectGetOwnPropertySymbols(val1); 294 if (symbolKeysA.length !== 0) { 295 let count = 0; 296 for (i = 0; i < symbolKeysA.length; i++) { 297 const key = symbolKeysA[i]; 298 if (ObjectPrototypePropertyIsEnumerable(val1, key)) { 299 if (!ObjectPrototypePropertyIsEnumerable(val2, key)) { 300 return false; 301 } 302 ArrayPrototypePush(aKeys, key); 303 count++; 304 } else if (ObjectPrototypePropertyIsEnumerable(val2, key)) { 305 return false; 306 } 307 } 308 const symbolKeysB = ObjectGetOwnPropertySymbols(val2); 309 if (symbolKeysA.length !== symbolKeysB.length && 310 getEnumerables(val2, symbolKeysB).length !== count) { 311 return false; 312 } 313 } else { 314 const symbolKeysB = ObjectGetOwnPropertySymbols(val2); 315 if (symbolKeysB.length !== 0 && 316 getEnumerables(val2, symbolKeysB).length !== 0) { 317 return false; 318 } 319 } 320 } 321 322 if (aKeys.length === 0 && 323 (iterationType === kNoIterator || 324 (iterationType === kIsArray && val1.length === 0) || 325 val1.size === 0)) { 326 return true; 327 } 328 329 // Use memos to handle cycles. 330 if (memos === undefined) { 331 memos = { 332 val1: new SafeMap(), 333 val2: new SafeMap(), 334 position: 0, 335 }; 336 } else { 337 // We prevent up to two map.has(x) calls by directly retrieving the value 338 // and checking for undefined. The map can only contain numbers, so it is 339 // safe to check for undefined only. 340 const val2MemoA = memos.val1.get(val1); 341 if (val2MemoA !== undefined) { 342 const val2MemoB = memos.val2.get(val2); 343 if (val2MemoB !== undefined) { 344 return val2MemoA === val2MemoB; 345 } 346 } 347 memos.position++; 348 } 349 350 memos.val1.set(val1, memos.position); 351 memos.val2.set(val2, memos.position); 352 353 const areEq = objEquiv(val1, val2, strict, aKeys, memos, iterationType); 354 355 memos.val1.delete(val1); 356 memos.val2.delete(val2); 357 358 return areEq; 359} 360 361function setHasEqualElement(set, val1, strict, memo) { 362 // Go looking. 363 for (const val2 of set) { 364 if (innerDeepEqual(val1, val2, strict, memo)) { 365 // Remove the matching element to make sure we do not check that again. 366 set.delete(val2); 367 return true; 368 } 369 } 370 371 return false; 372} 373 374// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#Loose_equality_using 375// Sadly it is not possible to detect corresponding values properly in case the 376// type is a string, number, bigint or boolean. The reason is that those values 377// can match lots of different string values (e.g., 1n == '+00001'). 378function findLooseMatchingPrimitives(prim) { 379 switch (typeof prim) { 380 case 'undefined': 381 return null; 382 case 'object': // Only pass in null as object! 383 return undefined; 384 case 'symbol': 385 return false; 386 case 'string': 387 prim = +prim; 388 // Loose equal entries exist only if the string is possible to convert to 389 // a regular number and not NaN. 390 // Fall through 391 case 'number': 392 if (NumberIsNaN(prim)) { 393 return false; 394 } 395 } 396 return true; 397} 398 399function setMightHaveLoosePrim(a, b, prim) { 400 const altValue = findLooseMatchingPrimitives(prim); 401 if (altValue != null) 402 return altValue; 403 404 return b.has(altValue) && !a.has(altValue); 405} 406 407function mapMightHaveLoosePrim(a, b, prim, item, memo) { 408 const altValue = findLooseMatchingPrimitives(prim); 409 if (altValue != null) { 410 return altValue; 411 } 412 const curB = b.get(altValue); 413 if ((curB === undefined && !b.has(altValue)) || 414 !innerDeepEqual(item, curB, false, memo)) { 415 return false; 416 } 417 return !a.has(altValue) && innerDeepEqual(item, curB, false, memo); 418} 419 420function setEquiv(a, b, strict, memo) { 421 // This is a lazily initiated Set of entries which have to be compared 422 // pairwise. 423 let set = null; 424 for (const val of a) { 425 // Note: Checking for the objects first improves the performance for object 426 // heavy sets but it is a minor slow down for primitives. As they are fast 427 // to check this improves the worst case scenario instead. 428 if (typeof val === 'object' && val !== null) { 429 if (set === null) { 430 set = new SafeSet(); 431 } 432 // If the specified value doesn't exist in the second set it's a non-null 433 // object (or non strict only: a not matching primitive) we'll need to go 434 // hunting for something that's deep-(strict-)equal to it. To make this 435 // O(n log n) complexity we have to copy these values in a new set first. 436 set.add(val); 437 } else if (!b.has(val)) { 438 if (strict) 439 return false; 440 441 // Fast path to detect missing string, symbol, undefined and null values. 442 if (!setMightHaveLoosePrim(a, b, val)) { 443 return false; 444 } 445 446 if (set === null) { 447 set = new SafeSet(); 448 } 449 set.add(val); 450 } 451 } 452 453 if (set !== null) { 454 for (const val of b) { 455 // We have to check if a primitive value is already 456 // matching and only if it's not, go hunting for it. 457 if (typeof val === 'object' && val !== null) { 458 if (!setHasEqualElement(set, val, strict, memo)) 459 return false; 460 } else if (!strict && 461 !a.has(val) && 462 !setHasEqualElement(set, val, strict, memo)) { 463 return false; 464 } 465 } 466 return set.size === 0; 467 } 468 469 return true; 470} 471 472function mapHasEqualEntry(set, map, key1, item1, strict, memo) { 473 // To be able to handle cases like: 474 // Map([[{}, 'a'], [{}, 'b']]) vs Map([[{}, 'b'], [{}, 'a']]) 475 // ... we need to consider *all* matching keys, not just the first we find. 476 for (const key2 of set) { 477 if (innerDeepEqual(key1, key2, strict, memo) && 478 innerDeepEqual(item1, map.get(key2), strict, memo)) { 479 set.delete(key2); 480 return true; 481 } 482 } 483 484 return false; 485} 486 487function mapEquiv(a, b, strict, memo) { 488 let set = null; 489 490 for (const { 0: key, 1: item1 } of a) { 491 if (typeof key === 'object' && key !== null) { 492 if (set === null) { 493 set = new SafeSet(); 494 } 495 set.add(key); 496 } else { 497 // By directly retrieving the value we prevent another b.has(key) check in 498 // almost all possible cases. 499 const item2 = b.get(key); 500 if (((item2 === undefined && !b.has(key)) || 501 !innerDeepEqual(item1, item2, strict, memo))) { 502 if (strict) 503 return false; 504 // Fast path to detect missing string, symbol, undefined and null 505 // keys. 506 if (!mapMightHaveLoosePrim(a, b, key, item1, memo)) 507 return false; 508 if (set === null) { 509 set = new SafeSet(); 510 } 511 set.add(key); 512 } 513 } 514 } 515 516 if (set !== null) { 517 for (const { 0: key, 1: item } of b) { 518 if (typeof key === 'object' && key !== null) { 519 if (!mapHasEqualEntry(set, a, key, item, strict, memo)) 520 return false; 521 } else if (!strict && 522 (!a.has(key) || 523 !innerDeepEqual(a.get(key), item, false, memo)) && 524 !mapHasEqualEntry(set, a, key, item, false, memo)) { 525 return false; 526 } 527 } 528 return set.size === 0; 529 } 530 531 return true; 532} 533 534function objEquiv(a, b, strict, keys, memos, iterationType) { 535 // Sets and maps don't have their entries accessible via normal object 536 // properties. 537 let i = 0; 538 539 if (iterationType === kIsSet) { 540 if (!setEquiv(a, b, strict, memos)) { 541 return false; 542 } 543 } else if (iterationType === kIsMap) { 544 if (!mapEquiv(a, b, strict, memos)) { 545 return false; 546 } 547 } else if (iterationType === kIsArray) { 548 for (; i < a.length; i++) { 549 if (ObjectPrototypeHasOwnProperty(a, i)) { 550 if (!ObjectPrototypeHasOwnProperty(b, i) || 551 !innerDeepEqual(a[i], b[i], strict, memos)) { 552 return false; 553 } 554 } else if (ObjectPrototypeHasOwnProperty(b, i)) { 555 return false; 556 } else { 557 // Array is sparse. 558 const keysA = ObjectKeys(a); 559 for (; i < keysA.length; i++) { 560 const key = keysA[i]; 561 if (!ObjectPrototypeHasOwnProperty(b, key) || 562 !innerDeepEqual(a[key], b[key], strict, memos)) { 563 return false; 564 } 565 } 566 if (keysA.length !== ObjectKeys(b).length) { 567 return false; 568 } 569 return true; 570 } 571 } 572 } 573 574 // The pair must have equivalent values for every corresponding key. 575 // Possibly expensive deep test: 576 for (i = 0; i < keys.length; i++) { 577 const key = keys[i]; 578 if (!innerDeepEqual(a[key], b[key], strict, memos)) { 579 return false; 580 } 581 } 582 return true; 583} 584 585function isDeepEqual(val1, val2) { 586 return innerDeepEqual(val1, val2, kLoose); 587} 588 589function isDeepStrictEqual(val1, val2) { 590 return innerDeepEqual(val1, val2, kStrict); 591} 592 593module.exports = { 594 isDeepEqual, 595 isDeepStrictEqual, 596}; 597