fix(native): retain dispatch reader failure during fallback

This commit is contained in:
morluto committed 2026-10-29 03:21:00 +08:00
1 parent d97aa0849d
commit e7884a704a
3 files changed
+141 -9

No files matched your search

@@ -0,0 +1,76 @@
import { expect, it } from "vitest";
import {
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { err, ok } from "../../domain/result.js";
import {
createAnalysisExecution,
type AnalysisOperationPort,
} from "../AnalysisProvider.js";
import { inspectNativeDispatch } from "./NativeDispatchMetadataInspection.js";
it("retains the binary reader refusal when the symbol fallback also fails", async () => {
const direct = new AnalysisCapabilityUnavailableError(
"native-macos",
"inspect_native_dispatch_metadata",
"Unsupported 32-bit Mach-O metadata",
);
const fallback = new AnalysisOutputError(
"list_names",
"symbol provider unavailable",
);
const analysis: AnalysisOperationPort = {
execute: async (operation) =>
err(operation === "list_names" ? fallback : direct),
};
const result = await inspectNativeDispatch(analysis, 10);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error).toBe(fallback);
expect(result.error.secondaryFailures).toContain(direct);
expect(projectAnalysisError(result.error)).toMatchObject({
details: {
diagnostics: {
binary_metadata_failure: {
details: { reason: expect.stringContaining("Unsupported 32-bit") },
},
},
},
});
});
it("retains both the reader refusal and malformed symbol observation", async () => {
const direct = new AnalysisCapabilityUnavailableError(
"native-macos",
"inspect_native_dispatch_metadata",
"Unsupported 32-bit Mach-O metadata",
);
const analysis: AnalysisOperationPort = {
execute: async (operation) =>
operation === "list_names"
? ok(
createAnalysisExecution(
{ invalid: true },
{ id: "fixture", name: "Fixture", version: "1" },
),
)
: err(direct),
};
const result = await inspectNativeDispatch(analysis, 10);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.secondaryFailures).toContain(direct);
expect(projectAnalysisError(result.error)).toMatchObject({
details: {
diagnostics: {
binary_metadata_failure: {
details: { reason: "Unsupported 32-bit Mach-O metadata" },
},
},
partial_observation: expect.any(Object),
},
});
});
@@ -6,6 +6,7 @@ import {
inspectNativeDispatchMetadata,
} from "../../domain/native/objcSwiftMetadata.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { err, ok } from "../../domain/result.js";
import type { AnalysisOperationPort } from "../AnalysisProvider.js";
@@ -83,7 +84,12 @@ export const inspectNativeDispatch = async (
{},
signal === undefined ? {} : { signal },
);
if (!execution.ok) return err(execution.error);
if (!execution.ok)
return err(
execution.error.retainSecondaryFailure(binaryMetadata.error, {
binary_metadata_failure: projectAnalysisError(binaryMetadata.error),
}),
);
const names = dispatchNameInventorySchema.safeParse(execution.value.result);
if (!names.success)
return err(
@@ -91,14 +97,18 @@ export const inspectNativeDispatch = async (
NATIVE_DISPATCH_METADATA_SOURCES.symbols,
"provider returned an invalid inventory",
{ cause: names.error },
).retainPartialObservation(
createAnalysisExecutionEvidence(
NATIVE_DISPATCH_METADATA_SOURCES.symbols,
{},
execution.value,
"observed",
),
),
)
.retainPartialObservation(
createAnalysisExecutionEvidence(
NATIVE_DISPATCH_METADATA_SOURCES.symbols,
{},
execution.value,
"observed",
),
)
.retainSecondaryFailure(binaryMetadata.error, {
binary_metadata_failure: projectAnalysisError(binaryMetadata.error),
}),
);
const result = inspectNativeDispatchMetadata(
names.data.map((entry) => ({
@@ -0,0 +1,46 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
cliTest.skipIf(process.platform !== "darwin")(
"retains the 32-bit reader refusal when symbol fallback is unavailable",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-dispatch-width-");
for (const cpu of [7, 12]) {
const image = Buffer.alloc(28);
image.writeUInt32LE(0xfeedface, 0);
image.writeUInt32LE(cpu, 4);
image.writeUInt32LE(cpu === 7 ? 3 : 9, 8);
image.writeUInt32LE(2, 12);
const path = join(root, `image-${cpu}`);
await writeFile(path, image);
const result = await cli.run({
arguments: [
"inspect-native-dispatch-metadata",
path,
"--format",
"json",
],
environment: {
...process.env,
REA_ANALYSIS_PROVIDER: "auto",
HOPPER_LAUNCHER_PATH: join(root, "missing-hopper"),
GHIDRA_INSTALL_DIR: join(root, "missing-ghidra"),
JAVA_HOME: join(root, "missing-java"),
},
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
details: {
diagnostics: {
binary_metadata_failure: {
details: { reason: expect.stringMatching(/32.bit/i) },
},
},
},
});
}
},
);