import canonicalize from "canonicalize"; import type { FunctionCollection, FunctionSnapshot, } from "./functionDossierEvidence.js"; import type { FunctionComparisonResult } from "./functionComparisonSchemas.js"; export const identityProjection = (snapshot: FunctionSnapshot) => ({ name: snapshot.procedure.name, signature: snapshot.procedure.signature, locals: snapshot.procedure.locals .map(({ description }) => description) .sort(), }); export const functionMatch = ( left: FunctionSnapshot, right: FunctionSnapshot, ): FunctionComparisonResult["function_match"] => { if ( !isAutoName(left.procedure.name) && left.procedure.name === right.procedure.name ) return { status: "matched", method: "symbol", left_name: left.procedure.name, right_name: right.procedure.name, }; return { status: left.procedure.name === right.procedure.name ? "ambiguous" : "mismatched", method: "explicit", left_name: left.procedure.name, right_name: right.procedure.name, }; }; export const normalizeCfg = ( blocks: FunctionSnapshot["collections"]["basic_blocks"]["items"], ): readonly unknown[] | null => { const parsed = blocks.map((block) => ({ block, start: parseAddress(block.start), end: parseAddress(block.end), })); if (parsed.some(({ start, end }) => start === null || end === null)) return null; const ordered = parsed.sort((left, right) => (left.start ?? 0n) < (right.start ?? 0n) ? -1 : 1, ); if (new Set(ordered.map(({ block }) => block.start)).size !== ordered.length) return null; if (ordered.some(({ start, end }) => (end ?? 0n) <= (start ?? 0n))) return null; const indices = new Map( ordered.map(({ block }, index) => [block.start, index]), ); const graph = []; for (const { block, start, end } of ordered) { const successors = block.successors.map((address) => indices.get(address)); if (successors.some((index) => index === undefined)) return null; graph.push({ size: String((end ?? 0n) - (start ?? 0n)), successors: successors.sort((left, right) => (left ?? 0) - (right ?? 0)), }); } return graph; }; export const referenceProjection = (snapshot: FunctionSnapshot) => sorted([ ...snapshot.collections.incoming_references.items.map((item) => ({ direction: "in", source: item.source_procedure?.name ?? null, target: item.target_procedure?.name ?? null, })), ...snapshot.collections.outgoing_references.items.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.collections.incoming_references.items.map((edge) => ({ direction: "in", edge, })), ...snapshot.collections.outgoing_references.items.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.collections.comments.items.map( ({ address, kind, text }) => ({ offset: relativeAddress(address, snapshot.procedure.address), kind, text, }), ); return values.some(({ offset }) => offset === null) ? null : sorted(values); }; export const stringAndNameProjection = ( snapshot: FunctionSnapshot, ): readonly unknown[] | null => { const values = [ ...snapshot.collections.referenced_strings.items.map( ({ source_address: sourceAddress, value }) => ({ kind: "string", source_offset: relativeAddress( sourceAddress, snapshot.procedure.address, ), value, }), ), ...snapshot.collections.referenced_names.items.map( ({ source_address: sourceAddress, value }) => ({ kind: "name", source_offset: relativeAddress( sourceAddress, snapshot.procedure.address, ), value, }), ), ]; return values.some(({ source_offset: offset }) => offset === null) ? null : sorted(values); }; export const sorted = (values: readonly unknown[]): readonly unknown[] => [...values].sort((left, right) => canonicalJson(left).localeCompare(canonicalJson(right)), ); export const combineCoverage = ( left: FunctionCollection, right: FunctionCollection, ): FunctionCollection => { const base = { items: [...left.items, ...right.items], total: left.total === null || right.total === null ? null : left.total + right.total, }; return left.complete && right.complete ? { ...base, complete: true, truncated: false } : { ...base, complete: false, truncated: left.truncated || right.truncated, }; }; export const project = ( collection: FunctionCollection, mapper: (item: Input) => Output, ): Output[] => collection.items.map(mapper); export const isAutoName = (name: string): boolean => /^(?:sub_[0-9a-f]+|fcn\.)/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); }; const canonicalJson = (value: unknown): string => { const encoded = canonicalize(value); if (encoded === undefined) throw new TypeError("Function normalization could not canonicalize data"); return encoded; };