Skip to content

Commit

Permalink
fix(assert): handle __proto__ correctly in assertObjectMatch
Browse files Browse the repository at this point in the history
  • Loading branch information
yuhr committed Jan 10, 2025
1 parent 0339121 commit 1413550
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 26 deletions.
57 changes: 32 additions & 25 deletions assert/object_match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,23 @@ function isObject(val: unknown): boolean {
return typeof val === "object" && val !== null;
}

function defineProperty(target: object, key: PropertyKey, value: unknown) {
return Object.defineProperty(target, key, {
value,
configurable: true,
enumerable: true,
writable: true,
});
}

function filter(a: loose, b: loose): loose {
const seen = new WeakMap();
return filterObject(a, b);

function filterObject(a: loose, b: loose): loose {
// Prevent infinite loop with circular references with same filter
if ((seen.has(a)) && (seen.get(a) === b)) {
return a;
}
const memo = seen.get(a);
if (memo && (memo === b)) return a;

try {
seen.set(a, b);
Expand All @@ -82,68 +90,67 @@ function filter(a: loose, b: loose): loose {
if (keysA.length && keysB.length && !entries.length) {
// If both objects are not empty but don't have the same keys or symbols,
// returns the entries in object a.
for (const key of keysA) {
filtered[key] = a[key];
}

for (const key of keysA) defineProperty(filtered, key, a[key]);
return filtered;
}

for (const [key, value] of entries) {
// On regexp references, keep value as it to avoid loosing pattern and flags
if (value instanceof RegExp) {
filtered[key] = value;
defineProperty(filtered, key, value);
continue;
}

const subset = (b as loose)[key];

// On array references, build a filtered array and filter nested objects inside
if (Array.isArray(value) && Array.isArray(subset)) {
filtered[key] = filterArray(value, subset);
defineProperty(filtered, key, filterArray(value, subset));
continue;
}

// On nested objects references, build a filtered object recursively
if (isObject(value) && isObject(subset)) {
// When both operands are maps, build a filtered map with common keys and filter nested objects inside
if ((value instanceof Map) && (subset instanceof Map)) {
filtered[key] = new Map(
[...value].filter(([k]) => subset.has(k)).map(
([k, v]) => {
const v2 = subset.get(k);
if (isObject(v) && isObject(v2)) {
return [k, filterObject(v as loose, v2 as loose)];
}

return [k, v];
},
defineProperty(
filtered,
key,
new Map(
[...value].filter(([k]) => subset.has(k)).map(
([k, v]) => {
const v2 = subset.get(k);
if (isObject(v) && isObject(v2)) {
return [k, filterObject(v as loose, v2 as loose)];
}
return [k, v];
},
),
),
);
continue;
}

// When both operands are set, build a filtered set with common values
if ((value instanceof Set) && (subset instanceof Set)) {
filtered[key] = value.intersection(subset);
defineProperty(filtered, key, value.intersection(subset));
continue;
}

filtered[key] = filterObject(value as loose, subset as loose);
defineProperty(filtered, key, filterObject(value as loose, subset as loose));
continue;
}

filtered[key] = value;
defineProperty(filtered, key, value);
}

return filtered;
}

function filterArray(a: unknown[], b: unknown[]): unknown[] {
// Prevent infinite loop with circular references with same filter
if (seen.has(a) && (seen.get(a) === b)) {
return a;
}
const memo = seen.get(a);
if (memo && (memo === b)) return a;

seen.set(a, b);

Expand Down
20 changes: 20 additions & 0 deletions assert/object_match_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,23 @@ Deno.test("assertObjectMatch() prints inputs correctly", () => {
}`,
);
});

Deno.test(
"assertObjectMatch() should be able to test target object's own `__proto__`",
() => {
const objectA = { ["__proto__"]: { polluted: true } };
const objectB = { ["__proto__"]: { polluted: true } };
const objectC = { ["__proto__"]: { polluted: false } };
assertObjectMatch(objectA, objectB)
assertThrows(
() => assertObjectMatch(objectA, objectC),
AssertionError,
` {
['__proto__']: {
- polluted: true,
+ polluted: false,
},
}`,
);
}
);
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"importMap": "./import_map.json",
"tasks": {
"test": "deno test --unstable-http --unstable-webgpu --doc --allow-all --parallel --coverage --trace-leaks --clean",
"test": "deno test --unstable-http --unstable-webgpu --unstable-unsafe-proto --doc --allow-all --parallel --coverage --trace-leaks --clean",
"test:browser": "git grep --name-only \"This module is browser compatible.\" | grep -v deno.json | grep -v .github/workflows | grep -v _tools | grep -v encoding/README.md | grep -v media_types/vendor/update.ts | xargs deno check --config browser-compat.tsconfig.json",
"test:node": "(cd _tools/node_test_runner && npm install) && node --import ./_tools/node_test_runner/register_deno_shim.mjs ./_tools/node_test_runner/run_test.mjs",
"fmt:licence-headers": "deno run --allow-read --allow-write ./_tools/check_licence.ts",
Expand Down

0 comments on commit 1413550

Please sign in to comment.