Files
reverse-engineering-anything/src/domain/desktopApplicationProjection.ts
T
JANG BYUNGKUNandgensokyo-dev abc647bf6c 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>
2026-11-04 19:53:00 +08:00

215 lines
7.3 KiB
TypeScript

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. */
export const DESKTOP_PROBES_PER_FACT = 2;
/** Interpret only the facts admitted by the directory reader, never target bytes. */
export const projectDesktopApplication = (
files: readonly DesktopFile[],
): Pick<DesktopApplication, "candidates" | "components" | "next_probes"> => {
const candidates: DesktopApplication["candidates"] = [];
const components: DesktopApplication["components"] = [];
const probes = new Map<string, DesktopApplication["next_probes"][number]>();
const has = (file: DesktopFile, kind: string) =>
file.facts.some((fact) => fact.kind === kind);
const candidate = (
family: DesktopApplication["candidates"][number]["family"],
basis: readonly DesktopFile[],
rationale: string,
confidence: "derived" | "inferred" = "derived",
) => {
if (basis.length === 0) return;
const semantic = {
family,
basis_file_ids: [...new Set(basis.map((file) => file.file_id))].sort(),
rationale,
confidence,
};
candidates.push({
candidate_id: `dsc_${digestCanonicalValue(semantic, "Desktop runtime candidate")}`,
...semantic,
});
};
const probe = (
operation: DesktopApplication["next_probes"][number]["operation"],
file: DesktopFile,
rationale: string,
) => {
const semantic = {
operation,
path: file.local_path,
file_id: file.file_id,
rationale,
confidence: "inferred" as const,
};
const probe_id = `dsp_${digestCanonicalValue(semantic, "Desktop next probe")}`;
probes.set(probe_id, { probe_id, ...semantic });
};
const managed = files.filter((file) => has(file, "pe-cli-directory"));
candidate(
"dotnet",
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.",
);
candidate(
"unity-mono",
mono,
"Managed assemblies in a Unity Data/Managed layout reference UnityEngine assemblies.",
);
const metadata = files.filter((file) => has(file, "il2cpp-metadata-header"));
for (const file of metadata)
if (
file.facts.some(
(fact) =>
fact.kind === "il2cpp-metadata-header" &&
IL2CPP_METADATA_VERSIONS.some(
(version) => version === fact.value.raw_version,
),
)
)
probe(
"inspect_il2cpp_metadata",
file,
"Inventory this supported-version metadata candidate; complete validity and native binary correspondence remain unverified.",
);
const il2cpp = files.filter(
(file) =>
has(file, "il2cpp-library-convention") &&
has(file, "native-image-header"),
);
if (metadata.length > 0 && il2cpp.length > 0) {
candidate(
"unity-il2cpp",
[...metadata, ...il2cpp],
"IL2CPP metadata magic and conventionally named native libraries coexist; metadata validity and build correspondence are unverified.",
"inferred",
);
for (const file of il2cpp)
probe(
"open_binary",
file,
"Inspect this native IL2CPP candidate; metadata-to-address correspondence remains unknown.",
);
}
candidate(
"godot",
files.filter((file) => has(file, "godot-pck-header")),
"A standalone or trailer-located Godot pack header was observed; pack contents and encryption are unexamined.",
);
for (const file of files.filter((file) => has(file, "unityfs-header")))
probe(
"open_binary",
file,
"A UnityFS AssetBundle header was observed; member contents and encryption are unexamined.",
);
candidate(
"gamemaker",
files.filter((file) => has(file, "gamemaker-form-header")),
"A FORM container begins with a GEN8 chunk; its game resources remain unexamined.",
);
candidate(
"unreal",
files.filter((file) => has(file, "unreal-container-convention")),
"Unreal Content/Paks paths or an IoStore TOC header suggest an Unreal package layout.",
"inferred",
);
const electron = files.filter((file) =>
has(file, "electron-asar-convention"),
);
candidate(
"electron",
electron,
"An app.asar in a resources directory suggests an Electron application; archive validity and execution are unverified.",
"inferred",
);
for (const file of electron) {
probe(
"open_binary",
file,
"Select this ASAR target with open_binary, then call inspect_artifact to inventory it before selecting JavaScript sources.",
);
probe(
"analyze_javascript_application",
file,
"Analyze the candidate application archive with the static JavaScript workflow.",
);
}
const componentMarkers = [
{
expression: /(?:^|\/)bepinex(?:\/|$)/iu,
name: "BepInEx",
category: "mod-loader" as const,
},
{
expression: /(?:^|\/)melonloader(?:\/|$)/iu,
name: "MelonLoader",
category: "mod-loader" as const,
},
{
expression: /(?:^|\/)easyanticheat(?:_eos)?(?:\/|$)/iu,
name: "Easy Anti-Cheat",
category: "protection" as const,
},
{
expression: /(?:^|\/)battleye(?:\/|$)/iu,
name: "BattlEye",
category: "protection" as const,
},
];
for (const marker of componentMarkers) {
const basis = files.filter((file) => marker.expression.test(file.path));
if (basis.length === 0) continue;
const semantic = {
name: marker.name,
category: marker.category,
basis_file_ids: basis.map((file) => file.file_id),
confidence: "inferred" as const,
runtime_presence: "unknown" as const,
};
components.push({
component_id: `dsm_${digestCanonicalValue(semantic, "Desktop component marker")}`,
...semantic,
});
}
return { candidates, components, next_probes: [...probes.values()] };
};