mirror of
https://github.com/morluto/rea.git
synced 2026-10-28 02:15:44 +00:00
Reject omitted segment bounds, procedure classification and body facts, unresolved-call observations, dossier facets, and Ghidra parameter flow metadata. Providers emit explicit nulls for unsupported facets. Verify those unknowns in the real Hopper workflow. BREAKING CHANGE: Provider analysis outputs must explicitly supply metadata previously repaired with defaults; unsupported nullable facets must be reported as null.
232 lines
6.1 KiB
TypeScript
232 lines
6.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
functionBodySchema,
|
|
procedureIdentitySchema,
|
|
parseDocuments,
|
|
parseListCount,
|
|
parseNames,
|
|
parseProcedures,
|
|
parseRelatedAddresses,
|
|
parseSegments,
|
|
} from "./hopperValues.js";
|
|
|
|
describe("Hopper boundary values", () => {
|
|
it("parses Hopper's address-keyed name and string maps", () => {
|
|
expect(parseNames({ "0x1000": "_main" })).toEqual({
|
|
ok: true,
|
|
value: [{ address: "0x1000", name: "_main" }],
|
|
});
|
|
expect(
|
|
parseListCount({ "0x1000": "hello", "0x2000": "world" }, "strings"),
|
|
).toEqual({ ok: true, value: 2 });
|
|
});
|
|
|
|
it("parses direct and wrapped protocol shapes", () => {
|
|
expect(parseProcedures({ procedures: { "0x1": "main" } })).toEqual({
|
|
ok: true,
|
|
value: [{ address: "0x1", name: "main" }],
|
|
});
|
|
expect(parseNames({ names: [{ address: "0x2", name: "label" }] }).ok).toBe(
|
|
true,
|
|
);
|
|
expect(parseNames({ items: [{ address: "0x2", value: "label" }] }).ok).toBe(
|
|
false,
|
|
);
|
|
expect(parseRelatedAddresses({ callers: ["0x3"] }, "callers")).toEqual({
|
|
ok: true,
|
|
value: ["0x3"],
|
|
});
|
|
expect(
|
|
parseSegments([
|
|
{
|
|
name: "__TEXT",
|
|
start: "0x1",
|
|
end: "0x2",
|
|
readable: true,
|
|
writable: false,
|
|
executable: null,
|
|
},
|
|
]),
|
|
).toEqual({
|
|
ok: true,
|
|
value: [
|
|
{
|
|
name: "__TEXT",
|
|
start: "0x1",
|
|
end: "0x2",
|
|
readable: true,
|
|
writable: false,
|
|
executable: null,
|
|
},
|
|
],
|
|
});
|
|
expect(parseDocuments(["fixture"])).toEqual({
|
|
ok: true,
|
|
value: ["fixture"],
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
["procedures", () => parseProcedures(["not-a-map"])],
|
|
["names", () => parseNames([{ address: 1, name: "bad" }])],
|
|
["relations", () => parseRelatedAddresses([1], "callees")],
|
|
["segments", () => parseSegments([{ name: 1 }])],
|
|
["documents", () => parseDocuments([1])],
|
|
])(
|
|
"rejects malformed %s instead of manufacturing empty output",
|
|
(_label, parse) => {
|
|
const result = parse();
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) expect(result.error._tag).toBe("HopperProtocolError");
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("provider-neutral inclusive function body evidence", () => {
|
|
const observed = () => ({
|
|
available: true,
|
|
provenance: "test-address-set",
|
|
ranges: [
|
|
{ start: "0x1000", end: "0x1002" },
|
|
{ start: "0x1010", end: "0x1011" },
|
|
],
|
|
total_bytes: 5,
|
|
span_bytes: 18,
|
|
non_contiguous: true,
|
|
contains_entry: true,
|
|
});
|
|
it("rejects missing provider body metadata", () => {
|
|
expect(
|
|
procedureIdentitySchema.safeParse({ address: "0x1000", name: "main" })
|
|
.success,
|
|
).toBe(false);
|
|
});
|
|
it("rejects segments when required coordinates or unknown permissions are omitted", () => {
|
|
expect(
|
|
parseSegments([
|
|
{
|
|
name: "__TEXT",
|
|
start: "0x1",
|
|
end: "0x2",
|
|
readable: null,
|
|
writable: null,
|
|
},
|
|
]).ok,
|
|
).toBe(false);
|
|
expect(
|
|
parseSegments([{ name: "__TEXT", start: "0x1", end: "0x2" }]).ok,
|
|
).toBe(false);
|
|
});
|
|
it("counts inclusive one-byte ranges without assuming an enclosing continuous body", () => {
|
|
expect(
|
|
functionBodySchema.safeParse({
|
|
...observed(),
|
|
ranges: [{ start: "0x1000", end: "0x1000" }],
|
|
total_bytes: 1,
|
|
span_bytes: 1,
|
|
non_contiguous: false,
|
|
}).success,
|
|
).toBe(true);
|
|
expect(functionBodySchema.parse(observed())).toMatchObject({
|
|
total_bytes: 5,
|
|
span_bytes: 18,
|
|
});
|
|
});
|
|
it("preserves address-space identity and leaves a multi-space span unknown", () => {
|
|
expect(
|
|
functionBodySchema.safeParse({
|
|
...observed(),
|
|
ranges: [
|
|
{ start: "a:0x1000", end: "a:0x1000" },
|
|
{ start: "b:0x1000", end: "b:0x1001" },
|
|
],
|
|
total_bytes: 3,
|
|
span_bytes: null,
|
|
}).success,
|
|
).toBe(true);
|
|
expect(
|
|
functionBodySchema.safeParse({
|
|
...observed(),
|
|
ranges: [{ start: "a:0x1000", end: "b:0x1000" }],
|
|
}).success,
|
|
).toBe(false);
|
|
});
|
|
it("allows an observed empty set without manufacturing entry membership", () => {
|
|
expect(
|
|
procedureIdentitySchema.safeParse({
|
|
address: "0x1000",
|
|
name: "external",
|
|
classification: null,
|
|
body: {
|
|
...observed(),
|
|
ranges: [],
|
|
total_bytes: 0,
|
|
span_bytes: 0,
|
|
non_contiguous: false,
|
|
contains_entry: false,
|
|
},
|
|
}).success,
|
|
).toBe(true);
|
|
});
|
|
it("checks entry membership using address space and inclusive endpoints", () => {
|
|
expect(
|
|
procedureIdentitySchema.safeParse({
|
|
address: "0x1002",
|
|
name: "last",
|
|
classification: null,
|
|
body: observed(),
|
|
}).success,
|
|
).toBe(true);
|
|
expect(
|
|
procedureIdentitySchema.safeParse({
|
|
address: "0x1003",
|
|
name: "gap",
|
|
classification: null,
|
|
body: observed(),
|
|
}).success,
|
|
).toBe(false);
|
|
expect(
|
|
procedureIdentitySchema.safeParse({
|
|
address: "other:0x1000",
|
|
name: "other",
|
|
classification: null,
|
|
body: observed(),
|
|
}).success,
|
|
).toBe(false);
|
|
});
|
|
it.each([
|
|
{ total_bytes: 18 },
|
|
{ span_bytes: 5 },
|
|
{ non_contiguous: false },
|
|
{
|
|
ranges: [
|
|
{ start: "0x1000", end: "0x1002" },
|
|
{ start: "0x1003", end: "0x1004" },
|
|
],
|
|
},
|
|
{
|
|
ranges: [
|
|
{ start: "0x1000", end: "0x1002" },
|
|
{ start: "0x1002", end: "0x1004" },
|
|
],
|
|
},
|
|
{
|
|
ranges: [
|
|
{ start: "0x1010", end: "0x1011" },
|
|
{ start: "0x1000", end: "0x1002" },
|
|
],
|
|
},
|
|
{
|
|
ranges: [{ start: "0x0", end: "0x20000000000000" }],
|
|
total_bytes: Number.MAX_SAFE_INTEGER,
|
|
span_bytes: Number.MAX_SAFE_INTEGER,
|
|
non_contiguous: false,
|
|
},
|
|
])("rejects contradictory or unsafe range facts %j", (change) => {
|
|
expect(
|
|
functionBodySchema.safeParse({ ...observed(), ...change }).success,
|
|
).toBe(false);
|
|
});
|
|
});
|