Files
reverse-engineering-anything/src/native/AppleObjcProtocols.ts
T

200 lines
6.8 KiB
TypeScript

import { readObjcPropertyList } from "./AppleObjcProperties.js";
import type { ObjcSwiftMetadata } from "../domain/native/objcSwiftMetadata.js";
import type { SwiftNominalReaders } from "./AppleSwiftNominalTypes.js";
/** Read protocol lists and absolute protocol method declarations with cycle-safe identities. */
export const createObjcProtocolReader = (input: {
readers: Pick<
SwiftNominalReaders,
"u32" | "string" | "location" | "evidence" | "admit"
> & {
pointer(address: bigint): bigint;
/** Plain 64-bit data, never a fixup. */
u64(address: bigint): bigint;
i32(address: bigint): number;
};
result: ObjcSwiftMetadata;
failures: string[];
}) => {
const { readers: read, result, failures } = input;
const visited = new Set<bigint>();
const name = (address: bigint) => read.string(read.pointer(address + 8n));
const list = (address: bigint, onTruncated?: () => void): string[] => {
if (address === 0n) return [];
// `count` is plain uintptr_t data, not a pointer fixup.
const count = read.u64(address);
if (count > 20000n)
throw new RangeError("Protocol list exceeds 20000 entries");
const names: string[] = [];
for (let index = 0n; index < count; index++) {
if (!read.admit()) {
onTruncated?.();
break;
}
const protocol = read.pointer(address + 8n + index * 8n);
names.push(name(protocol));
record(protocol);
}
return names;
};
const methods = (
address: bigint,
required: boolean,
instance: boolean,
/** Entries skipped for an empty selector, reported by the declaring protocol. */
emptySelectors: string[],
onTruncated?: () => void,
) => {
const output: ObjcSwiftMetadata["objc_protocols"][number]["methods"] = [];
if (address === 0n) return output;
const flags = read.u32(address),
count = read.u32(address + 4n);
const relative = (flags & 0x80000000) !== 0;
const stride = flags & 0xfffc;
if (stride !== (relative ? 12 : 24))
throw new TypeError("Unsupported Objective-C protocol method entry size");
for (let index = 0; index < count; index++) {
if (!read.admit()) {
onTruncated?.();
break;
}
const entry = address + 8n + BigInt(index * stride);
let selector = relative
? entry + BigInt(read.i32(entry))
: read.pointer(entry);
if (relative && (flags & 0x40000000) === 0)
selector = read.pointer(selector);
// One empty selector is reported on its own, so the protocol and its
// other method declarations keep their decoded records.
const selectorName = read.string(selector);
if (selectorName.length === 0) {
emptySelectors.push(
`${required ? "required" : "optional"} ${instance ? "instance" : "class"} method ${String(index)} at 0x${entry.toString(16)} has an empty selector`,
);
continue;
}
output.push({
selector: selectorName,
method_type: instance ? "instance" : "class",
address: null,
is_required: required,
is_optional: !required,
});
}
return output;
};
const record = (address: bigint) => {
if (address === 0n || visited.has(address) || !read.admit()) return;
visited.add(address);
try {
const protocolSize = read.u32(address + 64n);
if (protocolSize < 72)
throw new RangeError("Truncated Objective-C protocol declaration");
const protocolName = name(address);
// Adopted protocols are independent declarations; decode them before
// rejecting a parent whose own name is empty.
let budgetTruncated = false;
const onTruncated = () => {
budgetTruncated = true;
};
const adopted = list(read.pointer(address + 16n), onTruncated);
if (protocolName.length === 0)
throw new TypeError("Objective-C protocol has an empty name");
const skipped: string[] = [];
const required = [
...methods(
read.pointer(address + 24n),
true,
true,
skipped,
onTruncated,
),
...methods(
read.pointer(address + 32n),
true,
false,
skipped,
onTruncated,
),
];
const optional = [
...methods(
read.pointer(address + 40n),
false,
true,
skipped,
onTruncated,
),
...methods(
read.pointer(address + 48n),
false,
false,
skipped,
onTruncated,
),
];
for (const message of skipped)
failures.push(
`Protocol 0x${address.toString(16)}: Objective-C protocol ${protocolName} ${message}`,
);
let properties: ObjcSwiftMetadata["objc_protocols"][number]["properties"] =
[];
const propertyReasons: string[] = [];
try {
const decoded = readObjcPropertyList(
read,
read.pointer(address + 56n),
() => read.admit(),
onTruncated,
);
properties = decoded.properties;
for (const problem of decoded.issues)
propertyReasons.push(problem.message);
// Older protocol revisions end at size/flags. The later class-property
// list has no class/instance discriminator in the public property shape.
if (protocolSize >= 96 && read.pointer(address + 88n) !== 0n)
propertyReasons.push("protocol class properties are not decoded");
} catch (cause) {
propertyReasons.push(
cause instanceof Error ? cause.message : String(cause),
);
}
for (const message of propertyReasons)
failures.push(
`Protocol 0x${address.toString(16)}: Objective-C protocol ${protocolName} properties: ${message}`,
);
const decodeReasons = [
...propertyReasons,
...(skipped.length === 0 ? [] : ["protocol_method_selector_empty"]),
...(budgetTruncated ? ["max_records_reached"] : []),
];
result.objc_protocols.push({
name: protocolName,
methods: required,
optional_methods: optional,
properties,
});
result.objc_protocol_records.push({
name: protocolName,
adopted_protocols: adopted,
methods: required,
optional_methods: optional,
location: read.location(address),
decode:
decodeReasons.length === 0
? { status: "decoded", reason: null }
: { status: "partial", reason: decodeReasons.join("; ") },
evidence: read.evidence(
address,
"Objective-C protocol declaration and encoded method lists; IMP fields are not handler targets",
),
});
} catch (cause) {
failures.push(
`Protocol 0x${address.toString(16)}: ${cause instanceof Error ? cause.message : String(cause)}`,
);
}
};
return { list, record };
};