Files
reverse-engineering-anything/src/domain/native/objcSwiftMetadata.ts
T
morluto 6b7d429491 fix(apple): decode Swift conformance type references
Decode direct Objective-C class names separately from nominal descriptors, retain indirect class bindings as partial facts, and preserve readable protocol and witness evidence when related bytes are unavailable.

Validated with 98 focused tests, check:ci, docs:check, and named-schema CLI/MCP replays of compiler-produced imported-class and struct fixtures plus an inert kind-3 control. The fixtures were inspected statically, never executed.

Fixes #5738
2026-10-21 14:50:57 +08:00

545 lines
19 KiB
TypeScript

import { z } from "zod";
import {
BINARY_ARCHITECTURES,
isMachOFormat,
type BinaryTarget,
} from "../binaryTargetTypes.js";
import { providerSchema } from "../evidence.js";
import { isSwiftMangledSymbolName } from "../swiftSymbolClassification.js";
import { nativeCallReferenceSchema } from "./nativeCallReference.js";
import { machoTargetSelectionSchema } from "../apple/machoTargetSelection.js";
import {
compareUnicodeCodePoints,
compositeKey,
} from "../unicodeCodePointOrder.js";
/** Targets admitted to the Apple byte reader; the reader validates slice support. */
export const isAppleDispatchMetadataTarget = (target: BinaryTarget): boolean =>
target.kind === "executable" && isMachOFormat(target.format);
/** Objective-C method type. */
export const objcMethodTypeSchema = z.enum([
"instance",
"class",
"ivar_getter",
"ivar_setter",
]);
/** Objective-C property attribute. */
export const objcPropertyAttributeSchema = z.strictObject({
name: z.string().min(1),
value: z.string(),
});
/** Objective-C method metadata. */
export const objcMethodSchema = z.strictObject({
selector: z.string().min(1),
method_type: objcMethodTypeSchema,
address: z.number().int().nullable(),
is_required: z.boolean().nullable(),
is_optional: z.boolean().nullable(),
});
type ObjcMethod = z.infer<typeof objcMethodSchema>;
/** Objective-C property metadata. */
export const objcPropertySchema = z.strictObject({
name: z.string().min(1),
type_encoding: z.string().nullable(),
attributes: z.array(objcPropertyAttributeSchema),
atomicity: z.enum(["atomic", "nonatomic"]).nullable(),
is_readonly: z.boolean().nullable(),
getter: z.string().nullable(),
setter: z.string().nullable(),
});
/** Objective-C protocol metadata. */
export const objcProtocolSchema = z.strictObject({
name: z.string().min(1),
methods: z.array(objcMethodSchema).default([]),
optional_methods: z.array(objcMethodSchema).default([]),
properties: z.array(objcPropertySchema).default([]),
});
/** Objective-C class metadata. */
export const objcClassSchema = z.strictObject({
name: z.string().min(1),
super_class: z.string().nullable(),
is_meta_class: z.boolean().nullable(),
is_root_class: z.boolean().nullable(),
methods: z.array(objcMethodSchema).default([]),
properties: z.array(objcPropertySchema).default([]),
protocols: z.array(z.string()).default([]),
ivar_count: z.number().int().nonnegative().nullable(),
instance_size: z.number().int().nonnegative().nullable(),
location: z
.object({
address: z.string().nullable(),
file_offset: z.number().int().nonnegative().nullable(),
})
.optional(),
superclass_address: z.string().nullable().optional(),
metaclass_address: z.string().nullable().optional(),
decode: z
.object({
status: z.enum(["decoded", "partial", "unsupported", "invalid"]),
reason: z.string().nullable(),
})
.optional(),
evidence: z
.array(
z.object({
kind: z.literal("binary_metadata"),
description: z.string(),
location: z.object({
address: z.string().nullable(),
file_offset: z.number().int().nonnegative().nullable(),
}),
artifact_path: z.string(),
artifact_sha256: z.string(),
}),
)
.optional(),
});
type ObjcClass = z.infer<typeof objcClassSchema>;
/** Exact location of a decoded native metadata value when the provider knows it. */
export const nativeMetadataLocationSchema = z.strictObject({
address: z.string().nullable(),
file_offset: z.number().int().nonnegative().nullable(),
});
export type NativeMetadataLocation = z.infer<
typeof nativeMetadataLocationSchema
>;
/** Provider-neutral decode state; absence never silently means unsupported. */
export const nativeMetadataDecodeSchema = z.strictObject({
status: z.enum(["decoded", "partial", "unsupported", "invalid"]),
reason: z.string().nullable(),
});
/** Evidence attached to one dispatch edge or metadata record. */
export const nativeMetadataEvidenceSchema = z.strictObject({
kind: z.enum([
"binary_metadata",
"interface_builder_resource",
"symbol",
"reference",
"decompiler",
]),
description: z.string().min(1),
provider: providerSchema.optional(),
location: nativeMetadataLocationSchema,
/** Preserve a provider's source value when it does not establish a location. */
reported_address: z.string().optional(),
call_reference: nativeCallReferenceSchema.exactOptional(),
artifact_path: z.string().min(1).nullable().default(null),
artifact_sha256: z
.string()
.regex(/^[a-f0-9]{64}$/u)
.nullable()
.default(null),
});
export type NativeMetadataEvidence = z.infer<
typeof nativeMetadataEvidenceSchema
>;
/** Objective-C ivar layout, retaining encoded type and exact storage offset. */
export const objcIvarSchema = z.strictObject({
class_name: z.string().min(1),
name: z.string().min(1),
type_encoding: z.string().nullable(),
offset: z.number().int().nonnegative().nullable(),
size: z.number().int().nonnegative().nullable().default(null),
alignment: z.number().int().positive().nullable().default(null),
location: nativeMetadataLocationSchema,
decode: nativeMetadataDecodeSchema,
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
});
/** Encoded Objective-C protocol declaration with required and optional method facts. */
export const objcProtocolRecordSchema = z.strictObject({
name: z.string(),
adopted_protocols: z.array(z.string()),
methods: z.array(objcMethodSchema),
optional_methods: z.array(objcMethodSchema),
location: nativeMetadataLocationSchema,
decode: nativeMetadataDecodeSchema,
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
});
/** Selector implementation resolved from a class or metaclass method list. */
export const objcDispatchImplementationSchema = z.strictObject({
class_name: z.string().min(1),
selector: z.string().min(1),
method_type: z.enum(["instance", "class"]),
implementation_address: z.string().nullable(),
/** Category that adds this method, when it is not defined by the class itself. */
category: z.string().nullable().optional(),
location: nativeMetadataLocationSchema,
decode: nativeMetadataDecodeSchema,
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
});
type ObjcDispatchImplementation = z.infer<
typeof objcDispatchImplementationSchema
>;
/** Objective-C category: methods, protocols and properties added to a class. */
export const objcCategorySchema = z.strictObject({
name: z.string().min(1),
/** Extended class; external classes come from the category's bind symbol. */
class_name: z.string().min(1).nullable(),
class_source: z.enum(["local", "external", "unresolved"]),
instance_methods: z.array(z.string()),
class_methods: z.array(z.string()),
protocols: z.array(z.string()),
properties: z.array(objcPropertySchema),
location: nativeMetadataLocationSchema,
decode: nativeMetadataDecodeSchema,
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
});
/** Exact Mach-O import binding observed at a pointer slot. */
export const machoImportBindingSchema = z.strictObject({
requested_address: z.string().min(1),
symbol: z.string().min(1),
binding_source: z.enum(["chained", "bind", "weak", "lazy"]),
library_ordinal: z.number().int().min(-3).max(Number.MAX_SAFE_INTEGER),
library: z.string().nullable(),
weak: z.boolean(),
addend: z.string().regex(/^-?\d+$/u),
});
export type MachoImportBinding = z.infer<typeof machoImportBindingSchema>;
/** Swift protocol conformance and its associated witness-table location. */
export const swiftConformanceSchema = z.strictObject({
type_name: z.string().min(1).nullable(),
/** Raw three-bit Swift ABI type-reference kind; absent in older Evidence. */
type_reference_kind: z.number().int().min(0).max(7).optional(),
/** Address of the descriptor, class-name string, or resolved ObjC class reference. */
type_reference_target: nativeMetadataLocationSchema.optional(),
type_binding: machoImportBindingSchema.nullable().optional(),
protocol_name: z.string().min(1).nullable(),
/** Resolved protocol descriptor address, retained when its bytes are unavailable. */
protocol_target: nativeMetadataLocationSchema.optional(),
protocol_binding: machoImportBindingSchema.nullable().optional(),
module: z.string().nullable(),
witness_table: nativeMetadataLocationSchema,
location: nativeMetadataLocationSchema,
decode: nativeMetadataDecodeSchema,
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
});
/** A typed slot in a Swift protocol witness table or class vtable. */
export const swiftDispatchSlotSchema = z.strictObject({
owner: z.string().min(1),
table_kind: z.enum(["witness_table", "class_vtable"]),
slot_index: z.number().int().nonnegative(),
requirement: z.string().nullable(),
implementation: z.string().nullable(),
implementation_address: z.string().nullable(),
implementation_binding: machoImportBindingSchema.nullable().optional(),
thunk_address: z.string().nullable(),
location: nativeMetadataLocationSchema,
decode: nativeMetadataDecodeSchema,
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
});
/** Signed-relative pointer as stored in an Apple binary metadata structure. */
export const relativePointerSchema = z.strictObject({
field: nativeMetadataLocationSchema,
target: nativeMetadataLocationSchema,
displacement: z.number().int(),
indirectable: z.boolean(),
indirect: z.boolean().nullable(),
encoding_tag: z.number().int().nonnegative().optional(),
binding: machoImportBindingSchema.optional(),
decode: nativeMetadataDecodeSchema,
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
});
/** A raw Swift-mangled symbol when a provider has not decoded its metadata. */
export const swiftSymbolSchema = z.strictObject({
mangled_name: z.string().min(1),
address: z.string().min(1).nullable(),
decode: nativeMetadataDecodeSchema,
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
});
type SwiftSymbol = z.infer<typeof swiftSymbolSchema>;
/** Per-facet coverage; empty arrays are meaningful only with this record. */
export const nativeMetadataCoverageSchema = z.strictObject({
facet: z.string().min(1),
status: z.enum(["complete", "partial", "unsupported", "not_requested"]),
reason: z.string().nullable(),
examined: z.number().int().nonnegative(),
decoded: z.number().int().nonnegative(),
});
export const objcClassImportIdentitySchema = machoImportBindingSchema.omit({
requested_address: true,
});
export const objcClassImportBindingCoverageSchema = z.strictObject({
facet: z.literal("objc_class_import_bindings"),
status: z.enum(["complete", "partial", "unsupported", "not_requested"]),
reason: z.string().nullable(),
examined: z.number().int().nonnegative(),
decoded: z.number().int().nonnegative(),
});
export type ObjcClassImportBindingCoverage = z.infer<
typeof objcClassImportBindingCoverageSchema
>;
/** Swift declaration kind. */
export const swiftDeclKindSchema = z.enum([
"class",
"struct",
"enum",
"protocol",
"extension",
"actor",
"global_var",
"global_func",
]);
/** Swift access level. */
export const swiftAccessLevelSchema = z.enum([
"private",
"fileprivate",
"internal",
"public",
"open",
]);
/** Swift declaration metadata. */
export const swiftDeclSchema = z.strictObject({
kind: swiftDeclKindSchema,
name: z.string().min(1),
module: z.string().nullable(),
access_level: swiftAccessLevelSchema.default("internal"),
super_class: z.string().nullable(),
protocols: z.array(z.string()).default([]),
is_final: z.boolean().default(false),
is_required: z.boolean().default(false),
is_convenience_init: z.boolean().default(false),
is_override: z.boolean().default(false),
});
/** Database save/read operation type. */
export const dbOperationSchema = z.enum([
"save",
"readback",
"open_database",
"close_database",
"export",
"import",
]);
/** Database save operation result. */
export const dbSaveResultSchema = z.strictObject({
operation: dbOperationSchema,
succeeded: z.boolean(),
preserved_names: z.number().int().nonnegative().default(0),
preserved_comments: z.number().int().nonnegative().default(0),
preserved_bookmarks: z.number().int().nonnegative().default(0),
database_path: z.string().nullable(),
error: z.string().nullable(),
});
/** Result of deeper ObjC/Swift metadata extraction. */
export const objcSwiftMetadataSchema = z.strictObject({
objc_classes: z.array(objcClassSchema).default([]),
objc_protocols: z.array(objcProtocolSchema).default([]),
objc_categories: z.array(objcCategorySchema).default([]),
swift_decls: z.array(swiftDeclSchema).default([]),
objc_ivars: z.array(objcIvarSchema).default([]),
objc_protocol_records: z.array(objcProtocolRecordSchema).default([]),
objc_dispatch_implementations: z
.array(objcDispatchImplementationSchema)
.default([]),
swift_conformances: z.array(swiftConformanceSchema).default([]),
swift_dispatch_slots: z.array(swiftDispatchSlotSchema).default([]),
swift_symbols: z.array(swiftSymbolSchema).default([]),
objc_class_import_bindings: z.array(machoImportBindingSchema).optional(),
objc_class_import_binding_coverage:
objcClassImportBindingCoverageSchema.optional(),
target_selection: machoTargetSelectionSchema.optional(),
relative_pointers: z.array(relativePointerSchema).default([]),
coverage: z.array(nativeMetadataCoverageSchema).default([]),
db_save_result: dbSaveResultSchema.nullable(),
});
export type ObjcSwiftMetadata = z.infer<typeof objcSwiftMetadataSchema>;
/** Provider and target identity for one normalized dispatch-metadata result. */
export const nativeDispatchMetadataResultSchema = z.strictObject({
target_sha256: z
.string()
.regex(/^[a-f0-9]{64}$/u)
.nullable(),
target_architecture: z.enum(BINARY_ARCHITECTURES).nullable().optional(),
provider: z.strictObject({
id: z.string().min(1),
name: z.string().min(1),
version: z.string().nullable(),
}),
analysis_profile_digest: z.string().min(1).nullable(),
result: objcSwiftMetadataSchema,
});
/** Decode only facts visible in a provider's existing name inventory. */
export const inspectNativeDispatchMetadata = (
names: readonly { readonly address: string; readonly name: string }[],
maxRecords = 5_000,
): ObjcSwiftMetadata => {
const symbolEvidence = (
entry: (typeof names)[number],
address: string | null,
) => [
{
kind: "symbol" as const,
description: `Provider symbol ${entry.name}`,
location: { address, file_offset: null },
...(address === null ? { reported_address: entry.address } : {}),
artifact_path: null,
artifact_sha256: null,
},
];
const classNames = new Set<string>();
const implementations = new Map<string, ObjcDispatchImplementation>();
const swiftSymbols: SwiftSymbol[] = [];
const examinedNames = names.slice(0, maxRecords);
for (const entry of examinedNames) {
const address = entry.address === "" ? null : entry.address;
const classMatch = /(?:OBJC_CLASS_|OBJC_METACLASS_)\$_([^\s]+)/u.exec(
entry.name,
);
if (classMatch?.[1] !== undefined) classNames.add(classMatch[1]);
const methodMatch = /^([+-])\[([^\s\]]+)\s+([^\]]+)\]$/u.exec(entry.name);
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),
});
classNames.add(methodMatch[2]);
}
if (isSwiftMangledSymbolName(entry.name) || entry.name.startsWith("_Tt"))
swiftSymbols.push({
mangled_name: entry.name,
address,
decode: { status: "partial", reason: "mangled_symbol_not_demangled" },
evidence: symbolEvidence(entry, address),
});
}
const methodsByClass = new Map<string, ObjcMethod[]>();
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 classes: ObjcClass[] = [...classNames].sort().map((name) => ({
name,
super_class: null,
is_meta_class: null,
is_root_class: null,
methods: methodsByClass.get(name) ?? [],
properties: [],
protocols: [],
ivar_count: null,
instance_size: null,
}));
const decodedImplementations = [...implementations.values()].sort(
(left, right) =>
compareUnicodeCodePoints(
compositeKey([left.class_name, left.selector]),
compositeKey([right.class_name, right.selector]),
),
);
const swift = swiftSymbols.length > 0;
return objcSwiftMetadataSchema.parse({
objc_classes: classes,
objc_dispatch_implementations: decodedImplementations,
swift_symbols: swiftSymbols,
coverage: [
{
facet: "objc_class_symbols",
status: "partial",
reason:
names.length > examinedNames.length
? "record_limit_reached_and_symbol_names_are_not_runtime_class_metadata"
: "symbol_names_do_not_recover_all_runtime_class_metadata",
examined: examinedNames.length,
decoded: classes.length,
},
{
facet: "objc_dispatch_implementations",
status: decodedImplementations.length > 0 ? "partial" : "unsupported",
reason:
names.length > examinedNames.length
? "record_limit_reached_and_only_symbolized_methods_are_resolved"
: decodedImplementations.length > 0
? "only_symbolized_methods_are_resolved"
: "provider_name_inventory_has_no_objc_method_symbols",
examined: examinedNames.length,
decoded: decodedImplementations.length,
},
{
facet: "objc_ivar_layouts",
status: "unsupported",
reason: "symbol_inventory_does_not_expose_runtime_ivar_layouts",
examined: 0,
decoded: 0,
},
{
facet: "swift_declarations",
status: swift ? "partial" : "unsupported",
reason: swift
? "symbols_are_not_demangled_declarations"
: "no_swift_symbols_observed",
examined: swiftSymbols.length,
decoded: 0,
},
{
facet: "swift_dispatch_tables",
status: "unsupported",
reason:
"provider_name_inventory_does_not_decode_witness_or_class_vtables",
examined: 0,
decoded: 0,
},
{
facet: "relative_pointers",
status: "unsupported",
reason:
"provider_name_inventory_does_not_decode_binary_relative_pointers",
examined: 0,
decoded: 0,
},
],
db_save_result: null,
});
};
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;
};