Files
reverse-engineering-anything/src/domain/nativeConvention.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

127 lines
4.5 KiB
TypeScript

/** Path-segment conventions. These are inferences, not observations of runtime behavior. */
export type NativeRuntimeConvention = "react-native" | "flutter" | "unity";
const foldSegment = (segment: string): string =>
segment.toLocaleLowerCase("en-US");
const REACT_NATIVE_SEGMENTS = new Set([
"react.framework",
"reactnative.framework",
"libreactnative.so",
"libreactnativejni.so",
"libhermes_executor.so",
"libhermes.so",
"hermes.framework",
]);
const FLUTTER_SEGMENTS = new Set(["libflutter.so", "flutter.framework"]);
const UNITY_SEGMENTS = new Set(["libunity.so", "unityframework.framework"]);
const DESKTOP_IL2CPP_LIBRARIES = new Set([
"gameassembly.dll",
"gameassembly.so",
"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.
* A library-named ancestor directory does not establish a convention.
*/
export const nativeRuntimeConvention = (
path: string,
): NativeRuntimeConvention | null => {
const segments = path.split("/");
for (const [index, segment] of segments.entries()) {
const folded = foldSegment(segment);
if (index !== segments.length - 1 && !folded.endsWith(".framework"))
continue;
if (REACT_NATIVE_SEGMENTS.has(folded)) return "react-native";
if (FLUTTER_SEGMENTS.has(folded)) return "flutter";
if (UNITY_SEGMENTS.has(folded)) return "unity";
}
return null;
};
/** Swift runtime libraries are `libswift*.dylib` segments, not every dynamic library. */
export const isSwiftRuntimeLibrary = (path: string): boolean =>
path
.split("/")
.some((segment) => /^libswift.+\.dylib$/u.test(foldSegment(segment)));
/** 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 =
segment !== undefined && segment.length > 5 && segment.endsWith("_data");
const bundleDataDirectory =
segment === "data" &&
folded[index - 1] === "resources" &&
folded[index - 2] === "contents";
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" &&
(name === "app.asar" || name === "default_app.asar"),
unrealContainer:
(name.endsWith(".pak") || name.endsWith(".utoc")) &&
folded.some(
(segment, index) =>
segment === "content" &&
folded[index + 1] === "paks" &&
index + 2 < folded.length,
),
};
};