fix(ghidra): classify modern Swift manglings in swift type analysis (#1208)

* fix(ghidra): classify modern Swift manglings in swift type analysis

Categorize modern Swift 4.2+ manglings ($s, _$s) instead of dropping them
in analyze_swift_types. The classifier only accepted the legacy _Tt marker,
so procedure names reported by current providers produced an empty inventory.
Legacy _Tt families and behavior are unchanged. Fixes #1187.

* fix(swift): retain provider aliases and category unknowns

---------

Co-authored-by: Saim Shafique <sx4imm@gmail.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
Saim Shafique 2026-10-09 13:10:55 +05:00 committed by GitHub
parent 04a516b1e3
commit 6935dc7fe1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 968 additions and 31 deletions

View File

@ -322,11 +322,18 @@ prove behavior on the verified Ghidra 12.1.4 and JDK 21 build.
| Ghidra lane | Supported runner/target | Additional local tools |
| ------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `npm run verify:ghidra` | Linux x64/arm64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:swift` | macOS x64/arm64 Mach-O | Host Swift compiler, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:switch` | Linux x64 ELF; GCC/Clang optimized and stripped switch fixtures | GCC, Clang, GNU nm/objdump/strip, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:cross-format` | Any supported Ghidra host; also analyzes AArch64 ELF, x86-64 PE, and x86-64 Mach-O | `clang`, LLD, and `lld-link` in addition to host-lane prerequisites |
| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE; `-- --x86` selects native x86 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact |
The optional Swift lane compiles a source-owned fixture and checks modern and
provider-demangled procedure names through real stdio MCP, with and without the
dSYM companion. It checks unfiltered, structs, and literal-name filters,
same-address alias evidence, deduplication, and explicit unresolved categories.
It does not add a Swift prerequisite to the host C fixture lane.
The host lane also verifies namespaced C++ ABI symbols and annotation identity.
That facet requires Ghidra's matching `demangler_gnu_v2_41` native component in
`GPL/DemanglerGnu/os/<platform>/` or `build/os/<platform>/`, in addition to the

View File

@ -146,6 +146,7 @@
"verify:keyed-archive": "npm run build:cached && node scripts/verify/apple/keyed-archive.mjs",
"verify:macos-bundle": "npm run build:cached && node scripts/verify/apple/macos-bundle.mjs",
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
"verify:ghidra:swift": "npm run build:cached && node scripts/verify-real-ghidra-swift.mjs",
"verify:ida": "npm run build:cached && node scripts/verify-real-ida.mjs",
"verify:ghidra:switch": "npm run build:cached && node scripts/verify-real-ghidra-switch.mjs",
"verify:ghidra:dos": "npm run build:cached && node scripts/verify-real-ghidra-dos.mjs",

View File

@ -0,0 +1,160 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { copyFile, mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
import { toolContract } from "../dist/contracts/toolContracts.js";
import { requireMcpResult } from "./lib/mcp-verifier-results.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
const execute = promisify(execFile);
const run = createVerifierRun();
const installation = inspectGhidraInstallation({
installDir: process.env.GHIDRA_INSTALL_DIR,
javaHome: process.env.JAVA_HOME,
});
if (process.platform !== "darwin" || installation.status !== "available")
throw new Error(
"verify:ghidra:swift requires macOS, Ghidra 12.1.4 with its native decompiler, and JDK 21; set GHIDRA_INSTALL_DIR and JAVA_HOME.",
);
try {
await execute("swiftc", ["--version"], { timeout: 10_000 });
} catch (cause) {
throw new Error(
"verify:ghidra:swift requires the host Swift compiler (swiftc).",
{ cause },
);
}
const root = await mkdtemp(join(tmpdir(), "rea-ghidra-swift-"));
const client = new Client({ name: "ghidra-swift-verifier", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [fileURLToPath(new URL("./rea.mjs", import.meta.url)), "mcp"],
env: Object.fromEntries(
Object.entries(process.env).filter(([, value]) => value !== undefined),
),
stderr: "pipe",
});
const source = fileURLToPath(
new URL("../tests/conformance/swift/type-inventory.swift", import.meta.url),
);
const binary = join(root, "swift-inventory");
const reports = [];
try {
await execute("swiftc", ["-O", "-g", source, "-o", binary], {
timeout: 60_000,
});
await mkdir(join(root, "copied"));
const copied = join(root, "copied", "swift-inventory");
await copyFile(binary, copied);
await client.connect(transport);
for (const [variant, path] of [
["debug", binary],
["without-dsym", copied],
]) {
await call("open_binary", { path, provider_id: "ghidra" });
try {
const procedures = await call("list_procedures", {});
const inventory = await call("analyze_swift_types", {});
assert(
inventory.total > 0,
"Swift procedures disappeared from the inventory",
);
assert(
inventory.categories.other.count > 0,
"Top-level Swift symbols were lost",
);
const structs = await call("analyze_swift_types", {
category: "structs",
});
assert(
structs.categories.structs.count > 0,
"Modern struct symbols were lost",
);
const metadata = await call("analyze_swift_types", {
category: "structs",
pattern: "VMa",
});
assert.equal(
metadata.total,
2,
"Pair and Scorer metadata accessors must both be classified",
);
const pair = await call("analyze_swift_types", {
category: "structs",
pattern: "Pair",
});
assert(pair.total > 0, "Literal name filtering lost Pair symbols");
const unresolved = await call("analyze_swift_types", {
category: "structs",
pattern: "swiftIndirect",
});
assert(
unresolved.unclassified.length > 0,
"Unsupported categories must remain explicit",
);
const entries = Object.values(inventory.categories).flatMap(
(category) => category.items,
);
assert.equal(
new Set(entries.map((entry) => entry.name)).size,
inventory.total,
);
assert(
!entries.some(
(entry) =>
entry.name === "entry" || entry.name === "_swift_allocObject",
),
);
for (const entry of entries)
assert(
procedures.some(
(procedure) =>
procedure.address === entry.address &&
procedure.value === entry.name,
),
);
reports.push({
variant,
total: inventory.total,
structs: structs.total,
metadata_accessors: metadata.total,
unclassified: inventory.unclassified.length,
});
} finally {
await call("close_binary", {});
}
}
} finally {
await client.close();
await transport.close();
await rm(root, { recursive: true, force: true });
}
console.log(
JSON.stringify(
{
ok: true,
provider: "ghidra",
fixtures: reports,
verifier_run: await completeVerifierRun(run),
},
null,
2,
),
);
async function call(name, arguments_) {
const response = await client.callTool(
{ name, arguments: arguments_ },
{ timeout: 180_000 },
);
if (name === "analyze_swift_types")
toolContract(name).outputSchema.parse(response.structuredContent);
return requireMcpResult(response, name);
}

View File

@ -8,6 +8,7 @@ import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
import { enhancedInputSchemas } from "../contracts/enhancedInputs.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
@ -358,9 +359,30 @@ export class EnhancedTools {
signal?: AbortSignal,
): EnhancedResult {
const procedures = await this.#allAddressed("list_procedures", signal);
return procedures.ok
? ok(categorizeSwiftTypes(procedures.value, input))
: procedures;
if (!procedures.ok) return procedures;
const symbols = await this.#allAddressed("list_names", signal);
if (
!symbols.ok &&
!(symbols.error instanceof AnalysisCapabilityUnavailableError)
)
return symbols;
const result = categorizeSwiftTypes(
procedures.value,
input,
symbols.ok ? symbols.value : [],
);
return ok(
symbols.ok
? result
: {
...result,
symbol_inventory_error: projectAnalysisError(symbols.error),
limitations: [
...result.limitations,
"The provider's symbol inventory is unavailable; demangled procedures could not be joined to their Swift aliases.",
],
},
);
}
async #findXrefs(name: string, signal?: AbortSignal): EnhancedResult {

View File

@ -30,12 +30,14 @@ export const enhancedInputSchemas = {
category: z
.enum(["classes", "structs", "enums", "protocols", "extensions", "other"])
.optional()
.describe("Limit results to one Swift symbol category."),
.describe(
"Limit categorized results to one Swift category; unresolved category observations remain in unclassified.",
),
pattern: z
.string()
.optional()
.describe(
"Case-sensitive literal filter applied to mangled symbol names.",
"Case-sensitive literal filter applied to provider procedure names and their supporting Swift manglings.",
),
}),
find_xrefs_to_name: z.strictObject({ name: z.string() }),

View File

@ -54,7 +54,7 @@ export const ENHANCED_TOOL_CONTRACTS = [
),
enhanced(
"analyze_swift_types",
'Categorize analyzed procedure names into Swift classes, structs, enums, protocols, extensions, and other symbols. Returns deduplicated names grouped with counts. Optionally select one category and/or apply a case-sensitive literal name filter; for example, use {category: "classes", pattern: "Account"} to find matching class symbols.',
'Categorize analyzed Swift procedure names using their manglings or same-address Swift symbol aliases. Decodes legacy and literal modern nominal contexts into classes, structs, enums, protocols, and extensions. Preserves provider names and supporting mangled_names; unresolved categories appear in other and in unclassified, which remains inline even under category filters. Returns deduplicated procedure-name counts, not unique source types, plus identification limitations. Optionally select one category and/or apply a case-sensitive literal filter to provider names or supporting manglings; for example, use {category: "structs", pattern: "Pair"}.',
enhancedInputSchemas.analyze_swift_types,
),
enhanced(

View File

@ -283,9 +283,21 @@ export const enhancedOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
z.string(),
z.object({
count: z.number().int().min(0),
items: z.array(addressedEntry),
items: z.array(
addressedEntry.extend({
mangled_names: z.array(z.string()),
}),
),
}),
),
unclassified: z.array(
addressedEntry.extend({
mangled_names: z.array(z.string()),
reason: z.enum(["category_not_decoded", "conflicting_categories"]),
}),
),
limitations: z.array(z.string()),
symbol_inventory_error: analysisErrorProjectionSchema.exactOptional(),
}),
),
find_xrefs_to_name: resultOf(

View File

@ -0,0 +1,70 @@
const LEGACY_CATEGORIES = [
["_TtC", "classes"],
["_TtV", "structs"],
["_TtO", "enums"],
["_TtP", "protocols"],
["_TtE", "extensions"],
] as const;
/** Supported procedure categories derived from Swift symbol encodings. */
export type SwiftTypeCategory = (typeof LEGACY_CATEGORIES)[number][1] | "other";
const NOMINAL_CATEGORIES: ReadonlyMap<string, SwiftTypeCategory> = new Map([
["C", "classes"],
["V", "structs"],
["O", "enums"],
["P", "protocols"],
]);
/**
* Recognize Swift manglings and decode literal module/nominal contexts.
* Substitutions, generic contexts, and other encodings remain unknown.
*/
export const classifySwiftSymbol = (
name: string,
): { readonly category: SwiftTypeCategory | null } | undefined => {
const prefix = /^_?(?:\$[sS]|_T0)/u.exec(name);
if (prefix === null) {
if (!name.includes("_Tt")) return undefined;
return {
category:
LEGACY_CATEGORIES.find(([prefix]) => name.startsWith(prefix))?.[1] ??
null,
};
}
const mangling = name.slice(prefix[0].length);
let offset = identifierEnd(mangling, 0);
if (offset === undefined) return { category: null };
let category: SwiftTypeCategory | null = null;
for (;;) {
const end = identifierEnd(mangling, offset);
if (end === undefined) break;
const kind = mangling.charAt(end);
const nominal = NOMINAL_CATEGORIES.get(kind);
if (nominal === undefined) {
if (kind === "E" && category !== null) category = "extensions";
break;
}
category = nominal;
offset = end + 1;
}
return { category };
};
const identifierEnd = (
mangling: string,
offset: number,
): number | undefined => {
const lengthToken = /^[1-9][0-9]*/u.exec(mangling.slice(offset))?.[0];
if (lengthToken === undefined) return undefined;
const start = offset + lengthToken.length;
const end = start + Number(lengthToken);
// Swift identifiers with substitutions/punycode need a fuller decoder.
if (
!Number.isSafeInteger(end) ||
end > mangling.length ||
!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(mangling.slice(start, end))
)
return undefined;
return end;
};

View File

@ -80,4 +80,165 @@ describe("symbol analysis", () => {
},
});
});
it("classifies modern Swift manglings reported by current providers", () => {
const result = categorizeSwiftTypes([
{ address: "0x1000009f8", name: "$s4main4PairVMa" },
{ address: "0x100000ac4", name: "$s4main6ScorerVMa" },
{ address: "0x100000ab4", name: "_$s4main4PairVMa" },
{ address: "0x1", name: "_TtCClass" },
{ address: "0x1000009f8", name: "$s4main4PairVMa" },
]);
expect(result).toMatchObject({
total: 4,
categories: {
classes: { count: 1 },
structs: {
count: 3,
items: [
{ address: "0x1000009f8", name: "$s4main4PairVMa" },
{ address: "0x100000ac4", name: "$s4main6ScorerVMa" },
{ address: "0x100000ab4", name: "_$s4main4PairVMa" },
],
},
},
});
});
it("keeps non-Swift procedure names out of the Swift inventory", () => {
const result = categorizeSwiftTypes([
{ address: "0x1", name: "printf" },
{ address: "0x2", name: "_OBJC_CLASS_$_App" },
{ address: "0x3", name: "std::vector<int>::push_back" },
]);
expect(result).toMatchObject({ total: 0 });
});
});
describe("modern Swift category evidence", () => {
it.each([
["$s4main4PairVMa", "structs"],
["_$s4main6ScorerVMa", "structs"],
["$s4main7ServiceCMa", "classes"],
["$s4main5StateOMa", "enums"],
["$s4main7ScoringPMp", "protocols"],
["$s4main5OuterV5InnerCMa", "classes"],
["$s4main4PairV5otherE7doubledSiyF", "extensions"],
["$S4main4PairVMa", "structs"],
["_T04main4PairVMa", "structs"],
["$s4_TtX4PairVMa", "structs"],
] as const)("decodes nominal contexts for %s", (name, category) => {
expect(
categorizeSwiftTypes([{ address: "0x1", name }], { category }),
).toMatchObject({
total: 1,
categories: { [category]: { count: 1 } },
unclassified: [],
});
});
it("retains unresolved Swift observations under category filters", () => {
const names = [
"$s4main13swiftIndirectySiAA7Scoring_p_SitF",
"$sSi4mainE7doubledSiyF",
"$s9999999999999999999999BadVMa",
"$s4main04PairVMa",
];
const result = categorizeSwiftTypes(
names.map((name, index) => ({ address: String(index), name })),
{ category: "structs" },
);
expect(result).toMatchObject({
total: 0,
unclassified: names.map((name) => ({
name,
mangled_names: [name],
reason: "category_not_decoded",
})),
limitations: expect.arrayContaining([
expect.stringContaining("could not be established"),
]),
});
expect(
categorizeSwiftTypes([{ address: "0x1", name: names[0] ?? "" }], {
category: "structs",
pattern: "missing",
}),
).toMatchObject({ total: 0, unclassified: [] });
});
it("joins Swift aliases only at the exact procedure address", () => {
const result = categorizeSwiftTypes(
[
{ address: "0x1", name: "main::Pair::typeMetadataAccessor" },
{ address: "0x2", name: "std::vector<int>::push_back" },
{ address: "0x3", name: "main::Pair::other" },
],
{ category: "structs", pattern: "Pair" },
[
{ address: "0x1", name: "_$s4main4PairVMa" },
{ address: "0x4", name: "$s4main4PairVMa" },
],
);
expect(result).toMatchObject({
total: 1,
categories: {
structs: {
items: [
{
address: "0x1",
name: "main::Pair::typeMetadataAccessor",
mangled_names: ["_$s4main4PairVMa"],
},
],
},
},
unclassified: [],
});
});
it("deduplicates display names after selecting a category", () => {
expect(
categorizeSwiftTypes(
[
{ address: "0x1", name: "shared" },
{ address: "0x2", name: "shared" },
],
{ category: "structs" },
[
{ address: "0x1", name: "$s4main7ServiceCMa" },
{ address: "0x2", name: "$s4main4PairVMa" },
],
),
).toMatchObject({
total: 1,
categories: { structs: { items: [{ address: "0x2", name: "shared" }] } },
});
});
});
describe("Swift alias ambiguity", () => {
it("reports conflicting aliases rather than choosing a category", () => {
expect(
categorizeSwiftTypes(
[{ address: "0x1", name: "shared" }],
{ category: "structs" },
[
{ address: "0x1", name: "$s4main4PairVMa" },
{ address: "0x1", name: "$s4main7ServiceCMa" },
],
),
).toMatchObject({
total: 0,
unclassified: [
{
name: "shared",
reason: "conflicting_categories",
mangled_names: ["$s4main4PairVMa", "$s4main7ServiceCMa"],
},
],
});
});
});

View File

@ -1,5 +1,9 @@
import type { JsonValue } from "./jsonValue.js";
import type { AddressedName } from "./hopperValues.js";
import {
classifySwiftSymbol,
type SwiftTypeCategory,
} from "./swiftSymbolClassification.js";
/** Select and deduplicate Objective-C class labels. */
export const discoverObjcClasses = (
@ -34,50 +38,108 @@ export const discoverObjcProtocols = (
};
};
const SWIFT_CATEGORIES = [
["classes", "_TtC"],
["structs", "_TtV"],
["enums", "_TtO"],
["protocols", "_TtP"],
["extensions", "_TtE"],
] as const;
type SwiftTypeCategory = (typeof SWIFT_CATEGORIES)[number][0] | "other";
/** Categorize deduplicated Swift mangled symbols. */
/** Categorize procedure names using Swift manglings at their exact addresses. */
export const categorizeSwiftTypes = (
procedures: readonly AddressedName[],
filter: {
readonly category?: SwiftTypeCategory | undefined;
readonly pattern?: string | undefined;
} = {},
): JsonValue => {
const groups: Record<string, AddressedName[]> = Object.fromEntries(
[...SWIFT_CATEGORIES.map(([category]) => category), "other"].map(
symbols: readonly AddressedName[] = [],
) => {
const groups: Record<
string,
Array<{
address: string;
name: string;
mangled_names: string[];
}>
> = Object.fromEntries(
["classes", "structs", "enums", "protocols", "extensions", "other"].map(
(category) => [category, []],
),
);
const aliases = swiftAliasesByAddress(symbols);
const pattern = filter.pattern;
const seen = new Set<string>();
const unclassifiedNames = new Set<string>();
const unclassified: Array<{
address: string;
name: string;
mangled_names: string[];
reason: "category_not_decoded" | "conflicting_categories";
}> = [];
for (const entry of procedures) {
if (!entry.name.includes("_Tt") || seen.has(entry.name)) continue;
const category =
SWIFT_CATEGORIES.find(([, prefix]) =>
entry.name.startsWith(prefix),
)?.[0] ?? "other";
if (filter.category !== undefined && filter.category !== category) continue;
if (filter.pattern !== undefined && !entry.name.includes(filter.pattern))
if (seen.has(entry.name)) continue;
const mangledNames = [
...new Set([
...(classifySwiftSymbol(entry.name) === undefined ? [] : [entry.name]),
...(aliases.get(entry.address) ?? []),
]),
];
if (mangledNames.length === 0) continue;
if (
pattern !== undefined &&
![entry.name, ...mangledNames].some((name) => name.includes(pattern))
)
continue;
seen.add(entry.name);
groups[category]?.push(entry);
const kinds = new Set(
mangledNames.map((name) => classifySwiftSymbol(name)?.category ?? null),
);
const category = kinds.size === 1 ? ([...kinds][0] ?? null) : null;
const item = {
address: entry.address,
name: entry.name,
mangled_names: mangledNames,
};
if (category === null && !unclassifiedNames.has(entry.name)) {
unclassifiedNames.add(entry.name);
unclassified.push({
...item,
reason:
kinds.size > 1 && !kinds.has(null)
? "conflicting_categories"
: "category_not_decoded",
});
}
const bucket = category ?? "other";
if (filter.category === undefined || filter.category === bucket) {
seen.add(entry.name);
groups[bucket]?.push(item);
}
}
const categories = Object.fromEntries(
Object.entries(groups).map(([category, items]) => [
category,
{ count: items.length, items: items.map(toJsonEntry) },
{ count: items.length, items },
]),
);
return { total: seen.size, categories };
return {
total: Object.values(groups).reduce((sum, items) => sum + items.length, 0),
categories,
unclassified,
limitations: [
"Identification uses Swift mangled procedure names or same-address symbol aliases. Demangled names without retained Swift manglings cannot be identified. Counts deduplicate procedure names, not source declarations.",
...(unclassified.length === 0
? []
: [
"Some Swift categories could not be established from the retained manglings. Unclassified observations remain inline regardless of the category filter.",
]),
],
};
};
const swiftAliasesByAddress = (symbols: readonly AddressedName[]) => {
const aliases = new Map<string, string[]>();
for (const { address, name } of symbols) {
if (classifySwiftSymbol(name) === undefined) continue;
const existing = aliases.get(address);
if (existing === undefined) aliases.set(address, [name]);
else existing.push(name);
}
return aliases;
};
const uniqueByName = (entries: readonly AddressedName[]): AddressedName[] => {

View File

@ -0,0 +1,149 @@
import { afterEach, describe, expect, it } from "vitest";
import { EnhancedTools } from "../../../src/application/EnhancedTools.js";
import {
AnalysisArtifactChangedError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
} from "../../../src/domain/analysisErrorCore.js";
import { err } from "../../../src/domain/result.js";
import { observed as ok } from "../../fixtures/analysisExecution.js";
import { GHIDRA_SWIFT_INVENTORY } from "../../fixtures/swift/ghidraInventory.js";
import {
closeEnhancedToolResources,
connect,
jsonResult,
} from "./enhancedToolsHarness.js";
afterEach(closeEnhancedToolResources);
describe("Swift classification through MCP", () => {
it.each([
{ arguments: {}, total: 6 },
{ arguments: { category: "structs" }, total: 5 },
{ arguments: { category: "structs", pattern: "Pair" }, total: 3 },
{ arguments: { pattern: "_$s4main4PairVMa" }, total: 1 },
])("classifies golden Ghidra observations with $arguments", async (test) => {
const client = await connect({
execute: async (operation) =>
ok(
operation === "list_procedures"
? GHIDRA_SWIFT_INVENTORY.procedures
: GHIDRA_SWIFT_INVENTORY.symbols,
),
});
const result = await client.callTool({
name: "analyze_swift_types",
arguments: test.arguments,
});
expect(result.isError).not.toBe(true);
expect(jsonResult(result)).toMatchObject({ total: test.total });
if (test.arguments.pattern === undefined)
expect(jsonResult(result)).toMatchObject({
unclassified: [
{
name: "main::$swiftIndirect",
reason: "category_not_decoded",
mangled_names: ["_$s4main13swiftIndirectySiAA7Scoring_p_SitF"],
},
],
});
else expect(jsonResult(result)).toMatchObject({ unclassified: [] });
});
it("keeps provider names and supporting aliases inline", async () => {
const client = await connect({
execute: async (operation) =>
ok(
operation === "list_procedures"
? GHIDRA_SWIFT_INVENTORY.procedures
: GHIDRA_SWIFT_INVENTORY.symbols,
),
});
expect(
jsonResult(
await client.callTool({
name: "analyze_swift_types",
arguments: { category: "structs", pattern: "typeMetadataAccessor" },
}),
),
).toMatchObject({
total: 2,
categories: {
structs: {
items: [
{
address: "0x100000ab4",
name: "main::Pair::typeMetadataAccessor",
mangled_names: ["_$s4main4PairVMa"],
},
{
address: "0x100000ac4",
name: "main::Scorer::typeMetadataAccessor",
mangled_names: ["_$s4main6ScorerVMa"],
},
],
},
},
unclassified: [],
});
});
it("retains known facts when a provider has no symbol inventory", async () => {
const client = await connect({
execute: async (operation) =>
operation === "list_procedures"
? ok([{ address: "0x1", value: "$s4main4PairVMa" }])
: err(
new AnalysisCapabilityUnavailableError(
"fixture",
operation,
"no symbols",
),
),
});
const result = await client.callTool({
name: "analyze_swift_types",
arguments: { category: "structs" },
});
expect(result.isError).not.toBe(true);
expect(jsonResult(result)).toMatchObject({
total: 1,
symbol_inventory_error: { code: "capability_unavailable" },
limitations: expect.arrayContaining([
expect.stringContaining("symbol inventory is unavailable"),
]),
});
});
it.each([
new AnalysisOutputError("list_names", "malformed symbols"),
new AnalysisArtifactChangedError(
"list_names",
"/fixture",
"fixture changed",
),
])("preserves symbol-inventory failures: $_tag", async (failure) => {
const tools = new EnhancedTools({
execute: async (operation) =>
operation === "list_procedures"
? ok([{ address: "0x1", value: "$s4main4PairVMa" }])
: err(failure),
});
expect(await tools.execute("analyze_swift_types", {})).toEqual(
err(failure),
);
});
it("returns cancellation during the symbol join", async () => {
const controller = new AbortController();
const tools = new EnhancedTools({
execute: async (operation) => {
if (operation === "list_names") controller.abort();
return ok([{ address: "0x1", value: "$s4main4PairVMa" }]);
},
});
expect(
await tools.execute("analyze_swift_types", {}, controller.signal),
).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } });
});
});

View File

@ -0,0 +1,10 @@
protocol Scoring { func score(_ x: Int) -> Int }
struct Pair { var a: Int; var b: Int }
struct Scorer: Scoring {
@inline(never) func score(_ x: Int) -> Int { x + 29 }
}
@inline(never)
func swiftIndirect(_ s: any Scoring, _ x: Int) -> Int { s.score(x) }
@inline(never)
func swiftPair(_ p: Pair) -> Int { p.a + 3 * p.b }
print(swiftIndirect(Scorer(), CommandLine.arguments.count) + swiftPair(Pair(a: 1, b: 2)))

281
tests/fixtures/swift/ghidraInventory.ts vendored Normal file
View File

@ -0,0 +1,281 @@
// Golden projection of Ghidra 12.1.4 list_procedures/list_names on macOS arm64.
// Source: tests/conformance/swift/type-inventory.swift, compiled with swiftc -O -g.
// Includes selected procedure entries, all their symbols, and an off-entry metadata symbol.
export const GHIDRA_SWIFT_INVENTORY = {
procedures: [
{
address: "0x100000858",
value: "entry",
procedure: {
external: false,
thunk: false,
thunk_target: null,
},
},
{
address: "0x100000954",
value: "main::Pair::init",
procedure: {
external: false,
thunk: false,
thunk_target: null,
},
},
{
address: "0x100000958",
value: "main::$swiftIndirect",
procedure: {
external: false,
thunk: false,
thunk_target: null,
},
},
{
address: "0x1000009bc",
value: "main::Pair::set_a",
procedure: {
external: false,
thunk: false,
thunk_target: null,
},
},
{
address: "0x100000a28",
value: "main::Scorer::$score",
procedure: {
external: false,
thunk: false,
thunk_target: null,
},
},
{
address: "0x100000ab4",
value: "main::Pair::typeMetadataAccessor",
procedure: {
external: false,
thunk: false,
thunk_target: null,
},
},
{
address: "0x100000ac4",
value: "main::Scorer::typeMetadataAccessor",
procedure: {
external: false,
thunk: false,
thunk_target: null,
},
},
],
symbols: [
{
address: "0x100000858",
value: "_main",
symbol: {
primary: false,
dynamic: false,
external: false,
type: "label",
source: "imported",
},
},
{
address: "0x100000858",
value: "entry",
symbol: {
primary: true,
dynamic: false,
external: false,
type: "function",
source: "imported",
},
},
{
address: "0x100000954",
value: "_$s4main4PairV1a1bACSi_SitcfC",
symbol: {
primary: false,
dynamic: false,
external: false,
type: "label",
source: "imported",
},
},
{
address: "0x100000954",
value: "_$s4main4PairV1aSivM.resume.0",
symbol: {
primary: false,
dynamic: false,
external: false,
type: "label",
source: "imported",
},
},
{
address: "0x100000954",
value: "_$s4main4PairV1aSivg",
symbol: {
primary: false,
dynamic: false,
external: false,
type: "label",
source: "imported",
},
},
{
address: "0x100000954",
value: "_$s4main4PairV1bSivM.resume.0",
symbol: {
primary: false,
dynamic: false,
external: false,
type: "label",
source: "imported",
},
},
{
address: "0x100000954",
value: "_$s4main6ScorerVACycfC",
symbol: {
primary: false,
dynamic: false,
external: false,
type: "label",
source: "imported",
},
},
{
address: "0x100000954",
value: "main::Pair::init",
symbol: {
primary: true,
dynamic: false,
external: false,
type: "function",
source: "analysis",
},
},
{
address: "0x100000958",
value: "_$s4main13swiftIndirectySiAA7Scoring_p_SitF",
symbol: {
primary: false,
dynamic: false,
external: false,
type: "label",
source: "imported",
},
},
{
address: "0x100000958",
value: "main::$swiftIndirect",
symbol: {
primary: true,
dynamic: false,
external: false,
type: "function",
source: "analysis",
},
},
{
address: "0x1000009bc",
value: "_$s4main4PairV1aSivs",
symbol: {
primary: false,
dynamic: false,
external: false,
type: "label",
source: "imported",
},
},
{
address: "0x1000009bc",
value: "main::Pair::set_a",
symbol: {
primary: true,
dynamic: false,
external: false,
type: "function",
source: "analysis",
},
},
{
address: "0x100000a28",
value: "_$s4main6ScorerV5scoreyS2iFTf4nd_n",
symbol: {
primary: false,
dynamic: false,
external: false,
type: "label",
source: "imported",
},
},
{
address: "0x100000a28",
value: "main::Scorer::$score",
symbol: {
primary: true,
dynamic: false,
external: false,
type: "function",
source: "analysis",
},
},
{
address: "0x100000ab4",
value: "_$s4main4PairVMa",
symbol: {
primary: false,
dynamic: false,
external: false,
type: "label",
source: "imported",
},
},
{
address: "0x100000ab4",
value: "main::Pair::typeMetadataAccessor",
symbol: {
primary: true,
dynamic: false,
external: false,
type: "function",
source: "analysis",
},
},
{
address: "0x100000ac4",
value: "_$s4main6ScorerVMa",
symbol: {
primary: false,
dynamic: false,
external: false,
type: "label",
source: "imported",
},
},
{
address: "0x100000ac4",
value: "main::Scorer::typeMetadataAccessor",
symbol: {
primary: true,
dynamic: false,
external: false,
type: "function",
source: "analysis",
},
},
{
address: "0x100000c0c",
value: "_$s4main4PairVMn",
symbol: {
primary: false,
dynamic: false,
external: false,
type: "label",
source: "imported",
},
},
],
};