From e7884a704a3f57a38c1e7c7320fc0d1a93bb561c Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 29 Oct 2026 03:21:00 +0800 Subject: [PATCH] fix(native): retain dispatch reader failure during fallback --- ...ispatchMetadataInspection.fallback.test.ts | 76 +++++++++++++++++++ .../NativeDispatchMetadataInspection.ts | 28 ++++--- .../nativeDispatchFallbackFailure.test.ts | 46 +++++++++++ 3 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 src/application/native/NativeDispatchMetadataInspection.fallback.test.ts create mode 100644 tests/acceptance/nativeDispatchFallbackFailure.test.ts diff --git a/src/application/native/NativeDispatchMetadataInspection.fallback.test.ts b/src/application/native/NativeDispatchMetadataInspection.fallback.test.ts new file mode 100644 index 0000000000..c1efa9acd8 --- /dev/null +++ b/src/application/native/NativeDispatchMetadataInspection.fallback.test.ts @@ -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), + }, + }); +}); diff --git a/src/application/native/NativeDispatchMetadataInspection.ts b/src/application/native/NativeDispatchMetadataInspection.ts index 42a4cb45d0..c901a17ff1 100644 --- a/src/application/native/NativeDispatchMetadataInspection.ts +++ b/src/application/native/NativeDispatchMetadataInspection.ts @@ -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) => ({ diff --git a/tests/acceptance/nativeDispatchFallbackFailure.test.ts b/tests/acceptance/nativeDispatchFallbackFailure.test.ts new file mode 100644 index 0000000000..846cad586c --- /dev/null +++ b/tests/acceptance/nativeDispatchFallbackFailure.test.ts @@ -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) }, + }, + }, + }, + }); + } + }, +);