* feat(native): decode Swift nominal type declarations from __swift5_types Fill swift_decls from the nominal type context descriptors that survive stripping: kind (class, struct, enum, actor), own and qualified name, module, generic flag, anonymous-context flag, metadata accessor and descriptor location, with evidence citing the type record. Unresolved parent contexts keep qualified_name null and the entry partial. One shared reader walks each context once; class vtable recovery now consumes its descriptors instead of re-reading the records, so a type record that cannot be read keeps both facets partial. Remove the never-populated swift_decls fields that descriptors do not carry. Fixes #5341 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(native): bound Swift context walks and decode vtables in record order Address review of the nominal-type reader: - Cache intrinsic context depth (distance to the module) so a cached parent cannot bypass the 32-context bound, and never cache a stop that depends on where the walk began. Bound qualified names to 4096 characters so nesting cannot multiply name storage. - Visit each descriptor as soon as its kind and name are read, so a class vtable spends the record budget in record order instead of after every later declaration. - Report an accessor outside executable mapped code as null and keep the declaration partial. - Forward a name failure to vtable coverage only when the record is a class or its kind is unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
343 lines
11 KiB
TypeScript
343 lines
11 KiB
TypeScript
import type {
|
|
NativeMetadataEvidence,
|
|
NativeMetadataLocation,
|
|
ObjcSwiftMetadata,
|
|
} from "../domain/native/objcSwiftMetadata.js";
|
|
import { issue, type FacetDecodeFacts } from "./AppleDispatchDecodeFacts.js";
|
|
import {
|
|
unresolvedSwiftRelativeReason,
|
|
type SwiftRelativeField,
|
|
type SwiftRelativeValue,
|
|
} from "./AppleSwiftDispatchFacets.js";
|
|
import { expectedSwiftMetadataReadFailure } from "./AppleSwiftMetadataReadFailure.js";
|
|
|
|
/** Byte readers shared with the validated Mach-O metadata boundary. */
|
|
export interface SwiftNominalReaders {
|
|
u32(address: bigint): number;
|
|
relative(address: bigint, kind: SwiftRelativeField): SwiftRelativeValue;
|
|
string(address: bigint): string;
|
|
location(address: bigint): NativeMetadataLocation;
|
|
evidence(address: bigint, description: string): NativeMetadataEvidence[];
|
|
executable(address: bigint): boolean;
|
|
admit(): boolean;
|
|
}
|
|
|
|
/** A `__swift5_types` descriptor whose kind and own name were read. */
|
|
export interface SwiftNominalDescriptor {
|
|
readonly record: bigint;
|
|
readonly descriptor: bigint;
|
|
readonly flags: number;
|
|
readonly name: string;
|
|
}
|
|
|
|
/** A `__swift5_types` record that may be a class whose name is unknown. */
|
|
export interface SwiftNominalRecordFailure {
|
|
readonly record: bigint;
|
|
readonly message: string;
|
|
}
|
|
|
|
/**
|
|
* Receives each descriptor as soon as its kind and name are read, so a
|
|
* consumer spends the record budget in record order rather than after every
|
|
* declaration.
|
|
*/
|
|
export interface SwiftNominalVisitor {
|
|
descriptor(descriptor: SwiftNominalDescriptor): void;
|
|
failure(failure: SwiftNominalRecordFailure): void;
|
|
}
|
|
|
|
const FACET = "swift_declarations";
|
|
const CLASS = 16;
|
|
const STRUCT = 17;
|
|
const ENUM = 18;
|
|
const MODULE = 0;
|
|
const EXTENSION = 1;
|
|
const ANONYMOUS = 2;
|
|
const GENERIC = 0x80;
|
|
const CLASS_IS_ACTOR = 0x80;
|
|
/** Swift nesting is shallow; a longer chain is malformed or cyclic. */
|
|
const MAXIMUM_CONTEXT_DEPTH = 32;
|
|
/** Same bound as one metadata name, so nesting cannot multiply name storage. */
|
|
const MAXIMUM_QUALIFIED_NAME_LENGTH = 4096;
|
|
|
|
const hex = (value: bigint) => `0x${value.toString(16)}`;
|
|
|
|
/**
|
|
* Facts of one context. `depth` counts contexts up to and including its
|
|
* module, so a cached result is valid wherever a walk reaches it.
|
|
*/
|
|
interface ContextFacts {
|
|
readonly module: string | null;
|
|
/** Dotted named path below the module; null when unresolved. */
|
|
readonly path: string | null;
|
|
readonly depth: number;
|
|
readonly anonymous: boolean;
|
|
readonly reason: string | null;
|
|
/** The walk stopped at its own bound; the result depends on its start. */
|
|
readonly walkLimited: boolean;
|
|
}
|
|
|
|
const unresolved = (reason: string, walkLimited = false): ContextFacts => ({
|
|
module: null,
|
|
path: null,
|
|
depth: 0,
|
|
anonymous: false,
|
|
reason,
|
|
walkLimited,
|
|
});
|
|
|
|
const tooDeep = (): ContextFacts =>
|
|
unresolved(
|
|
`Swift context parent chain exceeds ${MAXIMUM_CONTEXT_DEPTH} contexts or is cyclic`,
|
|
);
|
|
|
|
/**
|
|
* Decode `__swift5_types` nominal type descriptors into `swift_decls`.
|
|
* Each context is read once, so shared parents add one relative-pointer
|
|
* record regardless of how many types they contain.
|
|
*/
|
|
export const decodeSwiftNominalTypes = (input: {
|
|
readers: SwiftNominalReaders;
|
|
entries: readonly bigint[];
|
|
result: ObjcSwiftMetadata;
|
|
visitor: SwiftNominalVisitor;
|
|
}): FacetDecodeFacts => {
|
|
const { readers: read, result, visitor } = input;
|
|
const issues = [] as ReturnType<typeof issue>[];
|
|
const contexts = new Map<bigint, ContextFacts>();
|
|
const names = new Map<bigint, string>();
|
|
let examined = 0;
|
|
let decoded = 0;
|
|
|
|
const typeName = (descriptor: bigint): string => {
|
|
const known = names.get(descriptor);
|
|
if (known !== undefined) return known;
|
|
const value = read.relative(descriptor + 8n, "typeName");
|
|
if (value.kind !== "target")
|
|
throw new RangeError(unresolvedSwiftRelativeReason(value, "typeName"));
|
|
const name = read.string(value.target);
|
|
if (name.length === 0)
|
|
throw new RangeError("Swift type descriptor name is empty");
|
|
names.set(descriptor, name);
|
|
return name;
|
|
};
|
|
|
|
const parentOf = (context: bigint, walk: number): ContextFacts => {
|
|
const value = read.relative(context + 4n, "contextParent");
|
|
if (value.kind === "target") return contextOf(value.target, walk + 1);
|
|
return unresolved(
|
|
value.kind === "null"
|
|
? "Swift context descriptor has no parent module"
|
|
: unresolvedSwiftRelativeReason(value, "contextParent"),
|
|
);
|
|
};
|
|
|
|
/** One level below `parent`, bounded in depth and qualified-name length. */
|
|
const child = (
|
|
parent: ContextFacts,
|
|
facts: Partial<ContextFacts>,
|
|
): ContextFacts => {
|
|
if (parent.depth + 1 > MAXIMUM_CONTEXT_DEPTH)
|
|
return { ...tooDeep(), walkLimited: parent.walkLimited };
|
|
const next = {
|
|
...parent,
|
|
depth: parent.reason === null ? parent.depth + 1 : 0,
|
|
...facts,
|
|
};
|
|
return next.path !== null &&
|
|
next.path.length > MAXIMUM_QUALIFIED_NAME_LENGTH
|
|
? {
|
|
...next,
|
|
path: null,
|
|
reason:
|
|
next.reason ??
|
|
`Swift qualified type name exceeds ${MAXIMUM_QUALIFIED_NAME_LENGTH} characters`,
|
|
}
|
|
: next;
|
|
};
|
|
|
|
const readContext = (context: bigint, walk: number): ContextFacts => {
|
|
const kind = read.u32(context) & 31;
|
|
switch (kind) {
|
|
case MODULE: {
|
|
const value = read.relative(context + 8n, "moduleName");
|
|
if (value.kind !== "target")
|
|
return unresolved(unresolvedSwiftRelativeReason(value, "moduleName"));
|
|
const module = read.string(value.target);
|
|
return module.length === 0
|
|
? unresolved("Swift module context name is empty")
|
|
: {
|
|
module,
|
|
path: "",
|
|
depth: 1,
|
|
anonymous: false,
|
|
reason: null,
|
|
walkLimited: false,
|
|
};
|
|
}
|
|
case CLASS:
|
|
case STRUCT:
|
|
case ENUM: {
|
|
const name = typeName(context);
|
|
const parent = parentOf(context, walk);
|
|
return child(parent, {
|
|
path:
|
|
parent.path === null
|
|
? null
|
|
: parent.path === ""
|
|
? name
|
|
: `${parent.path}.${name}`,
|
|
});
|
|
}
|
|
case ANONYMOUS:
|
|
return child(parentOf(context, walk), { anonymous: true });
|
|
case EXTENSION: {
|
|
// The extended type is a mangled name, often in another module, so
|
|
// the declaring module is known but the qualified path is not.
|
|
const parent = parentOf(context, walk);
|
|
return child(parent, {
|
|
path: null,
|
|
reason:
|
|
parent.reason ??
|
|
"Swift type is declared in an extension whose extended type is not demangled",
|
|
});
|
|
}
|
|
default:
|
|
return unresolved(`Unsupported Swift context descriptor kind ${kind}`);
|
|
}
|
|
};
|
|
|
|
const contextOf = (context: bigint, walk: number): ContextFacts => {
|
|
const known = contexts.get(context);
|
|
if (known !== undefined) return known;
|
|
// Bound recursion on cyclic or over-deep chains; this stop depends on
|
|
// where the walk began, so neither it nor its descendants are cached.
|
|
if (walk > MAXIMUM_CONTEXT_DEPTH)
|
|
return { ...tooDeep(), walkLimited: true };
|
|
let facts: ContextFacts;
|
|
try {
|
|
facts = readContext(context, walk);
|
|
} catch (cause) {
|
|
const reason = expectedSwiftMetadataReadFailure(cause);
|
|
if (reason === null) throw cause;
|
|
facts = unresolved(reason);
|
|
}
|
|
if (!facts.walkLimited) contexts.set(context, facts);
|
|
return facts;
|
|
};
|
|
|
|
const accessorOf = (
|
|
descriptor: bigint,
|
|
): { address: string | null; reason: string | null } => {
|
|
try {
|
|
const value = read.relative(descriptor + 12n, "typeAccessFunction");
|
|
if (value.kind === "null") return { address: null, reason: null };
|
|
if (value.kind !== "target")
|
|
return {
|
|
address: null,
|
|
reason: unresolvedSwiftRelativeReason(value, "typeAccessFunction"),
|
|
};
|
|
return read.executable(value.target)
|
|
? { address: hex(value.target), reason: null }
|
|
: {
|
|
address: null,
|
|
reason: `Swift metadata accessor ${hex(value.target)} is not in executable mapped code`,
|
|
};
|
|
} catch (cause) {
|
|
const reason = expectedSwiftMetadataReadFailure(cause);
|
|
if (reason === null) throw cause;
|
|
return { address: null, reason };
|
|
}
|
|
};
|
|
|
|
for (const record of input.entries) {
|
|
if (!read.admit()) {
|
|
issues.push(
|
|
issue(
|
|
FACET,
|
|
"record_budget_exhausted",
|
|
"max_records_reached",
|
|
hex(record),
|
|
),
|
|
);
|
|
break;
|
|
}
|
|
examined++;
|
|
let descriptor: bigint;
|
|
// Unknown until read; an unread kind may be a class.
|
|
let kind: number | null = null;
|
|
let flags: number;
|
|
let name: string;
|
|
try {
|
|
const value = read.relative(record, "typeMetadataRecord");
|
|
if (value.kind !== "target")
|
|
throw new RangeError(
|
|
unresolvedSwiftRelativeReason(value, "typeMetadataRecord"),
|
|
);
|
|
descriptor = value.target;
|
|
flags = read.u32(descriptor);
|
|
kind = flags & 31;
|
|
if (kind !== CLASS && kind !== STRUCT && kind !== ENUM) {
|
|
issues.push(
|
|
issue(
|
|
FACET,
|
|
"unsupported_type_descriptor_kind",
|
|
`Unsupported Swift type descriptor kind ${kind}`,
|
|
hex(record),
|
|
),
|
|
);
|
|
continue;
|
|
}
|
|
name = typeName(descriptor);
|
|
} catch (cause) {
|
|
const message = expectedSwiftMetadataReadFailure(cause);
|
|
if (message === null) throw cause;
|
|
if (kind === null || kind === CLASS) visitor.failure({ record, message });
|
|
issues.push(issue(FACET, "type_decode_failed", message, hex(record)));
|
|
continue;
|
|
}
|
|
visitor.descriptor({ record, descriptor, flags, name });
|
|
const context = contextOf(descriptor, 0);
|
|
const accessor = accessorOf(descriptor);
|
|
const reason = context.reason ?? accessor.reason;
|
|
result.swift_decls.push({
|
|
kind:
|
|
kind === STRUCT
|
|
? "struct"
|
|
: kind === ENUM
|
|
? "enum"
|
|
: ((flags >>> 16) & CLASS_IS_ACTOR) !== 0
|
|
? "actor"
|
|
: "class",
|
|
name,
|
|
qualified_name:
|
|
context.path === null || context.path === "" ? null : context.path,
|
|
module: context.module,
|
|
generic: (flags & GENERIC) !== 0,
|
|
anonymous_context: context.anonymous,
|
|
accessor_address: accessor.address,
|
|
location: read.location(descriptor),
|
|
decode:
|
|
reason === null
|
|
? { status: "decoded", reason: null }
|
|
: { status: "partial", reason },
|
|
evidence: read.evidence(
|
|
record,
|
|
"Swift __swift5_types record of the nominal type descriptor at location",
|
|
),
|
|
});
|
|
if (reason === null) decoded++;
|
|
else
|
|
issues.push(
|
|
issue(FACET, "type_context_unresolved", reason, hex(descriptor)),
|
|
);
|
|
}
|
|
return {
|
|
facet: FACET,
|
|
examined,
|
|
decoded,
|
|
exhaustive: issues.length === 0,
|
|
issues,
|
|
};
|
|
};
|