Files
reverse-engineering-anything/tests/composition/reverseEngineeringUnknownFailures.test.ts
T
morluto 422cff00ab refactor(process): give POSIX PATH search order one owner (#10891)
* refactor(process): give POSIX PATH search order one owner

Program lookup, the packet-tool locator and Jadx's Java lookup each split
PATH and resolved empty and relative entries themselves. They now take the
candidates from posixPathCandidates in src/process/executableSearch and keep
only their own probe and failure reporting. A counted architecture guard
keeps new PATH splits from returning; the remaining Windows and Ghidra
searches are baselined.

* refactor(process): move the colon-split PATH searches onto the search owner

REA's launcher-alias lookup and Ghidra's macOS Java lookup split PATH on
":" and skipped relative entries themselves; they now use
posixPathCandidates with relativeEntries: "skip", and the guard counts
colon splits of PATH as well.

* fix(process): produce PATH candidates lazily so an unused entry cannot fail an earlier match

* refactor(process): give Windows PATH/PATHEXT search order the executable search owner (#10916)

* refactor(process): give Windows PATH/PATHEXT search order the executable search owner

* fix(process): keep later PATH entries searchable after a stray quote or a removed working directory

* fix(reverse): refuse a Windows objdump or Rizin command that PATH does not find (#10953)

* fix(reverse): refuse an objdump or Rizin command Windows PATH does not find instead of launching the bare name

* fix(reverse): report a Windows lookup refusal after cancellation and to capability discovery

* fix(desktop): search a Windows host's PATH as Windows does when doctor looks up a program

programOnPath sent every host through the POSIX PATH search, so on Windows a
relative PATH entry reached absoluteCallerPath with platform linux and the
Windows working directory, which resolved against itself until the stack
overflowed. programOnPath now searches PATH and PATHEXT through
windowsPathCandidates on a Windows host, and absoluteCallerPath refuses a
non-POSIX working directory with a TypeError instead of recursing.
2026-11-15 18:24:04 +08:00

151 lines
5.1 KiB
TypeScript

import { stat, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it, vi } from "vitest";
import { ReverseEngineeringService } from "../../src/application/reverse/ReverseEngineeringService.js";
import { analysisErrorProjectionSchema } from "../../src/contracts/errorSchemas.js";
import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js";
import { ok } from "../../src/domain/result.js";
import { OwnedCommandFailure } from "../../src/process/OwnedCommand.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
it.each([false, true])(
"keeps owned output collection failure primary after late cancellation=%s",
async (cancel) => {
const controller = new AbortController();
const abortReason = { caller: "cancelled after collection failure" };
const physical = Object.assign(new Error("output pipe read failed"), {
code: "EIO",
});
const owned = new OwnedCommandFailure(
"process",
"owned output collection failed",
{
stdout: { text: "retained", bytes: 8, observedBytes: 8 },
stderr: { text: "", bytes: 0, observedBytes: 0 },
exitCode: 0,
signal: null,
},
null,
{ cause: physical, collectionFailure: true },
);
const profile = {
executable: "unused",
arguments: [],
identity: { id: "fixture", name: "fixture", version: null },
};
const service = new ReverseEngineeringService({
commands: { objdump: () => ok(profile), rizin: () => ok(profile) },
runOwned: async () => {
if (cancel) controller.abort(abortReason);
throw owned;
},
});
const result = await service.inspectWithObjdump(
{ operation: "architectures" },
{ signal: controller.signal },
);
if (result.ok) throw new Error("Expected output collection failure");
expect(result.error._tag).toBe("AnalysisOutputError");
expect(result.error.cause).toMatchObject({ cause: owned });
expect(owned.cause).toBe(physical);
expect(result.error.capturedOutput).toMatchObject({
stdout: "retained",
stdout_bytes: 8,
exit_code: null,
});
expect(result.error.secondaryFailures).toHaveLength(cancel ? 1 : 0);
if (cancel)
expect(result.error.secondaryFailures[0]?.cause).toBe(abortReason);
await service.close();
},
);
it("keeps the owned producer cancellation reason authoritative", async () => {
const controller = new AbortController();
const abortReason = { caller: "owned process cancelled" };
const profile = {
executable: "unused",
arguments: [],
identity: { id: "fixture", name: "fixture", version: null },
};
const service = new ReverseEngineeringService({
commands: { objdump: () => ok(profile), rizin: () => ok(profile) },
runOwned: async () => {
controller.abort(abortReason);
throw new OwnedCommandFailure("cancelled", "owned process cancelled");
},
});
const result = await service.inspectWithObjdump(
{ operation: "architectures" },
{ signal: controller.signal },
);
if (result.ok) throw new Error("Expected producer cancellation");
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(abortReason);
expect(result.error.secondaryFailures).toEqual([]);
await service.close();
});
it.each([
{ stage: "stat", cause: null },
{ stage: "stat", cause: undefined },
{ stage: "command", cause: null },
{ stage: "command", cause: undefined },
] as const)(
"preserves a $cause rejection from $stage without throwing while classifying it",
async ({ stage, cause }) => {
const root = await createTestTempDirectory("rea-reverse-unknown-failure-");
const path = join(root, "input.bin");
await writeFile(path, new Uint8Array([0x7f, 0x45, 0x4c, 0x46]));
const run = vi.fn(async () => {
throw cause;
});
const service = new ReverseEngineeringService({
stat:
stage === "stat"
? async () => {
throw cause;
}
: stat,
run,
commands: {
objdump: () =>
ok({
executable: "unused-objdump",
arguments: [path],
identity: {
id: "test.objdump",
name: "Test objdump",
version: null,
},
}),
rizin: () =>
ok({
executable: "unused-rizin",
arguments: [path],
identity: { id: "test.rizin", name: "Test Rizin", version: null },
}),
},
});
const result = await service.inspectWithObjdump({
path,
operation: "symbols",
});
if (result.ok) throw new Error("Expected injected boundary rejection");
expect(result.error.cause).toBe(cause);
const projected = projectAnalysisError(result.error);
expect(analysisErrorProjectionSchema.safeParse(projected).success).toBe(
true,
);
expect(projected.code).toBe(
stage === "stat" ? "execution_failure" : "capability_unavailable",
);
if (stage === "stat") expect(run).not.toHaveBeenCalled();
else expect(run).toHaveBeenCalledOnce();
await service.close();
},
);