• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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