rea/tests/ghidraFunctionValues.test.ts

201 lines
5.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { JsonValue } from "../src/domain/jsonValue.js";
import {
GHIDRA_FUNCTION_OPERATIONS,
isGhidraFunctionOperation,
parseGhidraFunctionInput,
parseGhidraFunctionResult,
type GhidraFunctionOperation,
} from "../src/ghidra/GhidraFunctionValues.js";
import {
ghidraBounded,
ghidraFunctionClassification,
ghidraFunctionDossier,
ghidraFunctionIdentity,
ghidraReferenceEdge,
} from "./fixtures/ghidraFunction.js";
describe("Ghidra function-analysis boundary values", () => {
it("defaults bounded inputs and rejects undeclared or implicit addresses", () => {
expect(
parseGhidraFunctionInput("procedure_info", { procedure: "main" }),
).toEqual({
ok: true,
value: { document: null, procedure: "main" },
});
expect(
parseGhidraFunctionInput("procedure_references", { procedure: "main" }),
).toEqual({
ok: true,
value: {
document: null,
procedure: "main",
direction: "outgoing",
offset: 0,
limit: 100,
max_instructions: 500,
},
});
expect(
parseGhidraFunctionInput("analyze_function", { procedure: "main" }),
).toMatchObject({
ok: true,
value: {
procedure: "main",
include_assembly: false,
limit: 100,
max_pseudocode_chars: 20_000,
max_instructions: 500,
pseudocode_offset: 0,
assembly_offset: 0,
collection_offset: { basic_blocks: 0, outgoing_references: 0 },
},
});
expect(parseGhidraFunctionInput("xrefs", {})).toMatchObject({
ok: false,
error: { _tag: "AnalysisInputError" },
});
expect(
parseGhidraFunctionInput("procedure_info", {
procedure: "main",
extra: true,
}),
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
});
it("keeps the admitted operation set closed", () => {
expect(GHIDRA_FUNCTION_OPERATIONS).toHaveLength(8);
expect(isGhidraFunctionOperation("analyze_function")).toBe(true);
expect(isGhidraFunctionOperation("procedure_pseudo_code")).toBe(true);
expect(isGhidraFunctionOperation("list_procedures")).toBe(false);
expect(isGhidraFunctionOperation("set_comment")).toBe(false);
});
it("parses provider-classified function facts and reference kinds", () => {
expect(
parseGhidraFunctionResult("procedure_info", {
name: "fixture_main",
entrypoint: "0x401000",
basicblock_count: 1,
length: 6,
signature: "int fixture_main(void)",
locals: [
{
description: "int local @ Stack[-0x4]:4",
provenance: "ghidra-function-database",
},
],
classification: ghidraFunctionClassification(),
}),
).toMatchObject({ ok: true });
expect(
parseGhidraFunctionResult("procedure_references", {
procedure: ghidraFunctionIdentity(),
direction: "outgoing",
references: ghidraBounded([ghidraReferenceEdge()]),
instructions_scanned: 2,
instruction_scan_truncated: false,
}),
).toMatchObject({
ok: true,
value: {
references: {
items: [
{
kind: {
available: true,
provenance: "ghidra-reference-manager",
data: true,
},
},
],
},
},
});
expect(
parseGhidraFunctionResult("analyze_function", ghidraFunctionDossier()),
).toMatchObject({ ok: true });
});
it.each(malformedOutputs())(
"rejects malformed %s output",
(_name, operation, value) => {
expect(parseGhidraFunctionResult(operation, value)).toMatchObject({
ok: false,
error: { _tag: "AnalysisOutputError" },
});
},
);
});
const malformedOutputs = (): Array<
[string, GhidraFunctionOperation, JsonValue]
> => {
const dossier = ghidraFunctionDossier();
if (typeof dossier !== "object" || dossier === null || Array.isArray(dossier))
throw new TypeError("Ghidra dossier fixture is invalid");
const edge = ghidraReferenceEdge();
return [
["non-canonical address", "procedure_callers", ["401000"]],
[
"missing classification",
"procedure_info",
{
name: "main",
entrypoint: "0x401000",
basicblock_count: 1,
length: 1,
signature: null,
locals: [],
},
],
[
"unavailable Ghidra reference kind",
"procedure_references",
{
procedure: ghidraFunctionIdentity(),
direction: "outgoing",
references: ghidraBounded([
{ ...edge, kind: { available: false, reason: "unknown" } },
]),
instructions_scanned: 1,
instruction_scan_truncated: false,
},
],
[
"Hopper local provenance",
"analyze_function",
{
...dossier,
procedure: {
...ghidraFunctionIdentity(),
signature: null,
locals: [
{
description: "opaque",
provenance: "hopper-public-python-api",
},
],
},
},
],
[
"inconsistent dossier bound",
"analyze_function",
{
...dossier,
instruction_scan: { scanned: -1, truncated: false },
},
],
[
"missing uncertainty limitations",
"analyze_function",
{
...dossier,
limitations: [],
},
],
];
};