Files
rea/tests/boundary/filesystem/desktopApplication.test.ts
T

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" }),
);
},
);