Files
rea/tests/boundary/filesystem/ghidraBridgeSources.test.ts
T
morluto d9660eb032 fix(verifiers): share package command and workspace ownership
Join canonical owned command cleanup before releasing package verifier workspaces. Replace raw package command wrappers, the updater supervisor, WASM cleanup composite, and boolean-only retention with explicit capability ownership.

Preserve selected environments, npm arguments, no-deadline calls, clean exit status, and distinct aggregate/per-stream output budgets. Exercise actual timeout descendants, cancellation, retained cleanup retry, and workspace admission.
2026-10-24 16:58:18 +08:00

299 lines
9.5 KiB
TypeScript

import { execFile } from "node:child_process";
import {
chmod,
copyFile,
cp,
mkdir,
readFile,
readdir,
rename,
unlink,
writeFile,
} from "node:fs/promises";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import { expect, it } from "vitest";
import { parse as parseYaml } from "yaml";
import { z } from "zod";
import { GHIDRA_BRIDGE_SOURCES } from "../../../src/ghidra/GhidraBridgeSources.js";
import {
SUPPORTED_GHIDRA_JAVA_MAJOR,
SUPPORTED_GHIDRA_VERSION,
} from "../../../src/ghidra/GhidraReleasePolicy.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const execute = promisify(execFile);
/** A stand-in compiler proves admission and cleanup, not real Java compilation. */
const installation = async (
properties = "application.version=12.1.4\n",
compilerStatus = 0,
) => {
const root = await createTestTempDirectory("rea-ghidra-bridge-test-");
const install = join(root, "Ghidra SDK");
await mkdir(join(install, "Ghidra/Framework/lib"), { recursive: true });
await writeFile(
join(install, "Ghidra/application.properties"),
properties,
"latin1",
);
await writeFile(join(install, "Ghidra/Framework/lib/Generic.jar"), "");
const javaHome = join(root, "JDK home");
await mkdir(join(javaHome, "bin"), { recursive: true });
const javac = join(javaHome, "bin/javac");
await writeFile(
javac,
[
"#!/bin/sh",
'if [ "$1" = "-version" ]; then echo "javac 21.0.12"; exit 0; fi',
...(compilerStatus === 0
? ["exit 0"]
: [
'echo "bridge/ghidra/ReaGhidraBridge.java:1: error: cannot find symbol" >&2',
"exit 1",
]),
].join("\n"),
);
await chmod(javac, 0o755);
const scratchParent = join(root, "scratch");
await mkdir(scratchParent);
return {
scratchParent,
environment: {
...process.env,
GHIDRA_INSTALL_DIR: install,
JAVA_HOME: javaHome,
TMPDIR: scratchParent,
},
};
};
type AdmissionCase = {
name: string;
properties: string;
compilerStatus?: number;
expectedVersion?: string;
error?: string;
};
const admissionCases: readonly AdmissionCase[] = [
{ name: "compiler succeeds", properties: "application.version=12.1.4\n" },
{
name: "compiler fails",
properties: "application.version=12.1.4\n",
compilerStatus: 1,
error: "bridge/ghidra/ReaGhidraBridge.java:1: error: cannot find symbol",
},
{ name: "colon separator", properties: "application.version : 12.1.4\n" },
{
name: "escaped key and continued value",
properties: "application\\u002eversion=12.1.\\\n 4\n",
},
{
name: "Latin-1 distro suffix",
properties: "application.version=12.1.4-é\n",
expectedVersion: "12.1.4-é",
},
{
name: "trailing version whitespace",
properties: "application.version=12.1.4 \n",
error: 'Observed application.version="12.1.4 ".',
},
{
name: "malformed Unicode escape",
properties: "application.version=12.1.4\nother=\\u00xx\n",
error: "Malformed Unicode escape in Java properties file",
},
];
// Only the stand-in executable is POSIX-specific. Real compilation runs in its
// hosted lane; the installed provider's Properties tests cover the parser too.
it.skipIf(process.platform === "win32").each(admissionCases)(
"preserves version admission and scratch cleanup: $name",
async ({ properties, compilerStatus, expectedVersion = "12.1.4", error }) => {
const fixture = await installation(properties, compilerStatus);
const run = execute(
process.execPath,
["scripts/verify-ghidra-bridge-sources.mjs"],
{
cwd: process.cwd(),
env: fixture.environment,
timeout: 30_000,
},
);
if (error === undefined)
await expect(run).resolves.toMatchObject({
stdout: expect.stringContaining(
`"ghidra_version":${JSON.stringify(expectedVersion)}`,
),
});
else
await expect(run).rejects.toMatchObject({
code: 1,
stdout: "",
stderr: expect.stringContaining(error),
});
expect(await readdir(fixture.scratchParent)).toEqual([]);
},
);
it.skipIf(process.platform === "win32").each([
{ source: GHIDRA_BRIDGE_SOURCES.prepareCom, action: "deleted" },
{ source: GHIDRA_BRIDGE_SOURCES.applySeeds, action: "renamed" },
{ source: "UnregisteredHelper.java", action: "added" },
])(
"rejects a $action launcher source before compilation: $source",
async ({ source, action }) => {
const repository = await createTestTempDirectory("rea-ghidra-source-set-");
await mkdir(join(repository, "scripts"));
await mkdir(join(repository, "src/ghidra"), { recursive: true });
await writeFile(join(repository, "package.json"), '{"type":"module"}\n');
for (const path of [
"scripts/verify-ghidra-bridge-sources.mjs",
"src/ghidra/GhidraBridgeSources.ts",
"src/ghidra/GhidraJavaProperties.ts",
"src/ghidra/GhidraReleasePolicy.ts",
])
await copyFile(resolve(path), join(repository, path));
await cp(resolve("bridge/ghidra"), join(repository, "bridge/ghidra"), {
recursive: true,
});
const path = join(repository, "bridge/ghidra", source);
if (action === "added")
await writeFile(path, "class UnregisteredHelper {}\n");
else if (action === "deleted") await unlink(path);
else {
const className = source.slice(0, -".java".length);
await writeFile(
path,
(await readFile(path, "utf8")).replaceAll(
className,
`Renamed${className}`,
),
);
await rename(path, join(repository, "bridge/ghidra", `Renamed${source}`));
}
const fixture = await installation();
await expect(
execute(
process.execPath,
[join(repository, "scripts/verify-ghidra-bridge-sources.mjs")],
{
cwd: repository,
env: fixture.environment,
timeout: 30_000,
},
),
).rejects.toMatchObject({
code: 1,
stderr: expect.stringContaining(
action === "added"
? `Ghidra bridge sources absent from the manifest: ${source}`
: `Required Ghidra bridge source is missing: bridge/ghidra/${source}`,
),
});
expect(await readdir(fixture.scratchParent)).toEqual([]);
},
);
it("rejects an omitted auxiliary source at the actual package verification boundary", async () => {
const source = GHIDRA_BRIDGE_SOURCES.prepareCom;
const packedFiles = [
"package/bridge/hopper_bridge.py",
"package/bridge/cutter/rea_bridge.py",
...Object.values(GHIDRA_BRIDGE_SOURCES)
.filter((name) => name !== source)
.map((name) => `package/bridge/ghidra/${name}`),
];
await expect(
execute(process.execPath, [
"--input-type=module",
"-e",
`import { verifyPackedBridge } from "./scripts/verify-packed-bridge.mjs";
import { createPackageVerifierCommands } from "./scripts/lib/verify-package-commands.mjs";
import { withVerifierCleanup } from "./scripts/lib/verifier-cleanup.mjs";
import { PrivateRuntimeRoot } from "./dist/process/PrivateRuntimeRoot.js";
const root = await PrivateRuntimeRoot.create({ prefix: "rea-bridge-admission-" });
let commands;
await withVerifierCleanup(async () => {
commands = await createPackageVerifierCommands({ workspace: root });
await verifyPackedBridge({ tarball: "unused-because-admission-must-fail", packedFiles: JSON.parse(process.argv[1]), commands });
}, [() => commands === undefined ? root.close() : commands.close()]);`,
JSON.stringify(packedFiles),
]),
).rejects.toMatchObject({
code: 1,
stderr: expect.stringContaining(
`package omitted package/bridge/ghidra/${source}`,
),
});
});
it("binds the workflow SDK pin and verified compile mode to the canonical release", async () => {
const { jobs } = z
.object({
jobs: z.object({
"ghidra-bridge-sources": z.object({
env: z.object({
GHIDRA_RELEASE_URL: z.string(),
GHIDRA_RELEASE_SHA256: z.string(),
}),
steps: z.array(
z.object({ run: z.string().optional() }).passthrough(),
),
}),
}),
})
.parse(
parseYaml(
await readFile(
resolve(".github/workflows/ghidra-bridge-sources.yml"),
"utf8",
),
),
);
const job = jobs["ghidra-bridge-sources"];
const pinIndex = job.steps.findIndex((step) =>
step.run?.includes("--check-sdk-pin"),
);
const downloadIndex = job.steps.findIndex((step) =>
step.run?.includes("curl "),
);
expect(pinIndex).toBeGreaterThanOrEqual(0);
expect(downloadIndex).toBeGreaterThan(pinIndex);
expect(job.steps.some((step) => step.run?.includes("--verified-sdk"))).toBe(
true,
);
const command = [
"scripts/verify-ghidra-bridge-sources.mjs",
"--check-sdk-pin",
];
await expect(
execute(process.execPath, command, {
env: { ...process.env, ...job.env },
}),
).resolves.toMatchObject({
stdout: `GHIDRA_JAVA_MAJOR=${SUPPORTED_GHIDRA_JAVA_MAJOR}\n`,
});
for (const env of [
{
...job.env,
GHIDRA_RELEASE_URL: job.env.GHIDRA_RELEASE_URL.replaceAll(
SUPPORTED_GHIDRA_VERSION,
"0.0.0",
),
},
{ ...job.env, GHIDRA_RELEASE_SHA256: "not-a-digest" },
])
await expect(
execute(process.execPath, command, {
env: { ...process.env, ...env },
}),
).rejects.toMatchObject({
code: 1,
stderr: expect.stringContaining(
"SDK URL and SHA256 must match verified Ghidra",
),
});
});