Files
rea/scripts/lib/real-hopper-assertions.mjs
T

192 lines
6.2 KiB
JavaScript

import { z } from "zod";
const addressSchema = z.string().regex(/^0x[0-9a-f]+$/iu);
const identitySchema = z.object({ address: addressSchema, name: z.string() });
/** Extract one real procedure address from the complete inline inventory. */
export const firstProcedureAddress = (input) => {
const first = z
.array(z.object({ address: addressSchema }))
.min(1)
.parse(input)[0];
if (first === undefined) throw new TypeError("Procedure inventory was empty");
return first.address;
};
/** Reject empty and success-shaped embedded decompilation failures. */
export const requirePseudocode = (input, operation) => {
if (typeof input !== "string" || input.trim().length === 0)
throw new TypeError(`${operation} returned empty pseudocode`);
if (input === "No output" || input.startsWith("Error:"))
throw new TypeError(`${operation} returned an embedded failure: ${input}`);
return input;
};
/** Validate a complete Hopper function dossier semantically. */
export const requireFunctionDossier = (input, expectedAddress) => {
const value = objectValue(
input,
"analyze_function returned no function dossier",
);
const procedure = identitySchema.parse(value.procedure);
if (procedure.address !== expectedAddress)
throw new TypeError("analyze_function returned the wrong procedure");
if (procedure.name.trim().length === 0)
throw new TypeError("analyze_function omitted the procedure name");
requirePseudocode(value.pseudocode, "analyze_function");
for (const field of COLLECTION_FIELDS) list(value, field);
for (const field of ["callers", "callees"])
z.array(identitySchema).parse(list(value, field));
validateBlocks(list(value, "basic_blocks"));
return value;
};
/** Reject a structurally valid dossier that omits fixture-proven semantics. */
export const requireFunctionDossierOracle = (input, oracle) => {
const value = requireFunctionDossier(input, oracle.procedure_address);
if (oracle.callee_address !== undefined) {
const callees = z.array(identitySchema).parse(list(value, "callees"));
if (!callees.some(({ address }) => address === oracle.callee_address))
throw new TypeError(
"analyze_function omitted the expected fixture callee",
);
const references = z
.array(
z.object({
source_address: addressSchema,
target_address: addressSchema,
}),
)
.parse(list(value, "outgoing_references"));
if (
!references.some(
({ target_address }) => target_address === oracle.callee_address,
)
)
throw new TypeError(
"analyze_function omitted the expected fixture reference",
);
}
if (oracle.caller_address !== undefined) {
const callers = z.array(identitySchema).parse(list(value, "callers"));
if (!callers.some(({ address }) => address === oracle.caller_address))
throw new TypeError(
"analyze_function omitted the expected fixture caller",
);
}
if (oracle.referenced_string !== undefined)
requireReferencedValue(
value,
"referenced_strings",
oracle.referenced_string,
(candidate) => candidate,
);
if (oracle.referenced_name !== undefined)
requireReferencedValue(
value,
"referenced_names",
oracle.referenced_name,
(candidate) => candidate.replace(/^_+/u, ""),
);
if (oracle.comment !== undefined) {
const comments = z
.array(
z.object({
address: addressSchema,
kind: z.enum(["comment", "inline"]),
text: z.string(),
}),
)
.parse(list(value, "comments"));
if (
!comments.some(
({ address, kind, text }) =>
address === oracle.procedure_address &&
kind === "comment" &&
text === oracle.comment,
)
)
throw new TypeError("analyze_function omitted the verifier comment");
}
if (oracle.require_assembly === true && list(value, "assembly").length === 0)
throw new TypeError("analyze_function omitted fixture assembly");
if (oracle.require_cfg_successor) {
const blocks = z
.array(z.object({ successors: z.array(addressSchema) }))
.parse(list(value, "basic_blocks"));
if (!blocks.some(({ successors }) => successors.length > 0))
throw new TypeError("analyze_function omitted a real CFG successor");
}
return value;
};
const referencedValueSchema = z.object({
address: addressSchema,
value: z.string(),
source_address: addressSchema,
});
const referenceEdgeSchema = z.object({
source_address: addressSchema,
target_address: addressSchema,
});
const requireReferencedValue = (dossier, field, expected, normalize) => {
const values = z.array(referencedValueSchema).parse(list(dossier, field));
const references = z
.array(referenceEdgeSchema)
.parse(list(dossier, "outgoing_references"));
const found = values.some(
({ address, value, source_address: sourceAddress }) =>
normalize(value) === expected &&
references.some(
({ source_address, target_address }) =>
source_address === sourceAddress && target_address === address,
),
);
if (!found)
throw new TypeError(
`analyze_function omitted the expected fixture ${field === "referenced_strings" ? "string" : "name"}`,
);
};
/** Require every relationship to be an actual hexadecimal address. */
export const requireAddressArray = (input, operation) => {
const parsed = z.array(addressSchema).parse(input);
if (parsed.some((address) => address.length === 0))
throw new TypeError(`${operation} returned an invalid address list`);
return parsed;
};
const COLLECTION_FIELDS = [
"assembly",
"comments",
"callers",
"callees",
"incoming_references",
"outgoing_references",
"referenced_strings",
"referenced_names",
"basic_blocks",
];
const list = (value, field) => z.array(z.unknown()).parse(value[field]);
const validateBlocks = (items) => {
for (const block of items)
if (!Array.isArray(objectValue(block, "Invalid basic block").successors))
throw new TypeError("analyze_function omitted CFG successor evidence");
};
const objectValue = (input, message) => {
const parsed = z.record(z.string(), z.unknown()).safeParse(input);
if (!parsed.success) throw new TypeError(message);
return parsed.data;
};