Files

244 lines
8.1 KiB
C#

using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
namespace gregExtractor;
public sealed class SourceScanner
{
private readonly Il2CppMetadataScanner _metadataScanner = new();
private static readonly Regex ClassRegex = new(
@"^\s*(?:public|private|protected|internal)\s+(?:sealed\s+)?class\s+(?<name>[A-Za-z0-9_]+)(\s*:\s*(?<base>[^\r\n{]+))?",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex MethodRegex = new(
@"^\s+(?:public|private|protected|internal)\s+(?<mods>(?:(?:static|unsafe|virtual|override|abstract|sealed|new|extern|partial|async)\s+)*)?(?<sig>.+?)\s*\((?<args>[^\)]*)\)\s*$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
public SourceSnapshot Scan(string sourceRoot)
{
if (string.IsNullOrWhiteSpace(sourceRoot))
throw new ArgumentException("Source root is required.", nameof(sourceRoot));
if (TryResolveIl2CppAssembliesRoot(sourceRoot, out string? il2CppAssembliesRoot))
{
try
{
return _metadataScanner.Scan(il2CppAssembliesRoot!);
}
catch when (Directory.Exists(sourceRoot))
{
}
}
if (!Directory.Exists(sourceRoot))
throw new DirectoryNotFoundException($"Source root not found: {sourceRoot}");
var methodList = new List<MethodSnapshot>(capacity: 4096);
string[] sourceDirectories = Directory.GetDirectories(sourceRoot)
.Where(path =>
{
string name = Path.GetFileName(path);
return name.StartsWith("Il2Cpp", StringComparison.OrdinalIgnoreCase)
|| name.StartsWith("Unity", StringComparison.OrdinalIgnoreCase)
|| name.StartsWith("UnityEngine", StringComparison.OrdinalIgnoreCase);
})
.ToArray();
if (sourceDirectories.Length == 0)
throw new InvalidOperationException("No supported source directories found. Expected Il2Cpp*/Unity*/UnityEngine* folders or an Il2CppAssemblies DLL path.");
foreach (string directory in sourceDirectories)
{
string assemblyName = Path.GetFileName(directory);
foreach (string file in Directory.EnumerateFiles(directory, "*.cs", SearchOption.AllDirectories))
{
ParseFile(file, assemblyName, methodList);
}
}
return new SourceSnapshot
{
CreatedUtc = DateTime.UtcNow,
SourceRoot = sourceRoot,
FileCount = sourceDirectories.Sum(d => Directory.EnumerateFiles(d, "*.cs", SearchOption.AllDirectories).Count()),
Methods = methodList
.GroupBy(m => m.SignatureKey, StringComparer.Ordinal)
.Select(g => g.First())
.OrderBy(m => m.SignatureKey, StringComparer.Ordinal)
.ToList(),
};
}
private static bool TryResolveIl2CppAssembliesRoot(string path, out string? il2CppAssembliesRoot)
{
il2CppAssembliesRoot = null;
string fullPath = Path.GetFullPath(path);
if (File.Exists(fullPath) && string.Equals(Path.GetFileName(fullPath), "Assembly-CSharp.dll", StringComparison.OrdinalIgnoreCase))
{
il2CppAssembliesRoot = Path.GetDirectoryName(fullPath)!;
return true;
}
if (!Directory.Exists(fullPath))
return false;
string[] candidateDirectories =
{
fullPath,
Path.Combine(fullPath, "MelonLoader", "Il2CppAssemblies"),
Path.Combine(fullPath, "Il2CppAssemblies"),
};
foreach (string candidate in candidateDirectories)
{
if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "Assembly-CSharp.dll")))
{
il2CppAssembliesRoot = candidate;
return true;
}
}
return false;
}
private static void ParseFile(string filePath, string assemblyName, List<MethodSnapshot> output)
{
string? currentClass = null;
string[] lines = File.ReadAllLines(filePath);
for (int index = 0; index < lines.Length; index++)
{
string line = lines[index];
Match classMatch = ClassRegex.Match(line);
if (classMatch.Success)
{
currentClass = classMatch.Groups["name"].Value.Trim();
continue;
}
if (string.IsNullOrWhiteSpace(currentClass))
continue;
Match methodMatch = MethodRegex.Match(line);
if (!methodMatch.Success)
continue;
string signature = methodMatch.Groups["sig"].Value.Trim();
int spaceIndex = signature.LastIndexOf(' ');
if (spaceIndex <= 0)
continue;
string methodName = signature[(spaceIndex + 1)..].Trim();
string argList = methodMatch.Groups["args"].Value.Trim();
string patchSignature = BuildPatchSignature(currentClass, methodName, argList);
string bodyHash = ComputeMethodBodyHash(lines, index);
output.Add(new MethodSnapshot
{
Assembly = assemblyName,
TypeName = currentClass,
MethodName = methodName,
SignatureKey = $"{assemblyName}|{patchSignature}",
BodyHash = bodyHash,
});
}
}
private static string BuildPatchSignature(string className, string methodName, string argList)
{
List<string> types = new();
foreach (string segment in SplitArgSegments(argList))
{
string token = Regex.Replace(segment, @"\s*=\s*[^,)]+$", string.Empty).Trim();
if (token.Length == 0)
continue;
token = Regex.Replace(token, @"^\s*(ref|out|in|readonly)\s+", string.Empty).Trim();
string[] parts = token.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2)
continue;
types.Add(string.Join(' ', parts[..^1]).Trim());
}
if (types.Count == 0)
return $"Il2Cpp.{className}::{methodName}()";
return $"Il2Cpp.{className}::{methodName}({string.Join(", ", types)})";
}
private static IEnumerable<string> SplitArgSegments(string argListText)
{
if (string.IsNullOrWhiteSpace(argListText))
yield break;
int depth = 0;
StringBuilder current = new();
for (int i = 0; i < argListText.Length; i++)
{
char c = argListText[i];
if (c == '<') depth++;
if (c == '>') depth--;
if (c == ',' && depth == 0)
{
string segment = current.ToString().Trim();
if (segment.Length > 0)
yield return segment;
current.Clear();
continue;
}
current.Append(c);
}
if (current.Length > 0)
{
string segment = current.ToString().Trim();
if (segment.Length > 0)
yield return segment;
}
}
private static string ComputeMethodBodyHash(string[] lines, int methodLineIndex)
{
var sb = new StringBuilder();
int braceDepth = 0;
bool startedBody = false;
for (int i = methodLineIndex; i < lines.Length; i++)
{
string line = lines[i];
sb.AppendLine(line.Trim());
foreach (char c in line)
{
if (c == '{')
{
braceDepth++;
startedBody = true;
}
else if (c == '}')
{
braceDepth--;
}
}
if (startedBody && braceDepth <= 0)
break;
if (!startedBody && line.Contains(';'))
break;
}
byte[] data = SHA256.HashData(Encoding.UTF8.GetBytes(sb.ToString()));
return Convert.ToHexString(data);
}
}