209 lines
7.2 KiB
C#
209 lines
7.2 KiB
C#
using System.Reflection;
|
|
using System.Reflection.Metadata;
|
|
using System.Runtime.Loader;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace gregExtractor;
|
|
|
|
public sealed class Il2CppMetadataScanner
|
|
{
|
|
public SourceSnapshot Scan(string il2CppAssembliesRoot)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(il2CppAssembliesRoot) || !Directory.Exists(il2CppAssembliesRoot))
|
|
throw new DirectoryNotFoundException($"Il2Cpp assemblies directory not found: {il2CppAssembliesRoot}");
|
|
|
|
string assemblyCSharpPath = Path.Combine(il2CppAssembliesRoot, "Assembly-CSharp.dll");
|
|
if (!File.Exists(assemblyCSharpPath))
|
|
throw new FileNotFoundException("Assembly-CSharp.dll not found in Il2Cpp assemblies directory.", assemblyCSharpPath);
|
|
|
|
string runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location)
|
|
?? throw new InvalidOperationException("Runtime directory could not be resolved.");
|
|
|
|
string[] runtimeAssemblies = Directory.EnumerateFiles(runtimeDir, "*.dll", SearchOption.TopDirectoryOnly).ToArray();
|
|
string[] gameAssemblies = Directory.EnumerateFiles(il2CppAssembliesRoot, "*.dll", SearchOption.TopDirectoryOnly).ToArray();
|
|
|
|
string[] resolverPaths = runtimeAssemblies
|
|
.Concat(gameAssemblies)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToArray();
|
|
|
|
var methods = new List<MethodSnapshot>(capacity: 8192);
|
|
|
|
var resolver = new PathAssemblyResolver(resolverPaths);
|
|
using var metadataContext = new MetadataLoadContext(resolver);
|
|
|
|
foreach (string assemblyPath in gameAssemblies)
|
|
{
|
|
Assembly? assembly = null;
|
|
try
|
|
{
|
|
assembly = metadataContext.LoadFromAssemblyPath(assemblyPath);
|
|
}
|
|
catch
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string normalizedAssembly = NormalizeAssemblyName(assembly.GetName().Name);
|
|
Type[] types;
|
|
try
|
|
{
|
|
types = assembly.GetTypes();
|
|
}
|
|
catch (ReflectionTypeLoadException ex)
|
|
{
|
|
types = ex.Types.Where(t => t != null).Cast<Type>().ToArray();
|
|
}
|
|
|
|
foreach (Type type in types)
|
|
{
|
|
if (type.IsGenericTypeDefinition)
|
|
continue;
|
|
|
|
string normalizedTypeName;
|
|
try
|
|
{
|
|
normalizedTypeName = NormalizeTypeName(type);
|
|
}
|
|
catch
|
|
{
|
|
continue;
|
|
}
|
|
|
|
MethodInfo[] declaredMethods;
|
|
try
|
|
{
|
|
declaredMethods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
|
|
}
|
|
catch
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (MethodInfo method in declaredMethods)
|
|
{
|
|
if (method.IsSpecialName)
|
|
continue;
|
|
|
|
string argList;
|
|
try
|
|
{
|
|
argList = string.Join(", ", method.GetParameters().Select(p => NormalizeParameterTypeName(p.ParameterType)));
|
|
}
|
|
catch
|
|
{
|
|
argList = string.Empty;
|
|
}
|
|
|
|
string patchSignature = $"{normalizedTypeName}::{method.Name}({argList})";
|
|
string bodyHash = ComputeMetadataHash(assembly.ManifestModule.ModuleVersionId, method.MetadataToken, patchSignature);
|
|
|
|
methods.Add(new MethodSnapshot
|
|
{
|
|
Assembly = normalizedAssembly,
|
|
TypeName = normalizedTypeName,
|
|
MethodName = method.Name,
|
|
SignatureKey = $"{normalizedAssembly}|{patchSignature}",
|
|
BodyHash = bodyHash,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return new SourceSnapshot
|
|
{
|
|
CreatedUtc = DateTime.UtcNow,
|
|
SourceRoot = il2CppAssembliesRoot,
|
|
FileCount = gameAssemblies.Length,
|
|
Methods = methods
|
|
.GroupBy(m => m.SignatureKey, StringComparer.Ordinal)
|
|
.Select(g => g.First())
|
|
.OrderBy(m => m.SignatureKey, StringComparer.Ordinal)
|
|
.ToList(),
|
|
};
|
|
}
|
|
|
|
private static string ComputeMetadataHash(Guid moduleVersionId, int metadataToken, string signature)
|
|
{
|
|
string raw = $"{moduleVersionId:N}:{metadataToken:X8}:{signature}";
|
|
byte[] data = SHA256.HashData(Encoding.UTF8.GetBytes(raw));
|
|
return Convert.ToHexString(data);
|
|
}
|
|
|
|
private static string NormalizeAssemblyName(string? assemblyName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(assemblyName))
|
|
return "UNKNOWN";
|
|
|
|
return assemblyName.Equals("Assembly-CSharp", StringComparison.OrdinalIgnoreCase)
|
|
? "Il2Cpp"
|
|
: assemblyName;
|
|
}
|
|
|
|
private static string NormalizeTypeName(Type type)
|
|
{
|
|
string fullName;
|
|
try
|
|
{
|
|
fullName = type.FullName?.Replace('+', '.') ?? type.Name;
|
|
}
|
|
catch
|
|
{
|
|
fullName = type.Name;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(fullName))
|
|
fullName = type.Name;
|
|
|
|
if (!fullName.StartsWith("Il2Cpp.", StringComparison.Ordinal))
|
|
fullName = $"Il2Cpp.{fullName}";
|
|
|
|
return fullName;
|
|
}
|
|
|
|
private static string NormalizeParameterTypeName(Type parameterType)
|
|
{
|
|
if (parameterType.IsByRef)
|
|
parameterType = parameterType.GetElementType() ?? parameterType;
|
|
|
|
if (parameterType.IsArray)
|
|
{
|
|
Type elementType = parameterType.GetElementType() ?? parameterType;
|
|
return $"{NormalizeParameterTypeName(elementType)}[]";
|
|
}
|
|
|
|
if (parameterType.IsGenericType)
|
|
{
|
|
string genericName = parameterType.Name;
|
|
int tickIndex = genericName.IndexOf('`');
|
|
if (tickIndex >= 0)
|
|
genericName = genericName[..tickIndex];
|
|
|
|
string genericArgs = string.Join(", ", parameterType.GetGenericArguments().Select(NormalizeParameterTypeName));
|
|
return $"{genericName}<{genericArgs}>";
|
|
}
|
|
|
|
return parameterType.FullName switch
|
|
{
|
|
"System.Void" => "void",
|
|
"System.Boolean" => "bool",
|
|
"System.Byte" => "byte",
|
|
"System.SByte" => "sbyte",
|
|
"System.Int16" => "short",
|
|
"System.UInt16" => "ushort",
|
|
"System.Int32" => "int",
|
|
"System.UInt32" => "uint",
|
|
"System.Int64" => "long",
|
|
"System.UInt64" => "ulong",
|
|
"System.Single" => "float",
|
|
"System.Double" => "double",
|
|
"System.Decimal" => "decimal",
|
|
"System.String" => "string",
|
|
"System.Char" => "char",
|
|
"System.Object" => "object",
|
|
_ => parameterType.FullName?.Replace('+', '.') ?? parameterType.Name,
|
|
};
|
|
}
|
|
}
|