mirror of
https://github.com/AssetRipper/Cpp2IL.git
synced 2026-11-13 18:07:02 -10:00
412 lines
14 KiB
C#
412 lines
14 KiB
C#
using System;
|
|
using System.Buffers.Binary;
|
|
using System.CodeDom.Compiler;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using Cpp2IL.Core.Api;
|
|
using Cpp2IL.Core.Extensions;
|
|
using Cpp2IL.Core.Logging;
|
|
using Cpp2IL.Core.Model.Contexts;
|
|
using Cpp2IL.Core.Utils;
|
|
using LibCpp2IL;
|
|
|
|
namespace Cpp2IL.Core.OutputFormats;
|
|
|
|
public class DiffableCsOutputFormat : Cpp2IlOutputFormat
|
|
{
|
|
public static bool IncludeMethodLength = false;
|
|
|
|
public override string OutputFormatId => "diffable-cs";
|
|
public override string OutputFormatName => "Diffable C#";
|
|
|
|
public override void DoOutput(ApplicationAnalysisContext context, string outputRoot)
|
|
{
|
|
//General principle of diffable CS:
|
|
//- Same-line method bodies ({ })
|
|
//- Attributes in alphabetical order
|
|
//- Members in alphabetical order and in nested type-field-event-prop-method member order
|
|
//- No info on addresses or tokens as these change with every rebuild
|
|
|
|
//The idea is to make it as easy as possible for software like WinMerge, github, etc, to diff the two versions of the code and show the user exactly what changed.
|
|
|
|
outputRoot = Path.Combine(outputRoot, "DiffableCs");
|
|
|
|
if (Directory.Exists(outputRoot))
|
|
{
|
|
Logger.InfoNewline("Removing old DiffableCs output directory...", "DiffableCsOutputFormat");
|
|
Directory.Delete(outputRoot, true);
|
|
}
|
|
|
|
Logger.InfoNewline("Building C# files and directory structure...", "DiffableCsOutputFormat");
|
|
var files = BuildOutput(context, outputRoot);
|
|
|
|
Logger.InfoNewline("Writing C# files...", "DiffableCsOutputFormat");
|
|
foreach (var (filePath, fileContent) in files)
|
|
{
|
|
File.WriteAllText(filePath, fileContent.ToString());
|
|
}
|
|
}
|
|
|
|
private static Dictionary<string, StringWriter> BuildOutput(ApplicationAnalysisContext context, string outputRoot)
|
|
{
|
|
var ret = new Dictionary<string, StringWriter>();
|
|
|
|
foreach (var assembly in context.Assemblies)
|
|
{
|
|
var asmPath = Path.Combine(outputRoot, assembly.CleanAssemblyName);
|
|
Directory.CreateDirectory(asmPath);
|
|
|
|
foreach (var type in assembly.TopLevelTypes)
|
|
{
|
|
if (type is InjectedTypeAnalysisContext)
|
|
continue;
|
|
|
|
var path = Path.Combine(asmPath, type.NamespaceAsSubdirs, MiscUtils.CleanPathElement(type.Name + ".cs"));
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
|
|
|
var stringWriter = new StringWriter();
|
|
var writer = new IndentedTextWriter(stringWriter, "\t");
|
|
|
|
//Namespace at top of file
|
|
if (!string.IsNullOrEmpty(type.Namespace))
|
|
{
|
|
writer.WriteLine($"namespace {type.Namespace};");
|
|
writer.WriteLineNoTabs(string.Empty);
|
|
}
|
|
else
|
|
{
|
|
writer.WriteLine("//Type is in global namespace");
|
|
writer.WriteLineNoTabs(string.Empty);
|
|
}
|
|
|
|
WriteType(writer, type);
|
|
|
|
ret[path] = stringWriter;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
private static void WriteType(IndentedTextWriter writer, TypeAnalysisContext type)
|
|
{
|
|
// if (type.IsCompilerGeneratedBasedOnCustomAttributes)
|
|
//Do not output compiler-generated types
|
|
// return;
|
|
|
|
//Custom attributes for type. Includes a trailing newline
|
|
WriteCustomAttributes(writer, type);
|
|
|
|
//Type declaration line
|
|
writer.Write(CsFileUtils.GetKeyWordsForType(type));
|
|
writer.Write(' ');
|
|
writer.Write(CsFileUtils.GetTypeName(type));
|
|
CsFileUtils.WriteInheritanceInfo(type, writer);
|
|
writer.WriteLine();
|
|
writer.WriteLine('{');
|
|
|
|
//Type declaration done, increase indent
|
|
writer.Indent++;
|
|
|
|
if (type.IsEnumType)
|
|
{
|
|
var enumValues = type.Fields.Where(f => f.IsStatic).ToList();
|
|
enumValues.SortByExtractedKey(e => e.Token); //Not as good as sorting by value but it'll do
|
|
foreach (var enumValue in enumValues)
|
|
{
|
|
writer.Write(enumValue.Name);
|
|
writer.Write(" = ");
|
|
writer.Write(InvariantValue(enumValue.ConstantValue));
|
|
writer.WriteLine(',');
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//Nested classes, alphabetical order
|
|
var nestedTypes = type.NestedTypes.Clone();
|
|
nestedTypes.SortByExtractedKey(t => t.Name);
|
|
foreach (var nested in nestedTypes)
|
|
WriteType(writer, nested);
|
|
|
|
//Fields, offset order, static first
|
|
var fields = type.Fields.Clone();
|
|
fields.SortByExtractedKey(f => f.IsStatic ? f.Offset : f.Offset + 0x1000);
|
|
foreach (var field in fields)
|
|
WriteField(writer, field);
|
|
|
|
writer.WriteLineNoTabs(string.Empty);
|
|
|
|
//Events, alphabetical order
|
|
var events = type.Events.Clone();
|
|
events.SortByExtractedKey(e => e.Name);
|
|
foreach (var evt in events)
|
|
WriteEvent(writer, evt);
|
|
|
|
//Properties, alphabetical order
|
|
var properties = type.Properties.Clone();
|
|
properties.SortByExtractedKey(p => p.Name);
|
|
foreach (var prop in properties)
|
|
WriteProperty(writer, prop);
|
|
|
|
//Methods, alphabetical order
|
|
var methods = type.Methods.Clone();
|
|
methods.SortByExtractedKey(m => m.Name);
|
|
foreach (var method in methods)
|
|
WriteMethod(writer, method);
|
|
}
|
|
|
|
//Decrease indent, close brace
|
|
writer.Indent--;
|
|
writer.WriteLine('}');
|
|
writer.WriteLineNoTabs(string.Empty);
|
|
}
|
|
|
|
private static void WriteField(IndentedTextWriter writer, FieldAnalysisContext field)
|
|
{
|
|
if (field is InjectedFieldAnalysisContext)
|
|
return;
|
|
|
|
//Custom attributes for field. Includes a trailing newline
|
|
WriteCustomAttributes(writer, field);
|
|
|
|
//Field declaration line
|
|
writer.Write(CsFileUtils.GetKeyWordsForField(field));
|
|
writer.Write(' ');
|
|
writer.Write(CsFileUtils.GetTypeName(field.FieldType));
|
|
writer.Write(' ');
|
|
writer.Write(field.Name);
|
|
|
|
if ((field.Attributes & FieldAttributes.HasFieldRVA) != 0)
|
|
{
|
|
var fieldRva = field.StaticArrayInitialValue;
|
|
if (fieldRva.Length > 0)
|
|
{
|
|
WriteFieldRvaInitializer(writer, field, fieldRva);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (field.Attributes.HasFlag(FieldAttributes.HasDefault))
|
|
{
|
|
writer.Write(" = ");
|
|
|
|
if (field.ConstantValue is string stringDefaultValue)
|
|
{
|
|
writer.Write('"');
|
|
writer.Write(stringDefaultValue);
|
|
writer.Write('"');
|
|
}
|
|
else if (field.ConstantValue is char charDefaultValue)
|
|
{
|
|
writer.Write("'\\u");
|
|
writer.Write(((int)charDefaultValue).ToString("X"));
|
|
writer.Write("'");
|
|
}
|
|
else
|
|
writer.Write(InvariantValue(field.ConstantValue));
|
|
}
|
|
|
|
writer.Write("; //Field offset: 0x");
|
|
writer.Write(field.Offset.ToString("X"));
|
|
|
|
if ((field.Attributes & FieldAttributes.HasFieldRVA) != 0)
|
|
writer.Write(" || Has Field RVA (address hidden for diffability)");
|
|
|
|
writer.WriteLine();
|
|
}
|
|
|
|
private static void WriteFieldRvaInitializer(IndentedTextWriter writer, FieldAnalysisContext field, byte[] data)
|
|
{
|
|
var tail = $" //Field offset: 0x{field.Offset.ToString("X")} || Has Field RVA (address hidden for diffability)";
|
|
|
|
if (TryAscendingInt32Array(data, out var ints))
|
|
{
|
|
writer.Write(" = new int[]");
|
|
writer.Write(tail);
|
|
writer.WriteLine();
|
|
writer.WriteLine('{');
|
|
writer.Indent++;
|
|
for (var i = 0; i < ints.Length; i += 12)
|
|
{
|
|
var n = Math.Min(12, ints.Length - i);
|
|
for (var j = 0; j < n; j++)
|
|
{
|
|
if (j > 0) writer.Write(", ");
|
|
writer.Write(ints[i + j]);
|
|
}
|
|
if (i + n < ints.Length) writer.Write(',');
|
|
writer.WriteLine();
|
|
}
|
|
writer.Indent--;
|
|
writer.WriteLine("};");
|
|
return;
|
|
}
|
|
|
|
writer.Write(" = new byte[]");
|
|
writer.Write(tail);
|
|
writer.WriteLine();
|
|
writer.WriteLine('{');
|
|
writer.Indent++;
|
|
for (var i = 0; i < data.Length; i += 16)
|
|
{
|
|
var n = Math.Min(16, data.Length - i);
|
|
for (var j = 0; j < n; j++)
|
|
{
|
|
if (j > 0) writer.Write(", ");
|
|
writer.Write("0x");
|
|
writer.Write(data[i + j].ToString("X2"));
|
|
}
|
|
if (i + n < data.Length) writer.Write(',');
|
|
writer.WriteLine();
|
|
}
|
|
writer.Indent--;
|
|
writer.WriteLine("};");
|
|
}
|
|
|
|
//blobs that decode as 0 followed by strictly ascending little-endian int32s are (probably) offset tables,
|
|
//so show them as int[] rather than a hex dump
|
|
private static bool TryAscendingInt32Array(byte[] b, [NotNullWhen(true)] out int[]? ints)
|
|
{
|
|
ints = null;
|
|
|
|
const int minElements = 8; //short blobs can pass the ascending check by pure coincidence
|
|
if (b.Length < sizeof(int) * minElements || b.Length % sizeof(int) != 0)
|
|
return false;
|
|
|
|
var values = new int[b.Length / sizeof(int)];
|
|
var prev = -1;
|
|
|
|
for (var i = 0; i < values.Length; i++)
|
|
{
|
|
var v = BinaryPrimitives.ReadInt32LittleEndian(b.AsSpan(i * sizeof(int), sizeof(int)));
|
|
|
|
if (i == 0 && v != 0)
|
|
return false;
|
|
|
|
if (v <= prev)
|
|
return false;
|
|
|
|
prev = v;
|
|
values[i] = v;
|
|
}
|
|
|
|
ints = values;
|
|
return true;
|
|
}
|
|
|
|
private static void WriteEvent(IndentedTextWriter writer, EventAnalysisContext evt)
|
|
{
|
|
//Custom attributes for event. Includes a trailing newline
|
|
WriteCustomAttributes(writer, evt);
|
|
|
|
//Event declaration line
|
|
writer.Write(CsFileUtils.GetKeyWordsForEvent(evt));
|
|
writer.Write(' ');
|
|
writer.Write(CsFileUtils.GetTypeName(evt.EventType));
|
|
writer.Write(' ');
|
|
writer.Write(evt.Name);
|
|
writer.WriteLine();
|
|
writer.WriteLine('{');
|
|
|
|
//Add/Remove/Invoke
|
|
writer.Indent++;
|
|
if (evt.Adder != null)
|
|
WriteAccessor(writer, evt.Adder, "add", evt.Visibility);
|
|
if (evt.Remover != null)
|
|
WriteAccessor(writer, evt.Remover, "remove", evt.Visibility);
|
|
if (evt.Invoker != null)
|
|
WriteAccessor(writer, evt.Invoker, "fire", evt.Visibility);
|
|
writer.Indent--;
|
|
|
|
writer.WriteLine('}');
|
|
writer.WriteLineNoTabs(string.Empty);
|
|
}
|
|
|
|
private static void WriteProperty(IndentedTextWriter writer, PropertyAnalysisContext prop)
|
|
{
|
|
//Custom attributes for property. Includes a trailing newline
|
|
WriteCustomAttributes(writer, prop);
|
|
|
|
//Property declaration line
|
|
writer.Write(CsFileUtils.GetKeyWordsForProperty(prop));
|
|
writer.Write(' ');
|
|
writer.Write(CsFileUtils.GetTypeName(prop.PropertyType));
|
|
writer.Write(' ');
|
|
writer.Write(prop.Name);
|
|
writer.WriteLine();
|
|
writer.WriteLine('{');
|
|
|
|
//Get/Set
|
|
writer.Indent++;
|
|
if (prop.Getter != null)
|
|
WriteAccessor(writer, prop.Getter, "get", prop.Visibility);
|
|
if (prop.Setter != null)
|
|
WriteAccessor(writer, prop.Setter, "set", prop.Visibility);
|
|
writer.Indent--;
|
|
|
|
writer.WriteLine('}');
|
|
writer.WriteLineNoTabs(string.Empty);
|
|
}
|
|
|
|
private static void WriteMethod(IndentedTextWriter writer, MethodAnalysisContext method)
|
|
{
|
|
if (method is InjectedMethodAnalysisContext)
|
|
return;
|
|
|
|
//Custom attributes for method. Includes a trailing newline
|
|
WriteCustomAttributes(writer, method);
|
|
|
|
//Method declaration line
|
|
writer.Write(CsFileUtils.GetKeyWordsForMethod(method));
|
|
writer.Write(' ');
|
|
if (method.Name is not ".ctor" and not ".cctor")
|
|
{
|
|
writer.Write(CsFileUtils.GetTypeName(method.ReturnType));
|
|
writer.Write(' ');
|
|
writer.Write(method.Name);
|
|
}
|
|
else
|
|
{
|
|
//Constructor
|
|
writer.Write(CsFileUtils.GetTypeName(method.DeclaringType!));
|
|
}
|
|
|
|
writer.Write('(');
|
|
writer.Write(CsFileUtils.GetMethodParameterString(method));
|
|
writer.Write(") { }");
|
|
|
|
if (IncludeMethodLength)
|
|
{
|
|
writer.Write(" //Length: ");
|
|
writer.Write(method.RawBytes.Length);
|
|
}
|
|
|
|
writer.WriteLine();
|
|
writer.WriteLineNoTabs(string.Empty);
|
|
}
|
|
|
|
//get/set/add/remove/raise
|
|
private static void WriteAccessor(IndentedTextWriter writer, MethodAnalysisContext accessor, string accessorType, MethodAttributes parentVisibility)
|
|
{
|
|
//Custom attributes for accessor. Includes a trailing newline
|
|
WriteCustomAttributes(writer, accessor);
|
|
|
|
writer.Write(CsFileUtils.GetKeyWordsForMethod(accessor, parentVisibility));
|
|
writer.Write(' ');
|
|
writer.Write(accessorType);
|
|
writer.Write(" { } //Length: ");
|
|
writer.Write(accessor.RawBytes.Length);
|
|
writer.WriteLine();
|
|
}
|
|
|
|
private static void WriteCustomAttributes(IndentedTextWriter writer, HasCustomAttributes owner)
|
|
=> CsFileUtils.WriteCustomAttributeStrings(owner, writer, true, true);
|
|
|
|
private static string InvariantValue(object? value)
|
|
=> value is null ? "null" : value is IFormattable f ? f.ToString(null, CultureInfo.InvariantCulture) : value.ToString() ?? "";
|
|
}
|