diff --git a/.changeset/eval-json-proto-keys.md b/.changeset/eval-json-proto-keys.md new file mode 100644 index 0000000..443902c --- /dev/null +++ b/.changeset/eval-json-proto-keys.md @@ -0,0 +1,5 @@ +--- +"obsidian-e2e": patch +--- + +`evalJson` and `evalJsonAsync` now keep an own `__proto__` key in a result as a plain data key. `evalJson` used to drop it, and `evalJsonAsync` timed out with reason `still-pending` although the operation had finished. The decoded result still has `Object.prototype` as its prototype, so the key cannot pollute any prototype. A poll read that fails inside Obsidian now throws its `DevEvalError` at once. diff --git a/src/dev/eval-json.ts b/src/dev/eval-json.ts index d4dc778..c9083ba 100644 --- a/src/dev/eval-json.ts +++ b/src/dev/eval-json.ts @@ -57,7 +57,8 @@ const EVAL_JSON_SERIALIZER = [ "if(Array.isArray(value)){return value.map((item,index)=>__obsidianE2ESerialize(item,`${path}[${index}]`));}", "const prototype=Object.getPrototypeOf(value);", "if(prototype!==Object.prototype&&prototype!==null){throw new Error(`Cannot serialize non-plain object at ${path}.`);}", - "const next={};", + // A null prototype keeps an own `__proto__` key as data instead of a prototype swap. + "const next=Object.create(null);", "for(const [key,entry] of Object.entries(value)){next[key]=__obsidianE2ESerialize(entry,`${path}.${key}`);}", "return next;", "};", @@ -240,6 +241,8 @@ export async function runEvalJsonAsync( directCommandOptions(ASYNC_EVAL_COMMAND_TIMEOUT_MS), ); } catch (error) { + // The read ran in-app and threw, so repeating it cannot succeed. + if (error instanceof DevEvalError) throw error; lastError = error; await pause(); continue; diff --git a/tests/dev/eval-json-async.test.ts b/tests/dev/eval-json-async.test.ts index 20afb8a..06ada48 100644 --- a/tests/dev/eval-json-async.test.ts +++ b/tests/dev/eval-json-async.test.ts @@ -163,6 +163,37 @@ describe("runEvalJsonAsync", () => { await expect(runEvalJsonAsync(dev, "await Promise.resolve('polled')")).resolves.toBe("polled"); }); + it("resolves an own __proto__ key as data without touching any prototype", async () => { + const { dev } = createInAppEmulator(); + + const value = await runEvalJsonAsync<{ savedFeeds: Record }>( + dev, + 'JSON.parse(\'{"savedFeeds":{"__proto__":{"title":"__proto__"}}}\')', + { timeoutMs: 2_000 }, + ); + + expect(Object.getOwnPropertyDescriptor(value.savedFeeds, "__proto__")?.value).toEqual({ + title: "__proto__", + }); + expect(Object.getPrototypeOf(value.savedFeeds)).toBe(Object.prototype); + expect(({} as Record).title).toBeUndefined(); + }); + + it("rethrows a poll that fails in-app instead of reporting the operation as pending", async () => { + const { dev } = createInAppEmulator(() => { + // A completed entry the poll read cannot serialize. + for (const entry of Object.values(registryEntries())) { + if (entry.state === "done") + Object.assign(entry, { envelope: { ok: true, value: new Map() } }); + } + return undefined; + }); + + await expect( + runEvalJsonAsync(dev, "await Promise.resolve('done')", { timeoutMs: 2_000 }), + ).rejects.toThrowError(DevEvalError); + }); + it("fails with reason 'still-pending' when the promise never settles", async () => { const { dev } = createInAppEmulator(); diff --git a/tests/dev/eval-json.test.ts b/tests/dev/eval-json.test.ts index 7ccb522..13c5997 100644 --- a/tests/dev/eval-json.test.ts +++ b/tests/dev/eval-json.test.ts @@ -44,6 +44,18 @@ describe("buildEvalJsonCode executed", () => { expect(thrown).toBeInstanceOf(DevEvalError); expect((thrown as DevEvalError).message).toBe("Failed to evaluate Obsidian code: nope"); }); + + it("keeps an own __proto__ key as data without touching any prototype", () => { + const value = evalGenerated<{ savedFeeds: Record }>( + 'JSON.parse(\'{"savedFeeds":{"__proto__":{"title":"__proto__"}}}\')', + ); + + expect(Object.getOwnPropertyDescriptor(value.savedFeeds, "__proto__")?.value).toEqual({ + title: "__proto__", + }); + expect(Object.getPrototypeOf(value.savedFeeds)).toBe(Object.prototype); + expect(({} as Record).title).toBeUndefined(); + }); }); // Regression for https://github.com/chhoumann/obsidian-e2e/issues/18 on the