Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(assert): handle __proto__ correctly in assertObjectMatch #6342

Merged
merged 5 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ jobs:
deno-version: ${{ matrix.deno }}

- name: Run tests
run: deno task test
run: |
deno task test
deno task test:with-unsafe-proto

- name: Run timezone-dependent tests
run: |
Expand Down
63 changes: 37 additions & 26 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();
const seen = new WeakMap<loose | unknown[], loose | unknown[]>();
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;
Comment on lines +70 to +71
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change necessary? What happens if memo is the value false?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just performance thing. If you read the context of the changed lines, you'll realize that will never happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See these types: cfbff35

If you say this change should be done in another PR, I understand it and revert this part, but I'd say such a trivial and small change can be okay to come along with...


try {
seen.set(a, b);
Expand All @@ -82,68 +90,71 @@ 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,
},
}`,
);
},
);
1 change: 1 addition & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"importMap": "./import_map.json",
"tasks": {
"test": "deno test --unstable-http --unstable-webgpu --doc --allow-all --parallel --coverage --trace-leaks --clean",
"test:with-unsafe-proto": "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
Loading