mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
1297 lines
45 KiB
TypeScript
1297 lines
45 KiB
TypeScript
import {
|
|
mkdir,
|
|
writeFile,
|
|
symlink,
|
|
open,
|
|
rename,
|
|
realpath,
|
|
unlink,
|
|
} from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { execFileSync } from "node:child_process";
|
|
import { createServer as createNetServer } from "node:net";
|
|
import { expect, it, onTestFinished, vi } from "vitest";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { createDesktopApplicationService } from "../../../src/composition/desktop.js";
|
|
import { desktopApplicationSchema } from "../../../src/domain/desktopApplication.js";
|
|
import { createDesktopApplicationEvidence } from "../../../src/domain/desktopApplicationEvidence.js";
|
|
import { parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { inspectAnalysisView } from "../../../src/application/analysisView/AnalysisViewService.js";
|
|
import {
|
|
buildManagedPeFixture,
|
|
buildNativePeFixture,
|
|
} from "../../../src/dotnet/ManagedPe.fixture.js";
|
|
import { readManagedMetadataLayout } from "../../../src/dotnet/ManagedMetadataLayout.js";
|
|
import { readAssemblyReference } from "../../../src/dotnet/ManagedMetadataInventoryRows.js";
|
|
import {
|
|
CPU,
|
|
FILE_TYPE,
|
|
fatImage,
|
|
machoImage,
|
|
} from "../../../src/artifacts/apple/MachoImage.fixture.js";
|
|
import { DirectoryArtifactReader } from "../../../src/artifacts/DirectoryArtifactReader.js";
|
|
import { ArtifactReaderFailure } from "../../../src/artifacts/ArtifactReader.js";
|
|
import { DesktopApplicationReader } from "../../../src/artifacts/DesktopApplicationReader.js";
|
|
import { DesktopApplicationService } from "../../../src/application/artifacts/DesktopApplicationService.js";
|
|
|
|
const inspect = async (path: string, extra: Record<string, number> = {}) => {
|
|
const service = createDesktopApplicationService();
|
|
try {
|
|
const result = await service.inspect({ path, ...extra });
|
|
if (!result.ok) throw result.error;
|
|
return {
|
|
evidence: parseEvidence(result.value),
|
|
report: desktopApplicationSchema.parse(result.value.normalized_result),
|
|
};
|
|
} finally {
|
|
await service.close();
|
|
}
|
|
};
|
|
const put = async (root: string, path: string, bytes: Buffer | string) => {
|
|
const destination = join(root, path);
|
|
await mkdir(join(destination, ".."), { recursive: true });
|
|
await writeFile(destination, bytes);
|
|
};
|
|
|
|
it.skipIf(process.platform !== "linux")(
|
|
"keeps desktop sibling files with partial coverage for invalid UTF-8 names",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-filename-bytes-");
|
|
const rawName = Buffer.from([
|
|
0x62, 0x61, 0x64, 0xff, 0x2e, 0x74, 0x78, 0x74,
|
|
]);
|
|
const rawPath = Buffer.concat([Buffer.from(`${root}/`), rawName]);
|
|
await Promise.all([
|
|
writeFile(rawPath, "RAW_FILE"),
|
|
writeFile(join(root, "bad\ufffd.txt"), "UNICODE_FILE"),
|
|
writeFile(join(root, "good.txt"), "GOOD_FILE"),
|
|
]);
|
|
|
|
const { report } = await inspect(root);
|
|
expect(report.coverage).toMatchObject({
|
|
status: "partial",
|
|
entries_examined: 3,
|
|
});
|
|
expect(
|
|
report.files.filter(({ path }) => path === "bad\ufffd.txt"),
|
|
).toHaveLength(1);
|
|
expect(report.files.map(({ path }) => path)).toContain("good.txt");
|
|
expect(report.limitations.join(" ")).toContain(
|
|
`First raw basename bytes: ${rawName.toString("hex")}`,
|
|
);
|
|
},
|
|
);
|
|
|
|
it("joins real PE/CLI reference facts and independent engine markers without picking a winner", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-mixed-");
|
|
await put(
|
|
root,
|
|
"Game_Data/Managed/Assembly-CSharp.dll",
|
|
buildManagedPeFixture({ references: ["UnityEngine.CoreModule"] }),
|
|
);
|
|
await put(root, "GameAssembly.dll", buildNativePeFixture());
|
|
const metadata = Buffer.alloc(8);
|
|
metadata.writeUInt32LE(0xfab11baf);
|
|
metadata.writeUInt32LE(31, 4);
|
|
await put(
|
|
root,
|
|
"Game_Data/il2cpp_data/Metadata/global-metadata.dat",
|
|
metadata,
|
|
);
|
|
const godot = Buffer.alloc(20);
|
|
godot.write("GDPC");
|
|
godot.writeUInt32LE(3, 4);
|
|
godot.writeUInt32LE(4, 8);
|
|
await put(root, "game.pck", godot);
|
|
const form = Buffer.alloc(16);
|
|
form.write("FORM");
|
|
form.writeUInt32LE(8, 4);
|
|
form.write("GEN8", 8);
|
|
await put(root, "data.win", form);
|
|
await put(
|
|
root,
|
|
"resources/app.asar",
|
|
"not an archive; this is only a path convention",
|
|
);
|
|
await put(root, "Content/Paks/game.pak", "path convention only");
|
|
await mkdir(join(root, "BepInEx"));
|
|
await mkdir(join(root, "EasyAntiCheat"));
|
|
const { report, evidence } = await inspect(root);
|
|
expect(report.candidates.map((row) => row.family).sort()).toEqual(
|
|
[
|
|
"dotnet",
|
|
"electron",
|
|
"gamemaker",
|
|
"godot",
|
|
"unity-il2cpp",
|
|
"unity-mono",
|
|
"unreal",
|
|
].sort(),
|
|
);
|
|
expect(
|
|
report.components.map((row) => [row.name, row.runtime_presence]),
|
|
).toEqual([
|
|
["BepInEx", "unknown"],
|
|
["Easy Anti-Cheat", "unknown"],
|
|
]);
|
|
expect(report.coverage.status).toBe("complete-within-scan");
|
|
const files = new Map(report.files.map((file) => [file.file_id, file]));
|
|
for (const row of report.candidates)
|
|
for (const id of row.basis_file_ids) expect(files.has(id)).toBe(true);
|
|
for (const probe of report.next_probes)
|
|
expect(probe.path).toBe(
|
|
join(await realpath(root), files.get(probe.file_id)!.path),
|
|
);
|
|
const summary = await inspectAnalysisView({
|
|
source: { kind: "inline", evidence },
|
|
view: { kind: "summary" },
|
|
});
|
|
if (!summary.ok) throw summary.error;
|
|
expect(JSON.stringify(summary.value)).not.toContain('"files":[');
|
|
const selected = await inspectAnalysisView({
|
|
source: { kind: "inline", evidence },
|
|
view: {
|
|
kind: "desktop-application",
|
|
collection: "files",
|
|
paths: ["Game_Data/Managed/Assembly-CSharp.dll"],
|
|
fields: ["path", "facts"],
|
|
limit: 1,
|
|
},
|
|
});
|
|
if (!selected.ok) throw selected.error;
|
|
expect(selected.value.evidence_links).toEqual([evidence.evidence_id]);
|
|
expect(selected.value.normalized_result).toMatchObject({
|
|
items: [
|
|
{
|
|
file_id: expect.any(String),
|
|
path: "Game_Data/Managed/Assembly-CSharp.dll",
|
|
facts: expect.arrayContaining([
|
|
{
|
|
kind: "unity-assembly-reference",
|
|
offset: expect.any(Number),
|
|
value: {
|
|
name: "UnityEngine.CoreModule",
|
|
token: expect.any(String),
|
|
managed_directory: "Game_Data/Managed",
|
|
},
|
|
},
|
|
]),
|
|
},
|
|
],
|
|
coverage: { total: 1, exhausted: true },
|
|
});
|
|
});
|
|
|
|
it("reads sparse embedded PCK ranges without hashing or retaining a large executable", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-sparse-");
|
|
const path = join(root, "game.exe");
|
|
const file = await open(path, "w");
|
|
const header = buildNativePeFixture();
|
|
const size = 1024 * 1024 * 1024;
|
|
const pck = Buffer.alloc(20);
|
|
pck.write("GDPC");
|
|
pck.writeUInt32LE(3, 4);
|
|
pck.writeUInt32LE(4, 8);
|
|
const trailer = Buffer.alloc(12);
|
|
trailer.writeBigUInt64LE(BigInt(pck.length));
|
|
trailer.write("GDPC", 8);
|
|
await file.write(header, 0, header.length, 0);
|
|
await file.write(pck, 0, pck.length, size - 32);
|
|
await file.write(trailer, 0, 12, size - 12);
|
|
await file.close();
|
|
const { report } = await inspect(root);
|
|
expect(report.candidates.map((row) => row.family)).toContain("godot");
|
|
expect(report.coverage.bytes_read).toBeLessThan(4096);
|
|
expect(report.files[0]?.bytes).toBe(size);
|
|
expect(report.files[0]?.facts).toContainEqual(
|
|
expect.objectContaining({ kind: "godot-pck-header", offset: size - 32 }),
|
|
);
|
|
});
|
|
|
|
it("does not promote filenames, arbitrary pak files or symlink targets into byte facts", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-unknown-");
|
|
await put(root, "GameAssembly.dll", "x");
|
|
await put(root, "global-metadata.dat", "");
|
|
await put(root, "game.pck", "GDPC");
|
|
await put(root, "resources.pak", "not Unreal");
|
|
await symlink("/outside/nonexistent", join(root, "foreign"));
|
|
const { report } = await inspect(root);
|
|
expect(report.candidates).toEqual([]);
|
|
expect(report.coverage.symlinks_not_followed).toBe(1);
|
|
expect(
|
|
report.files.find((file) => file.path === "foreign")?.read_status,
|
|
).toBe("not-selected");
|
|
});
|
|
|
|
it("retains bounded coverage and stable pages without claiming absence after a traversal limit", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-limit-");
|
|
for (let i = 0; i < 4; i++) await put(root, `${i}.txt`, "ordinary data");
|
|
const { report, evidence } = await inspect(root, { max_entries: 2 });
|
|
expect(report.coverage).toMatchObject({
|
|
status: "partial",
|
|
entries_examined: 2,
|
|
});
|
|
expect(report.files).toHaveLength(2);
|
|
const result = await inspectAnalysisView({
|
|
source: { kind: "inline", evidence },
|
|
view: {
|
|
kind: "desktop-application",
|
|
collection: "files",
|
|
paths: [],
|
|
fields: ["path"],
|
|
limit: 1,
|
|
},
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.normalized_result).toMatchObject({
|
|
items: [],
|
|
source_coverage: { status: "partial" },
|
|
coverage: { total: 0, exhausted: true },
|
|
unknowns: expect.arrayContaining([expect.stringContaining("partial")]),
|
|
});
|
|
});
|
|
|
|
it("preserves caller cancellation and refuses a replaced selected entry", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-identity-");
|
|
await put(root, "entry", "original");
|
|
const reader = new DirectoryArtifactReader(root);
|
|
const values = [];
|
|
for await (const row of reader.entries()) values.push(row);
|
|
const entry = values[0]!;
|
|
await expect(
|
|
reader.inspectFile(entry, async () => {
|
|
await rename(join(root, "entry"), join(root, "old"));
|
|
await put(root, "entry", "replacement");
|
|
return "untrusted";
|
|
}),
|
|
).rejects.toMatchObject({ reason: "integrity" });
|
|
await reader.close();
|
|
const service = createDesktopApplicationService();
|
|
const reason = new Error("caller cancellation");
|
|
const result = await service.inspect(
|
|
{ path: root },
|
|
{ signal: AbortSignal.abort(reason) },
|
|
);
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok)
|
|
expect(result.error).toMatchObject({
|
|
_tag: "AnalysisCancelledError",
|
|
cause: reason,
|
|
});
|
|
await service.close();
|
|
});
|
|
|
|
it("retains an entry whose close failed and observes cancellation after final close", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-close-");
|
|
await put(root, "entry", "bytes");
|
|
const reader = new DirectoryArtifactReader(root);
|
|
const entries = [];
|
|
for await (const entry of reader.entries()) entries.push(entry);
|
|
let restore: (() => void) | undefined;
|
|
await expect(
|
|
reader.inspectFile(entries[0]!, async (file) => {
|
|
const close = file.close.bind(file);
|
|
file.close = () => Promise.reject(new Error("close denied"));
|
|
restore = () => {
|
|
file.close = close;
|
|
};
|
|
return "observed";
|
|
}),
|
|
).rejects.toBeInstanceOf(ArtifactReaderFailure);
|
|
await expect(reader.close()).rejects.toMatchObject({
|
|
cleanup: expect.any(Object),
|
|
});
|
|
restore?.();
|
|
await reader.close();
|
|
const cancelled = new DirectoryArtifactReader(root);
|
|
const controller = new AbortController();
|
|
const reason = new Error("late cancellation");
|
|
await expect(
|
|
cancelled.inspectFile(
|
|
entries[0]!,
|
|
async (file) => {
|
|
const close = file.close.bind(file);
|
|
file.close = async () => {
|
|
await close();
|
|
controller.abort(reason);
|
|
};
|
|
return "completed bytes";
|
|
},
|
|
controller.signal,
|
|
),
|
|
).rejects.toMatchObject({ reason: "cancelled", cause: reason });
|
|
await cancelled.close();
|
|
});
|
|
|
|
it.each(["Game_Data/Managed", "Contents/Resources/Data/Managed"])(
|
|
"requires a Unity reference in %s and reports a metadata budget refusal as partial",
|
|
async (managedDirectory) => {
|
|
const root = await createTestTempDirectory("rea-desktop-mono-boundary-");
|
|
await put(
|
|
root,
|
|
`${managedDirectory}/ordinary.dll`,
|
|
buildManagedPeFixture({ references: ["System.Runtime"] }),
|
|
);
|
|
const ordinary = await inspect(root);
|
|
expect(ordinary.report.candidates.map((row) => row.family)).toEqual([
|
|
"dotnet",
|
|
]);
|
|
await put(
|
|
root,
|
|
`${managedDirectory}/unity.dll`,
|
|
buildManagedPeFixture({
|
|
references: ["UnityEngine.CoreModule", "System.Runtime"],
|
|
additionalEmptyTypes: 5000,
|
|
}),
|
|
);
|
|
const bounded = await inspect(root, { max_probe_bytes: 4096 });
|
|
expect(bounded.report.coverage.status).toBe("partial");
|
|
expect(bounded.report.coverage.bytes_read).toBeLessThanOrEqual(4096);
|
|
expect(bounded.report.candidates.map((row) => row.family)).not.toContain(
|
|
"unity-mono",
|
|
);
|
|
expect(
|
|
bounded.report.files.some((file) =>
|
|
file.issues.some((issue) => issue.includes("budget")),
|
|
),
|
|
).toBe(true);
|
|
},
|
|
);
|
|
|
|
it("retains complete observations through cancellation during directory finalization", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-finalize-cancel-");
|
|
await put(root, "notes.txt", "target content");
|
|
const controller = new AbortController();
|
|
const reason = new Error("cancel during final close");
|
|
const service = new DesktopApplicationService(
|
|
new DesktopApplicationReader((path) => {
|
|
const reader = new DirectoryArtifactReader(path);
|
|
const close = reader.close.bind(reader);
|
|
reader.close = async () => {
|
|
await close();
|
|
controller.abort(reason);
|
|
};
|
|
return reader;
|
|
}),
|
|
);
|
|
const result = await service.inspect(
|
|
{ path: root },
|
|
{ signal: controller.signal },
|
|
);
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) throw new Error("Expected cancellation");
|
|
expect(result.error).toMatchObject({ reason: "cancelled", cause: reason });
|
|
const retained = parseEvidence(result.error.partialObservation);
|
|
expect(
|
|
desktopApplicationSchema.parse(retained.normalized_result).files,
|
|
).toHaveLength(1);
|
|
await service.close();
|
|
});
|
|
|
|
it("keeps a completed directory report and exact cleanup authority until retry confirms release", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-finalize-retry-");
|
|
await put(root, "notes.txt", "target content");
|
|
let denied = true;
|
|
const service = new DesktopApplicationService(
|
|
new DesktopApplicationReader((path) => {
|
|
const reader = new DirectoryArtifactReader(path);
|
|
const close = reader.close.bind(reader);
|
|
reader.close = async () => {
|
|
if (denied) throw new Error("owned close denied");
|
|
await close();
|
|
};
|
|
return reader;
|
|
}),
|
|
);
|
|
const result = await service.inspect({ path: root });
|
|
if (result.ok) throw new Error("Expected incomplete cleanup");
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
expect(
|
|
desktopApplicationSchema.parse(
|
|
parseEvidence(result.error.partialObservation).normalized_result,
|
|
).files,
|
|
).toHaveLength(1);
|
|
await expect(service.close()).rejects.toBeInstanceOf(ArtifactReaderFailure);
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
denied = false;
|
|
await service.close();
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
});
|
|
|
|
it("bounds simultaneous directory scans without cancelling the admitted caller", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-capacity-");
|
|
await put(root, "notes.txt", "observed");
|
|
let release!: () => void;
|
|
let entered!: () => void;
|
|
const barrier = new Promise<void>((resolve) => {
|
|
release = resolve;
|
|
});
|
|
const started = new Promise<void>((resolve) => {
|
|
entered = resolve;
|
|
});
|
|
const service = new DesktopApplicationService(
|
|
new DesktopApplicationReader((path) => {
|
|
const reader = new DirectoryArtifactReader(path);
|
|
const entries = reader.entries.bind(reader);
|
|
reader.entries = async function* (...args: Parameters<typeof entries>) {
|
|
entered();
|
|
await barrier;
|
|
yield* entries(...args);
|
|
};
|
|
return reader;
|
|
}),
|
|
);
|
|
const first = service.inspect({ path: root });
|
|
try {
|
|
await started;
|
|
const second = await service.inspect({ path: root });
|
|
expect(second.ok).toBe(false);
|
|
if (!second.ok)
|
|
expect(second.error).toMatchObject({
|
|
_tag: "AnalysisResourceConstraintError",
|
|
resource: "capacity",
|
|
retryable: true,
|
|
});
|
|
} finally {
|
|
release();
|
|
}
|
|
const result = await first;
|
|
if (!result.ok) throw result.error;
|
|
expect(
|
|
desktopApplicationSchema.parse(result.value.normalized_result).files,
|
|
).toHaveLength(1);
|
|
await service.close();
|
|
});
|
|
|
|
it("keeps directory-only marker identities separate across installation roots", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-namespaces-");
|
|
const left = join(root, "left");
|
|
const right = join(root, "right");
|
|
await mkdir(join(left, "BepInEx"), { recursive: true });
|
|
await mkdir(join(right, "BepInEx"), { recursive: true });
|
|
const a = await inspect(left);
|
|
const b = await inspect(right);
|
|
expect(a.report.files[0]?.path).toBe(b.report.files[0]?.path);
|
|
expect(a.report.files[0]?.identity).toBeNull();
|
|
expect(a.report.files[0]?.file_id).not.toBe(b.report.files[0]?.file_id);
|
|
expect(a.report.components[0]?.component_id).not.toBe(
|
|
b.report.components[0]?.component_id,
|
|
);
|
|
expect(a.report.root.snapshot_sha256).not.toBe(b.report.root.snapshot_sha256);
|
|
});
|
|
|
|
it.skipIf(process.platform === "win32").each(["path", "kind"] as const)(
|
|
"commits omitted special-entry %s changes and exposes selected omission facts",
|
|
async (change) => {
|
|
const root = await createTestTempDirectory("rea-desktop-omissions-");
|
|
const original = join(root, "special");
|
|
execFileSync("mkfifo", [original], { timeout: 10_000 });
|
|
const before = await inspect(root);
|
|
let selected = "special";
|
|
if (change === "path") {
|
|
selected = "renamed";
|
|
await rename(original, join(root, selected));
|
|
} else {
|
|
await unlink(original);
|
|
const server = createNetServer();
|
|
onTestFinished(async () => {
|
|
if (!server.listening) return;
|
|
await new Promise<void>((resolve, reject) =>
|
|
server.close((cause) => (cause ? reject(cause) : resolve())),
|
|
);
|
|
});
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(original, resolve);
|
|
});
|
|
}
|
|
const after = await inspect(root);
|
|
expect(after.report.root.snapshot_sha256).not.toBe(
|
|
before.report.root.snapshot_sha256,
|
|
);
|
|
expect(before.report.omissions).toEqual([
|
|
{
|
|
omission_id: expect.stringMatching(/^dso_[a-f0-9]{64}$/u),
|
|
path: "special",
|
|
kind: "fifo",
|
|
},
|
|
]);
|
|
expect(after.report.omissions[0]?.omission_id).not.toBe(
|
|
before.report.omissions[0]?.omission_id,
|
|
);
|
|
expect(after.report.coverage).toMatchObject({
|
|
status: "partial",
|
|
entries_examined: 1,
|
|
special_entries_omitted: 1,
|
|
files_probed: 0,
|
|
bytes_read: 0,
|
|
});
|
|
const page = await inspectAnalysisView({
|
|
source: { kind: "inline", evidence: after.evidence },
|
|
view: {
|
|
kind: "desktop-application",
|
|
collection: "omissions",
|
|
paths: [selected],
|
|
fields: ["kind"],
|
|
offset: 0,
|
|
limit: 1,
|
|
},
|
|
});
|
|
if (!page.ok) throw page.error;
|
|
expect(page.value.evidence_links).toEqual([after.evidence.evidence_id]);
|
|
expect(page.value.normalized_result).toHaveProperty("items", [
|
|
{
|
|
omission_id: after.report.omissions[0]?.omission_id,
|
|
kind: change === "path" ? "fifo" : "socket",
|
|
},
|
|
]);
|
|
expect(page.value.normalized_result).toMatchObject({
|
|
coverage: { total: 1, exhausted: true },
|
|
source_coverage: { status: "partial" },
|
|
});
|
|
},
|
|
);
|
|
|
|
it("bounds omitted-entry retention before allocating rows or serializing identities", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-omission-capacity-");
|
|
let examined = 0;
|
|
// Simulate bounded parsed paths without creating hundreds of deep FIFO trees.
|
|
const prefix = Array.from({ length: 15 }, () => "d".repeat(255)).join("/");
|
|
const service = new DesktopApplicationService(
|
|
new DesktopApplicationReader((path) => {
|
|
const reader = new DirectoryArtifactReader(path);
|
|
reader.entries = async function* (_signal, options) {
|
|
if (typeof options === "function")
|
|
throw new Error("Expected traversal options");
|
|
for (let index = 0; index < 400; index++) {
|
|
examined++;
|
|
options?.onOmission?.({
|
|
path: `${prefix}/${index}/entry`,
|
|
kind: "fifo",
|
|
});
|
|
}
|
|
};
|
|
return reader;
|
|
}),
|
|
);
|
|
onTestFinished(() => service.close());
|
|
const result = await service.inspect({ path: root });
|
|
if (!result.ok) throw result.error;
|
|
const report = desktopApplicationSchema.parse(result.value.normalized_result);
|
|
expect(examined).toBeLessThan(400);
|
|
expect(report.omissions).toHaveLength(examined - 1);
|
|
expect(report.coverage).toMatchObject({
|
|
status: "partial",
|
|
entries_examined: examined,
|
|
special_entries_omitted: examined,
|
|
});
|
|
expect(report.limitations).toContainEqual(
|
|
expect.stringContaining("representation budget"),
|
|
);
|
|
});
|
|
|
|
it("preserves the selected root alias and directs follow-ups to the admitted physical root", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-root-alias-");
|
|
const admitted = join(root, "admitted");
|
|
const replacement = join(root, "replacement");
|
|
const alias = join(root, "selected");
|
|
await put(admitted, "resources/app.asar", "observed marker");
|
|
await mkdir(replacement);
|
|
await symlink(
|
|
admitted,
|
|
alias,
|
|
process.platform === "win32" ? "junction" : "dir",
|
|
);
|
|
const service = new DesktopApplicationService(
|
|
new DesktopApplicationReader((path) => {
|
|
const reader = new DirectoryArtifactReader(path);
|
|
const entries = reader.entries.bind(reader);
|
|
reader.entries = async function* (...args: Parameters<typeof entries>) {
|
|
await unlink(alias);
|
|
await symlink(
|
|
replacement,
|
|
alias,
|
|
process.platform === "win32" ? "junction" : "dir",
|
|
);
|
|
yield* entries(...args);
|
|
};
|
|
return reader;
|
|
}),
|
|
);
|
|
try {
|
|
const result = await service.inspect({ path: alias });
|
|
if (!result.ok) throw result.error;
|
|
const report = desktopApplicationSchema.parse(
|
|
result.value.normalized_result,
|
|
);
|
|
expect(report.root.path).toBe(alias);
|
|
expect(report.root.canonical_path).toBe(await realpath(admitted));
|
|
expect(report.candidates.map((candidate) => candidate.family)).toEqual([
|
|
"electron",
|
|
]);
|
|
expect(report.next_probes[0]?.path).toBe(
|
|
join(await realpath(admitted), "resources", "app.asar"),
|
|
);
|
|
} finally {
|
|
await service.close();
|
|
}
|
|
});
|
|
|
|
it.each([false, true])(
|
|
"retains verified header facts and exact release authority when file close fails (late cancellation: %s)",
|
|
async (cancelDuringClose) => {
|
|
const root = await createTestTempDirectory("rea-desktop-probe-close-");
|
|
const header = Buffer.alloc(20);
|
|
header.write("GDPC");
|
|
header.writeUInt32LE(2, 4);
|
|
header.writeUInt32LE(4, 8);
|
|
await put(root, "game.pck", header);
|
|
const controller = new AbortController();
|
|
const reason = new Error("cancel accepted while file close failed");
|
|
let restore: (() => void) | undefined;
|
|
const service = new DesktopApplicationService(
|
|
new DesktopApplicationReader((path) => {
|
|
const reader = new DirectoryArtifactReader(path);
|
|
const inspectFile = reader.inspectFile.bind(reader);
|
|
reader.inspectFile = (entry, inspect, signal) =>
|
|
inspectFile(
|
|
entry,
|
|
async (file) => {
|
|
const close = file.close.bind(file);
|
|
restore = () => {
|
|
file.close = close;
|
|
};
|
|
file.close = async () => {
|
|
if (cancelDuringClose) controller.abort(reason);
|
|
throw new Error("descriptor close denied");
|
|
};
|
|
return inspect(file);
|
|
},
|
|
signal,
|
|
);
|
|
return reader;
|
|
}),
|
|
);
|
|
try {
|
|
const result = await service.inspect(
|
|
{ path: root },
|
|
{ signal: controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected incomplete descriptor cleanup");
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
if (cancelDuringClose)
|
|
expect(result.error).toMatchObject({
|
|
reason: "cancelled",
|
|
cause: reason,
|
|
});
|
|
const report = desktopApplicationSchema.parse(
|
|
parseEvidence(result.error.partialObservation).normalized_result,
|
|
);
|
|
expect(report.coverage.status).toBe("partial");
|
|
const successfulRead = await inspect(root);
|
|
expect(report.files[0]?.file_id).toBe(
|
|
successfulRead.report.files[0]?.file_id,
|
|
);
|
|
expect(report.files[0]?.ranges).toEqual(
|
|
successfulRead.report.files[0]?.ranges,
|
|
);
|
|
expect(report.root.snapshot_sha256).toBe(
|
|
successfulRead.report.root.snapshot_sha256,
|
|
);
|
|
expect(report.files[0]).toMatchObject({
|
|
read_status: "examined",
|
|
ranges: [{ offset: 0, bytes: 20, sha256: expect.any(String) }],
|
|
facts: [{ kind: "godot-pck-header", offset: 0 }],
|
|
});
|
|
expect(report.candidates.map((candidate) => candidate.family)).toEqual([
|
|
"godot",
|
|
]);
|
|
await expect(service.close()).rejects.toMatchObject({
|
|
cleanup: expect.any(Object),
|
|
});
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
restore?.();
|
|
await service.close();
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
} finally {
|
|
restore?.();
|
|
await service.close();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
"Game_Data/Managed",
|
|
"Contents/Resources/Data/Managed",
|
|
"Nested/Game.app/Contents/Resources/Data/Managed",
|
|
])(
|
|
"reads AssemblyRef names in %s independently of unrelated key/hash payload validity",
|
|
async (managedDirectory) => {
|
|
const root = await createTestTempDirectory("rea-desktop-reference-fields-");
|
|
const image = buildManagedPeFixture({
|
|
references: ["System.Runtime", "UnityEngine.CoreModule"],
|
|
});
|
|
const metadataOffset = image.indexOf(Buffer.from("BSJB"));
|
|
const layout = readManagedMetadataLayout(
|
|
image,
|
|
metadataOffset,
|
|
image.length - metadataOffset,
|
|
);
|
|
if (layout.blob === null)
|
|
throw new Error("Fixture requires a #Blob stream");
|
|
const blobSize = layout.blob.size;
|
|
const reference = layout.table(35)!;
|
|
// ECMA-335 AssemblyRef: 8 version bytes, 4 flag bytes, then PublicKeyOrToken.
|
|
// Keep its name valid while making that unrelated blob index invalid.
|
|
if (layout.blobIndexSize === 2)
|
|
image.writeUInt16LE(0xffff, reference.offset + 12);
|
|
else image.writeUInt32LE(0xffff_ffff, reference.offset + 12);
|
|
expect(() => readAssemblyReference(image, layout, 1, blobSize)).toThrow();
|
|
await put(root, `${managedDirectory}/Assembly-CSharp.dll`, image);
|
|
const { report } = await inspect(root);
|
|
expect(report.candidates.map((candidate) => candidate.family)).toEqual([
|
|
"dotnet",
|
|
"unity-mono",
|
|
]);
|
|
expect(
|
|
report.files.find((file) => file.kind === "file")?.facts,
|
|
).toContainEqual({
|
|
kind: "unity-assembly-reference",
|
|
offset: expect.any(Number),
|
|
value: {
|
|
name: "UnityEngine.CoreModule",
|
|
token: "0x23000002",
|
|
managed_directory: managedDirectory,
|
|
},
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
"Data/Managed",
|
|
"Other/Resources/Data/Managed",
|
|
"Contents/Resource/Data/Managed",
|
|
])("does not infer Unity from an unrelated layout %s", async (directory) => {
|
|
const root = await createTestTempDirectory("rea-desktop-unity-layout-");
|
|
await put(
|
|
root,
|
|
`${directory}/Assembly-CSharp.dll`,
|
|
buildManagedPeFixture({ references: ["UnityEngine.CoreModule"] }),
|
|
);
|
|
const { report } = await inspect(root);
|
|
expect(report.candidates.map((candidate) => candidate.family)).toEqual([
|
|
"dotnet",
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
{ container: 8, chunk: 0, bytes: 16, valid: true },
|
|
{ container: 16, chunk: 8, bytes: 32, valid: true },
|
|
{ container: 0, chunk: 0, bytes: 16, valid: false },
|
|
{ container: 7, chunk: 0, bytes: 16, valid: false },
|
|
{ container: 8, chunk: 1, bytes: 17, valid: false },
|
|
{ container: 12, chunk: 8, bytes: 24, valid: false },
|
|
{ container: 17, chunk: 8, bytes: 24, valid: false },
|
|
{ container: 16, chunk: 9, bytes: 25, valid: false },
|
|
{ container: 8, chunk: 0xffff_ffff, bytes: 16, valid: false },
|
|
])(
|
|
"requires GEN8 within FORM and the file: $container/$chunk/$bytes (valid=$valid)",
|
|
async ({ container, chunk, bytes, valid }) => {
|
|
const root = await createTestTempDirectory("rea-desktop-form-extents-");
|
|
const form = Buffer.alloc(bytes);
|
|
form.write("FORM");
|
|
form.writeUInt32LE(container, 4);
|
|
form.write("GEN8", 8);
|
|
form.writeUInt32LE(chunk, 12);
|
|
await put(root, "data.win", form);
|
|
const { report } = await inspect(root);
|
|
expect(report.candidates.map((candidate) => candidate.family)).toEqual(
|
|
valid ? ["gamemaker"] : [],
|
|
);
|
|
expect(
|
|
report.files
|
|
.find((file) => file.path === "data.win")
|
|
?.facts.some((fact) => fact.kind === "gamemaker-form-header"),
|
|
).toBe(valid);
|
|
expect(report.coverage.status).toBe("complete-within-scan");
|
|
},
|
|
);
|
|
|
|
it.each(["assets/game.unx", "Example.app/Contents/Resources/game.ios"])(
|
|
"probes the GameMaker data file at %s",
|
|
async (path) => {
|
|
const root = await createTestTempDirectory("rea-desktop-gamemaker-data-");
|
|
const form = Buffer.alloc(16);
|
|
form.write("FORM");
|
|
form.writeUInt32LE(8, 4);
|
|
form.write("GEN8", 8);
|
|
form.writeUInt32LE(0, 12);
|
|
await put(root, path, form);
|
|
|
|
const { report } = await inspect(root);
|
|
expect(report.candidates.map((candidate) => candidate.family)).toContain(
|
|
"gamemaker",
|
|
);
|
|
expect(report.files).toContainEqual(
|
|
expect.objectContaining({
|
|
path,
|
|
read_status: "examined",
|
|
facts: [
|
|
expect.objectContaining({
|
|
kind: "gamemaker-form-header",
|
|
offset: 0,
|
|
value: { container: "FORM", first_chunk: "GEN8" },
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
expect(report.coverage.status).toBe("complete-within-scan");
|
|
},
|
|
);
|
|
|
|
it("compares exact signature bytes without masking high bits in engine headers or trailers", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-exact-signatures-");
|
|
const pck = Buffer.alloc(20);
|
|
pck.write("GDPC");
|
|
const form = Buffer.alloc(16);
|
|
form.write("FORM");
|
|
form.writeUInt32LE(8, 4);
|
|
form.write("GEN8", 8);
|
|
const toc = Buffer.from("-==-".repeat(4));
|
|
for (const [path, source, offset] of [
|
|
["invalid.pck", pck, 0],
|
|
["bad-form/data.win", form, 0],
|
|
["bad-chunk/data.win", form, 8],
|
|
["invalid.utoc", toc, 0],
|
|
] as const) {
|
|
const bytes = Buffer.from(source);
|
|
bytes[offset] = bytes[offset]! | 0x80;
|
|
await put(root, path, bytes);
|
|
}
|
|
const elf = Buffer.alloc(64);
|
|
elf.set([0x7f, 0x45, 0x4c, 0x46]);
|
|
const trailer = Buffer.alloc(12);
|
|
trailer.writeBigUInt64LE(BigInt(pck.length));
|
|
trailer.write("GDPC", 8);
|
|
const badTrailer = Buffer.from(trailer);
|
|
badTrailer[8] = badTrailer[8]! | 0x80;
|
|
await put(root, "bad-trailer.exe", Buffer.concat([elf, pck, badTrailer]));
|
|
const badHeader = Buffer.from(pck);
|
|
badHeader[0] = badHeader[0]! | 0x80;
|
|
await put(root, "bad-header.exe", Buffer.concat([elf, badHeader, trailer]));
|
|
const { report } = await inspect(root);
|
|
expect(report.candidates).toEqual([]);
|
|
expect(report.files.flatMap((file) => file.facts)).not.toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ kind: "godot-pck-header" }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("yields between reference-name batches so accepted cancellation precedes inspection completion", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-reference-cancel-");
|
|
await put(
|
|
root,
|
|
"Game_Data/Managed/Assembly-CSharp.dll",
|
|
buildManagedPeFixture({
|
|
references: Array.from({ length: 2048 }, (_, index) => `Library${index}`),
|
|
}),
|
|
);
|
|
const controller = new AbortController();
|
|
const reason = new Error("cancel while references are being examined");
|
|
let completed = false;
|
|
let completedWhenCancelled: boolean | undefined;
|
|
let scheduled = false;
|
|
const service = new DesktopApplicationService(
|
|
new DesktopApplicationReader((path) => {
|
|
const reader = new DirectoryArtifactReader(path);
|
|
const inspectFile = reader.inspectFile.bind(reader);
|
|
reader.inspectFile = (entry, inspection, signal) =>
|
|
inspectFile(
|
|
entry,
|
|
async (file) => {
|
|
const observed = new Proxy(file, {
|
|
get(target, property, receiver) {
|
|
if (property !== "read")
|
|
return Reflect.get(target, property, receiver);
|
|
return async (...args: unknown[]) => {
|
|
const result: unknown = await Reflect.apply(
|
|
target.read,
|
|
target,
|
|
args,
|
|
);
|
|
const buffer = args[0];
|
|
if (
|
|
!scheduled &&
|
|
Buffer.isBuffer(buffer) &&
|
|
buffer.subarray(0, 4).equals(Buffer.from("BSJB"))
|
|
) {
|
|
scheduled = true;
|
|
// Let the first batch start, then cancel on the following turn.
|
|
setImmediate(() =>
|
|
setImmediate(() => {
|
|
completedWhenCancelled = completed;
|
|
controller.abort(reason);
|
|
}),
|
|
);
|
|
}
|
|
return result;
|
|
};
|
|
},
|
|
});
|
|
try {
|
|
return await inspection(observed);
|
|
} finally {
|
|
completed = true;
|
|
}
|
|
},
|
|
signal,
|
|
);
|
|
return reader;
|
|
}),
|
|
);
|
|
try {
|
|
const result = await service.inspect(
|
|
{ path: root },
|
|
{ signal: controller.signal },
|
|
);
|
|
if (result.ok)
|
|
throw new Error("Expected cancellation while examining references");
|
|
expect(scheduled).toBe(true);
|
|
expect(completedWhenCancelled).toBe(false);
|
|
expect(result.error).toMatchObject({ reason: "cancelled", cause: reason });
|
|
} finally {
|
|
await service.close();
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
format: "ELF",
|
|
path: "GameAssembly.so",
|
|
image: Buffer.from([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1, 0]),
|
|
},
|
|
{
|
|
format: "thin Mach-O",
|
|
path: "Contents/Frameworks/GameAssembly.dylib",
|
|
image: Buffer.from([0xcf, 0xfa, 0xed, 0xfe, 7, 0, 0, 1]),
|
|
},
|
|
...[false, true].map((littleEndian) => ({
|
|
format: `FAT64 ${littleEndian ? "little" : "big"}-endian`,
|
|
path: "Contents/Frameworks/GameAssembly.dylib",
|
|
image: Buffer.from(
|
|
fatImage(
|
|
[
|
|
{
|
|
cpu: CPU.arm64,
|
|
bytes: machoImage({ cpu: CPU.arm64, fileType: FILE_TYPE.dylib }),
|
|
},
|
|
],
|
|
true,
|
|
littleEndian,
|
|
),
|
|
),
|
|
})),
|
|
])(
|
|
"joins IL2CPP metadata with $format library $path",
|
|
async ({ path, image }) => {
|
|
const root = await createTestTempDirectory("rea-desktop-unix-il2cpp-");
|
|
await put(root, path, image);
|
|
const metadata = Buffer.alloc(8);
|
|
metadata.writeUInt32LE(0xfab11baf);
|
|
metadata.writeUInt32LE(31, 4);
|
|
await put(
|
|
root,
|
|
"Game_Data/il2cpp_data/Metadata/global-metadata.dat",
|
|
metadata,
|
|
);
|
|
const { report } = await inspect(root);
|
|
// Native probes read a fixed prefix and, where present, the existing PCK
|
|
// trailer; the FAT architecture table and slice payload are not traversed.
|
|
expect(report.coverage.bytes_read).toBeLessThanOrEqual(
|
|
64 + 12 + metadata.length,
|
|
);
|
|
expect(
|
|
report.files.find((file) => file.path === path)?.ranges[0],
|
|
).toMatchObject({
|
|
offset: 0,
|
|
bytes: Math.min(image.length, 64),
|
|
});
|
|
expect(report.candidates.map((candidate) => candidate.family)).toEqual([
|
|
"unity-il2cpp",
|
|
]);
|
|
expect(report.next_probes).toEqual([
|
|
expect.objectContaining({
|
|
operation: "inspect_il2cpp_metadata",
|
|
path: join(
|
|
await realpath(root),
|
|
"Game_Data/il2cpp_data/Metadata/global-metadata.dat",
|
|
),
|
|
file_id: report.files.find((file) =>
|
|
file.path.endsWith("global-metadata.dat"),
|
|
)?.file_id,
|
|
}),
|
|
expect.objectContaining({
|
|
operation: "open_binary",
|
|
path: join(await realpath(root), path),
|
|
file_id: expect.any(String),
|
|
}),
|
|
]);
|
|
},
|
|
);
|
|
|
|
it("bounds diagnostic previews while preserving complete caller-provided issue facts", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-diagnostic-preview-");
|
|
const malformed = Buffer.alloc(64);
|
|
malformed.write("MZ");
|
|
malformed.writeUInt32LE(0xffffffff, 0x3c);
|
|
await put(root, "broken.exe", malformed);
|
|
const { report, evidence } = await inspect(root);
|
|
const source = structuredClone(report);
|
|
const row = source.files.find((file) => file.path === "broken.exe");
|
|
if (row === undefined) throw new Error("Missing diagnostic fixture");
|
|
const longReason = "A" + "😀".repeat(1000);
|
|
row.issues = [longReason, "second probe reason"];
|
|
const callerEvidence = createDesktopApplicationEvidence(
|
|
{
|
|
path: root,
|
|
max_entries: 20_000,
|
|
max_probe_bytes: 8 * 1024 * 1024,
|
|
},
|
|
source,
|
|
evidence.provider,
|
|
);
|
|
const summarized = await inspectAnalysisView({
|
|
source: { kind: "inline", evidence: callerEvidence },
|
|
view: { kind: "summary" },
|
|
});
|
|
if (!summarized.ok) throw summarized.error;
|
|
expect(summarized.value.normalized_result).toMatchObject({
|
|
summary: {
|
|
probe_issues: {
|
|
file_count: 1,
|
|
issue_count: 2,
|
|
preview_truncated: false,
|
|
preview: [
|
|
{
|
|
file_id: row.file_id,
|
|
path_preview: "broken.exe",
|
|
read_status: "partial",
|
|
issue_count: 2,
|
|
first_issue_preview: "A" + "😀".repeat(127),
|
|
text_truncated: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
});
|
|
for (const ids of [undefined, []]) {
|
|
const selected = await inspectAnalysisView({
|
|
source: { kind: "inline", evidence: callerEvidence },
|
|
view: {
|
|
kind: "desktop-application",
|
|
collection: "files",
|
|
has_issues: true,
|
|
...(ids === undefined ? {} : { file_ids: ids }),
|
|
fields: ["issues"],
|
|
},
|
|
});
|
|
if (!selected.ok) throw selected.error;
|
|
expect(selected.value.normalized_result).toMatchObject({
|
|
items:
|
|
ids === undefined
|
|
? [
|
|
{
|
|
file_id: row.file_id,
|
|
issues: [longReason, "second probe reason"],
|
|
},
|
|
]
|
|
: [],
|
|
source_coverage: { status: "partial" },
|
|
});
|
|
expect(selected.value.evidence_links).toEqual([callerEvidence.evidence_id]);
|
|
}
|
|
expect(row.issues[0]).toBe(longReason);
|
|
});
|
|
|
|
it("keeps snapshot identity stable across limits that preserve source observations", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-snapshot-policy-");
|
|
await put(root, "GameAssembly.dll", buildNativePeFixture());
|
|
const metadata = Buffer.alloc(8);
|
|
metadata.writeUInt32LE(0xfab11baf);
|
|
metadata.writeUInt32LE(31, 4);
|
|
await put(root, "global-metadata.dat", metadata);
|
|
await mkdir(join(root, "BepInEx"));
|
|
const first = await inspect(root, { max_entries: 10, max_probe_bytes: 4096 });
|
|
const second = await inspect(root, {
|
|
max_entries: 20,
|
|
max_probe_bytes: 8192,
|
|
});
|
|
expect(first.report.files).toEqual(second.report.files);
|
|
expect(first.report.omissions).toEqual(second.report.omissions);
|
|
expect(first.report.coverage.status).toBe("complete-within-scan");
|
|
expect(second.report.coverage.status).toBe("complete-within-scan");
|
|
expect(first.report.root.snapshot_sha256).toBe(
|
|
second.report.root.snapshot_sha256,
|
|
);
|
|
expect(first.evidence.subject?.digest.sha256).toBe(
|
|
second.evidence.subject?.digest.sha256,
|
|
);
|
|
expect(first.evidence.evidence_id).not.toBe(second.evidence.evidence_id);
|
|
expect(first.evidence.parameters).toMatchObject({
|
|
max_entries: 10,
|
|
max_probe_bytes: 4096,
|
|
});
|
|
expect(second.evidence.parameters).toMatchObject({
|
|
max_entries: 20,
|
|
max_probe_bytes: 8192,
|
|
});
|
|
expect(first.report.coverage.limits).toEqual({
|
|
max_entries: 10,
|
|
max_probe_bytes: 4096,
|
|
});
|
|
expect(second.report.coverage.limits).toEqual({
|
|
max_entries: 20,
|
|
max_probe_bytes: 8192,
|
|
});
|
|
const subset = await inspect(root, { max_entries: 1, max_probe_bytes: 8192 });
|
|
expect(subset.report.coverage.status).toBe("partial");
|
|
expect(subset.report.root.snapshot_sha256).not.toBe(
|
|
first.report.root.snapshot_sha256,
|
|
);
|
|
});
|
|
|
|
it("changes snapshot identity when probe limits change retained read ranges", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-snapshot-ranges-");
|
|
for (let index = 0; index < 20; index++)
|
|
await put(
|
|
root,
|
|
`probe${String(index).padStart(2, "0")}.exe`,
|
|
buildNativePeFixture(),
|
|
);
|
|
const partial = await inspect(root, { max_probe_bytes: 4096 });
|
|
const complete = await inspect(root, { max_probe_bytes: 8192 });
|
|
expect(partial.report.coverage.status).toBe("partial");
|
|
expect(complete.report.coverage.status).toBe("complete-within-scan");
|
|
expect(partial.report.files.map((file) => file.file_id)).toEqual(
|
|
complete.report.files.map((file) => file.file_id),
|
|
);
|
|
expect(partial.report.files.map((file) => file.ranges)).not.toEqual(
|
|
complete.report.files.map((file) => file.ranges),
|
|
);
|
|
expect(partial.report.root.snapshot_sha256).not.toBe(
|
|
complete.report.root.snapshot_sha256,
|
|
);
|
|
});
|
|
|
|
it("rejects expanded managed facts before serializing an over-budget value", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-fact-capacity-");
|
|
const name = `UnityEngine.${"\u0001".repeat(600_000)}`;
|
|
await put(
|
|
root,
|
|
"Game_Data/Managed/Assembly-CSharp.dll",
|
|
buildManagedPeFixture({ references: [name] }),
|
|
);
|
|
const serialized = vi.spyOn(JSON, "stringify");
|
|
onTestFinished(() => serialized.mockRestore());
|
|
const observed = await inspect(root);
|
|
const materializedFacts = serialized.mock.calls.filter(
|
|
([value]) =>
|
|
value !== null &&
|
|
typeof value === "object" &&
|
|
Reflect.get(value, "name") === name,
|
|
).length;
|
|
serialized.mockRestore();
|
|
const file = observed.report.files.find((row) => row.path.endsWith(".dll"));
|
|
expect(file?.facts.map((fact) => fact.kind)).toEqual([
|
|
"native-image-header",
|
|
"pe-cli-directory",
|
|
]);
|
|
expect(file?.issues).toContain(
|
|
"Directory facts reached the retained metadata budget",
|
|
);
|
|
expect(observed.report.coverage.status).toBe("partial");
|
|
expect(
|
|
observed.report.candidates.map((candidate) => candidate.family),
|
|
).not.toContain("unity-mono");
|
|
expect(materializedFacts).toBe(0);
|
|
});
|
|
|
|
it("preserves admitted Unicode and escaped AssemblyRef names in complete Evidence", async () => {
|
|
const root = await createTestTempDirectory("rea-desktop-fact-text-");
|
|
const name = `UnityEngine.${'雪🧪"\\\n\u0001'.repeat(1024)}`;
|
|
await put(
|
|
root,
|
|
"Game_Data/Managed/Assembly-CSharp.dll",
|
|
buildManagedPeFixture({ references: [name] }),
|
|
);
|
|
const { report } = await inspect(root);
|
|
const file = report.files.find((row) => row.path.endsWith(".dll"));
|
|
expect(file?.issues).toEqual([]);
|
|
expect(file?.facts).toContainEqual({
|
|
kind: "unity-assembly-reference",
|
|
offset: expect.any(Number),
|
|
value: {
|
|
name,
|
|
token: "0x23000001",
|
|
managed_directory: "Game_Data/Managed",
|
|
},
|
|
});
|
|
expect(report.coverage.status).toBe("complete-within-scan");
|
|
expect(report.candidates.map((candidate) => candidate.family)).toContain(
|
|
"unity-mono",
|
|
);
|
|
});
|
|
|
|
it.each([27, 28, 31, 106, 0xffffffff])(
|
|
"preserves raw metadata version %s and suggests inspection only when supported",
|
|
async (version) => {
|
|
const root = await createTestTempDirectory("rea-desktop-metadata-probe-");
|
|
const bytes = Buffer.alloc(8);
|
|
bytes.writeUInt32LE(0xfab11baf);
|
|
bytes.writeUInt32LE(version, 4);
|
|
await put(root, "global-metadata.dat", bytes);
|
|
const { report } = await inspect(root);
|
|
expect(report.candidates).toEqual([]);
|
|
expect(report.files[0]?.facts).toContainEqual({
|
|
kind: "il2cpp-metadata-header",
|
|
offset: 0,
|
|
value: { magic: "0xfab11baf", raw_version: version },
|
|
});
|
|
expect(report.next_probes).toEqual(
|
|
version === 27 || version === 31
|
|
? [
|
|
expect.objectContaining({
|
|
operation: "inspect_il2cpp_metadata",
|
|
path: join(await realpath(root), "global-metadata.dat"),
|
|
file_id: report.files[0]?.file_id,
|
|
}),
|
|
]
|
|
: [],
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each([4, 5, 6, 7])(
|
|
"retains IL2CPP magic with an unknown version for a %i-byte prefix",
|
|
async (length) => {
|
|
const root = await createTestTempDirectory("rea-desktop-metadata-prefix-");
|
|
const bytes = Buffer.alloc(length);
|
|
bytes.writeUInt32LE(0xfab11baf);
|
|
await put(root, "global-metadata.dat", bytes);
|
|
const { report } = await inspect(root);
|
|
const metadata = report.files.find((file) =>
|
|
file.path.endsWith("global-metadata.dat"),
|
|
);
|
|
|
|
expect(metadata?.facts).toContainEqual({
|
|
kind: "il2cpp-metadata-header",
|
|
offset: 0,
|
|
value: { magic: "0xfab11baf", raw_version: null },
|
|
});
|
|
expect(report.next_probes).not.toContainEqual(
|
|
expect.objectContaining({ operation: "inspect_il2cpp_metadata" }),
|
|
);
|
|
},
|
|
);
|