fix(ghidra): preserve project access failures

This commit is contained in:
morluto committed 2026-10-27 09:35:15 +08:00
1 parent 6734dee6a8
commit 6979c589b0
2 files changed
+68 -11

No files matched your search

+17 -11
View File
@@ -63,18 +63,24 @@ const selectionFailure = (
new ConfigurationError(message, {
settings: [{ setting, constraint: message }],
});
const entriesFor = (name: string) => [`${name}.gpr`, `${name}.rep`];
const entriesFor = (name: string) => [`${name}.gpr`, `${name}.rep`] as const;
const absentProjectEntry = (cause: unknown): undefined => {
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
return undefined;
throw cause;
};
const validateProject = async (root: string, name: string): Promise<void> => {
if (!(await lstat(root)).isDirectory())
if (!(await lstat(root).catch(absentProjectEntry))?.isDirectory())
throw selectionFailure(
`Existing Ghidra project directory does not exist: ${root}`,
);
const [marker, data] = await Promise.all(
entriesFor(name).map((entry) =>
lstat(join(root, entry)).catch(() => undefined),
),
);
// Inspect both entries even when one is missing; a physical failure of the
// other entry must not be mistaken for an invalid project selection.
const [markerName, dataName] = entriesFor(name);
const marker = await lstat(join(root, markerName)).catch(absentProjectEntry);
const data = await lstat(join(root, dataName)).catch(absentProjectEntry);
if (!marker?.isFile() || !data?.isDirectory())
throw selectionFailure(
`Existing Ghidra project requires a regular ${name}.gpr marker and ${name}.rep directory; restore saved .grez archives before selecting the project: ${root}`,
@@ -233,10 +239,10 @@ export const copyExistingGhidraProject = async (
resources,
} = options;
assertArtifactReadActive(signal);
const source = await realpath(sourcePath).catch(() => undefined);
const runtime = await realpath(dirname(destinationRoot)).catch(() =>
resolve(dirname(destinationRoot)),
);
const source = await realpath(sourcePath).catch(absentProjectEntry);
const runtime =
(await realpath(dirname(destinationRoot)).catch(absentProjectEntry)) ??
resolve(dirname(destinationRoot));
if (
source !== undefined &&
entriesFor(projectName).some((entry) =>
@@ -1,4 +1,6 @@
import {
chmod,
rm,
mkdir,
readFile,
readdir,
@@ -222,3 +224,52 @@ it.skipIf(process.platform === "win32")(
).rejects.toMatchObject({ diagnostics: { system_code: "ENOENT" } });
},
);
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"preserves denied access between profile resolution and snapshot copying",
async () => {
const { source, destination } = await fixture();
const project = await resolveGhidraProject(
{ path: join(source, "selected.gpr") },
{ path: "/tmp/target" },
resources,
);
try {
await chmod(source, 0);
await expect(
copyExistingGhidraProject(project.project_path, destination, {
projectName: project.project_name,
expectedSha256: project.sha256,
resources,
}),
).rejects.toMatchObject({
_tag: "AnalysisAccessDeniedError",
operation: "open_binary",
systemCode: "EACCES",
path: join(project.project_path, "selected.gpr"),
});
} finally {
await chmod(source, 0o700);
}
},
);
it.skipIf(process.platform === "win32")(
"keeps missing entries and symbolic links classified as invalid selections",
async () => {
const { source, destination } = await fixture();
await rm(join(source, "selected.gpr"));
await expect(
copyExistingGhidraProject(source, destination, {
projectName: "selected",
}),
).rejects.toMatchObject({ _tag: "ConfigurationError" });
await writeFile(join(source, "other.gpr"), "marker");
await symlink("other.gpr", join(source, "selected.gpr"));
await expect(
copyExistingGhidraProject(source, destination, {
projectName: "selected",
}),
).rejects.toMatchObject({ _tag: "ConfigurationError" });
},
);