fix(native): pass Swift symbols to swift-demangle as operands

`swift-demangle` parsed a symbol beginning with `-` as an option, so
`-help` replaced the results with usage text and the call failed as
unreadable output. A symbol containing a line feed split into extra
output records, and the CRLF-tolerant parser stripped a carriage return
that belonged to the final symbol.

Pass the symbols after `--`, split records on the LF the demangler
emits, and reject multi-line symbols in the shared CLI/MCP input schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Iwakurakaoru0822 2026-10-06 22:40:19 -07:00
parent 676959ecb5
commit 6c88094da4
11 changed files with 175 additions and 16 deletions

View File

@ -645,9 +645,9 @@
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "5d4fa325e1649ae3f75aa6f9cbba7fac1175440a0e2792e6e1a469849e2c2dc3",
"tools_sha256": "83e7791a5c733162c76b2d5654863b9ef686feed51ec23b8400035552e8a2b6d",
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
"combined_sha256": "d8e9c865c40f7d8f96e21f575d7157cbd000e86991a1944ba2aa07c2f1d70bc7",
"combined_sha256": "4a56d8682120ad808f3843f09e31fbe13d2a80b5b911c7b60343b28cb3c298e1",
"providers_sha256": "0bccfa9588376a90350d18abded999db7e1d0e4424a61a21d03d392a1c5418fe"
}
}

View File

@ -1,5 +1,5 @@
{
"manifest_id": "ecm_6376482bb14d464efebd1947cd6820fac3edfdd17592cedb0ddfe5ad58ef2493",
"manifest_id": "ecm_18e8db09b517abbcb288f199882a13efe238d78f2c63972778b05e134ef21f19",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "f765c513b854f5ea2db449801cc39a27e34dc05c378760eda5a049b529e80392"
"sha256": "797d0110c457bf1234787dc6d6a073cc7d1c1e260e580a333fc374a052672f9b"
}
],
"claims": [

View File

@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
metadata:
version: "26"
tool_count: 127
catalog_digest: "d8e9c865c40f7d8f96e21f575d7157cbd000e86991a1944ba2aa07c2f1d70bc7"
catalog_digest: "4a56d8682120ad808f3843f09e31fbe13d2a80b5b911c7b60343b28cb3c298e1"
---
# REA

View File

@ -13,6 +13,7 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { PRODUCT_IDENTITY } from "../identity.js";
import { logCliCommand } from "../cliLogging.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { swiftSymbolsSchema } from "../contracts/nativeToolContracts.js";
import type { Logger } from "../logger.js";
import { isReferenceSourceImportCliFailure } from "./referenceSourceImportStatus.js";
import type { CliInstance } from "./types.js";
@ -152,10 +153,7 @@ const registerNativeCommands = (cli: CliInstance, logger: Logger): void => {
description: "Demangle Swift symbols without Hopper",
args: z.object({
path: z.string().describe("Artifact path used for evidence identity"),
symbols: z
.array(z.string().min(1))
.min(1)
.describe("Swift mangled symbols to demangle"),
symbols: swiftSymbolsSchema.describe("Swift mangled symbols to demangle"),
}),
run: ({ args }) =>
logCliCommand(logger, "demangle-swift", () =>

View File

@ -48,6 +48,16 @@ const native = <Name extends string, Schema extends z.ZodObject>(
} satisfies ToolContract<Name, Schema, typeof outputSchema>;
};
/** Ordered single-line symbols; `swift-demangle` ends each result with LF. */
export const swiftSymbolsSchema = z
.array(
z
.string()
.min(1)
.regex(/^[^\n]*$/u, "Each Swift symbol must be one line."),
)
.min(1);
/** Provider-neutral semantic operations backed initially by macOS utilities. */
export const NATIVE_TOOL_CONTRACTS = [
native(
@ -84,9 +94,9 @@ export const NATIVE_TOOL_CONTRACTS = [
),
native(
"demangle_swift",
"Demangle an ordered list of Swift symbols without requiring Hopper. Each input returns demangled, unchanged, or invalid status.",
"Demangle an ordered list of single-line Swift symbols without requiring Hopper. Each input returns demangled, unchanged, or invalid status.",
z.object({
symbols: z.array(z.string().min(1)).min(1),
symbols: swiftSymbolsSchema,
}),
),
] as const satisfies readonly ToolContract[];

File diff suppressed because one or more lines are too long

View File

@ -270,7 +270,8 @@ class NativeMacOSClient implements AnalysisClient {
const capture = await this.#run(
"demangle_swift",
"swift-demangle",
["--compact", ...symbols.data],
// `--` keeps symbols that begin with `-` from selecting demangler options.
["--compact", "--", ...symbols.data],
{ signal },
);
if (!capture.ok) return capture;

View File

@ -10,11 +10,20 @@ it.each(["ordinary name ", "ordinary\t", " "])(
},
);
it.each(["carriage\r", "\r", "mid\rdle"])(
"keeps carriage returns that belong to the symbol: %j",
(input) => {
expect(parseDemangledSymbols([input], `${input}\n`)).toEqual([
{ input, output: input, status: "unchanged" },
]);
},
);
it("removes only the line terminator and preserves mixed output ordering", () => {
expect(
parseDemangledSymbols(
["$s4main5helloyyF", "plain "],
"main.hello() -> ()\r\nplain \r\n",
"main.hello() -> ()\nplain \n",
),
).toEqual([
{

View File

@ -6,12 +6,16 @@ const resultSchema = z.object({
status: z.enum(["demangled", "unchanged", "invalid"]),
});
/** Preserve input ordering while parsing `swift-demangle --compact` lines. */
/**
* Preserve input ordering while parsing `swift-demangle --compact` lines.
* The demangler ends each record with LF, so a carriage return belongs to the
* symbol text.
*/
export const parseDemangledSymbols = (
inputs: readonly string[],
output: string,
) => {
const lines = output.replace(/\r?\n$/u, "").split(/\r?\n/u);
const lines = output.replace(/\n$/u, "").split("\n");
if (lines.length !== inputs.length)
throw new TypeError("swift-demangle output count does not match input");
return inputs.map((input, index) => {

View File

@ -0,0 +1,61 @@
import { copyFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const environment = {
REA_LOG_LEVEL: "silent",
REA_ANALYSIS_PROVIDER: "auto",
} as const;
/** Copy a host program to serve as the evidence-identity artifact. */
const evidenceTarget = async () => {
const path = join(await createTestTempDirectory("rea-demangle-"), "tool");
await copyFile("/usr/bin/true", path);
return path;
};
describe.skipIf(process.platform !== "darwin")(
"native Swift demangling CLI with line-terminator characters",
() => {
cliTest("keeps a carriage return in the symbol", async ({ cli }) => {
const result = await cli.run({
arguments: [
"demangle-swift",
await evidenceTarget(),
"$s4main3FooV",
"carriage\r",
"--json",
],
environment,
});
expect(result.exitCode).toBe(0);
expect(result.json).toMatchObject({
normalized_result: {
symbols: [
{ input: "$s4main3FooV", output: "main.Foo", status: "demangled" },
{ input: "carriage\r", output: "carriage\r", status: "unchanged" },
],
},
});
});
cliTest("rejects a symbol spanning lines as input", async ({ cli }) => {
const result = await cli.run({
arguments: [
"demangle-swift",
await evidenceTarget(),
"$s4main3FooV",
"$s4main\n3FooV",
"--json",
],
environment,
});
expect(result.exitCode).not.toBe(0);
expect(result.json).toMatchObject({ code: "VALIDATION_ERROR" });
});
},
);

View File

@ -0,0 +1,76 @@
import { copyFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect } from "vitest";
import { z } from "zod";
import { createBinarySession } from "../../../src/application/runtime.js";
import { parseConfig } from "../../../src/config.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { mcpTest } from "../../support/mcp/mcpFixture.js";
const demangledSchema = z.object({
result: z.object({
symbols: z.array(
z.object({ input: z.string(), output: z.string(), status: z.string() }),
),
}),
});
describe.skipIf(process.platform !== "darwin")(
"native Swift demangling through MCP",
() => {
mcpTest(
"passes option-like symbols to swift-demangle as symbols",
async ({ mcp, onTestFinished }) => {
const target = join(
await createTestTempDirectory("rea-demangle-mcp-"),
"tool",
);
await copyFile("/usr/bin/true", target);
const configured = parseConfig({ REA_ANALYSIS_PROVIDER: "auto" });
if (!configured.ok) throw configured.error;
const session = createBinarySession(configured.value);
onTestFinished(async () => {
await session.close();
});
const client = await mcp.connect(createServer(session, session));
const opened = await client.callTool({
name: "open_binary",
arguments: { path: target },
});
expect(opened.isError, JSON.stringify(opened.content)).not.toBe(true);
const called = await client.callTool({
name: "demangle_swift",
arguments: {
symbols: ["-help", "--simplified", "$s4main3FooV", "carriage\r"],
},
});
expect(called.isError, JSON.stringify(called.content)).not.toBe(true);
expect(
demangledSchema.parse(called.structuredContent).result.symbols,
).toEqual([
{ input: "-help", output: "-help", status: "unchanged" },
{
input: "--simplified",
output: "--simplified",
status: "unchanged",
},
{ input: "$s4main3FooV", output: "main.Foo", status: "demangled" },
{ input: "carriage\r", output: "carriage\r", status: "unchanged" },
]);
const spanning = await client.callTool({
name: "demangle_swift",
arguments: { symbols: ["$s4main\n3FooV"] },
});
expect(spanning.isError).toBe(true);
expect(JSON.stringify(spanning.content)).toContain(
"Each Swift symbol must be one line.",
);
},
);
},
);