fix(ghidra): retain typed Java prerequisite outcomes

Replace ambiguous absent observations with an owning Result producer for
Java prerequisite probes. Preserve missing-command, runtime-failure and
unresolved-property outcomes, process error codes, both bounded streams,
raw byte counts and partial properties through provider and doctor adapters.
Reject malformed numeric-prefix versions while retaining their source facts.
Migrate all injected hosts to the named probe contract and remove the old
parser/observation ownership from installation discovery.

Validation: 17 focused files, 350 passed; npm run check passed. Public doctor
fixtures distinguish absent/nonzero/malformed commands. Real installed JDK
and Ghidra readiness facts matched across 20 baseline/candidate CLI tasks.
Warm repeat raw replies6392tokens each, elapsed .448/.444s, CPU .662/.660s;
sampled family memory varied and reversed direction on repeat. No causal
performance improvement claim, true-cold or full-agent/fault-path measurement.

Synchronous probe deadline/cancellation/family cleanup remains #4561 and needs
coordinated discovery/admission supervision; no claim that it is fixed here.
No real failing JVM or Windows runtime verification performed.

Refs #4560, #4561
This commit is contained in:
morluto committed 2026-10-17 03:33:16 +08:00
1 parent 8116dc0651
commit c75cf827e7
14 files changed
+831 -301

No files matched your search

+24 -19
View File
@@ -1,3 +1,6 @@
import { missingJavaProbe } from "./GhidraInstallation.fixture.js";
import { ok as javaProbeOk } from "../domain/result.js";
import type { GhidraJavaObservation } from "./GhidraJavaProbe.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { runDoctor, type DoctorHost } from "../application/Doctor.js";
@@ -19,15 +22,16 @@ const installationHost = (
): GhidraInstallationHost => ({
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
executable: () => true,
probeJava: () => ({
osArch: "amd64",
osName: platform === "win32" ? "Windows 11" : "Linux",
version: "21.0.11",
major: 21,
home: "/usr/lib/jvm/jdk-21",
bits: 64,
runtime: "jdk",
}),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
osArch: "amd64",
osName: platform === "win32" ? "Windows 11" : "Linux",
version: "21.0.11",
major: 21,
home: "/usr/lib/jvm/jdk-21",
bits: 64,
runtime: "jdk",
}),
...overrides,
});
@@ -130,15 +134,16 @@ describe("Ghidra doctor integration", () => {
inspection(
{ installDir: INSTALL },
{
probeJava: () => ({
osArch: "amd64",
osName: "Linux",
version: "17.0.12",
major: 17,
home: "/usr/lib/jvm/jdk-17",
bits: 64,
runtime: "jdk",
}),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
osArch: "amd64",
osName: "Linux",
version: "17.0.12",
major: 17,
home: "/usr/lib/jvm/jdk-17",
bits: 64,
runtime: "jdk",
}),
},
),
"ghidra-java",
@@ -364,7 +369,7 @@ describe("Windows native doctor failure diagnostics", () => {
installDir: "C:\\tools\\ghidra_12.1.4_PUBLIC",
platform: "win32",
},
{ probeJava: () => undefined },
{ probeJava: () => missingJavaProbe },
),
);
expect(projected.available).toBe(false);
+26 -4
View File
@@ -33,7 +33,11 @@ export const projectGhidraDoctorInspection = (
: {},
checks: [
...selectedChecks(inspection).map((check) =>
projectCheck(check, inspection.installDir !== null),
projectCheck(
check,
inspection.installDir !== null,
inspection.javaProbeFailure,
),
),
...(readiness !== undefined
? [
@@ -107,11 +111,25 @@ const selectedChecks = (
const projectCheck = (
check: GhidraInstallationCheck,
configured: boolean,
javaFailure: GhidraInstallationInspection["javaProbeFailure"],
): DoctorProviderCheck => ({
name: check.name,
ok: check.status === "passed",
code: check.status === "failed" ? check.code : null,
detail: check.detail,
...(check.name === "java" && javaFailure?.output !== undefined
? {
details: {
output: { ...javaFailure.output },
...(javaFailure.processErrorCode === undefined
? {}
: { process_error_code: javaFailure.processErrorCode }),
...(javaFailure.properties === undefined
? {}
: { properties: { ...javaFailure.properties } }),
},
}
: {}),
remediation: check.status === "failed" ? check.remediation : null,
classification:
check.name === "platform" || check.name === "architecture"
@@ -120,7 +138,11 @@ const projectCheck = (
check.status === "failed" &&
check.code === "runtime_missing"
? "missing_dependency"
: configured
? "config_drift"
: "missing_analysis_engine",
: check.name === "java" &&
check.status === "failed" &&
check.code === "runtime_unavailable"
? "runtime_failure"
: configured
? "config_drift"
: "missing_analysis_engine",
});
+21 -9
View File
@@ -1,3 +1,7 @@
import { ok as javaProbeOk } from "../domain/result.js";
import type { GhidraJavaObservation } from "./GhidraJavaProbe.js";
import { err } from "../domain/result.js";
import type { GhidraJavaProbeFailure } from "./GhidraJavaProbe.js";
import type { GhidraInstallationHost } from "./GhidraInstallation.js";
/** Simulated valid installation for profile tests; never starts Ghidra or Java. */
@@ -6,13 +10,21 @@ export const ghidraProfileInstallationHost: GhidraInstallationHost = {
architecture: "x64",
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
executable: () => true,
probeJava: () => ({
version: "21.0.11",
major: 21,
home: "/jdk-21",
bits: 64,
runtime: "jdk",
osArch: "amd64",
osName: "Linux",
}),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
version: "21.0.11",
major: 21,
home: "/jdk-21",
bits: 64,
runtime: "jdk",
osArch: "amd64",
osName: "Linux",
}),
};
/** A confirmed absent Java command for installation admission fixtures. */
export const missingJavaProbe = err<GhidraJavaProbeFailure>({
_tag: "GhidraJavaProbeFailure",
code: "runtime_missing",
detail: "Java command was not found",
});
+137 -59
View File
@@ -1,3 +1,6 @@
import { missingJavaProbe } from "./GhidraInstallation.fixture.js";
import { ok as javaProbeOk } from "../domain/result.js";
import type { GhidraJavaObservation } from "./GhidraJavaProbe.js";
import { chmod, mkdir, symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
@@ -12,7 +15,6 @@ import {
inspectGhidraInstallation,
type GhidraExecutableHeaderProbe,
type GhidraInstallationHost,
type GhidraJavaObservation,
} from "./GhidraInstallation.js";
import {
ghidraApplicationProperties,
@@ -66,7 +68,7 @@ const host = (
: undefined,
executable: (path) =>
path === HEADLESS || path === MAC_DECOMPILER || path === LINUX_DECOMPILER,
probeJava: () => JAVA,
probeJava: () => javaProbeOk<GhidraJavaObservation>(JAVA),
...overrides,
});
@@ -183,8 +185,8 @@ it("probes the selected Java environment without reading conflicting ambient set
environment.REA_GHIDRA_CONTEXT === "selected" &&
environment.REA_GHIDRA_AMBIENT === undefined &&
environment.PATH === "/selected-bin"
? JAVA
: undefined,
? javaProbeOk<GhidraJavaObservation>(JAVA)
: missingJavaProbe,
}),
);
expect(inspection.status).toBe("available");
@@ -251,11 +253,12 @@ describe("Ghidra installation inspection", () => {
host({
readText: () =>
`application.version=${providerVersion}\napplication.java.min=21\napplication.java.max=\n`,
probeJava: () => ({
...JAVA,
version: javaVersion,
major,
}),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
...JAVA,
version: javaVersion,
major,
}),
}),
);
expect(result.status).toBe("available");
@@ -286,11 +289,12 @@ describe("Ghidra installation inspection", () => {
{ environment: {}, installDir: INSTALL, platform, architecture },
host({
executable: (path) => path === HEADLESS || path === decompiler,
probeJava: () => ({
...JAVA,
osArch: architecture === "arm64" ? "aarch64" : "amd64",
osName: platform === "darwin" ? "Mac OS X" : "Linux",
}),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
...JAVA,
osArch: architecture === "arm64" ? "aarch64" : "amd64",
osName: platform === "darwin" ? "Mac OS X" : "Linux",
}),
}),
),
).toMatchObject({
@@ -302,7 +306,9 @@ describe("Ghidra installation inspection", () => {
}
},
);
});
describe("Ghidra Windows installation commitment", () => {
it("accepts the exact Windows x64 batch launcher and JDK commitment", () => {
const windowsHost: GhidraInstallationHost = {
platform: "win32",
@@ -316,11 +322,11 @@ describe("Ghidra installation inspection", () => {
probeJava: (command, environment) => {
expect(command).toBe("C:\\Java\\jdk-21\\bin\\java.exe");
expect(environment.PATH).toMatch(/^C:\\Java\\jdk-21\\bin;/u);
return {
return javaProbeOk<GhidraJavaObservation>({
...JAVA,
home: "C:\\Java\\jdk-21",
osName: "Windows 11",
};
});
},
};
@@ -430,7 +436,7 @@ describe("Ghidra installation rejection diagnostics", () => {
platform: "linux" as const,
architecture: "x64" as const,
},
override: { probeJava: () => undefined },
override: { probeJava: () => missingJavaProbe },
failed: "java",
code: "runtime_missing",
},
@@ -441,7 +447,14 @@ describe("Ghidra installation rejection diagnostics", () => {
platform: "linux" as const,
architecture: "x64" as const,
},
override: { probeJava: () => ({ ...JAVA, major: 17, version: "17" }) },
override: {
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
...JAVA,
major: 17,
version: "17",
}),
},
failed: "java",
code: "unsupported_version",
},
@@ -452,7 +465,13 @@ describe("Ghidra installation rejection diagnostics", () => {
platform: "linux" as const,
architecture: "x64" as const,
},
override: { probeJava: () => ({ ...JAVA, runtime: "jre" as const }) },
override: {
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
...JAVA,
runtime: "jre" as const,
}),
},
failed: "java",
code: "unsupported_version",
},
@@ -718,7 +737,12 @@ describe("Ghidra installation version admission", () => {
name: "JDK above the installation maximum",
readText: () =>
"application.version=12.1.4\napplication.java.min=21\napplication.java.max=25\n",
probeJava: () => ({ ...JAVA, major: 27, version: "27" }),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
...JAVA,
major: 27,
version: "27",
}),
failed: "java",
code: "unsupported_version",
},
@@ -726,28 +750,28 @@ describe("Ghidra installation version admission", () => {
name: "unreadable Java minimum",
readText: () =>
"application.version=12.1.4\napplication.java.min=latest\n",
probeJava: () => JAVA,
probeJava: () => javaProbeOk<GhidraJavaObservation>(JAVA),
failed: "java",
code: "version_unresolved",
},
{
name: "missing Java minimum",
readText: () => "application.version=12.1.4\n",
probeJava: () => JAVA,
probeJava: () => javaProbeOk<GhidraJavaObservation>(JAVA),
failed: "java",
code: "version_unresolved",
},
{
name: "unparseable Ghidra version",
readText: () => "application.version=latest\napplication.java.min=21\n",
probeJava: () => JAVA,
probeJava: () => javaProbeOk<GhidraJavaObservation>(JAVA),
failed: "version",
code: "version_unresolved",
},
{
name: "different Ghidra line",
readText: () => "application.version=12.2.0\napplication.java.min=21\n",
probeJava: () => JAVA,
probeJava: () => javaProbeOk<GhidraJavaObservation>(JAVA),
failed: "version",
code: "unsupported_version",
},
@@ -779,7 +803,12 @@ describe("Ghidra installation version admission", () => {
host({
readText: () =>
"application.version=12.0.4\napplication.java.min=21\napplication.java.max=\n",
probeJava: () => ({ ...JAVA, major: 17, version: "17.0.20.1" }),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
...JAVA,
major: 17,
version: "17.0.20.1",
}),
}),
);
expect(result.status).toBe("unavailable");
@@ -804,7 +833,12 @@ describe("Ghidra installation version admission", () => {
host({
readText: () =>
`application.version=${SUPPORTED_GHIDRA_VERSION}\napplication.java.min=17\n`,
probeJava: () => ({ ...JAVA, major: 17, version: "17.0.20.1" }),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
...JAVA,
major: 17,
version: "17.0.20.1",
}),
}),
),
).toMatchObject({ status: "available", javaVersion: "17.0.20.1" });
@@ -863,7 +897,7 @@ describe("Ghidra configuration paths", () => {
host({
probeJava: (command) => {
probed.push(command);
return JAVA;
return javaProbeOk<GhidraJavaObservation>(JAVA);
},
}),
);
@@ -920,7 +954,12 @@ describe("Ghidra FAT native decompiler admission", () => {
architecture: "arm64",
},
host({
probeJava: () => ({ ...JAVA, osArch: "aarch64", osName: "Mac OS X" }),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
...JAVA,
osArch: "aarch64",
osName: "Mac OS X",
}),
executableHeader: () => headerProbe(bytes),
}),
);
@@ -941,7 +980,12 @@ describe("Ghidra FAT native decompiler admission", () => {
architecture: "arm64",
},
host({
probeJava: () => ({ ...JAVA, osArch: "aarch64", osName: "Mac OS X" }),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
...JAVA,
osArch: "aarch64",
osName: "Mac OS X",
}),
executableHeader: () => headerProbe(bytes),
}),
);
@@ -993,11 +1037,12 @@ describe("Ghidra native decompiler admission", () => {
const result = inspectGhidraInstallation(
options,
host({
probeJava: () => ({
...JAVA,
osArch: options.architecture === "arm64" ? "aarch64" : "amd64",
osName: options.platform === "darwin" ? "Mac OS X" : "Linux",
}),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
...JAVA,
osArch: options.architecture === "arm64" ? "aarch64" : "amd64",
osName: options.platform === "darwin" ? "Mac OS X" : "Linux",
}),
executableHeader,
}),
);
@@ -1029,7 +1074,9 @@ describe("Ghidra native decompiler admission", () => {
detail: `${LINUX_DECOMPILER} reports elf x86_64, which matches linux/x64`,
});
});
});
describe("Ghidra native decompiler compatibility", () => {
it("keeps an unreadable header unknown instead of failing the install", () => {
const result = inspectGhidraInstallation(
{
@@ -1060,7 +1107,12 @@ describe("Ghidra native decompiler admission", () => {
architecture: "arm64",
},
host({
probeJava: () => ({ ...JAVA, osArch: "aarch64", osName: "Mac OS X" }),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
...JAVA,
osArch: "aarch64",
osName: "Mac OS X",
}),
executableHeader: (path: string) =>
path === MAC_DECOMPILER ? headerProbe(bytes) : undefined,
}),
@@ -1084,7 +1136,12 @@ describe("Ghidra native decompiler admission", () => {
architecture: "arm64",
},
host({
probeJava: () => ({ ...JAVA, osArch: "aarch64", osName: "Mac OS X" }),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
...JAVA,
osArch: "aarch64",
osName: "Mac OS X",
}),
executableHeader: (path: string) =>
path === MAC_DECOMPILER ? headerProbe(elf(2, 1, 62)) : undefined,
}),
@@ -1113,7 +1170,12 @@ describe("Ghidra JVM native directory admission", () => {
architecture: "arm64",
},
host({
probeJava: () => ({ ...JAVA, osArch: "x86_64", osName: "Mac OS X" }),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
...JAVA,
osArch: "x86_64",
osName: "Mac OS X",
}),
}),
);
expect(result.status).toBe("unavailable");
@@ -1139,7 +1201,12 @@ describe("Ghidra JVM native directory admission", () => {
architecture: "arm64",
},
host({
probeJava: () => ({ ...JAVA, osArch: "aarch64", osName: "Mac OS X" }),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
...JAVA,
osArch: "aarch64",
osName: "Mac OS X",
}),
}),
);
expect(result.status).toBe("available");
@@ -1154,7 +1221,10 @@ describe("Ghidra JVM native directory admission", () => {
platform: "darwin",
architecture: "arm64",
},
host({ probeJava: () => withoutArchitecture }),
host({
probeJava: () =>
javaProbeOk<GhidraJavaObservation>(withoutArchitecture),
}),
);
expect(result.status).toBe("unavailable");
expect(
@@ -1177,7 +1247,8 @@ describe("Ghidra JVM native directory admission", () => {
architecture: "arm64",
},
host({
probeJava: () => ({ ...JAVA, osArch }),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({ ...JAVA, osArch }),
}),
);
expect(result.status).toBe("unavailable");
@@ -1200,11 +1271,12 @@ describe("Ghidra JVM native directory admission", () => {
architecture: "arm64",
},
host({
probeJava: () => ({
...JAVA,
osArch: "aarch64",
osName: "Windows 11",
}),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
...JAVA,
osArch: "aarch64",
osName: "Windows 11",
}),
}),
);
expect(result.status).toBe("unavailable");
@@ -1230,7 +1302,9 @@ describe("Ghidra JVM OS admission", () => {
platform: "linux",
architecture: "x64",
},
host({ probeJava: () => withoutOsName }),
host({
probeJava: () => javaProbeOk<GhidraJavaObservation>(withoutOsName),
}),
);
expect(result.status).toBe("unavailable");
expect(
@@ -1253,7 +1327,8 @@ describe("Ghidra JVM OS admission", () => {
architecture: "x64",
},
host({
probeJava: () => ({ ...JAVA, osName }),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({ ...JAVA, osName }),
}),
);
expect(result.status).toBe("unavailable");
@@ -1272,18 +1347,21 @@ describe("Ghidra Java probe os.arch extraction", () => {
it("extracts os.arch through the real Java probe into the installation diagnostics", () => {
vi.mocked(spawnSync).mockReturnValue({
status: 0,
stdout: "",
stderr: [
"Property settings:",
" file.encoding = UTF-8",
" java.home = /usr/lib/jvm/jdk-21",
" java.version = 21.0.11",
" java.vm.name = OpenJDK 64-Bit Server VM",
" os.arch = x86_64",
" os.name = Linux",
" sun.arch.data.model = 64",
"",
].join("\n"),
stdout: Buffer.alloc(0),
signal: null,
stderr: Buffer.from(
[
"Property settings:",
" file.encoding = UTF-8",
" java.home = /usr/lib/jvm/jdk-21",
" java.version = 21.0.11",
" java.vm.name = OpenJDK 64-Bit Server VM",
" os.arch = x86_64",
" os.name = Linux",
" sun.arch.data.model = 64",
"",
].join("\n"),
),
} as never);
try {
// No host override: the real system host runs the real probeJava
+72 -139
View File
@@ -1,5 +1,10 @@
import {
probeGhidraJava,
type GhidraJavaObservation,
type GhidraJavaProbeFailure,
} from "./GhidraJavaProbe.js";
import type { Result } from "../domain/result.js";
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import { spawnSync } from "node:child_process";
import { constants } from "node:fs";
import {
accessSync,
@@ -75,15 +80,6 @@ const NATIVE_ARCHITECTURES: readonly {
/** Bound installation discovery reads; larger headers remain unknown. */
const EXECUTABLE_HEADER_PROBE_BYTES = 4096;
/**
* Bound the Java version probe so a chatty or hostile JVM cannot force an
* unbounded in-memory capture. Well-behaved JVMs print a few kilobytes for
* `-XshowSettings:properties`; the same 1 MiB budget used for other Java
* listings leaves ample headroom. A probe that exceeds the budget yields no
* observation and is reported through the existing failed-probe path.
*/
const JAVA_PROBE_MAX_OUTPUT_BYTES = 1024 * 1024;
/** Compatibility of one present native decompiler with the inspected host. */
type NativeDecompilerCompatibility =
| {
@@ -206,6 +202,7 @@ interface GhidraInstallationObservation {
readonly configurationDirectoryName: string | null;
readonly nativeDecompilerPath: string | null;
readonly javaCommand: string;
readonly javaProbeFailure?: GhidraJavaProbeFailure;
/** Observed java.vm.name; omitted when the probe cannot establish VM identity. */
readonly javaVmName?: string;
/**
@@ -252,29 +249,6 @@ export type GhidraInstallationInspection =
| AvailableGhidraInstallation
| UnavailableGhidraInstallation;
/** Parsed identity of the JDK selected for Ghidra. */
export interface GhidraJavaObservation {
readonly version: string;
readonly major: number;
readonly home: string;
readonly bits: number | null;
/** Present when Java reported a bitness value that could not be parsed exactly. */
readonly unparsedBits?: string;
readonly runtime: "jdk" | "jre";
readonly vmName?: string;
/**
* Observed JVM os.arch property, which Ghidra uses to select its native
* decompiler directory. Omitted when the probe cannot establish it.
*/
readonly osArch?: string;
/**
* Observed JVM os.name property, which Ghidra uses with os.arch to select
* its native decompiler OS directory. Omitted when the probe cannot
* establish it.
*/
readonly osName?: string;
}
/** A bounded executable-header probe: a small prefix plus the whole file size. */
export interface GhidraExecutableHeaderProbe {
/** A bounded prefix; a larger or unreadable header remains unknown. */
@@ -299,7 +273,7 @@ export interface GhidraInstallationHost {
probeJava(
command: string,
environment: NodeJS.ProcessEnv,
): GhidraJavaObservation | undefined;
): Result<GhidraJavaObservation, GhidraJavaProbeFailure>;
/** Resolve a PATH launcher to a validated installation without executing it. */
discoverInstallDirOnPath?(
environment: Readonly<NodeJS.ProcessEnv>,
@@ -315,6 +289,7 @@ interface GhidraInstallationCoordinates {
readonly properties: GhidraApplicationProperties | undefined;
readonly javaCommand: string;
readonly java: GhidraJavaObservation | undefined;
readonly javaProbeFailure: GhidraJavaProbeFailure | undefined;
}
interface GhidraInstallationCheckContext {
@@ -349,6 +324,8 @@ export const inspectGhidraInstallation = (
(check): check is Extract<GhidraInstallationCheck, { status: "failed" }> =>
check.status === "failed",
);
const javaObservation =
coordinates.java ?? coordinates.javaProbeFailure?.properties;
const observation = {
platform,
architecture,
@@ -357,15 +334,18 @@ export const inspectGhidraInstallation = (
coordinates.properties?.configurationDirectoryName ?? null,
nativeDecompilerPath: coordinates.nativeDecompilerPath,
javaCommand: coordinates.javaCommand,
...(coordinates.java?.vmName === undefined
...(coordinates.javaProbeFailure === undefined
? {}
: { javaVmName: coordinates.java.vmName }),
...(coordinates.java?.osArch === undefined
: { javaProbeFailure: coordinates.javaProbeFailure }),
...(javaObservation?.vmName == null
? {}
: { javaOsArch: coordinates.java.osArch }),
...(coordinates.java?.osName === undefined
: { javaVmName: javaObservation.vmName }),
...(javaObservation?.osArch == null
? {}
: { javaOsName: coordinates.java.osName }),
: { javaOsArch: javaObservation.osArch }),
...(javaObservation?.osName == null
? {}
: { javaOsName: javaObservation.osName }),
checks,
};
if (failed !== undefined)
@@ -375,8 +355,8 @@ export const inspectGhidraInstallation = (
installDir: coordinates.installDir,
analyzeHeadlessPath: coordinates.analyzeHeadlessPath,
providerVersion: coordinates.properties?.providerVersion ?? null,
javaHome: coordinates.java?.home ?? options.javaHome ?? null,
javaVersion: coordinates.java?.version ?? null,
javaHome: javaObservation?.home ?? options.javaHome ?? null,
javaVersion: javaObservation?.version ?? null,
rejection: { code: failed.code, reason: failed.remediation },
};
return availableInstallation(coordinates, observation);
@@ -476,6 +456,18 @@ const installationCoordinates = (
"bin",
platform === "win32" ? "java.exe" : "java",
);
// Relative JAVA_HOME is refused by configuration validation without execution.
const javaProbe =
options.javaHome === undefined || path.isAbsolute(options.javaHome)
? host.probeJava(
javaCommand,
ghidraJavaEnvironment(
options.javaHome,
options.environment,
platform,
),
)
: undefined;
return {
installDir,
applicationPropertiesPath,
@@ -486,19 +478,9 @@ const installationCoordinates = (
? undefined
: ghidraApplicationProperties(properties),
javaCommand,
// The configuration check rejects a relative JAVA_HOME. Probing it would
// run whatever `bin/java` the current directory holds.
java:
options.javaHome === undefined || path.isAbsolute(options.javaHome)
? host.probeJava(
javaCommand,
ghidraJavaEnvironment(
options.javaHome,
options.environment,
platform,
),
)
: undefined,
java: javaProbe?.ok === true ? javaProbe.value : undefined,
javaProbeFailure:
javaProbe !== undefined && !javaProbe.ok ? javaProbe.error : undefined,
};
};
@@ -552,6 +534,7 @@ const installationChecks = ({
coordinates.javaCommand,
javaHome,
coordinates.properties,
coordinates.javaProbeFailure,
),
];
@@ -589,7 +572,7 @@ const configurationCheck = (
});
};
/** Project an installation probe into caller-visible, secret-free diagnostics. */
/** Project an installation probe into caller-visible, bounded diagnostics. */
export const ghidraInstallationDiagnostics = (
inspection: GhidraInstallationInspection,
): Readonly<Record<string, JsonValue>> => ({
@@ -599,6 +582,26 @@ export const ghidraInstallationDiagnostics = (
application_properties_path: inspection.applicationPropertiesPath,
provider_version: inspection.providerVersion,
java_command: inspection.javaCommand,
...(inspection.javaProbeFailure === undefined
? {}
: {
java_probe_failure: {
code: inspection.javaProbeFailure.code,
...(inspection.javaProbeFailure.processErrorCode === undefined
? {}
: {
process_error_code:
inspection.javaProbeFailure.processErrorCode,
}),
detail: inspection.javaProbeFailure.detail,
...(inspection.javaProbeFailure.properties === undefined
? {}
: { properties: { ...inspection.javaProbeFailure.properties } }),
...(inspection.javaProbeFailure.output === undefined
? {}
: { output: { ...inspection.javaProbeFailure.output } }),
},
}),
java_home: inspection.javaHome,
java_version: inspection.javaVersion,
java_vm_name: inspection.javaVmName ?? null,
@@ -688,8 +691,14 @@ const nativeDecompilerCheck = ({
// Ghidra resolves the OS directory from the JVM os.name/os.arch pair, so a
// JVM reporting a non-host os.name (for example through -Dos.name=Windows)
// selects a different OS directory even when os.arch matches.
const osArch = coordinates.java?.osArch;
const osName = coordinates.java?.osName;
const osArch =
coordinates.java?.osArch ??
coordinates.javaProbeFailure?.properties?.osArch ??
undefined;
const osName =
coordinates.java?.osName ??
coordinates.javaProbeFailure?.properties?.osName ??
undefined;
const selectedDirectory = jvmOsArchNativeDirectory(platform, osArch);
const validatedDirectory = NATIVE_PLATFORMS[platform]?.[architecture];
// Without a JVM observation the java check reports the missing runtime;
@@ -814,6 +823,7 @@ const javaCheck = (
command: string,
configuredHome: string | undefined,
properties: GhidraApplicationProperties | undefined,
failure: GhidraJavaProbeFailure | undefined,
): GhidraInstallationCheck => {
const requirement = ghidraJavaRequirement(properties);
const range = ghidraJavaRangeLabel(requirement);
@@ -836,8 +846,8 @@ const javaCheck = (
const detail =
observation === undefined
? requirement.unresolved === null
? command
: `${command}; ${requirement.unresolved}`
? (failure?.detail ?? `${command}: Java probe was not attempted`)
: `${failure?.detail ?? command}; ${requirement.unresolved}`
: `${observation.version}; ${bitness}; ${observation.runtime === "jdk" ? "JDK" : "runtime only"}; ${observation.home}; accepted ${range}`;
return installationCheck({
name: "java",
@@ -846,7 +856,7 @@ const javaCheck = (
requirement.unresolved !== null
? "version_unresolved"
: observation === undefined
? "runtime_missing"
? (failure?.code ?? "version_unresolved")
: knownMismatch
? "unsupported_version"
: "version_unresolved",
@@ -909,72 +919,7 @@ const systemGhidraInstallationHost = (): GhidraInstallationHost => ({
return false;
}
},
probeJava(command, environment) {
const observed = spawnSync(
command,
[
"-Dstdout.encoding=UTF-8",
"-Dstderr.encoding=UTF-8",
// JDK 17/18 use the legacy names; JDK 19+ use the names above.
"-Dsun.stdout.encoding=UTF-8",
"-Dsun.stderr.encoding=UTF-8",
"-XX:-UsePerfData",
"-XshowSettings:properties",
"-version",
],
{
encoding: "utf8",
env: environment,
timeout: 5_000,
maxBuffer: JAVA_PROBE_MAX_OUTPUT_BYTES,
windowsHide: true,
},
);
if (observed.error !== undefined || observed.status !== 0) return undefined;
const output = `${observed.stdout ?? ""}\n${observed.stderr ?? ""}`;
const version = javaProperty(output, "java.version");
const home = javaProperty(output, "java.home");
const vmName = javaProperty(output, "java.vm.name");
const osArch = javaProperty(output, "os.arch");
const osName = javaProperty(output, "os.name");
const reportedBits = javaProperty(output, "sun.arch.data.model");
const parsedBits =
reportedBits !== null && /^\d+$/.test(reportedBits)
? Number(reportedBits)
: null;
const bits =
parsedBits !== null && Number.isSafeInteger(parsedBits)
? parsedBits
: null;
if (
version === null ||
version.length === 0 ||
home === null ||
home.length === 0
)
return undefined;
return {
version,
major: javaMajor(version),
home,
...(vmName === null ? {} : { vmName }),
...(osArch === null ? {} : { osArch }),
...(osName === null ? {} : { osName }),
bits,
...(reportedBits === null || bits !== null
? {}
: { unparsedBits: reportedBits }),
runtime: executablePath(
(process.platform === "win32" ? win32 : posix).join(
home,
"bin",
process.platform === "win32" ? "javac.exe" : "javac",
),
)
? "jdk"
: "jre",
};
},
probeJava: probeGhidraJava,
discoverInstallDirOnPath: discoverGhidraInstallationOnPath,
});
@@ -1031,15 +976,3 @@ const executablePath = (path: string): boolean => {
return false;
}
};
const javaProperty = (output: string, name: string): string | null =>
new RegExp(
`^[ \\t]*${name.replaceAll(".", "\\.")}[ \\t]*=[ \\t]*([^\\r\\n]*?)[ \\t]*\\r?$`,
"mu",
).exec(output)?.[1] ?? null;
const javaMajor = (version: string): number => {
const first = Number.parseInt(version.split(".")[0] ?? "0", 10);
if (first !== 1) return first;
return Number.parseInt(version.split(".")[1] ?? "0", 10);
};
+146
View File
@@ -0,0 +1,146 @@
import { spawnSync } from "node:child_process";
import { afterEach, describe, expect, it, vi } from "vitest";
import { probeGhidraJava } from "./GhidraJavaProbe.js";
vi.mock("node:child_process", async (original) => ({
...(await original<typeof import("node:child_process")>()),
spawnSync: vi.fn(),
}));
afterEach(() => vi.mocked(spawnSync).mockReset());
const probe = (values: Partial<ReturnType<typeof spawnSync>>) => {
vi.mocked(spawnSync).mockReturnValue({
pid: 1,
status: 0,
signal: null,
stdout: Buffer.alloc(0),
stderr: Buffer.alloc(0),
output: [],
...values,
} as never);
return probeGhidraJava("/selected/bin/java", {});
};
describe("Ghidra Java prerequisite process facts", () => {
it.each([
["ENOENT", "runtime_missing"],
["EACCES", "runtime_unavailable"],
["ETIMEDOUT", "runtime_unavailable"],
["ENOBUFS", "runtime_unavailable"],
])("keeps %s distinct from other runtime failures", (code, expected) => {
const error = Object.assign(new Error(`controlled ${code}`), { code });
expect(probe({ error, status: null })).toMatchObject({
ok: false,
error: {
_tag: "GhidraJavaProbeFailure",
code: expected,
processErrorCode: code,
detail: expect.stringContaining(code),
output: { exit_code: null, truncated: code === "ENOBUFS" },
},
});
});
it("keeps raw byte counts and both streams on a nonzero exit", () => {
const result = probe({
status: 7,
stdout: Buffer.from([0xff]),
stderr: Buffer.from("controlled runtime failure\n"),
});
expect(result).toMatchObject({
ok: false,
error: {
code: "runtime_unavailable",
detail: expect.stringContaining("Java exited 7"),
output: {
stdout: "\ufffd",
stdout_bytes: 1,
stderr: "controlled runtime failure\n",
stderr_bytes: 27,
exit_code: 7,
signal: null,
truncated: false,
},
},
});
});
it("retains partial parsed properties when a successful probe cannot establish its version", () => {
expect(
probe({
stdout: Buffer.from(
"java.home = /selected/jdk\nos.arch = aarch64\nsun.arch.data.model = 64\n",
),
}),
).toMatchObject({
ok: false,
error: {
code: "version_unresolved",
properties: {
home: "/selected/jdk",
version: null,
osArch: "aarch64",
bits: 64,
},
output: { exit_code: 0 },
},
});
});
it("bounds diagnostic text and preserves examined stream sizes", () => {
expect(probe({ status: 7, stdout: Buffer.alloc(12000, 65) })).toMatchObject(
{
ok: false,
error: {
output: {
stdout: "A".repeat(8192),
stdout_bytes: 12000,
stderr_bytes: 0,
truncated: true,
},
},
},
);
});
});
it.each([
"21garbage",
"garbage",
"21.foo",
"9007199254740993",
"21.9999999999999999",
"21+9999999999999999",
"1.21.0",
"21.0",
"021",
])("keeps malformed java.version %s unresolved", (version) => {
expect(
probe({
stdout: Buffer.from(
`java.home = /selected/jdk\njava.version = ${version}\nsun.arch.data.model = 64\n`,
),
}),
).toMatchObject({
ok: false,
error: {
code: "version_unresolved",
properties: { version, home: "/selected/jdk" },
},
});
});
it.each([
["21-ea", 21],
["21.0.11+9-LTS", 21],
["27-internal+0-adhoc.user.build", 27],
["1.8.0_452-b09", 8],
] as const)("recognizes the Java major in %s", (version, major) => {
expect(
probe({
stdout: Buffer.from(
`java.home = /selected/jdk\njava.version = ${version}\nsun.arch.data.model = 64\n`,
),
}),
).toMatchObject({ ok: true, value: { version, major } });
});
+198
View File
@@ -0,0 +1,198 @@
import { spawnSync, type SpawnSyncReturns } from "node:child_process";
import { constants, accessSync, statSync } from "node:fs";
import { posix, win32 } from "node:path";
import { err, ok, type Result } from "../domain/result.js";
import type { AnalysisCapturedOutput } from "../domain/analysisErrorBase.js";
/**
* Bound the Java version probe so a chatty or hostile JVM cannot force an
* unbounded in-memory capture. Well-behaved JVMs print a few kilobytes for
* `-XshowSettings:properties`; the same 1 MiB budget used for other Java
* listings leaves ample headroom. A probe that exceeds the budget yields no
* observation and is reported through the existing failed-probe path.
*/
const JAVA_PROBE_MAX_OUTPUT_BYTES = 1024 * 1024;
/** Parsed identity of the JDK selected for Ghidra. */
export interface GhidraJavaObservation {
readonly version: string;
readonly major: number;
readonly home: string;
readonly bits: number | null;
/** Present when Java reported a bitness value that could not be parsed exactly. */
readonly unparsedBits?: string;
readonly runtime: "jdk" | "jre";
readonly vmName?: string;
/**
* Observed JVM os.arch property, which Ghidra uses to select its native
* decompiler directory. Omitted when the probe cannot establish it.
*/
readonly osArch?: string;
/**
* Observed JVM os.name property, which Ghidra uses with os.arch to select
* its native decompiler OS directory. Omitted when the probe cannot
* establish it.
*/
readonly osName?: string;
}
/** Expected probe failures retain their producing process facts. */
export interface GhidraJavaProbeFailure {
readonly _tag: "GhidraJavaProbeFailure";
readonly code:
| "runtime_missing"
| "runtime_unavailable"
| "version_unresolved";
readonly detail: string;
readonly processErrorCode?: string;
readonly output?: AnalysisCapturedOutput;
readonly properties?: {
readonly version: string | null;
readonly home: string | null;
readonly vmName: string | null;
readonly osArch: string | null;
readonly osName: string | null;
readonly bits: number | null;
};
}
const JAVA_DIAGNOSTIC_STREAM_BYTES = 8192;
const capturedProbeOutput = (
observed: SpawnSyncReturns<Buffer>,
): AnalysisCapturedOutput => {
const stdout = observed.stdout ?? Buffer.alloc(0);
const stderr = observed.stderr ?? Buffer.alloc(0);
return {
stdout: stdout.subarray(0, JAVA_DIAGNOSTIC_STREAM_BYTES).toString("utf8"),
stderr: stderr.subarray(0, JAVA_DIAGNOSTIC_STREAM_BYTES).toString("utf8"),
truncated:
stdout.length > JAVA_DIAGNOSTIC_STREAM_BYTES ||
stderr.length > JAVA_DIAGNOSTIC_STREAM_BYTES ||
(observed.error !== undefined &&
"code" in observed.error &&
observed.error.code === "ENOBUFS"),
stdout_bytes: stdout.length,
stderr_bytes: stderr.length,
exit_code: observed.status,
signal: observed.signal,
};
};
const executablePath = (path: string): boolean => {
try {
accessSync(path, constants.X_OK);
return statSync(path).isFile();
} catch {
return false;
}
};
export const probeGhidraJava = (
command: string,
environment: NodeJS.ProcessEnv,
): Result<GhidraJavaObservation, GhidraJavaProbeFailure> => {
const observed = spawnSync(
command,
[
"-Dstdout.encoding=UTF-8",
"-Dstderr.encoding=UTF-8",
// JDK 17/18 use the legacy names; JDK 19+ use the names above.
"-Dsun.stdout.encoding=UTF-8",
"-Dsun.stderr.encoding=UTF-8",
"-XX:-UsePerfData",
"-XshowSettings:properties",
"-version",
],
{
encoding: "buffer",
env: environment,
timeout: 5_000,
maxBuffer: JAVA_PROBE_MAX_OUTPUT_BYTES,
windowsHide: true,
},
);
if (observed.error !== undefined || observed.status !== 0) {
const code =
observed.error !== undefined && "code" in observed.error
? observed.error.code
: undefined;
return err({
_tag: "GhidraJavaProbeFailure",
code: code === "ENOENT" ? "runtime_missing" : "runtime_unavailable",
...(typeof code === "string" ? { processErrorCode: code } : {}),
detail: `${command}: ${observed.error?.message ?? `Java exited ${String(observed.status)}${observed.signal === null ? "" : ` (${observed.signal})`}`}`,
output: capturedProbeOutput(observed),
});
}
const output = `${observed.stdout ?? ""}\n${observed.stderr ?? ""}`;
const version = javaProperty(output, "java.version") || null;
const home = javaProperty(output, "java.home") || null;
const vmName = javaProperty(output, "java.vm.name");
const osArch = javaProperty(output, "os.arch");
const osName = javaProperty(output, "os.name");
const reportedBits = javaProperty(output, "sun.arch.data.model");
const parsedBits =
reportedBits !== null && /^\d+$/.test(reportedBits)
? Number(reportedBits)
: null;
const bits =
parsedBits !== null && Number.isSafeInteger(parsedBits) ? parsedBits : null;
const major = version === null ? undefined : javaMajor(version);
if (version === null || home === null || major === undefined)
return err({
_tag: "GhidraJavaProbeFailure",
code: "version_unresolved",
detail: `${command}: successful Java probe did not establish a valid java.version and nonempty java.home`,
output: capturedProbeOutput(observed),
properties: { version, home, vmName, osArch, osName, bits },
});
return ok({
version,
major,
home,
...(vmName === null ? {} : { vmName }),
...(osArch === null ? {} : { osArch }),
...(osName === null ? {} : { osName }),
bits,
...(reportedBits === null || bits !== null
? {}
: { unparsedBits: reportedBits }),
runtime: executablePath(
(process.platform === "win32" ? win32 : posix).join(
home,
"bin",
process.platform === "win32" ? "javac.exe" : "javac",
),
)
? "jdk"
: "jre",
});
};
const javaProperty = (output: string, name: string): string | null =>
new RegExp(
`^[ \\t]*${name.replaceAll(".", "\\.")}[ \\t]*=[ \\t]*([^\\r\\n]*?)[ \\t]*\\r?$`,
"mu",
).exec(output)?.[1] ?? null;
// Runtime.Version's numeric/pre-release/build grammar; Java 8 used 1.8.0_... .
// https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/lang/Runtime.Version.html
const javaMajor = (version: string): number | undefined => {
const legacy =
/^1\.([1-8])(?:\.[0-9]+)*(?:_[0-9]+)?(?:-[-a-zA-Z0-9.]+)?$/u.exec(version);
if (legacy?.[1] !== undefined) return Number(legacy[1]);
const modern =
/^(?<number>[1-9][0-9]*(?:(?:\.0)*\.[1-9][0-9]*)*)(?:(?:-[a-zA-Z0-9]+)?\+(?<build>0|[1-9][0-9]*)(?:-[-a-zA-Z0-9.]+)?|-[a-zA-Z0-9]+(?:-[-a-zA-Z0-9.]+)?|\+-[-a-zA-Z0-9.]+)?$/u.exec(
version,
);
const number = modern?.groups?.number;
if (number === undefined) return undefined;
const parts = number.split(".").map(Number);
const build = modern?.groups?.build;
// Runtime.Version parses numeric components and the build as Java integers.
if (
[...parts, ...(build === undefined ? [] : [Number(build)])].some(
(value) => !Number.isSafeInteger(value) || value > 0x7fffffff,
)
)
return undefined;
return parts[0];
};
+25 -21
View File
@@ -1,3 +1,5 @@
import { ok as javaProbeOk } from "../domain/result.js";
import type { GhidraJavaObservation } from "./GhidraJavaProbe.js";
import type { GhidraSeedReport } from "./GhidraAnalysisSeeds.js";
import { createHash } from "node:crypto";
import { execFile } from "node:child_process";
@@ -50,16 +52,17 @@ const installationHost = (
architecture: "x64",
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
executable: () => true,
probeJava: () => ({
osArch: "amd64",
osName: platform === "win32" ? "Windows 11" : "Linux",
version: "21.0.11",
major: 21,
home: "/usr/lib/jvm/jdk-21",
bits: 64,
runtime: "jdk",
vmName: "OpenJDK 64-Bit Server VM",
}),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
osArch: "amd64",
osName: platform === "win32" ? "Windows 11" : "Linux",
version: "21.0.11",
major: 21,
home: "/usr/lib/jvm/jdk-21",
bits: 64,
runtime: "jdk",
vmName: "OpenJDK 64-Bit Server VM",
}),
});
const provider = (
@@ -208,7 +211,7 @@ describe("Ghidra provider", () => {
...installationHost(),
probeJava: () => {
probeCount += 1;
return {
return javaProbeOk<GhidraJavaObservation>({
osArch: "amd64",
osName: "Linux",
version: "21.0.11",
@@ -216,7 +219,7 @@ describe("Ghidra provider", () => {
home: "/usr/lib/jvm/jdk-21",
bits: 64,
runtime: "jdk",
};
});
},
};
const ghidra = provider(host);
@@ -2292,15 +2295,16 @@ describe("compatible Ghidra builds", () => {
...installationHost(),
readText: () =>
"application.version=12.1.2\napplication.java.min=21\napplication.java.max=\n",
probeJava: () => ({
osArch: "amd64",
osName: "Linux",
version: "27",
major: 27,
home: "/usr/lib/jvm/java-27-openjdk",
bits: 64,
runtime: "jdk",
}),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
osArch: "amd64",
osName: "Linux",
version: "27",
major: 27,
home: "/usr/lib/jvm/java-27-openjdk",
bits: 64,
runtime: "jdk",
}),
},
() => ({
start: () =>
+12 -9
View File
@@ -1,3 +1,5 @@
import { ok as javaProbeOk } from "../domain/result.js";
import type { GhidraJavaObservation } from "./GhidraJavaProbe.js";
import { describe, expect, it } from "vitest";
import { createTestBinarySession } from "../../tests/fixtures/binarySession.js";
@@ -43,15 +45,16 @@ const fixture = (version = "12.1.4", coverage?: RelationshipCoverage) => {
readText: () =>
`application.version=${version}\napplication.java.min=21\n`,
executable: () => true,
probeJava: () => ({
osArch: "amd64",
osName: "Linux",
version: "21.0.11",
major: 21,
home: "/usr/lib/jvm/jdk-21",
bits: 64,
runtime: "jdk",
}),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
osArch: "amd64",
osName: "Linux",
version: "21.0.11",
major: 21,
home: "/usr/lib/jvm/jdk-21",
bits: 64,
runtime: "jdk",
}),
},
(options) => ({
start: async () =>
+117
View File
@@ -0,0 +1,117 @@
import { chmod, mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { elf, thinMach } from "../../../src/domain/binaryTarget.fixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest.skipIf(!["darwin", "linux"].includes(process.platform)).for([
["missing", "missing_dependency", "runtime_missing"],
["nonzero", "runtime_failure", "runtime_unavailable"],
["properties", "config_drift", "version_unresolved"],
] as const)(
"preserves the %s Java prerequisite result through doctor",
async ([failure, classification, code], { cli }) => {
const root = await createTestTempDirectory("rea-java-doctor-");
const jdk = join(root, "jdk");
const install = join(root, "ghidra");
const platform = process.platform === "darwin" ? "mac" : "linux";
const architecture = process.arch === "arm64" ? "arm_64" : "x86_64";
const native = join(
install,
"Ghidra/Features/Decompiler/os",
`${platform}_${architecture}`,
"decompile",
);
for (const path of [
join(jdk, "bin"),
join(install, "support"),
join(install, "Ghidra"),
join(native, ".."),
])
await mkdir(path, { recursive: true });
await writeFile(
join(install, "Ghidra/application.properties"),
"application.version=12.1.4\napplication.java.min=21\n",
);
await writeFile(
join(install, "support/analyzeHeadless"),
"#!/bin/sh\nexit 99\n",
);
await chmod(join(install, "support/analyzeHeadless"), 0o700);
await writeFile(
native,
process.platform === "darwin"
? thinMach(
0xfeedfacf,
process.arch === "arm64" ? 0x0100000c : 0x01000007,
)
: elf(2, 1, process.arch === "arm64" ? 183 : 62),
);
await chmod(native, 0o700);
if (failure !== "missing") {
const body =
failure === "nonzero"
? 'process.stderr.write("controlled runtime failure\\n"); process.exit(7);'
: `process.stdout.write(${JSON.stringify(`java.home = ${jdk}\nos.arch = aarch64\n`)});`;
await writeFile(
join(jdk, "bin/java"),
`#!${process.execPath}\n${body}\n`,
);
await chmod(join(jdk, "bin/java"), 0o700);
}
const result = await cli.run({
arguments: ["doctor", "--provider", "ghidra", "--json"],
environment: {
JAVA_HOME: jdk,
GHIDRA_INSTALL_DIR: install,
REA_LOG_LEVEL: "silent",
},
});
expect(result.exitCode, `${result.stdout}\n${result.stderr}`).toBe(1);
expect(result.json).toMatchObject({
checks: expect.arrayContaining([
expect.objectContaining({
name: "ghidra-java",
classification,
ok: false,
}),
]),
providerInspections: expect.arrayContaining([
expect.objectContaining({
id: "ghidra",
checks: expect.arrayContaining([
expect.objectContaining({ name: "java", code }),
]),
}),
]),
});
if (failure === "nonzero")
expect(result.json).toMatchObject({
checks: expect.arrayContaining([
expect.objectContaining({
name: "ghidra-java",
detail: expect.stringContaining("Java exited 7"),
details: expect.objectContaining({
output: expect.objectContaining({
stderr: "controlled runtime failure\n",
exit_code: 7,
stderr_bytes: 27,
}),
}),
}),
]),
});
if (failure === "properties")
expect(result.json).toMatchObject({
checks: expect.arrayContaining([
expect.objectContaining({
name: "ghidra-java",
details: expect.objectContaining({
properties: expect.objectContaining({ home: jdk, version: null }),
}),
}),
]),
});
},
);
@@ -1,3 +1,5 @@
import { ok as javaProbeOk } from "../../../src/domain/result.js";
import type { GhidraJavaObservation } from "../../../src/ghidra/GhidraJavaProbe.js";
import { mkdir, rename, symlink, truncate, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { describe, expect, it, vi } from "vitest";
@@ -55,15 +57,16 @@ const host: GhidraInstallationHost = {
architecture: "x64",
readText: () => "application.version=12.1.3\napplication.java.min=21\n",
executable: () => true,
probeJava: () => ({
osArch: "amd64",
osName: "Linux",
version: "21.0.12",
major: 21,
home: "/jdk-21",
bits: 64,
runtime: "jdk",
}),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
osArch: "amd64",
osName: "Linux",
version: "21.0.12",
major: 21,
home: "/jdk-21",
bits: 64,
runtime: "jdk",
}),
};
async function installation() {
const directory = await createTestTempDirectory("rea-psp-profile-");
+12 -9
View File
@@ -1,3 +1,5 @@
import { ok as javaProbeOk } from "../../../src/domain/result.js";
import type { GhidraJavaObservation } from "../../../src/ghidra/GhidraJavaProbe.js";
import { expect } from "vitest";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
@@ -105,15 +107,16 @@ export const ghidraInstallationHost = (): GhidraInstallationHost => ({
architecture: "x64",
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
executable: () => true,
probeJava: () => ({
osArch: "amd64",
osName: "Linux",
version: "21.0.11",
major: 21,
home: "/usr/lib/jvm/jdk-21",
bits: 64,
runtime: "jdk",
}),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
osArch: "amd64",
osName: "Linux",
version: "21.0.11",
major: 21,
home: "/usr/lib/jvm/jdk-21",
bits: 64,
runtime: "jdk",
}),
});
const sessionInfo = (profileDigest: string, targetSha256: string) => ({
+12 -9
View File
@@ -1,3 +1,5 @@
import { ok as javaProbeOk } from "../../../src/domain/result.js";
import type { GhidraJavaObservation } from "../../../src/ghidra/GhidraJavaProbe.js";
import { spawn, type ChildProcess } from "node:child_process";
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -101,15 +103,16 @@ const installationHost: GhidraInstallationHost = {
architecture: "x64",
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
executable: () => true,
probeJava: () => ({
osArch: "amd64",
osName: "Linux",
version: "21.0.11",
major: 21,
home: "/fixture/jdk-21",
bits: 64,
runtime: "jdk",
}),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
osArch: "amd64",
osName: "Linux",
version: "21.0.11",
major: 21,
home: "/fixture/jdk-21",
bits: 64,
runtime: "jdk",
}),
};
// Only installation discovery and the Java bridge are simulated. Requests use
@@ -1,3 +1,5 @@
import { ok as javaProbeOk } from "../../../../src/domain/result.js";
import type { GhidraJavaObservation } from "../../../../src/ghidra/GhidraJavaProbe.js";
import { mkdir, stat, writeFile } from "node:fs/promises";
import { createHash, randomUUID } from "node:crypto";
import { join } from "node:path";
@@ -443,20 +445,21 @@ describe.skipIf(process.platform === "win32")(
readText: () =>
"application.version=12.1.4\napplication.java.min=21\n",
executable: () => true,
probeJava: () => ({
osArch: process.arch === "arm64" ? "aarch64" : "amd64",
osName:
process.platform === "darwin"
? "Mac OS X"
: process.platform === "win32"
? "Windows 11"
: "Linux",
version: "21.0.12",
major: 21,
home: "/fixture/jdk",
bits: 64,
runtime: "jdk",
}),
probeJava: () =>
javaProbeOk<GhidraJavaObservation>({
osArch: process.arch === "arm64" ? "aarch64" : "amd64",
osName:
process.platform === "darwin"
? "Mac OS X"
: process.platform === "win32"
? "Windows 11"
: "Linux",
version: "21.0.12",
major: 21,
home: "/fixture/jdk",
bits: 64,
runtime: "jdk",
}),
},
(options) => new GhidraClient({ ...options, launcher }),
);