mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(electron): block trash calls in active observation hook
This commit is contained in:
1 parent
d3700f709b
commit
816a9dc988
3 files changed
+49
-5
No files matched your search
@@ -129,8 +129,10 @@ native-addon, process, and IPC timeline events. IPC channels and argument-shape
|
||||
metadata retain their observed lengths; payload values are never retained.
|
||||
Renderer crash/restart and deep-link actions are synthetic
|
||||
scenario controls; their attempted and observed outcomes remain in the
|
||||
timeline. The active hook blocks and records external shell/navigation,
|
||||
permission, download, popup, updater, and OS-integration effects. The timeline
|
||||
timeline. The active hook blocks and records the shell methods listed below
|
||||
and selected navigation, permission, download, popup, updater, and
|
||||
OS-integration APIs. These method patches do not contain direct filesystem,
|
||||
network, native-addon, or other unpatched target effects. The timeline
|
||||
is explicitly partial when attachment starts after application activity. The
|
||||
startup, including the initial state snapshot, has a 60-second deadline.
|
||||
Actions wait until completion or request cancellation; a click whose target
|
||||
@@ -139,11 +141,14 @@ window, remains pending until cancelled. After cancellation, the final state
|
||||
read has 5 seconds before REA keeps the initial snapshot instead; the
|
||||
cancellation keeps the caller's reason and records that expiry in its
|
||||
diagnostics.
|
||||
Blocked synchronous shell APIs keep their return contracts:
|
||||
The shell policy is owned by `blockedShellResults` in
|
||||
`scripts/electron-active-hook-boundaries.cjs`. Its synchronous blocks are:
|
||||
`showItemInFolder` returns `undefined` and `writeShortcutLink` returns `false`.
|
||||
Their `shell-attempt` events record the blocked phase and argument shapes;
|
||||
neither result means the requested host operation occurred. The existing
|
||||
`openExternal` and `openPath` blocks continue to reject their promises.
|
||||
`openExternal`, `openPath`, and `trashItem` blocks reject their promises.
|
||||
The `trashItem` block prevents that call from reaching the OS trash operation;
|
||||
it does not intercept other ways the application can remove files.
|
||||
CLI SIGINT/SIGTERM requests cancellation and awaits owned-process cleanup.
|
||||
Window IDs, URLs, and cached titles come from one main-process Electron snapshot,
|
||||
so a crashed renderer does not require a successful renderer-side title query.
|
||||
|
||||
@@ -350,6 +350,8 @@ const createElectronActiveBoundaryPatches = ({
|
||||
Promise.reject(new Error("External shell effects are blocked")),
|
||||
openPath: () =>
|
||||
Promise.reject(new Error("External shell effects are blocked")),
|
||||
trashItem: () =>
|
||||
Promise.reject(new Error("External shell effects are blocked")),
|
||||
// Synchronous `void`: reveals a path in the host file manager.
|
||||
showItemInFolder: () => undefined,
|
||||
// Synchronous `boolean`: creates or replaces a Windows shortcut file.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { copyFile, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
@@ -63,6 +63,7 @@ globalThis.__nativeShellCalls = [];
|
||||
const shell = {
|
||||
openExternal: () => Promise.resolve(),
|
||||
openPath: () => Promise.resolve(),
|
||||
trashItem: (path) => require("node:fs/promises").unlink(path),
|
||||
showItemInFolder: (path) => { globalThis.__nativeShellCalls.push(["showItemInFolder", path]); },
|
||||
writeShortcutLink: (path) => { globalThis.__nativeShellCalls.push(["writeShortcutLink", path]); return true; },
|
||||
};
|
||||
@@ -300,6 +301,42 @@ process.stdout.write(JSON.stringify({
|
||||
electronActiveTimelineEventSchema.array().parse(result.snapshot.events);
|
||||
});
|
||||
|
||||
it("blocks trashItem asynchronously, records the attempt, and preserves the selected file", async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-hook-trash-");
|
||||
temporary.push(root);
|
||||
await writeElectronFixture(root);
|
||||
const selected = join(root, "retained.txt");
|
||||
await writeFile(selected, "must remain");
|
||||
const script = join(root, "trash.cjs");
|
||||
await writeFile(
|
||||
script,
|
||||
`const { shell } = require("electron");
|
||||
const result = shell.trashItem(${JSON.stringify(selected)});
|
||||
const promise = result instanceof Promise;
|
||||
result.then(
|
||||
() => { throw new Error("trashItem unexpectedly succeeded"); },
|
||||
error => process.stdout.write(JSON.stringify({ promise, message: error.message, snapshot: globalThis.__reaElectronActiveSnapshot() }) + "\\n"),
|
||||
);
|
||||
`,
|
||||
);
|
||||
const execution = await runNode(script, root);
|
||||
expect(execution.code, execution.stderr).toBe(0);
|
||||
const result = JSON.parse(execution.stdout.trim());
|
||||
expect(result.promise).toBe(true);
|
||||
expect(result.message).toBe("External shell effects are blocked");
|
||||
expect(await readFile(selected, "utf8")).toBe("must remain");
|
||||
expect(result.snapshot.events).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "shell-attempt",
|
||||
event: "trashItem",
|
||||
phase: "blocked",
|
||||
argument_shapes: ["string"],
|
||||
error: true,
|
||||
}),
|
||||
);
|
||||
expect(JSON.stringify(result.snapshot)).not.toContain(selected);
|
||||
});
|
||||
|
||||
it("classifies a dropped browser preload as preload configuration at capture", async () => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-electron-hook-browser-preload-",
|
||||
|
||||
Reference in new issue
Block a user