Skip to content

Commit c9364cd

Browse files
committed
assert: make partialDeepStrictEqual work with ArrayBuffers
Fixes: nodejs#56097
1 parent 3f9c6c0 commit c9364cd

File tree

3 files changed

+349
-77
lines changed

3 files changed

+349
-77
lines changed

lib/assert.js

+175-74
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,17 @@
2121
'use strict';
2222

2323
const {
24+
ArrayBufferIsView,
25+
ArrayBufferPrototypeGetByteLength,
2426
ArrayFrom,
2527
ArrayIsArray,
2628
ArrayPrototypeIndexOf,
2729
ArrayPrototypeJoin,
2830
ArrayPrototypePush,
2931
ArrayPrototypeSlice,
32+
DataViewPrototypeGetBuffer,
33+
DataViewPrototypeGetByteLength,
34+
DataViewPrototypeGetByteOffset,
3035
Error,
3136
FunctionPrototypeCall,
3237
MapPrototypeDelete,
@@ -38,10 +43,12 @@ const {
3843
ObjectIs,
3944
ObjectKeys,
4045
ObjectPrototypeIsPrototypeOf,
46+
ObjectPrototypeToString,
4147
ReflectApply,
4248
ReflectHas,
4349
ReflectOwnKeys,
4450
RegExpPrototypeExec,
51+
SafeArrayIterator,
4552
SafeMap,
4653
SafeSet,
4754
SafeWeakSet,
@@ -50,6 +57,8 @@ const {
5057
StringPrototypeSlice,
5158
StringPrototypeSplit,
5259
SymbolIterator,
60+
TypedArrayPrototypeGetLength,
61+
Uint8Array,
5362
} = primordials;
5463

5564
const {
@@ -65,6 +74,8 @@ const AssertionError = require('internal/assert/assertion_error');
6574
const { inspect } = require('internal/util/inspect');
6675
const { Buffer } = require('buffer');
6776
const {
77+
isArrayBuffer,
78+
isDataView,
6879
isKeyObject,
6980
isPromise,
7081
isRegExp,
@@ -73,6 +84,8 @@ const {
7384
isDate,
7485
isWeakSet,
7586
isWeakMap,
87+
isSharedArrayBuffer,
88+
isAnyArrayBuffer,
7689
} = require('internal/util/types');
7790
const { isError, deprecate, emitExperimentalWarning } = require('internal/util');
7891
const { innerOk } = require('internal/assert/utils');
@@ -369,9 +382,159 @@ function isSpecial(obj) {
369382
}
370383

371384
const typesToCallDeepStrictEqualWith = [
372-
isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer,
385+
isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer, isSharedArrayBuffer,
373386
];
374387

388+
function compareMaps(actual, expected, comparedObjects) {
389+
if (actual.size !== expected.size) {
390+
return false;
391+
}
392+
const safeIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], actual);
393+
394+
comparedObjects ??= new SafeWeakSet();
395+
396+
for (const { 0: key, 1: val } of safeIterator) {
397+
if (!MapPrototypeHas(expected, key)) {
398+
return false;
399+
}
400+
if (!compareBranch(val, MapPrototypeGet(expected, key), comparedObjects)) {
401+
return false;
402+
}
403+
}
404+
return true;
405+
}
406+
407+
function compareArrayBuffers(actual, expected) {
408+
let actualView, expectedView, expectedViewLength;
409+
410+
if (!ArrayBufferIsView(actual)) {
411+
let actualViewLength;
412+
413+
if (isArrayBuffer(actual) && isArrayBuffer(expected)) {
414+
actualViewLength = ArrayBufferPrototypeGetByteLength(actual);
415+
expectedViewLength = ArrayBufferPrototypeGetByteLength(expected);
416+
} else if (isSharedArrayBuffer(actual) && isSharedArrayBuffer(expected)) {
417+
actualViewLength = actual.byteLength;
418+
expectedViewLength = expected.byteLength;
419+
} else {
420+
// Cannot compare ArrayBuffers with SharedArrayBuffers
421+
return false;
422+
}
423+
424+
if (expectedViewLength > actualViewLength) {
425+
return false;
426+
}
427+
actualView = new Uint8Array(actual);
428+
expectedView = new Uint8Array(expected);
429+
430+
} else if (isDataView(actual)) {
431+
if (!isDataView(expected)) {
432+
return false;
433+
}
434+
const actualByteLength = DataViewPrototypeGetByteLength(actual);
435+
expectedViewLength = DataViewPrototypeGetByteLength(expected);
436+
if (expectedViewLength > actualByteLength) {
437+
return false;
438+
}
439+
440+
actualView = new Uint8Array(
441+
DataViewPrototypeGetBuffer(actual),
442+
DataViewPrototypeGetByteOffset(actual),
443+
actualByteLength,
444+
);
445+
expectedView = new Uint8Array(
446+
DataViewPrototypeGetBuffer(expected),
447+
DataViewPrototypeGetByteOffset(expected),
448+
expectedViewLength,
449+
);
450+
} else {
451+
if (ObjectPrototypeToString(actual) !== ObjectPrototypeToString(expected)) {
452+
return false;
453+
}
454+
actualView = actual;
455+
expectedView = expected;
456+
expectedViewLength = TypedArrayPrototypeGetLength(expected);
457+
458+
if (expectedViewLength > TypedArrayPrototypeGetLength(actual)) {
459+
return false;
460+
}
461+
}
462+
463+
for (let i = 0; i < expectedViewLength; i++) {
464+
if (actualView[i] !== expectedView[i]) {
465+
return false;
466+
}
467+
}
468+
469+
return true;
470+
}
471+
472+
function compareSets(actual, expected, comparedObjects) {
473+
if (expected.size > actual.size) {
474+
return false; // `expected` can't be a subset if it has more elements
475+
}
476+
477+
if (isDeepEqual === undefined) lazyLoadComparison();
478+
479+
const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual));
480+
const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected);
481+
const usedIndices = new SafeSet();
482+
483+
expectedIteration: for (const expectedItem of expectedIterator) {
484+
for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) {
485+
if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) {
486+
usedIndices.add(actualIdx);
487+
continue expectedIteration;
488+
}
489+
}
490+
return false;
491+
}
492+
493+
return true;
494+
}
495+
496+
function compareArrays(actual, expected, comparedObjects) {
497+
if (expected.length > actual.length) {
498+
return false;
499+
}
500+
501+
if (isDeepEqual === undefined) lazyLoadComparison();
502+
503+
// Create a map to count occurrences of each element in the expected array
504+
const expectedCounts = new SafeMap();
505+
for (const expectedItem of expected) {
506+
let found = false;
507+
for (const { 0: key, 1: count } of expectedCounts) {
508+
if (isDeepStrictEqual(key, expectedItem)) {
509+
MapPrototypeSet(expectedCounts, key, count + 1);
510+
found = true;
511+
break;
512+
}
513+
}
514+
if (!found) {
515+
MapPrototypeSet(expectedCounts, expectedItem, 1);
516+
}
517+
}
518+
519+
const safeActual = new SafeArrayIterator(actual);
520+
521+
// Create a map to count occurrences of relevant elements in the actual array
522+
for (const actualItem of safeActual) {
523+
for (const { 0: key, 1: count } of expectedCounts) {
524+
if (isDeepStrictEqual(key, actualItem)) {
525+
if (count === 1) {
526+
MapPrototypeDelete(expectedCounts, key);
527+
} else {
528+
MapPrototypeSet(expectedCounts, key, count - 1);
529+
}
530+
break;
531+
}
532+
}
533+
}
534+
535+
return !expectedCounts.size;
536+
}
537+
375538
/**
376539
* Compares two objects or values recursively to check if they are equal.
377540
* @param {any} actual - The actual value to compare.
@@ -388,22 +551,16 @@ function compareBranch(
388551
) {
389552
// Check for Map object equality
390553
if (isMap(actual) && isMap(expected)) {
391-
if (actual.size !== expected.size) {
392-
return false;
393-
}
394-
const safeIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], actual);
395-
396-
comparedObjects ??= new SafeWeakSet();
554+
return compareMaps(actual, expected, comparedObjects);
555+
}
397556

398-
for (const { 0: key, 1: val } of safeIterator) {
399-
if (!MapPrototypeHas(expected, key)) {
400-
return false;
401-
}
402-
if (!compareBranch(val, MapPrototypeGet(expected, key), comparedObjects)) {
403-
return false;
404-
}
405-
}
406-
return true;
557+
if (
558+
ArrayBufferIsView(actual) ||
559+
isAnyArrayBuffer(actual) ||
560+
ArrayBufferIsView(expected) ||
561+
isAnyArrayBuffer(expected)
562+
) {
563+
return compareArrayBuffers(actual, expected);
407564
}
408565

409566
for (const type of typesToCallDeepStrictEqualWith) {
@@ -415,68 +572,12 @@ function compareBranch(
415572

416573
// Check for Set object equality
417574
if (isSet(actual) && isSet(expected)) {
418-
if (expected.size > actual.size) {
419-
return false; // `expected` can't be a subset if it has more elements
420-
}
421-
422-
if (isDeepEqual === undefined) lazyLoadComparison();
423-
424-
const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual));
425-
const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected);
426-
const usedIndices = new SafeSet();
427-
428-
expectedIteration: for (const expectedItem of expectedIterator) {
429-
for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) {
430-
if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) {
431-
usedIndices.add(actualIdx);
432-
continue expectedIteration;
433-
}
434-
}
435-
return false;
436-
}
437-
438-
return true;
575+
return compareSets(actual, expected, comparedObjects);
439576
}
440577

441578
// Check if expected array is a subset of actual array
442579
if (ArrayIsArray(actual) && ArrayIsArray(expected)) {
443-
if (expected.length > actual.length) {
444-
return false;
445-
}
446-
447-
if (isDeepEqual === undefined) lazyLoadComparison();
448-
449-
// Create a map to count occurrences of each element in the expected array
450-
const expectedCounts = new SafeMap();
451-
for (const expectedItem of expected) {
452-
let found = false;
453-
for (const { 0: key, 1: count } of expectedCounts) {
454-
if (isDeepStrictEqual(key, expectedItem)) {
455-
MapPrototypeSet(expectedCounts, key, count + 1);
456-
found = true;
457-
break;
458-
}
459-
}
460-
if (!found) {
461-
MapPrototypeSet(expectedCounts, expectedItem, 1);
462-
}
463-
}
464-
465-
// Create a map to count occurrences of relevant elements in the actual array
466-
for (const actualItem of actual) {
467-
for (const { 0: key, 1: count } of expectedCounts) {
468-
if (isDeepStrictEqual(key, actualItem)) {
469-
if (count === 1) {
470-
MapPrototypeDelete(expectedCounts, key);
471-
} else {
472-
MapPrototypeSet(expectedCounts, key, count - 1);
473-
}
474-
break;
475-
}
476-
}
477-
}
478-
479-
return !expectedCounts.size;
580+
return compareArrays(actual, expected, comparedObjects);
480581
}
481582

482583
// Comparison done when at least one of the values is not an object

0 commit comments

Comments
 (0)