implement (redux cli only) support for specifying a name translation map

This commit is contained in:
LukeFZ
2026-01-14 17:40:51 +01:00
parent 9fe77fdb1e
commit 8c2f7a9960
9 changed files with 328 additions and 5 deletions

View File

@@ -60,6 +60,10 @@
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="Next\NameTranslation\" />
</ItemGroup>
<Target DependsOnTargets="ResolveReferences" Name="CopyProjectReferencesToPackage">
<ItemGroup>
<BuildOutputInPackage Include="@(ReferenceCopyLocalPaths-&gt;WithMetadataValue('ReferenceSourceTarget', 'ProjectReference'))" />

View File

@@ -0,0 +1,97 @@
using dnlib.DotNet;
using Il2CppInspector.Reflection;
using System.Diagnostics.CodeAnalysis;
namespace Il2CppInspector.Next.NameTranslation;
public readonly struct NameTranslationApplierContext
{
private readonly NameTranslationInfo _nameTranslationInfo;
private NameTranslationApplierContext(NameTranslationInfo nameTranslationInfo)
{
_nameTranslationInfo = nameTranslationInfo;
}
public static void Process(Assembly assemblyDefinition, NameTranslationInfo nameTranslationInfo)
{
var ctx = new NameTranslationApplierContext(nameTranslationInfo);
ctx.Process(assemblyDefinition);
}
private string TranslateName(string name) =>
_nameTranslationInfo.NameTranslation.GetValueOrDefault(name, name);
private bool TryTranslateName(string name, out string translatedName)
{
if (_nameTranslationInfo.NameTranslation.TryGetValue(name, out var translated))
{
translatedName = translated;
return true;
}
translatedName = null;
return false;
}
private string TranslateNamespace(string className, string currentNamespace) =>
_nameTranslationInfo.ClassNamespaces.GetValueOrDefault(className, currentNamespace);
private void Process(Assembly assemblyDefinition)
{
foreach (var module in assemblyDefinition.DefinedTypes)
Process(module);
}
private void Process(TypeInfo typeDefinition)
{
if (TryTranslateName(typeDefinition.Name, out var translatedName))
{
typeDefinition.Namespace = TranslateNamespace(typeDefinition.Name, typeDefinition.Namespace);
typeDefinition.Name = translatedName;
}
foreach (var method in typeDefinition.DeclaredMethods)
Process(method);
foreach (var field in typeDefinition.DeclaredFields)
Process(field);
foreach (var property in typeDefinition.DeclaredProperties)
Process(property);
foreach (var evt in typeDefinition.DeclaredEvents)
Process(evt);
foreach (var nestedType in typeDefinition.DeclaredNestedTypes)
Process(nestedType);
}
private void Process(MethodInfo methodDefinition)
{
methodDefinition.Name = TranslateName(methodDefinition.Name);
foreach (var parameter in methodDefinition.DeclaredParameters)
Process(parameter);
}
private void Process(FieldInfo fieldDefinition)
{
fieldDefinition.Name = TranslateName(fieldDefinition.Name);
}
private void Process(PropertyInfo propertyDefinition)
{
propertyDefinition.Name = TranslateName(propertyDefinition.Name);
}
private void Process(EventInfo eventDefinition)
{
eventDefinition.Name = TranslateName(eventDefinition.Name);
}
private void Process(ParameterInfo parameterDefinition)
{
parameterDefinition.Name = TranslateName(parameterDefinition.Name);
}
}

View File

@@ -0,0 +1,8 @@
using System.Collections.Frozen;
namespace Il2CppInspector.Next.NameTranslation;
public sealed record NameTranslationInfo(
FrozenDictionary<string, string> NameTranslation,
FrozenDictionary<string, string> ClassNamespaces
);

View File

@@ -0,0 +1,182 @@
using dnlib.DotNet;
using System.Collections.Frozen;
using System.Diagnostics;
namespace Il2CppInspector.Next.NameTranslation;
public ref struct NameTranslationParserContext
{
private enum CurrentSection
{
None,
Classes,
Methods,
Fields,
Properties,
Events,
Parameters
}
private CurrentSection _currentSection;
private bool _reverseOrder;
private readonly ReadOnlySpan<string> _nameTranslationLines;
private readonly char _seperator;
private NameTranslationParserContext(ReadOnlySpan<string> nameTranslationLines, char seperator)
{
_currentSection = CurrentSection.None;
_nameTranslationLines = nameTranslationLines;
_seperator = seperator;
}
public static NameTranslationInfo Parse(ReadOnlySpan<string> nameTranslationLines, char seperator = '⇨')
{
var ctx = new NameTranslationParserContext(nameTranslationLines, seperator);
return ctx.Parse();
}
private void HandleMetadataLine(string line)
{
switch (line)
{
case "#ReverseOrder":
_reverseOrder = true;
break;
case "#Classes":
_currentSection = CurrentSection.Classes;
break;
case "#Methods":
_currentSection = CurrentSection.Methods;
break;
case "#Fields":
_currentSection = CurrentSection.Fields;
break;
case "#Properties":
_currentSection = CurrentSection.Properties;
break;
case "#Events":
_currentSection = CurrentSection.Events;
break;
case "#Parameters":
_currentSection = CurrentSection.Parameters;
break;
default:
Debug.Assert(false);
break;
}
}
private NameTranslationInfo Parse()
{
var nameTranslationMap = new Dictionary<string, string>(_nameTranslationLines.Length);
var namespaceTranslationMap = new Dictionary<string, string>();
foreach (var line in _nameTranslationLines)
{
if (line.StartsWith('#'))
{
HandleMetadataLine(line);
continue;
}
var split = line.Split(_seperator, 2);
var obfuscatedName = split[0];
var deobfuscatedFullName = split[1].TrimEnd();
if (!_reverseOrder)
{
(obfuscatedName, deobfuscatedFullName) = (deobfuscatedFullName, obfuscatedName);
}
string deobfuscatedName;
string? deobfuscatedNamespace;
if (_currentSection != CurrentSection.Classes)
{
deobfuscatedNamespace = null;
deobfuscatedName = ParseFullName(deobfuscatedFullName);
}
else
{
(deobfuscatedName, deobfuscatedNamespace) = ParseFullClassName(deobfuscatedFullName);
}
// This sometimes happens, just ignore it if so
if (obfuscatedName == deobfuscatedName)
continue;
if (nameTranslationMap.TryGetValue(obfuscatedName, out var alreadySeenName))
{
if (alreadySeenName != deobfuscatedName)
throw new InvalidDataException(
$"Name translation collision detected for name {obfuscatedName}: {alreadySeenName} vs. {deobfuscatedName}");
}
else
{
nameTranslationMap[obfuscatedName] = deobfuscatedName;
if (deobfuscatedNamespace != null)
namespaceTranslationMap[obfuscatedName] = deobfuscatedNamespace;
}
}
return new NameTranslationInfo(
nameTranslationMap
.ToFrozenDictionary(),
namespaceTranslationMap
.ToFrozenDictionary());
}
private string ParseFullName(string fullName)
=> _currentSection switch
{
CurrentSection.Classes => ParseFullClassName(fullName).Item1,
CurrentSection.Methods or CurrentSection.Properties => ParseFullMemberWithArgumentsName(fullName),
CurrentSection.Fields or CurrentSection.Events => ParseFullMemberName(fullName),
CurrentSection.Parameters => ParseFullParameterName(fullName),
_ => throw new UnreachableException()
};
private static (string, string?) ParseFullClassName(string fullName)
{
// For the namespace, return everything up until the last part of the fully qualified class name
var lastNamespaceIdx = fullName.LastIndexOf('.');
var ns = lastNamespaceIdx == -1
? null
: fullName[..lastNamespaceIdx];
// If we have a nested class, return that name
var nestedClassIdx = fullName.LastIndexOf('/') + 1;
if (nestedClassIdx != 0)
return (fullName[nestedClassIdx..], ns);
// Otherwise, if we have a namespace, return the last part of the fully qualified name
if (lastNamespaceIdx != -1)
return (fullName[(lastNamespaceIdx + 1)..], ns);
// Otherwise the class has no namespace, return the full name
return (fullName, ns);
}
// Used for both fields and events
private static string ParseFullMemberName(string fullName)
{
var memberNameStartIdx = fullName.LastIndexOf("::", StringComparison.Ordinal) + 2;
return fullName[memberNameStartIdx..];
}
private static string ParseFullMemberWithArgumentsName(string fullName)
{
var methodNameWithParameters = ParseFullMemberName(fullName);
var parametersStartIdx = methodNameWithParameters.IndexOf('(');
return methodNameWithParameters[..parametersStartIdx];
}
private static string ParseFullParameterName(string fullName)
{
var parameterNameStartIdx = fullName.LastIndexOf(' ') + 1;
return fullName[parameterNameStartIdx..];
}
}

View File

@@ -48,7 +48,7 @@ namespace Il2CppInspector.Reflection
public int MetadataToken { get; }
// Name of parameter
public string Name { get; }
public string Name { get; set; }
public string CSharpName => Constants.Keywords.Contains(Name) ? "@" + Name : Name.ToCIdentifier();
// Type of this parameter

View File

@@ -11,6 +11,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Il2CppInspector.Next.BinaryMetadata;
using Il2CppInspector.Next.NameTranslation;
namespace Il2CppInspector.Reflection
{
@@ -187,6 +188,19 @@ namespace Il2CppInspector.Reflection
PluginHooks.PostProcessTypeModel(this);
}
public void ApplyNameTranslationFromFile(string nameTranslationMapPath)
{
var lines = File.ReadAllLines(nameTranslationMapPath);
ApplyNameTranslation(lines);
}
public void ApplyNameTranslation(ReadOnlySpan<string> nameTranslationLines)
{
var info = NameTranslationParserContext.Parse(nameTranslationLines);
foreach (var assembly in Assemblies)
NameTranslationApplierContext.Process(assembly, info);
}
// Get generic arguments from either a type or method instanceIndex from a MethodSpec
public TypeInfo[] ResolveGenericArguments(Il2CppGenericInst inst) {

View File

@@ -72,6 +72,9 @@ internal sealed class ProcessCommand(PortProvider portProvider) : ManualCommand<
[CommandOption("--image-base")]
public string? ImageBase { get; init; }
[CommandOption("--name-translation-map")]
public string? NameTranslationMap { get; init; }
}
protected override async Task<int> ExecuteAsync(CliClient client, Settings settings)
@@ -79,17 +82,19 @@ internal sealed class ProcessCommand(PortProvider portProvider) : ManualCommand<
var inspectorVersion = await client.GetInspectorVersion();
AnsiConsole.MarkupLineInterpolated($"Using inspector [gray]{inspectorVersion}[/]");
var imageBase = 0uL;
if (settings.ImageBase != null)
{
var imageBase = ulong.Parse(settings.ImageBase,
imageBase = ulong.Parse(settings.ImageBase,
settings.ImageBase.StartsWith("0x")
? NumberStyles.HexNumber
: NumberStyles.Integer);
AnsiConsole.MarkupLineInterpolated($"Setting image base to [white]0x{imageBase:x}[/]");
await client.SetSettings(new InspectorSettings(imageBase));
}
await client.SetSettings(new InspectorSettings(imageBase, settings.NameTranslationMap));
await client.SubmitInputFiles(settings.InputPaths.ToList());
await client.WaitForLoadingToFinishAsync();
if (!client.ImportCompleted)
@@ -176,6 +181,9 @@ internal sealed class ProcessCommand(PortProvider portProvider) : ManualCommand<
return ValidationResult.Error(
$"Provided extracted IL2CPP files path {settings.ExtractIl2CppFilesPath} already exists as a file.");
if (settings.NameTranslationMap != null && !File.Exists(settings.NameTranslationMap))
return ValidationResult.Error($"Provided name translation map {settings.NameTranslationMap} does not exist.");
if (settings is
{
CppScaffolding: false, CSharpStubs: false, DisassemblerMetadata: false, DummyDlls: false,

View File

@@ -1,3 +1,3 @@
namespace Il2CppInspector.Redux.FrontendCore;
public sealed record InspectorSettings(ulong ImageBase);
public sealed record InspectorSettings(ulong ImageBase = 0, string? NameTranslationMapPath = null);

View File

@@ -23,6 +23,7 @@ public class UiContext
private readonly List<UnityHeaders> _potentialUnityVersions = [];
private readonly LoadOptions _loadOptions = new();
private InspectorSettings _settings = new();
private readonly List<(string FormatId, string OutputDirectory, Dictionary<string, string> Settings)> _queuedExports = [];
@@ -96,6 +97,12 @@ public class UiContext
{
var typeModel = new TypeModel(inspector);
if (_settings.NameTranslationMapPath != null)
{
await client.ShowLogMessage("Applying name translation map");
typeModel.ApplyNameTranslationFromFile(_settings.NameTranslationMapPath);
}
// Just create the app model, do not initialize it - this is done lazily depending on the exports
_appModels.Add(new AppModel(typeModel, makeDefaultBuild: false));
}
@@ -127,6 +134,8 @@ public class UiContext
{
await using (await LoadingSession.Start(client))
{
_loadOptions.ImageBase = _settings.ImageBase;
var streams = Inspector.GetStreamsFromPackage(inputFiles);
if (streams != null)
{
@@ -160,6 +169,7 @@ public class UiContext
else if (_binary == null && PathHeuristics.IsBinaryPath(inputFile))
{
stream.Position = 0;
_loadOptions.BinaryFilePath = inputFile;
if (await TryLoadBinaryFromStreamAsync(client, stream))
@@ -251,7 +261,7 @@ public class UiContext
public Task SetSettingsAsync(UiClient client, InspectorSettings settings)
{
_loadOptions.ImageBase = settings.ImageBase;
_settings = settings;
return Task.CompletedTask;
}
}