mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 14:26:07 +00:00
feat(desktop): suggest managed members for Unity script assemblies (#5511)
* feat(desktop): suggest managed members for Unity predefined script assemblies Unity Mono triage listed only inspect_managed_artifact probes, mostly for engine and framework assemblies. Assembly-CSharp.dll and Assembly-CSharp-firstpass.dll directly in a managed directory with a UnityEngine reference witness now get an inspect_managed_members probe, ordered before every managed artifact probe. * docs(desktop): describe Unity Mono managed member probes * refactor(desktop): read Unity managed directory from the witness fact * refactor(desktop): reuse the managed directory index for predefined assemblies * feat(desktop): point Unity member probes at summary delivery first * test(desktop): no member probes without a Unity reference witness * docs(desktop): trim Unity member probe note * feat(desktop): suggest managed members in Unity IL2CPP backup folders Unity IL2CPP builds write <Product>_BackUpThisFolder_ButDontShipItWithYourGame next to the player. When shipped, its Managed/ holds pre-IL2CPP assemblies. A path convention now records the folder on Assembly-CSharp*.dll directly in its Managed/, including when the admitted root is the folder itself, and triage offers inspect_managed_members for those files. Correspondence to the shipped native code stays unknown. * docs(desktop): note IL2CPP backup folder member probes * docs(desktop): list IL2CPP metadata versions 24 and 27 for the metadata probe * style(desktop): merge stacked doc comments on desktopPathConventions * test(desktop): cover member probes in a macOS bundle managed directory * docs(desktop): note that member probes need the game folder as root * test(desktop): no member probe when the root is the Unity data folder * test(desktop): check the data-folder root still gets managed probes * test(desktop): check the IL2CPP backup convention against the probe bound --------- Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
1 parent
398eb0d1f1
commit
abc647bf6c
7 files changed
+307
-10
No files matched your search
@@ -110,6 +110,15 @@ not establish full metadata validity or correspondence to a native binary;
|
||||
version 24 inspection still applies only to supported header layouts. See
|
||||
[IL2CPP metadata](il2cpp-metadata.md).
|
||||
|
||||
In a managed directory with a Unity Mono witness, `Assembly-CSharp.dll` and
|
||||
`Assembly-CSharp-firstpass.dll` get an `inspect_managed_members` probe listed
|
||||
first; project-named asmdef assemblies keep only the ordinary managed probes.
|
||||
The same probe is offered for those names in `Managed/` of a
|
||||
`<Product>_BackUpThisFolder_ButDontShipItWithYourGame` folder (or when that
|
||||
folder is the root); whether its IL matches the shipped IL2CPP code is unknown.
|
||||
Point the scan at the game folder: a root that is the `<Product>_Data` or
|
||||
`Managed` folder itself has no Mono witness path and gets no member probe.
|
||||
|
||||
Candidates are derived or inferred from these facts and do not prove loading or
|
||||
execution. BepInEx, MelonLoader, Easy Anti-Cheat and BattlEye labels identify
|
||||
path conventions only; runtime presence stays unknown. No bypass is provided.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { desktopPathConventions } from "../domain/nativeConvention.js";
|
||||
import { isAbsolute } from "node:path";
|
||||
import { basename, isAbsolute } from "node:path";
|
||||
import {
|
||||
DirectoryArtifactReader,
|
||||
DirectoryArtifactRootFailure,
|
||||
@@ -236,6 +236,7 @@ const scanDirectory = async (
|
||||
const files: DesktopFile[] = [];
|
||||
const omissions: DesktopOmission[] = [];
|
||||
const unityWitnessRoots = new Set<string>();
|
||||
const rootName = basename(root.canonical_path);
|
||||
const budget: DesktopReadBudget = {
|
||||
bytes: 0,
|
||||
maximum: input.max_probe_bytes,
|
||||
@@ -299,9 +300,12 @@ const scanDirectory = async (
|
||||
entries++;
|
||||
abortIfNeeded(signal);
|
||||
const conventions =
|
||||
entry.kind === "file" ? pathConventions(entry.path) : undefined;
|
||||
entry.kind === "file"
|
||||
? pathConventions(entry.path, rootName)
|
||||
: undefined;
|
||||
// Reserve the row and every repeated path before retaining them: each
|
||||
// convention fact copies the path and its probes copy the local path.
|
||||
// convention fact copies at most the path and its probes copy the
|
||||
// local path.
|
||||
reserve(
|
||||
RETAINED_ROW_BYTES +
|
||||
retainedTextBytes(entry.path) +
|
||||
@@ -487,10 +491,19 @@ const fileRow = (
|
||||
};
|
||||
};
|
||||
|
||||
/** Path-convention facts of one file; each repeats the path as its value. */
|
||||
const pathConventions = (path: string) => {
|
||||
const conventions = desktopPathConventions(path);
|
||||
/** Path-convention facts of one file; each value repeats at most the path. */
|
||||
const pathConventions = (path: string, rootName: string) => {
|
||||
const conventions = desktopPathConventions(path, rootName);
|
||||
const facts: DesktopFact[] = [];
|
||||
if (
|
||||
conventions.unityIl2cppBackupAssembly &&
|
||||
conventions.unityIl2cppBackupDirectory !== null
|
||||
)
|
||||
facts.push({
|
||||
kind: "unity-il2cpp-backup-convention",
|
||||
offset: null,
|
||||
value: { backup_directory: conventions.unityIl2cppBackupDirectory },
|
||||
});
|
||||
if (conventions.electronArchive)
|
||||
facts.push({ kind: "electron-asar-convention", offset: null, value: path });
|
||||
if (conventions.unrealContainer)
|
||||
|
||||
@@ -93,6 +93,11 @@ const desktopFactSchema = z.discriminatedUnion("kind", [
|
||||
offset: z.null(),
|
||||
value: z.string(),
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("unity-il2cpp-backup-convention"),
|
||||
offset: z.null(),
|
||||
value: z.strictObject({ backup_directory: z.string() }),
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("electron-asar-convention"),
|
||||
offset: z.null(),
|
||||
@@ -162,6 +167,7 @@ export const desktopProbeSchema = z.strictObject({
|
||||
probe_id: prefixedDigestSchema("dsp"),
|
||||
operation: z.enum([
|
||||
"inspect_managed_artifact",
|
||||
"inspect_managed_members",
|
||||
"inspect_il2cpp_metadata",
|
||||
"open_binary",
|
||||
"analyze_javascript_application",
|
||||
|
||||
@@ -62,6 +62,11 @@ const facts: DesktopFact[] = [
|
||||
value: { container: "FORM", first_chunk: "GEN8" },
|
||||
},
|
||||
{ kind: "il2cpp-library-convention", offset: null, value: "p" },
|
||||
{
|
||||
kind: "unity-il2cpp-backup-convention",
|
||||
offset: null,
|
||||
value: { backup_directory: "B" },
|
||||
},
|
||||
{ kind: "electron-asar-convention", offset: null, value: "p" },
|
||||
{ kind: "unreal-container-convention", offset: null, value: "p" },
|
||||
];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IL2CPP_METADATA_VERSIONS } from "./il2cpp/il2cppMetadata.js";
|
||||
import { digestCanonicalValue } from "./canonicalDigest.js";
|
||||
import { desktopPathConventions } from "./nativeConvention.js";
|
||||
import type { DesktopApplication, DesktopFile } from "./desktopApplication.js";
|
||||
|
||||
/** The most next probes this projection derives from any one fact. */
|
||||
@@ -53,13 +54,42 @@ export const projectDesktopApplication = (
|
||||
managed,
|
||||
"PE optional headers declare nonempty CLI directories; CLR execution and metadata validity remain unverified.",
|
||||
);
|
||||
const mono = managed.filter((file) => has(file, "unity-assembly-reference"));
|
||||
const monoDirectories = new Set(
|
||||
mono.flatMap((file) =>
|
||||
file.facts.flatMap((fact) =>
|
||||
fact.kind === "unity-assembly-reference"
|
||||
? [fact.value.managed_directory]
|
||||
: [],
|
||||
),
|
||||
),
|
||||
);
|
||||
for (const file of managed) {
|
||||
const conventions = desktopPathConventions(file.path);
|
||||
if (
|
||||
conventions.unityPredefinedAssembly &&
|
||||
conventions.unityManagedDirectory !== null &&
|
||||
monoDirectories.has(conventions.unityManagedDirectory)
|
||||
)
|
||||
probe(
|
||||
"inspect_managed_members",
|
||||
file,
|
||||
'Inventory types, members and user strings of this Unity predefined script assembly; its references and contents are unverified. Start with detail "summary" and page rows through inspect_analysis_view.',
|
||||
);
|
||||
}
|
||||
for (const file of managed)
|
||||
if (has(file, "unity-il2cpp-backup-convention"))
|
||||
probe(
|
||||
"inspect_managed_members",
|
||||
file,
|
||||
'Inventory types, members and user strings of this predefined script assembly in a Unity IL2CPP backup folder; its correspondence to the shipped native code is unknown. Start with detail "summary" and page rows through inspect_analysis_view.',
|
||||
);
|
||||
for (const file of managed)
|
||||
probe(
|
||||
"inspect_managed_artifact",
|
||||
file,
|
||||
"Inspect the declared managed metadata and references.",
|
||||
);
|
||||
const mono = managed.filter((file) => has(file, "unity-assembly-reference"));
|
||||
candidate(
|
||||
"unity-mono",
|
||||
mono,
|
||||
|
||||
@@ -22,6 +22,11 @@ const DESKTOP_IL2CPP_LIBRARIES = new Set([
|
||||
"gameassembly.dylib",
|
||||
"libil2cpp.so",
|
||||
]);
|
||||
/** Unity's predefined script assembly names; asmdef assembly names are project-chosen. */
|
||||
const UNITY_PREDEFINED_ASSEMBLIES = new Set([
|
||||
"assembly-csharp.dll",
|
||||
"assembly-csharp-firstpass.dll",
|
||||
]);
|
||||
|
||||
/**
|
||||
* A runtime family named by the library filename or a containing framework.
|
||||
@@ -48,11 +53,19 @@ export const isSwiftRuntimeLibrary = (path: string): boolean =>
|
||||
.split("/")
|
||||
.some((segment) => /^libswift.+\.dylib$/u.test(foldSegment(segment)));
|
||||
|
||||
/** Parse desktop layout conventions once while preserving original path segments. */
|
||||
export const desktopPathConventions = (path: string) => {
|
||||
/** Unity names the pre-IL2CPP managed backup `<Product>_BackUpThisFolder_ButDontShipItWithYourGame`. */
|
||||
const UNITY_IL2CPP_BACKUP_SUFFIX =
|
||||
"_backupthisfolder_butdontshipitwithyourgame";
|
||||
|
||||
/**
|
||||
* Parse desktop layout conventions once while preserving original path segments.
|
||||
* `rootName` is the admitted root directory name; `.` denotes that root in returned directories.
|
||||
*/
|
||||
export const desktopPathConventions = (path: string, rootName?: string) => {
|
||||
const segments = path.split("/");
|
||||
const folded = segments.map(foldSegment);
|
||||
let unityManagedDirectory: string | null = null;
|
||||
let unityManagedIndex = -1;
|
||||
for (let index = 0; index + 2 < folded.length; index++) {
|
||||
const segment = folded[index];
|
||||
const namedDataDirectory =
|
||||
@@ -64,12 +77,39 @@ export const desktopPathConventions = (path: string) => {
|
||||
if (
|
||||
(namedDataDirectory || bundleDataDirectory) &&
|
||||
folded[index + 1] === "managed"
|
||||
)
|
||||
) {
|
||||
unityManagedDirectory = segments.slice(0, index + 2).join("/");
|
||||
unityManagedIndex = index + 1;
|
||||
}
|
||||
}
|
||||
// The admitted root may itself be the backup folder, so its name precedes the relative segments.
|
||||
const rooted =
|
||||
rootName === undefined ? folded : [foldSegment(rootName), ...folded];
|
||||
const shift = rooted.length - folded.length;
|
||||
let unityIl2cppBackupDirectory: string | null = null;
|
||||
let unityIl2cppBackupIndex = -1;
|
||||
for (let index = 0; index + 1 < rooted.length; index++) {
|
||||
const segment = rooted[index] ?? "";
|
||||
if (
|
||||
segment.length > UNITY_IL2CPP_BACKUP_SUFFIX.length &&
|
||||
segment.endsWith(UNITY_IL2CPP_BACKUP_SUFFIX)
|
||||
) {
|
||||
unityIl2cppBackupDirectory =
|
||||
index < shift ? "." : segments.slice(0, index - shift + 1).join("/");
|
||||
unityIl2cppBackupIndex = index - shift;
|
||||
}
|
||||
}
|
||||
const name = folded.at(-1) ?? "";
|
||||
return {
|
||||
unityIl2cppBackupDirectory,
|
||||
unityIl2cppBackupAssembly:
|
||||
unityIl2cppBackupIndex === segments.length - 3 &&
|
||||
folded[unityIl2cppBackupIndex + 1] === "managed" &&
|
||||
UNITY_PREDEFINED_ASSEMBLIES.has(name),
|
||||
unityManagedDirectory,
|
||||
unityPredefinedAssembly:
|
||||
unityManagedIndex === segments.length - 2 &&
|
||||
UNITY_PREDEFINED_ASSEMBLIES.has(name),
|
||||
il2cppLibrary: DESKTOP_IL2CPP_LIBRARIES.has(name),
|
||||
electronArchive:
|
||||
folded.at(-2) === "resources" &&
|
||||
|
||||
@@ -500,6 +500,200 @@ it.each(["Game_Data/Managed", "Contents/Resources/Data/Managed"])(
|
||||
},
|
||||
);
|
||||
|
||||
it("orders member probes for Unity predefined script assemblies before every managed artifact probe", async () => {
|
||||
const root = await createTestTempDirectory("rea-desktop-mono-members-");
|
||||
const unity = buildManagedPeFixture({
|
||||
references: ["UnityEngine.CoreModule"],
|
||||
});
|
||||
const plain = buildManagedPeFixture({ references: ["System.Runtime"] });
|
||||
await put(root, "Game_Data/Managed/Assembly-CSharp.dll", unity);
|
||||
await put(root, "Game_Data/Managed/ASSEMBLY-CSHARP-FIRSTPASS.DLL", plain);
|
||||
await put(root, "Game_Data/Managed/Game.Logic.dll", unity);
|
||||
await put(root, "Game_Data/Managed/UnityEngine.CoreModule.dll", plain);
|
||||
await put(root, "Game_Data/Managed/Nested/Assembly-CSharp.dll", plain);
|
||||
await put(root, "Other_Data/Managed/Assembly-CSharp.dll", plain);
|
||||
await put(root, "Plugins/Assembly-CSharp.dll", unity);
|
||||
const { report } = await inspect(root);
|
||||
const relative = new Map(
|
||||
report.files.map((file) => [file.file_id, file.path]),
|
||||
);
|
||||
const rows = report.next_probes.map((probe) => [
|
||||
probe.operation,
|
||||
relative.get(probe.file_id),
|
||||
]);
|
||||
expect(
|
||||
rows.filter(([operation]) => operation === "inspect_managed_members"),
|
||||
).toEqual([
|
||||
[
|
||||
"inspect_managed_members",
|
||||
"Game_Data/Managed/ASSEMBLY-CSHARP-FIRSTPASS.DLL",
|
||||
],
|
||||
["inspect_managed_members", "Game_Data/Managed/Assembly-CSharp.dll"],
|
||||
]);
|
||||
expect(rows.slice(0, 2).map(([operation]) => operation)).toEqual([
|
||||
"inspect_managed_members",
|
||||
"inspect_managed_members",
|
||||
]);
|
||||
expect(
|
||||
rows.filter(([operation]) => operation === "inspect_managed_artifact"),
|
||||
).toHaveLength(7);
|
||||
for (const probe of report.next_probes)
|
||||
if (probe.operation === "inspect_managed_members")
|
||||
expect(probe.rationale).toContain(
|
||||
'Start with detail "summary" and page rows through inspect_analysis_view.',
|
||||
);
|
||||
});
|
||||
|
||||
it("offers member probes for predefined script assemblies in a macOS bundle's managed directory", async () => {
|
||||
const root = await createTestTempDirectory("rea-desktop-mono-bundle-");
|
||||
const managed = "Contents/Resources/Data/Managed";
|
||||
await put(
|
||||
root,
|
||||
`${managed}/Assembly-CSharp.dll`,
|
||||
buildManagedPeFixture({ references: ["UnityEngine.CoreModule"] }),
|
||||
);
|
||||
await put(
|
||||
root,
|
||||
`${managed}/Game.Logic.dll`,
|
||||
buildManagedPeFixture({ references: ["System.Runtime"] }),
|
||||
);
|
||||
const { report } = await inspect(root);
|
||||
const relative = new Map(
|
||||
report.files.map((file) => [file.file_id, file.path]),
|
||||
);
|
||||
expect(
|
||||
report.next_probes
|
||||
.filter((probe) => probe.operation === "inspect_managed_members")
|
||||
.map((probe) => relative.get(probe.file_id)),
|
||||
).toEqual([`${managed}/Assembly-CSharp.dll`]);
|
||||
expect(report.next_probes[0]?.operation).toBe("inspect_managed_members");
|
||||
});
|
||||
|
||||
it.each(["Game_Data", "Game_Data/Managed"])(
|
||||
"offers no member probe when the root is %s itself",
|
||||
async (rootDirectory) => {
|
||||
const root = await createTestTempDirectory("rea-desktop-mono-data-root-");
|
||||
await put(
|
||||
root,
|
||||
"Game_Data/Managed/Assembly-CSharp.dll",
|
||||
buildManagedPeFixture({ references: ["UnityEngine.CoreModule"] }),
|
||||
);
|
||||
const { report } = await inspect(join(root, rootDirectory));
|
||||
const operations = report.next_probes.map((probe) => probe.operation);
|
||||
expect(operations).toContain("inspect_managed_artifact");
|
||||
expect(operations).not.toContain("inspect_managed_members");
|
||||
},
|
||||
);
|
||||
|
||||
it("offers no member probes when no managed directory has a Unity reference witness", async () => {
|
||||
const root = await createTestTempDirectory("rea-desktop-mono-no-witness-");
|
||||
const plain = buildManagedPeFixture({ references: ["System.Runtime"] });
|
||||
await put(root, "Game_Data/Managed/Assembly-CSharp.dll", plain);
|
||||
await put(root, "Game_Data/Managed/Assembly-CSharp-firstpass.dll", plain);
|
||||
await put(root, "Contents/Resources/Data/Managed/Assembly-CSharp.dll", plain);
|
||||
const { report } = await inspect(root);
|
||||
expect(report.candidates.map((row) => row.family)).toEqual(["dotnet"]);
|
||||
expect(report.next_probes.map((probe) => probe.operation)).toEqual([
|
||||
"inspect_managed_artifact",
|
||||
"inspect_managed_artifact",
|
||||
"inspect_managed_artifact",
|
||||
]);
|
||||
});
|
||||
|
||||
const BACKUP = "Game_BackUpThisFolder_ButDontShipItWithYourGame";
|
||||
|
||||
it("offers member probes for predefined script assemblies in a game directory's IL2CPP backup folder", async () => {
|
||||
const root = await createTestTempDirectory("rea-desktop-il2cpp-backup-");
|
||||
const plain = buildManagedPeFixture({ references: ["System.Runtime"] });
|
||||
await put(root, `${BACKUP}/Managed/Assembly-CSharp.dll`, plain);
|
||||
await put(
|
||||
root,
|
||||
"OTHER_BACKUPTHISFOLDER_BUTDONTSHIPITWITHYOURGAME/MANAGED/ASSEMBLY-CSHARP-FIRSTPASS.DLL",
|
||||
plain,
|
||||
);
|
||||
await put(root, `${BACKUP}/Managed/Game.Logic.dll`, plain);
|
||||
await put(root, `${BACKUP}/Managed/Nested/Assembly-CSharp.dll`, plain);
|
||||
await put(root, `${BACKUP}/Assembly-CSharp.dll`, plain);
|
||||
await put(
|
||||
root,
|
||||
"_BackUpThisFolder_ButDontShipItWithYourGame/Managed/Assembly-CSharp.dll",
|
||||
plain,
|
||||
);
|
||||
await put(root, "Game_BackUpThisFolder/Managed/Assembly-CSharp.dll", plain);
|
||||
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);
|
||||
const relative = new Map(report.files.map((file) => [file.file_id, file]));
|
||||
const members = report.next_probes.filter(
|
||||
(probe) => probe.operation === "inspect_managed_members",
|
||||
);
|
||||
expect(
|
||||
members.map((probe) => relative.get(probe.file_id)?.path).sort(),
|
||||
).toEqual(
|
||||
[
|
||||
`${BACKUP}/Managed/Assembly-CSharp.dll`,
|
||||
"OTHER_BACKUPTHISFOLDER_BUTDONTSHIPITWITHYOURGAME/MANAGED/ASSEMBLY-CSHARP-FIRSTPASS.DLL",
|
||||
].sort(),
|
||||
);
|
||||
for (const probe of members) {
|
||||
expect(probe.rationale).toContain(
|
||||
"correspondence to the shipped native code is unknown",
|
||||
);
|
||||
expect(relative.get(probe.file_id)?.facts).toContainEqual({
|
||||
kind: "unity-il2cpp-backup-convention",
|
||||
offset: null,
|
||||
value: {
|
||||
backup_directory: relative.get(probe.file_id)!.path.split("/")[0],
|
||||
},
|
||||
});
|
||||
}
|
||||
const order = report.next_probes.map((probe) => probe.operation);
|
||||
expect(order.lastIndexOf("inspect_managed_members")).toBeLessThan(
|
||||
order.indexOf("inspect_il2cpp_metadata"),
|
||||
);
|
||||
expect(report.candidates.map((row) => row.family)).not.toContain(
|
||||
"unity-mono",
|
||||
);
|
||||
expect(
|
||||
report.files.filter((file) =>
|
||||
file.facts.some((fact) => fact.kind === "unity-il2cpp-backup-convention"),
|
||||
),
|
||||
).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("recognizes an admitted root that is itself the IL2CPP backup folder", async () => {
|
||||
const parent = await createTestTempDirectory(
|
||||
"rea-desktop-il2cpp-backup-root-",
|
||||
);
|
||||
const plain = buildManagedPeFixture({ references: ["System.Runtime"] });
|
||||
await put(parent, `${BACKUP}/Managed/Assembly-CSharp.dll`, plain);
|
||||
await put(parent, "Ordinary/Managed/Assembly-CSharp.dll", plain);
|
||||
const backup = await inspect(join(parent, BACKUP));
|
||||
expect(backup.report.next_probes.map((probe) => probe.operation)).toEqual([
|
||||
"inspect_managed_members",
|
||||
"inspect_managed_artifact",
|
||||
]);
|
||||
expect(
|
||||
backup.report.files.find(
|
||||
(file) => file.path === "Managed/Assembly-CSharp.dll",
|
||||
)?.facts,
|
||||
).toContainEqual({
|
||||
kind: "unity-il2cpp-backup-convention",
|
||||
offset: null,
|
||||
value: { backup_directory: "." },
|
||||
});
|
||||
const ordinary = await inspect(join(parent, "Ordinary"));
|
||||
expect(ordinary.report.next_probes.map((probe) => probe.operation)).toEqual([
|
||||
"inspect_managed_artifact",
|
||||
]);
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
Reference in new issue
Block a user