mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(native): retain Objective-C implementation alternatives
This commit is contained in:
@@ -358,6 +358,74 @@ describe("native UI action trace", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("native dispatch candidate identity", () => {
|
||||
it.each([
|
||||
["0x1000", "0x3000"],
|
||||
["ram:0x1000", "rom:0x1000"],
|
||||
])(
|
||||
"keeps same-selector implementation alternatives unresolved: %j and %j",
|
||||
async (left, right) => {
|
||||
for (const addresses of [
|
||||
[left, right],
|
||||
[right, left],
|
||||
]) {
|
||||
const result = await new EnhancedTools(
|
||||
testAnalysis({
|
||||
list_names: () =>
|
||||
execution(
|
||||
"list_names",
|
||||
addresses.map((address) => ({
|
||||
...methodInventoryEntry,
|
||||
address,
|
||||
})),
|
||||
),
|
||||
}),
|
||||
).execute("trace_native_ui_action", { action: "buildTapped:" });
|
||||
if (!result.ok) throw result.error;
|
||||
const trace = nativeInvestigationTraceSchema.parse(result.value);
|
||||
const dispatch = trace.unresolved.find(
|
||||
({ relation }) => relation === "objc_dispatch",
|
||||
);
|
||||
expect(dispatch).toMatchObject({
|
||||
to: null,
|
||||
resolution: "unresolved",
|
||||
reason: "selector_has_multiple_candidate_implementations",
|
||||
});
|
||||
expect(
|
||||
dispatch?.evidence.map(({ location }) => location.address),
|
||||
).toEqual(expect.arrayContaining(addresses));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("uses one canonical function node for equivalent coordinate spellings", async () => {
|
||||
const addresses = ["0x1000", "0X001000"];
|
||||
for (const ordered of [addresses, [...addresses].reverse()]) {
|
||||
const result = await new EnhancedTools(
|
||||
testAnalysis({
|
||||
list_names: () =>
|
||||
execution(
|
||||
"list_names",
|
||||
ordered.map((address) => ({ ...methodInventoryEntry, address })),
|
||||
),
|
||||
}),
|
||||
).execute("trace_native_ui_action", { action: "buildTapped:" });
|
||||
if (!result.ok) throw result.error;
|
||||
const trace = nativeInvestigationTraceSchema.parse(result.value);
|
||||
const dispatch = trace.edges.find(
|
||||
({ relation }) => relation === "objc_dispatch",
|
||||
);
|
||||
expect(dispatch).toMatchObject({
|
||||
to: "native:function:0x1000",
|
||||
resolution: "inferred",
|
||||
});
|
||||
expect(
|
||||
dispatch?.evidence.map(({ location }) => location.address),
|
||||
).toEqual(expect.arrayContaining(addresses));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("native UI action selector origin", () => {
|
||||
it("marks provider-reported function addresses as derived and the action seed as caller-supplied", async () => {
|
||||
const inner = testAnalysis({
|
||||
|
||||
@@ -2,6 +2,106 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { inspectNativeDispatchMetadata } from "./objcSwiftMetadata.js";
|
||||
|
||||
describe("symbol-only Objective-C implementation identity", () => {
|
||||
it.each([
|
||||
{ source: "0X00002000", canonical: "0x2000", numeric: 8192 },
|
||||
{
|
||||
source: "0x1fffffffffffff",
|
||||
canonical: "0x1fffffffffffff",
|
||||
numeric: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
{
|
||||
source: "0x20000000000000",
|
||||
canonical: "0x20000000000000",
|
||||
numeric: null,
|
||||
},
|
||||
{ source: "ram:0x2000", canonical: "ram:0x2000", numeric: null },
|
||||
{ source: "rom:0x2000", canonical: "rom:0x2000", numeric: null },
|
||||
])(
|
||||
"preserves $source without inventing a numeric method identity",
|
||||
({ source, canonical, numeric }) => {
|
||||
const result = inspectNativeDispatchMetadata([
|
||||
{ address: source, name: "-[StoreController buildTapped:]" },
|
||||
]);
|
||||
expect(result.objc_classes[0]?.methods[0]?.address).toBe(numeric);
|
||||
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
||||
implementation_address: canonical,
|
||||
location: { address: canonical },
|
||||
evidence: [{ location: { address: source } }],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("retains unparseable symbol coordinates as reported evidence, without claiming a location", () => {
|
||||
const source = "invalid::0x2000";
|
||||
const result = inspectNativeDispatchMetadata([
|
||||
{ address: source, name: "-[StoreController buildTapped:]" },
|
||||
{ address: source, name: "$s5Store7launchyyF" },
|
||||
]);
|
||||
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
||||
implementation_address: null,
|
||||
location: { address: null },
|
||||
evidence: [{ location: { address: null }, reported_address: source }],
|
||||
});
|
||||
expect(result.swift_symbols[0]).toMatchObject({
|
||||
address: null,
|
||||
evidence: [{ location: { address: null }, reported_address: source }],
|
||||
});
|
||||
});
|
||||
|
||||
it("merges equivalent method coordinates and retains every source spelling", () => {
|
||||
const addresses = ["0x2000", "0X002000"];
|
||||
for (const ordered of [addresses, [...addresses].reverse()]) {
|
||||
const result = inspectNativeDispatchMetadata(
|
||||
ordered.map((address) => ({
|
||||
address,
|
||||
name: "-[StoreController buildTapped:]",
|
||||
})),
|
||||
);
|
||||
expect(result.objc_dispatch_implementations).toHaveLength(1);
|
||||
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
||||
implementation_address: "0x2000",
|
||||
location: { address: "0x2000" },
|
||||
});
|
||||
expect(
|
||||
result.objc_dispatch_implementations[0]?.evidence.map(
|
||||
({ location }) => location.address,
|
||||
),
|
||||
).toEqual(ordered);
|
||||
expect(result.objc_classes[0]?.methods).toMatchObject([
|
||||
{ selector: "buildTapped:", address: 8192 },
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
["0x2000", "0x3000"],
|
||||
["ram:0x2000", "rom:0x2000"],
|
||||
["", "0x2000"],
|
||||
["unresolved", "unresolved"],
|
||||
])(
|
||||
"retains distinct or unknown implementation identities: %j and %j",
|
||||
(left, right) => {
|
||||
const result = inspectNativeDispatchMetadata(
|
||||
[left, right].map((address) => ({
|
||||
address,
|
||||
name: "-[StoreController buildTapped:]",
|
||||
})),
|
||||
);
|
||||
expect(result.objc_dispatch_implementations).toHaveLength(2);
|
||||
expect(result.objc_classes[0]?.methods).toEqual([
|
||||
{
|
||||
selector: "buildTapped:",
|
||||
method_type: "instance",
|
||||
address: null,
|
||||
is_required: null,
|
||||
is_optional: null,
|
||||
},
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("ObjC/Swift metadata", () => {
|
||||
it("bounds symbol projections and labels symbol-only metadata as partial", () => {
|
||||
const result = inspectNativeDispatchMetadata(
|
||||
|
||||
@@ -8,6 +8,10 @@ import { providerSchema } from "../evidence.js";
|
||||
import { isSwiftMangledSymbolName } from "../swiftSymbolClassification.js";
|
||||
import { nativeCallReferenceSchema } from "./nativeCallReference.js";
|
||||
import { machoTargetSelectionSchema } from "../apple/machoTargetSelection.js";
|
||||
import {
|
||||
nativeAddressCoordinateInputSchema,
|
||||
nativeAddressInputSchema,
|
||||
} from "./nativeAddress.js";
|
||||
|
||||
import {
|
||||
compareUnicodeCodePoints,
|
||||
@@ -36,7 +40,12 @@ export const objcPropertyAttributeSchema = z.strictObject({
|
||||
export const objcMethodSchema = z.strictObject({
|
||||
selector: z.string().min(1),
|
||||
method_type: objcMethodTypeSchema,
|
||||
address: z.int().nullable(),
|
||||
address: z
|
||||
.int()
|
||||
.nullable()
|
||||
.describe(
|
||||
"Unique unqualified implementation address; null if unknown, ambiguous, or not a safe integer.",
|
||||
),
|
||||
is_required: z.boolean().nullable(),
|
||||
is_optional: z.boolean().nullable(),
|
||||
});
|
||||
@@ -184,6 +193,33 @@ type ObjcDispatchImplementation = z.infer<
|
||||
typeof objcDispatchImplementationSchema
|
||||
>;
|
||||
|
||||
/** Project class declarations without selecting an ambiguous or unrepresentable address. */
|
||||
export const projectObjcClassMethods = (
|
||||
implementations: readonly ObjcDispatchImplementation[],
|
||||
): ObjcMethod[] => {
|
||||
const methods = new Map<string, ObjcMethod>();
|
||||
for (const implementation of implementations) {
|
||||
const key = compositeKey([
|
||||
implementation.selector,
|
||||
implementation.method_type,
|
||||
]);
|
||||
const address = parseMetadataAddress(implementation.implementation_address);
|
||||
const previous = methods.get(key);
|
||||
if (previous !== undefined) {
|
||||
if (previous.address !== address) previous.address = null;
|
||||
continue;
|
||||
}
|
||||
methods.set(key, {
|
||||
selector: implementation.selector,
|
||||
method_type: implementation.method_type,
|
||||
address,
|
||||
is_required: null,
|
||||
is_optional: null,
|
||||
});
|
||||
}
|
||||
return [...methods.values()];
|
||||
};
|
||||
|
||||
/** Objective-C category: methods, protocols and properties added to a class. */
|
||||
export const objcCategorySchema = z.strictObject({
|
||||
name: z.string().min(1),
|
||||
@@ -453,12 +489,17 @@ export const inspectNativeDispatchMetadata = (
|
||||
},
|
||||
];
|
||||
const classNames = new Set<string>();
|
||||
const implementations = new Map<string, ObjcDispatchImplementation>();
|
||||
const implementations = new Map<
|
||||
string | symbol,
|
||||
ObjcDispatchImplementation
|
||||
>();
|
||||
const swiftSymbols: SwiftSymbol[] = [];
|
||||
|
||||
const examinedNames = names.slice(0, maxRecords);
|
||||
for (const entry of examinedNames) {
|
||||
const address = entry.address === "" ? null : entry.address;
|
||||
const parsedAddress = nativeAddressInputSchema.safeParse(entry.address);
|
||||
const address = parsedAddress.success ? parsedAddress.data : null;
|
||||
const sourceAddress = parsedAddress.success ? entry.address : null;
|
||||
const classMatch = /(?:OBJC_CLASS_|OBJC_METACLASS_)\$_([^\s]+)/u.exec(
|
||||
entry.name,
|
||||
);
|
||||
@@ -468,15 +509,27 @@ export const inspectNativeDispatchMetadata = (
|
||||
if (methodMatch?.[1] !== undefined && methodMatch[2] && methodMatch[3]) {
|
||||
const methodType = methodMatch[1] === "+" ? "class" : "instance";
|
||||
const selector = methodMatch[3];
|
||||
implementations.set(`${methodMatch[2]}:${selector}:${methodType}`, {
|
||||
class_name: methodMatch[2],
|
||||
selector,
|
||||
method_type: methodType,
|
||||
implementation_address: address,
|
||||
location: { address, file_offset: null },
|
||||
decode: { status: "partial", reason: "symbol_name_only" },
|
||||
evidence: symbolEvidence(entry, address),
|
||||
});
|
||||
const key = parsedAddress.success
|
||||
? compositeKey([
|
||||
methodMatch[2],
|
||||
selector,
|
||||
methodType,
|
||||
parsedAddress.data,
|
||||
])
|
||||
: Symbol();
|
||||
const evidence = symbolEvidence(entry, sourceAddress);
|
||||
const previous = implementations.get(key);
|
||||
if (previous !== undefined) previous.evidence.push(...evidence);
|
||||
else
|
||||
implementations.set(key, {
|
||||
class_name: methodMatch[2],
|
||||
selector,
|
||||
method_type: methodType,
|
||||
implementation_address: address,
|
||||
location: { address, file_offset: null },
|
||||
decode: { status: "partial", reason: "symbol_name_only" },
|
||||
evidence,
|
||||
});
|
||||
classNames.add(methodMatch[2]);
|
||||
}
|
||||
|
||||
@@ -485,28 +538,26 @@ export const inspectNativeDispatchMetadata = (
|
||||
mangled_name: entry.name,
|
||||
address,
|
||||
decode: { status: "partial", reason: "mangled_symbol_not_demangled" },
|
||||
evidence: symbolEvidence(entry, address),
|
||||
evidence: symbolEvidence(entry, sourceAddress),
|
||||
});
|
||||
}
|
||||
|
||||
const methodsByClass = new Map<string, ObjcMethod[]>();
|
||||
const implementationsByClass = new Map<
|
||||
string,
|
||||
ObjcDispatchImplementation[]
|
||||
>();
|
||||
for (const implementation of implementations.values()) {
|
||||
const methods = methodsByClass.get(implementation.class_name) ?? [];
|
||||
methods.push({
|
||||
selector: implementation.selector,
|
||||
method_type: implementation.method_type,
|
||||
address: parseMetadataAddress(implementation.implementation_address),
|
||||
is_required: null,
|
||||
is_optional: null,
|
||||
});
|
||||
methodsByClass.set(implementation.class_name, methods);
|
||||
const candidates =
|
||||
implementationsByClass.get(implementation.class_name) ?? [];
|
||||
candidates.push(implementation);
|
||||
implementationsByClass.set(implementation.class_name, candidates);
|
||||
}
|
||||
const classes: ObjcClass[] = [...classNames].sort().map((name) => ({
|
||||
name,
|
||||
super_class: null,
|
||||
is_meta_class: null,
|
||||
is_root_class: null,
|
||||
methods: methodsByClass.get(name) ?? [],
|
||||
methods: projectObjcClassMethods(implementationsByClass.get(name) ?? []),
|
||||
properties: [],
|
||||
protocols: [],
|
||||
ivar_count: null,
|
||||
@@ -515,8 +566,18 @@ export const inspectNativeDispatchMetadata = (
|
||||
const decodedImplementations = [...implementations.values()].sort(
|
||||
(left, right) =>
|
||||
compareUnicodeCodePoints(
|
||||
compositeKey([left.class_name, left.selector]),
|
||||
compositeKey([right.class_name, right.selector]),
|
||||
compositeKey([
|
||||
left.class_name,
|
||||
left.selector,
|
||||
left.method_type,
|
||||
left.implementation_address ?? "",
|
||||
]),
|
||||
compositeKey([
|
||||
right.class_name,
|
||||
right.selector,
|
||||
right.method_type,
|
||||
right.implementation_address ?? "",
|
||||
]),
|
||||
),
|
||||
);
|
||||
const swift = swiftSymbols.length > 0;
|
||||
@@ -586,9 +647,10 @@ export const inspectNativeDispatchMetadata = (
|
||||
};
|
||||
|
||||
const parseMetadataAddress = (address: string | null): number | null => {
|
||||
if (address === null) return null;
|
||||
const match = /(?:^|:)0x([0-9a-f]+)$/iu.exec(address);
|
||||
if (match?.[1] === undefined) return null;
|
||||
const value = Number.parseInt(match[1], 16);
|
||||
return Number.isSafeInteger(value) ? value : null;
|
||||
const parsed = nativeAddressCoordinateInputSchema.safeParse(address);
|
||||
return parsed.success &&
|
||||
parsed.data.space === null &&
|
||||
parsed.data.offset <= BigInt(Number.MAX_SAFE_INTEGER)
|
||||
? Number(parsed.data.offset)
|
||||
: null;
|
||||
};
|
||||
|
||||
@@ -120,6 +120,25 @@ const fixture = (
|
||||
};
|
||||
const provenance = { path: "/fixture/app", sha256: "a".repeat(64) };
|
||||
|
||||
it("retains duplicate encoded method candidates without choosing one class-method address", () => {
|
||||
const bytes = objcRecordBudgetMacho("class", [0x700, 0x720]);
|
||||
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
||||
expect(
|
||||
result.objc_dispatch_implementations.map(
|
||||
({ implementation_address }) => implementation_address,
|
||||
),
|
||||
).toEqual(["0x100000700", "0x100000720"]);
|
||||
expect(result.objc_classes[0]?.methods).toEqual([
|
||||
{
|
||||
selector: "classMethod",
|
||||
method_type: "instance",
|
||||
address: null,
|
||||
is_required: null,
|
||||
is_optional: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const swiftExternalSwiftBindingImage = (
|
||||
boundField: "protocol" | "type" | "objc-class" = "protocol",
|
||||
) => {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import type { ObjcSwiftMetadata } from "../domain/native/objcSwiftMetadata.js";
|
||||
import {
|
||||
projectObjcClassMethods,
|
||||
type ObjcSwiftMetadata,
|
||||
} from "../domain/native/objcSwiftMetadata.js";
|
||||
import type { Segment, Section } from "./AppleMachoSelection.js";
|
||||
import { boundClassName, decodeObjcCategories } from "./AppleObjcCategories.js";
|
||||
import { readObjcPropertiesOf } from "./AppleObjcProperties.js";
|
||||
@@ -366,24 +369,13 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
super_class: superclass.name,
|
||||
is_meta_class: meta,
|
||||
is_root_class: root,
|
||||
methods: result.objc_dispatch_implementations
|
||||
.filter(
|
||||
methods: projectObjcClassMethods(
|
||||
result.objc_dispatch_implementations.filter(
|
||||
(item) =>
|
||||
item.class_name === name &&
|
||||
item.method_type === (meta ? "class" : "instance"),
|
||||
)
|
||||
.map((item) => ({
|
||||
selector: item.selector,
|
||||
method_type: item.method_type,
|
||||
address:
|
||||
item.implementation_address !== null &&
|
||||
BigInt(item.implementation_address) <=
|
||||
BigInt(Number.MAX_SAFE_INTEGER)
|
||||
? Number(BigInt(item.implementation_address))
|
||||
: null,
|
||||
is_required: null,
|
||||
is_optional: null,
|
||||
})),
|
||||
),
|
||||
),
|
||||
properties,
|
||||
protocols,
|
||||
ivar_count: ivarCount,
|
||||
|
||||
@@ -14,6 +14,64 @@ import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
import { nativeObjcClassImportMachO } from "../fixtures/nativeObjcClassImportMachO.js";
|
||||
import { objcRecordBudgetMacho } from "../fixtures/objcRecordBudgetMacho.js";
|
||||
|
||||
cliTest.skipIf(process.platform !== "darwin")(
|
||||
"preserves ambiguous encoded method implementations through the public CLI and MCP",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-objc-method-candidates-");
|
||||
const target = join(root, "methods.macho");
|
||||
const bytes = objcRecordBudgetMacho("class", [0x700, 0x720]);
|
||||
await writeFile(target, bytes);
|
||||
const environment = {
|
||||
PATH: process.env.PATH ?? "",
|
||||
REA_ANALYSIS_PROVIDER: "auto",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
GHIDRA_INSTALL_DIR: "/rea-unconfigured-deep-provider/ghidra",
|
||||
JAVA_HOME: "/rea-unconfigured-deep-provider/java",
|
||||
REA_IDA_MCP_CONFIG: "/rea-unconfigured-deep-provider/ida-mcp.json",
|
||||
REA_KUNA_COMMAND: "/rea-unconfigured-deep-provider/kuna",
|
||||
REA_KUNA_CACHE_DIR: "/rea-unconfigured-deep-provider/kuna-cache",
|
||||
REA_LOG_LEVEL: "silent",
|
||||
};
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-native-dispatch-metadata", target, "--json"],
|
||||
environment,
|
||||
});
|
||||
expect(
|
||||
result.exitCode,
|
||||
`${result.stderr}\n${JSON.stringify(result.json)}`,
|
||||
).toBe(0);
|
||||
const cliEvidence = toolContract(
|
||||
"inspect_native_dispatch_metadata",
|
||||
).outputSchema.parse(result.json);
|
||||
const { client, call } = await connectLocalToolsMcp(environment);
|
||||
const opened = await call("open_binary", { path: target });
|
||||
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
|
||||
const inspected = await call("inspect_native_dispatch_metadata", {});
|
||||
expect(inspected.isError, JSON.stringify(inspected)).not.toBe(true);
|
||||
const mcpEvidence = parseEvidence(inspected.structuredContent);
|
||||
for (const evidence of [cliEvidence, mcpEvidence]) {
|
||||
expect(evidence.normalized_result).toMatchObject({
|
||||
result: {
|
||||
objc_classes: [
|
||||
{
|
||||
name: "FixtureClass",
|
||||
methods: [{ selector: "classMethod", address: null }],
|
||||
},
|
||||
],
|
||||
objc_dispatch_implementations: [
|
||||
{ selector: "classMethod", implementation_address: "0x100000700" },
|
||||
{ selector: "classMethod", implementation_address: "0x100000720" },
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
expect(await readFile(target)).toEqual(bytes);
|
||||
await client.ping();
|
||||
},
|
||||
30000,
|
||||
);
|
||||
|
||||
cliTest.skipIf(process.platform !== "darwin")(
|
||||
"retains and reopens profileless native dispatch workflow snapshots through CLI and stdio MCP",
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createAnalysisExecution } from "../../../src/application/AnalysisProvid
|
||||
import { ok as resultOk } from "../../../src/domain/result.js";
|
||||
import { err } from "../../../src/domain/result.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { nativeDispatchMetadataResultSchema } from "../../../src/domain/native/objcSwiftMetadata.js";
|
||||
|
||||
const targetObservation = (value: unknown) =>
|
||||
resultOk(
|
||||
@@ -481,3 +482,54 @@ it("retains unlocated provider symbols through the advertised native dispatch MC
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ addresses: ["0x2000", "0x3000"], candidates: 2, numeric: null },
|
||||
{ addresses: ["ram:0x2000", "rom:0x2000"], candidates: 2, numeric: null },
|
||||
{ addresses: ["0x2000", "0X002000"], candidates: 1, numeric: 8192 },
|
||||
{
|
||||
addresses: ["overlay%2f1:0x2000", "overlay%2F1:0X002000"],
|
||||
candidates: 1,
|
||||
numeric: null,
|
||||
},
|
||||
])(
|
||||
"retains implementation identity and source spelling through MCP: $addresses",
|
||||
async ({ addresses, candidates, numeric }) => {
|
||||
const ordinary = fixturePort();
|
||||
const client = await connect({
|
||||
execute: (operation, parameters, options) =>
|
||||
operation === "list_names"
|
||||
? Promise.resolve(
|
||||
targetObservation(
|
||||
addresses.map((address) => ({
|
||||
address,
|
||||
value: "-[Fixture buildTapped:]",
|
||||
})),
|
||||
),
|
||||
)
|
||||
: ordinary.execute(operation, parameters, options),
|
||||
});
|
||||
const response = await client.callTool({
|
||||
name: "inspect_native_dispatch_metadata",
|
||||
arguments: {},
|
||||
});
|
||||
expect(
|
||||
response.isError,
|
||||
JSON.stringify(response.structuredContent),
|
||||
).not.toBe(true);
|
||||
const result = nativeDispatchMetadataResultSchema.parse(
|
||||
jsonResult(response),
|
||||
).result;
|
||||
expect(result.objc_dispatch_implementations).toHaveLength(candidates);
|
||||
expect(result.objc_classes[0]?.methods).toMatchObject([
|
||||
{ selector: "buildTapped:", address: numeric },
|
||||
]);
|
||||
expect(
|
||||
result.objc_dispatch_implementations
|
||||
.flatMap(({ evidence }) =>
|
||||
evidence.map(({ location }) => location.address),
|
||||
)
|
||||
.sort(),
|
||||
).toEqual([...addresses].sort());
|
||||
},
|
||||
);
|
||||
|
||||
+11
-5
@@ -1,7 +1,10 @@
|
||||
const base = 0x100000000n;
|
||||
|
||||
/** Minimal inert Mach-O with one class instance method and protocol method. */
|
||||
export const objcRecordBudgetMacho = (owner: "class" | "protocol"): Buffer => {
|
||||
export const objcRecordBudgetMacho = (
|
||||
owner: "class" | "protocol",
|
||||
classMethodOffsets: readonly number[] = [0x700],
|
||||
): Buffer => {
|
||||
const bytes = Buffer.alloc(4096);
|
||||
const u32 = (offset: number, value: number) =>
|
||||
bytes.writeUInt32LE(value, offset);
|
||||
@@ -51,10 +54,13 @@ export const objcRecordBudgetMacho = (owner: "class" | "protocol"): Buffer => {
|
||||
nullPointer(0x340);
|
||||
bytes.write("FixtureClass\0", 0x900);
|
||||
u32(0x500, 24);
|
||||
u32(0x504, 1);
|
||||
pointer(0x508, 0x920);
|
||||
pointer(0x510, 0x940);
|
||||
pointer(0x518, 0x700);
|
||||
u32(0x504, classMethodOffsets.length);
|
||||
for (const [index, implementation] of classMethodOffsets.entries()) {
|
||||
const entry = 0x508 + index * 24;
|
||||
pointer(entry, 0x920);
|
||||
pointer(entry + 8, 0x940);
|
||||
pointer(entry + 16, implementation);
|
||||
}
|
||||
bytes.write("classMethod\0", 0x920);
|
||||
bytes.write("v@:\0", 0x940);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user