mirror of
https://github.com/morluto/rea
synced 2026-10-28 15:07:26 -04:00
c6b62754b0
Reuse the validated string inventory's numeric address order for O(log n + matches) exact reads, preserving every encoding at the selected address without a second lookup index. Keep streaming validation, session cache lifetime, full listings, provenance and caller choices. Validation: complete latest-head CI passed. Original real Kuna 1.833 Linux correctness and warm-lookup measurements remain pinned to their recorded feature revisions in the PR. The latest local 300 ms export-deadline observation and unchanged 31-test provider recheck are explicitly documented there; no assertion or timeout was weakened. Refs #4695.
327 lines
10 KiB
TypeScript
327 lines
10 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { Readable } from "node:stream";
|
|
import { pipeline } from "node:stream/promises";
|
|
import * as v8 from "node:v8";
|
|
|
|
import { expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
KunaStringInventoryReader,
|
|
kunaStringsAtAddress,
|
|
} from "../../../../src/kuna/KunaStringInventory.js";
|
|
|
|
vi.mock("node:v8", { spy: true });
|
|
|
|
const input = "/private/program";
|
|
const row = {
|
|
address: "@9007199254740993",
|
|
address_hex: "0x20000000000001",
|
|
text: "a\nΔelta",
|
|
length: 7,
|
|
byte_length: 9,
|
|
nul_terminated: true,
|
|
encoding: "utf8",
|
|
section: ".rodata",
|
|
xrefs_count: 0,
|
|
functions: [],
|
|
};
|
|
const document = () => ({
|
|
binary: input,
|
|
encoding: "all",
|
|
min_length: 5,
|
|
filter: null,
|
|
section: null,
|
|
termination: "any",
|
|
scanned: "sections",
|
|
xrefs: false,
|
|
count: 1,
|
|
strings: [row],
|
|
});
|
|
const wire = (value: unknown) =>
|
|
JSON.stringify(value).replace(/"@(\d+)"/gu, "$1");
|
|
|
|
/** Stream `text` through a reader in `chunkBytes` pieces, as Kuna's stdout arrives. */
|
|
const read = async (text: string, chunkBytes = 64 * 1024) => {
|
|
const bytes = Buffer.from(text);
|
|
const chunks: Buffer[] = [];
|
|
for (let offset = 0; offset < bytes.byteLength; offset += chunkBytes)
|
|
chunks.push(bytes.subarray(offset, offset + chunkBytes));
|
|
const reader = new KunaStringInventoryReader();
|
|
await pipeline(Readable.from(chunks), reader);
|
|
return { reader, result: reader.finish(input) };
|
|
};
|
|
|
|
it("preserves exact u64 string identity, multiline text and typed source facts", async () => {
|
|
const text = wire(document());
|
|
const { result } = await read(text, 1);
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
sha256: createHash("sha256").update(text).digest("hex"),
|
|
bytes: Buffer.byteLength(text),
|
|
scanned: "sections",
|
|
items: [
|
|
{
|
|
address: "0x20000000000001",
|
|
value: "a\nΔelta",
|
|
string: {
|
|
encoding: "utf8",
|
|
encoding_status: "observed",
|
|
termination: "present_or_not_required",
|
|
byte_length: 9,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("skips members it does not interpret without retaining them", async () => {
|
|
const { result } = await read(
|
|
wire({
|
|
...document(),
|
|
padding: "x".repeat(1024),
|
|
notes: [{ a: [1] }],
|
|
strings: [{ ...row, score: 1, origin: { tool: [1] } }],
|
|
}).replace('"functions":[]', '"functions":[],"__proto__":{"text":1}'),
|
|
);
|
|
expect(result).toMatchObject({ ok: true, value: { items: [{}] } });
|
|
});
|
|
|
|
it.each([
|
|
["input", { binary: "/other/program" }],
|
|
["count", { count: 2 }],
|
|
["filter", { filter: "narrowed" }],
|
|
["xrefs", { xrefs: true }],
|
|
["encoding", { encoding: "ascii" }],
|
|
["scan scope", { scanned: "unknown" }],
|
|
["header shape", { section: [] }],
|
|
["missing rows", { strings: undefined }],
|
|
[
|
|
"address identity",
|
|
{ strings: [{ ...row, address_hex: "0x20000000000000" }] },
|
|
],
|
|
["u64 overflow", { strings: [{ ...row, address: "@18446744073709551616" }] }],
|
|
["textual address", { strings: [{ ...row, address: "0x20000000000001" }] }],
|
|
["partial row", { strings: [{ ...row, byte_length: null }] }],
|
|
["unexpected references", { strings: [{ ...row, xrefs_count: 1 }] }],
|
|
[
|
|
"referencing functions",
|
|
{ strings: [{ ...row, functions: [{ name: "main" }] }] },
|
|
],
|
|
["duplicate identity", { count: 2, strings: [row, row] }],
|
|
[
|
|
"nonadjacent duplicate identity",
|
|
{
|
|
count: 3,
|
|
strings: [row, { ...row, address: "@16", address_hex: "0x10" }, row],
|
|
},
|
|
],
|
|
])(
|
|
"rejects inconsistent %s before publishing any strings",
|
|
async (_label, changes) => {
|
|
const { result } = await read(wire({ ...document(), ...changes }));
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisOutputError", operation: "kuna:strings" },
|
|
});
|
|
},
|
|
);
|
|
|
|
it("indexes canonical u64 addresses and keeps each encoding at the same address", async () => {
|
|
const entry = (
|
|
address: bigint,
|
|
encoding: "ascii" | "utf8" | "utf16" = "ascii",
|
|
) => ({
|
|
...row,
|
|
address: `@${address}`,
|
|
address_hex: `0x${address.toString(16)}`,
|
|
text: "hello",
|
|
length: 5,
|
|
byte_length: encoding === "utf16" ? 12 : 6,
|
|
encoding,
|
|
});
|
|
const rows = [
|
|
entry(0xffffffffffffffffn),
|
|
entry(16n, "utf16"),
|
|
entry(15n),
|
|
entry(16n),
|
|
entry(0x20000000000001n, "utf8"),
|
|
entry(0n),
|
|
entry(16n, "utf8"),
|
|
entry(17n),
|
|
];
|
|
const { result } = await read(
|
|
wire({ ...document(), count: rows.length, strings: rows }),
|
|
3,
|
|
);
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.items.map(({ address }) => address)).toEqual([
|
|
"0x0",
|
|
"0xf",
|
|
"0x10",
|
|
"0x10",
|
|
"0x10",
|
|
"0x11",
|
|
"0x20000000000001",
|
|
"0xffffffffffffffff",
|
|
]);
|
|
for (const address of [
|
|
"0x0",
|
|
"0x1",
|
|
"0xe",
|
|
"0xf",
|
|
"0x10",
|
|
"0x11",
|
|
"0x12",
|
|
"0x20000000000000",
|
|
"0x20000000000001",
|
|
"0x20000000000002",
|
|
"0xffffffffffffffff",
|
|
])
|
|
expect(kunaStringsAtAddress(result.value, address)).toEqual(
|
|
result.value.items.filter((item) => item.address === address),
|
|
);
|
|
expect(
|
|
kunaStringsAtAddress(result.value, "0x10").map(
|
|
(item) => item.string?.encoding,
|
|
),
|
|
).toEqual(["ascii", "utf16", "utf8"]);
|
|
});
|
|
|
|
it("looks up an empty validated inventory", async () => {
|
|
const { result } = await read(wire({ ...document(), count: 0, strings: [] }));
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) throw result.error;
|
|
expect(kunaStringsAtAddress(result.value, "0x0")).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
["truncated", wire(document()).slice(0, -2)],
|
|
["trailing", `${wire(document())} {}`],
|
|
["array root", "[]"],
|
|
["empty", ""],
|
|
])("rejects a %s document", async (_label, text) => {
|
|
expect((await read(text)).result.ok).toBe(false);
|
|
});
|
|
|
|
it("rejects invalid UTF-8 instead of substituting characters", async () => {
|
|
const reader = new KunaStringInventoryReader();
|
|
const text = Buffer.from(wire(document()));
|
|
const at = text.indexOf("Δ");
|
|
text[at] = 0xff;
|
|
await pipeline(Readable.from([text]), reader);
|
|
expect(reader.finish(input).ok).toBe(false);
|
|
});
|
|
|
|
it("signals the first contradiction while Kuna is still writing", async () => {
|
|
const reader = new KunaStringInventoryReader();
|
|
const rows = [{ ...row, byte_length: 1 }, row];
|
|
const text = wire({ ...document(), count: 2, strings: rows });
|
|
reader.write(Buffer.from(text.slice(0, text.lastIndexOf("{"))));
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
expect(reader.rejected.aborted).toBe(true);
|
|
expect(reader.rejection).toMatchObject({ _tag: "AnalysisOutputError" });
|
|
reader.end(Buffer.from(text.slice(text.lastIndexOf("{"))));
|
|
await new Promise((resolve) => reader.once("finish", resolve));
|
|
expect(reader.finish(input)).toMatchObject({
|
|
ok: false,
|
|
error: reader.rejection,
|
|
});
|
|
});
|
|
|
|
it("refuses retention beyond live heap headroom and stops the scan", async () => {
|
|
const heap = v8.getHeapStatistics();
|
|
// The first retained row samples headroom.
|
|
vi.mocked(v8.getHeapStatistics).mockReturnValueOnce({
|
|
...heap,
|
|
total_available_size: 1024,
|
|
});
|
|
const { reader, result } = await read(wire(document()));
|
|
expect(reader.rejected.aborted).toBe(true);
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "AnalysisResourceConstraintError",
|
|
operation: "kuna:strings",
|
|
resource: "memory",
|
|
reportedLimits: { retained_rows: 0, available_heap_bytes: 1024 },
|
|
},
|
|
});
|
|
});
|
|
|
|
it("refuses a kept string while it is assembled, before Kuna finishes it", async () => {
|
|
const heap = v8.getHeapStatistics();
|
|
const text = wire(document());
|
|
const opening = text.slice(0, text.indexOf("a\\nΔelta"));
|
|
const reader = new KunaStringInventoryReader();
|
|
reader.write(Buffer.from(opening));
|
|
vi.mocked(v8.getHeapStatistics).mockReturnValueOnce({
|
|
...heap,
|
|
total_available_size: 1024,
|
|
});
|
|
// One sampling interval of text, still inside one unterminated string.
|
|
reader.write(Buffer.from("x".repeat(64 * 1024)));
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
expect(reader.rejection).toMatchObject({
|
|
_tag: "AnalysisResourceConstraintError",
|
|
resource: "memory",
|
|
});
|
|
reader.destroy();
|
|
});
|
|
|
|
it("bounds keys and numbers: overlong keys match no field, overlong kept numbers reject", async () => {
|
|
const longKey = `count${"x".repeat(1024 * 1024)}`;
|
|
const ignored = wire({ ...document(), [longKey]: "@1" + "0".repeat(4096) });
|
|
expect((await read(ignored, 4096)).result).toMatchObject({
|
|
ok: true,
|
|
value: { items: [{}] },
|
|
});
|
|
const kept = wire({ ...document(), count: "@1" + "0".repeat(64) });
|
|
expect((await read(kept)).result).toMatchObject({
|
|
ok: false,
|
|
error: { reason: "count is not a number Kuna writes" },
|
|
});
|
|
});
|
|
|
|
it("drops ignored strings unassembled, without sampling headroom", async () => {
|
|
const sampled = vi.mocked(v8.getHeapStatistics).mock.calls.length;
|
|
const { result } = await read(
|
|
wire({ ...document(), padding: "x".repeat(4 * 1024 * 1024) }),
|
|
);
|
|
expect(result.ok).toBe(true);
|
|
// Only the one retained row's admission is sampled.
|
|
expect(vi.mocked(v8.getHeapStatistics).mock.calls.length - sampled).toBe(1);
|
|
});
|
|
|
|
it.each([
|
|
{ encoding: "utf8", text: "😀😀😀😀😀", length: 5, byte_length: 21 },
|
|
{ encoding: "utf16", text: "café!", length: 5, byte_length: 12 },
|
|
])("counts $encoding source units without losing text", async (facts) => {
|
|
const value = { ...document(), strings: [{ ...row, ...facts }] };
|
|
expect((await read(wire(value), 3)).result).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
items: [
|
|
{ value: facts.text, string: { byte_length: facts.byte_length } },
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{ byte_length: 0 },
|
|
{ length: 6 },
|
|
{ nul_terminated: false },
|
|
{ encoding: "ascii" },
|
|
{ encoding: "utf16", text: "hello", length: 5, byte_length: 11 },
|
|
{ encoding: "utf8", text: "hi", length: 2, byte_length: 3 },
|
|
{ encoding: "utf8", text: "hello\u0000", length: 6, byte_length: 7 },
|
|
{ encoding: "utf8", text: "hello\ud800", length: 6, byte_length: 9 },
|
|
{ encoding: "utf16", text: "Δelta", length: 5, byte_length: 12 },
|
|
])("rejects conflicting encoded string facts: %j", async (facts) => {
|
|
const value = { ...document(), strings: [{ ...row, ...facts }] };
|
|
expect((await read(wire(value))).result.ok).toBe(false);
|
|
});
|