Large literals now draw on one heap budget per dossier, measured once before decoding, instead of a separate check per literal. A literal that does not fit is listed in a new swift_string_literal_omissions field with its address, byte length and reason, so consumers read omissions from structured data rather than from limitation text. strings_names comparison checks referenced strings and names first and reports a change there even when Swift literal coverage differs; it returns unknown only when those facts match and literal coverage is absent on one side or partial. A computed jump counts as a tail call only when it has no resolved target inside the function, so jump tables no longer qualify, and a return requires a RETURN p-code operation, so traps no longer qualify. Decoded literals keep control characters, which Swift strings may contain and JSON escapes. The Swift verifier builds the literal fixture for arm64 on any Mac host and now also covers the empty string and a literal containing a tab.
220 lines
6.6 KiB
TypeScript
220 lines
6.6 KiB
TypeScript
import { canonicalJson } from "./comparisonSemantics.js";
|
|
import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js";
|
|
|
|
import type { FunctionSnapshot } from "./functionDossierEvidence.js";
|
|
import type { FunctionComparisonResult } from "./functionComparisonSchemas.js";
|
|
|
|
export const identityProjection = (snapshot: FunctionSnapshot) => ({
|
|
name: snapshot.dossier.procedure.name,
|
|
signature: snapshot.dossier.procedure.signature,
|
|
locals: snapshot.dossier.procedure.locals
|
|
.map(({ description }) => description)
|
|
.sort(),
|
|
});
|
|
|
|
export const functionMatch = (
|
|
left: FunctionSnapshot,
|
|
right: FunctionSnapshot,
|
|
): FunctionComparisonResult["function_match"] => {
|
|
if (
|
|
!isAutoName(left.dossier.procedure.name) &&
|
|
left.dossier.procedure.name === right.dossier.procedure.name
|
|
)
|
|
return {
|
|
status: "matched",
|
|
method: "symbol",
|
|
left_name: left.dossier.procedure.name,
|
|
right_name: right.dossier.procedure.name,
|
|
};
|
|
return {
|
|
status:
|
|
left.dossier.procedure.name === right.dossier.procedure.name
|
|
? "ambiguous"
|
|
: "mismatched",
|
|
method: "explicit",
|
|
left_name: left.dossier.procedure.name,
|
|
right_name: right.dossier.procedure.name,
|
|
};
|
|
};
|
|
|
|
export const normalizeCfg = (
|
|
blocks: FunctionSnapshot["dossier"]["basic_blocks"],
|
|
): readonly unknown[] | null => {
|
|
const parsed: {
|
|
block: (typeof blocks)[number];
|
|
start: bigint;
|
|
end: bigint;
|
|
}[] = [];
|
|
for (const block of blocks) {
|
|
const start = parseAddress(block.start);
|
|
const end = parseAddress(block.end);
|
|
if (start === null || end === null || end <= start) return null;
|
|
parsed.push({ block, start, end });
|
|
}
|
|
const ordered = parsed.sort((left, right) =>
|
|
left.start < right.start ? -1 : left.start > right.start ? 1 : 0,
|
|
);
|
|
if (new Set(ordered.map(({ start }) => start)).size !== ordered.length)
|
|
return null;
|
|
const indices = new Map(ordered.map(({ start }, index) => [start, index]));
|
|
const graph = [];
|
|
for (const { block, start, end } of ordered) {
|
|
const successors: number[] = [];
|
|
for (const address of block.successors) {
|
|
const parsedAddress = parseAddress(address);
|
|
if (parsedAddress === null) return null;
|
|
const index = indices.get(parsedAddress);
|
|
if (index === undefined) return null;
|
|
successors.push(index);
|
|
}
|
|
graph.push({
|
|
size: String(end - start),
|
|
successors: successors.sort((left, right) => left - right),
|
|
});
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
export const referenceProjection = (snapshot: FunctionSnapshot) =>
|
|
sorted([
|
|
...snapshot.dossier.incoming_references.map((item) => ({
|
|
direction: "in",
|
|
source: item.source_procedure?.name ?? null,
|
|
target: item.target_procedure?.name ?? null,
|
|
})),
|
|
...snapshot.dossier.outgoing_references.map((item) => ({
|
|
direction: "out",
|
|
source: item.source_procedure?.name ?? null,
|
|
target: item.target_procedure?.name ?? null,
|
|
})),
|
|
]);
|
|
|
|
export const referenceKindProjection = (
|
|
snapshot: FunctionSnapshot,
|
|
): readonly unknown[] | null => {
|
|
const edges = [
|
|
...snapshot.dossier.incoming_references.map((edge) => ({
|
|
direction: "in",
|
|
edge,
|
|
})),
|
|
...snapshot.dossier.outgoing_references.map((edge) => ({
|
|
direction: "out",
|
|
edge,
|
|
})),
|
|
];
|
|
if (edges.some(({ edge }) => !edge.kind.available)) return null;
|
|
return sorted(
|
|
edges.map(({ direction, edge }) => {
|
|
if (!edge.kind.available)
|
|
throw new TypeError(
|
|
"Reference-kind availability changed during projection",
|
|
);
|
|
return {
|
|
direction,
|
|
source: edge.source_procedure?.name ?? null,
|
|
target: edge.target_procedure?.name ?? null,
|
|
type: edge.kind.type,
|
|
flow: edge.kind.flow,
|
|
call: edge.kind.call,
|
|
jump: edge.kind.jump,
|
|
data: edge.kind.data,
|
|
read: edge.kind.read,
|
|
write: edge.kind.write,
|
|
indirect: edge.kind.indirect,
|
|
computed: edge.kind.computed,
|
|
conditional: edge.kind.conditional,
|
|
terminal: edge.kind.terminal,
|
|
primary: edge.kind.primary,
|
|
operand_index: edge.kind.operand_index,
|
|
external: edge.kind.external,
|
|
};
|
|
}),
|
|
);
|
|
};
|
|
|
|
export const commentProjection = (
|
|
snapshot: FunctionSnapshot,
|
|
): readonly unknown[] | null => {
|
|
const values = snapshot.dossier.comments.map(({ address, kind, text }) => ({
|
|
offset: relativeAddress(address, snapshot.dossier.procedure.address),
|
|
kind,
|
|
text,
|
|
}));
|
|
return values.some(({ offset }) => offset === null) ? null : sorted(values);
|
|
};
|
|
|
|
export const stringAndNameProjection = (
|
|
snapshot: FunctionSnapshot,
|
|
includeSwiftLiterals = true,
|
|
): readonly unknown[] | null => {
|
|
const values = [
|
|
...snapshot.dossier.referenced_strings.map(
|
|
({ source_address: sourceAddress, value }) => ({
|
|
kind: "string",
|
|
source_offset: relativeAddress(
|
|
sourceAddress,
|
|
snapshot.dossier.procedure.address,
|
|
),
|
|
value,
|
|
}),
|
|
),
|
|
...snapshot.dossier.referenced_names.map(
|
|
({ source_address: sourceAddress, value }) => ({
|
|
kind: "name",
|
|
source_offset: relativeAddress(
|
|
sourceAddress,
|
|
snapshot.dossier.procedure.address,
|
|
),
|
|
value,
|
|
}),
|
|
),
|
|
...(includeSwiftLiterals
|
|
? (snapshot.dossier.swift_string_literals ?? [])
|
|
: []
|
|
).map(
|
|
({
|
|
source_address: sourceAddress,
|
|
value,
|
|
representation,
|
|
registers,
|
|
}) => ({
|
|
kind: "swift_string",
|
|
source_offset: relativeAddress(
|
|
sourceAddress,
|
|
snapshot.dossier.procedure.address,
|
|
),
|
|
value,
|
|
representation,
|
|
registers,
|
|
}),
|
|
),
|
|
];
|
|
return values.some(({ source_offset: offset }) => offset === null)
|
|
? null
|
|
: sorted(values);
|
|
};
|
|
|
|
export const sorted = (values: readonly unknown[]): readonly unknown[] =>
|
|
values
|
|
.map((value) => ({
|
|
value,
|
|
json: canonicalJson(value, "Function normalization"),
|
|
}))
|
|
.sort((left, right) => compareUnicodeCodePoints(left.json, right.json))
|
|
.map(({ value }) => value);
|
|
|
|
/** Recognize address-derived procedure names without excluding named FUN_ symbols. */
|
|
export const isAutoName = (name: string): boolean =>
|
|
/^(?:sub_[0-9a-f]+|fcn\.[0-9a-f]+|FUN_[0-9a-f]+)$/iu.test(name);
|
|
|
|
const parseAddress = (value: string): bigint | null =>
|
|
/^0x[0-9a-f]+$/iu.test(value) ? BigInt(value) : null;
|
|
|
|
const relativeAddress = (value: string, base: string): string | null => {
|
|
const parsed = parseAddress(value);
|
|
const parsedBase = parseAddress(base);
|
|
return parsed === null || parsedBase === null || parsed < parsedBase
|
|
? null
|
|
: String(parsed - parsedBase);
|
|
};
|