mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
02987d8e8c
Remove OS-based assumptions about long and non-ASCII string recognition. Require the dedicated fragmented fixture, independently verify full source bytes, and reject gaps, overlaps, incomplete reads and corrupted decoding. Report unrecognized native strings explicitly and retain the actual procedure count. Verified through 14 verifier regressions and the complete macOS arm64 Hopper 6.6.0-demo CLI/MCP lane with cleanup.
195 lines
5.2 KiB
TypeScript
195 lines
5.2 KiB
TypeScript
import { expect, it } from "vitest";
|
|
|
|
import {
|
|
verifyHopperStringFragments,
|
|
verifyHopperStringObjects,
|
|
} from "../../../../scripts/lib/real-hopper-search.mjs";
|
|
|
|
const value = "REA_UTF8_é_😀";
|
|
const address = "0x402020";
|
|
const bytes = Buffer.from(`${value}\0`);
|
|
const item = {
|
|
address,
|
|
value,
|
|
string: {
|
|
encoding: "utf-8",
|
|
encoding_status: "inferred" as const,
|
|
termination: "present_or_not_required" as const,
|
|
byte_length: bytes.length,
|
|
},
|
|
};
|
|
const expectation = {
|
|
value,
|
|
encoding: "utf-8",
|
|
symbol: "rea_utf8_literal",
|
|
recognitionOptional: true,
|
|
};
|
|
|
|
const fragments = [
|
|
{
|
|
...item,
|
|
value: "REA_",
|
|
string: { ...item.string, termination: "missing" as const, byte_length: 4 },
|
|
},
|
|
{
|
|
...item,
|
|
address: "0x402024",
|
|
value: value.slice(4),
|
|
string: { ...item.string, byte_length: bytes.length - 4 },
|
|
},
|
|
] as const;
|
|
|
|
const fragmentBytes = async (
|
|
name: string,
|
|
args: Record<string, unknown> = {},
|
|
) => {
|
|
if (name !== "read_bytes") throw new Error(`Unmodeled call: ${name}`);
|
|
const offset =
|
|
args.address === expectation.symbol
|
|
? 0
|
|
: Number(BigInt(String(args.address)) - BigInt(address));
|
|
return {
|
|
address: `0x${(BigInt(address) + BigInt(offset)).toString(16)}`,
|
|
complete: true,
|
|
bytes_hex: bytes
|
|
.subarray(offset, offset + Number(args.length))
|
|
.toString("hex"),
|
|
};
|
|
};
|
|
|
|
it("verifies contiguous native fragments against the complete source bytes", async () => {
|
|
const result = await verifyHopperStringFragments(
|
|
fragmentBytes,
|
|
fragments,
|
|
expectation,
|
|
);
|
|
expect(result.recognition).toEqual({
|
|
address,
|
|
state: "observed",
|
|
decodedObject: "verified",
|
|
});
|
|
expect(result.sourceBytes).toBe(bytes.length);
|
|
});
|
|
|
|
it("reports unrecognized source bytes without accepting identical text at another address", async () => {
|
|
const elsewhere = [{ ...item, address: "0x403000" }];
|
|
const result = await verifyHopperStringFragments(
|
|
fragmentBytes,
|
|
elsewhere,
|
|
expectation,
|
|
);
|
|
expect(result.recognition).toEqual({
|
|
address,
|
|
state: "not_observed",
|
|
decodedObject: "unverified",
|
|
});
|
|
await expect(
|
|
verifyHopperStringFragments(fragmentBytes, elsewhere, {
|
|
...expectation,
|
|
recognitionOptional: false,
|
|
}),
|
|
).rejects.toThrow(/typed string fragments/u);
|
|
});
|
|
|
|
it.each([
|
|
["gap", [fragments[0], { ...fragments[1], address: "0x402025" }]],
|
|
["overlap", [fragments[0], { ...fragments[1], address: "0x402023" }]],
|
|
["missing prefix", [fragments[1]]],
|
|
["wrong decoded value", [{ ...item, value: "X" + value.slice(1) }]],
|
|
["missing tail", [fragments[0]]],
|
|
])(
|
|
"rejects %s even when fragment recognition is optional",
|
|
async (_label, inventory) => {
|
|
await expect(
|
|
verifyHopperStringFragments(fragmentBytes, inventory, expectation),
|
|
).rejects.toThrow();
|
|
},
|
|
);
|
|
|
|
it("rejects incomplete source bytes even with no native string objects", async () => {
|
|
await expect(
|
|
verifyHopperStringFragments(
|
|
async (name: string, args: Record<string, unknown> = {}) => ({
|
|
...(await fragmentBytes(name, args)),
|
|
complete: false,
|
|
}),
|
|
[],
|
|
expectation,
|
|
),
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it("keeps source identity and unknown decoding when the same text exists elsewhere", async () => {
|
|
const report = await verifyHopperStringObjects(
|
|
fixtureCall(),
|
|
[expectation],
|
|
[{ ...item, address: "0x403000" }],
|
|
);
|
|
expect(report.recognition).toEqual([
|
|
{ value, address, state: "not_observed", decodedObject: "unverified" },
|
|
]);
|
|
});
|
|
|
|
it("reuses source bytes and inventory while checking real address filters and searches", async () => {
|
|
const calls: string[] = [];
|
|
const report = await verifyHopperStringObjects(
|
|
fixtureCall(calls),
|
|
[expectation],
|
|
[item],
|
|
);
|
|
expect(report.recognition).toEqual([
|
|
{ value, address, state: "observed", decodedObject: "verified" },
|
|
]);
|
|
expect(calls).toEqual([
|
|
"read_bytes:rea_utf8_literal",
|
|
`list_strings:${address}`,
|
|
`search_strings:${value}`,
|
|
"search_strings:\\xC3\\xA9",
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
["changed source bytes", { bytes_hex: "00" }, item],
|
|
["incomplete source read", { complete: false }, item],
|
|
["wrong decoded value", {}, { ...item, value: "wrong" }],
|
|
[
|
|
"wrong decoded extent",
|
|
{},
|
|
{ ...item, string: { ...item.string, byte_length: 1 } },
|
|
],
|
|
])(
|
|
"rejects %s even when native recognition is optional",
|
|
async (_label, override, decoded) => {
|
|
await expect(
|
|
verifyHopperStringObjects(
|
|
fixtureCall([], override),
|
|
[expectation],
|
|
[decoded],
|
|
),
|
|
).rejects.toThrow();
|
|
},
|
|
);
|
|
|
|
function fixtureCall(
|
|
calls: string[] = [],
|
|
override: Record<string, unknown> = {},
|
|
) {
|
|
return async (name: string, arguments_: Record<string, unknown> = {}) => {
|
|
calls.push(
|
|
`${name}:${String(arguments_.address ?? arguments_.pattern ?? "")}`,
|
|
);
|
|
if (name === "read_bytes")
|
|
return {
|
|
address,
|
|
bytes_hex: bytes.toString("hex"),
|
|
complete: true,
|
|
...override,
|
|
};
|
|
if (name === "list_strings" && arguments_.address === address)
|
|
return [item];
|
|
if (name === "search_strings")
|
|
return arguments_.pattern === value ? [item] : [];
|
|
throw new Error(`Unmodeled verifier call: ${name}`);
|
|
};
|
|
}
|