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:
parent
676959ecb5
commit
6c88094da4
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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", () =>
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
@ -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.",
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
Loading…
Reference in New Issue