diff --git a/Common/Diagnostics/Logger.cs b/Common/Diagnostics/Logger.cs index 285bced..cb3d039 100644 --- a/Common/Diagnostics/Logger.cs +++ b/Common/Diagnostics/Logger.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using Amethyst.Common.Utility; +using System.Reflection; namespace Amethyst.Common.Diagnostics { @@ -10,43 +11,28 @@ public static void WriteLine(object? message = null, ConsoleColor color = Consol Console.WriteLine(message); Console.ResetColor(); } - - public static void Info(object? message) + + public static void Info(object? message, CursorLocation? location = null) { - WriteLine($"{( - Assembly.GetEntryAssembly()?.GetName()?.Name is { } name ? - $"[{name}] " : - "")}[INFO] {message}", ConsoleColor.White); + WriteLine($"{(location is not null && location.File != "" ? location.ToString() + ": " : (Assembly.GetEntryAssembly()?.GetName().Name is not string name ? "Unknown: " : name.Trim() + ": "))} message: {message}", ConsoleColor.White); } - public static void Debug(object? message) + public static void Debug(object? message, CursorLocation? location = null) { #if DEBUG - WriteLine($"{( - Assembly.GetEntryAssembly()?.GetName()?.Name is { } name ? - $"[{name}] " : - "")}[DEBUG] {message}", ConsoleColor.White); + WriteLine($"{(location is not null && location.File != "" ? location.ToString() + ": " : (Assembly.GetEntryAssembly()?.GetName().Name is not string name ? "Unknown: " : name.Trim() + ": "))}message: {message}", ConsoleColor.White); #endif } - public static void Warn(string message) => - WriteLine($"{( - Assembly.GetEntryAssembly()?.GetName()?.Name is { } name ? - $"[{name}] " : - "")}[WARN] {message}", ConsoleColor.Yellow); + public static void Warn(string message, CursorLocation? location = null) => + WriteLine($"{(location is not null && location.File != "" ? location?.ToString() + ": " : (Assembly.GetEntryAssembly()?.GetName().Name is not string name ? "Unknown: " : name.Trim() + ": "))}warning: {message}", ConsoleColor.Yellow); - public static void Error(string message) => - WriteLine($"{( - Assembly.GetEntryAssembly()?.GetName()?.Name is { } name ? - $"[{name}] " : - "")}[ERROR] {message}", ConsoleColor.Red); + public static void Error(string message, CursorLocation? location = null) => + WriteLine($"{(location is not null && location.File != "" ? location?.ToString() + ": " : (Assembly.GetEntryAssembly()?.GetName().Name is not string name ? "Unknown: " : name.Trim() + ": "))}error: {message}", ConsoleColor.Red); - public static void Fatal(string message) + public static void Fatal(string message, CursorLocation? location = null) { - WriteLine($"{( - Assembly.GetEntryAssembly()?.GetName()?.Name is { } name ? - $"[{name}] " : - "")}[FATAL] {message}", ConsoleColor.Magenta); + WriteLine($"{(location is not null && location.File != "" ? location?.ToString() + ": " : (Assembly.GetEntryAssembly()?.GetName().Name is not string name ? "Unknown: " : name.Trim() + ": "))}fatal error: {message}", ConsoleColor.Magenta); Environment.Exit(1); } } diff --git a/Common/Models/VariableSymbolModel.cs b/Common/Models/VariableSymbolModel.cs index 850e518..aaa7d4c 100644 --- a/Common/Models/VariableSymbolModel.cs +++ b/Common/Models/VariableSymbolModel.cs @@ -14,5 +14,8 @@ public class VariableSymbolModel [JsonProperty("address")] public string Address { get; set; } = string.Empty; + + [JsonProperty("is_vaddress")] + public bool IsVirtualTableAddress { get; set; } = false; } } diff --git a/Common/Models/VirtualFunctionSymbolModel.cs b/Common/Models/VirtualFunctionSymbolModel.cs index 332e336..d7f39ab 100644 --- a/Common/Models/VirtualFunctionSymbolModel.cs +++ b/Common/Models/VirtualFunctionSymbolModel.cs @@ -23,5 +23,8 @@ public class VirtualFunctionSymbolModel [JsonProperty("index")] public uint Index { get; set; } = 0; + + [JsonProperty("is_vdtor")] + public bool IsVirtualDestructor { get; set; } = false; } } diff --git a/Common/Tracking/FileTracker.cs b/Common/Tracking/FileTracker.cs index 44c8df7..1b05cf1 100644 --- a/Common/Tracking/FileTracker.cs +++ b/Common/Tracking/FileTracker.cs @@ -72,7 +72,7 @@ public FileTracker(DirectoryInfo inputDirectory, FileInfo checksumFile, string[] if (Filters.Any() && !Filters.Any(f => Path.GetRelativePath(InputDirectory.FullName, file.FullName).StartsWith(f))) continue; string filePath = file.FullName.NormalizeSlashes(); -#if !DEBUG +#if DEBUG string content = File.ReadAllText(file.FullName); ulong hash = XXH64.DigestOf(Encoding.UTF8.GetBytes(content)); diff --git a/Common/Utility/CursorLocation.cs b/Common/Utility/CursorLocation.cs new file mode 100644 index 0000000..fbd4ad5 --- /dev/null +++ b/Common/Utility/CursorLocation.cs @@ -0,0 +1,27 @@ +using Amethyst.Common.Extensions; + +namespace Amethyst.Common.Utility { + public class CursorLocation + { + public string File { get; set; } + public uint Line { get; set; } + public uint Column { get; set; } + + public CursorLocation(string file, uint line, uint column) + { + if (string.IsNullOrEmpty(file) || !System.IO.File.Exists(file)) + File = ""; + else + File = Path.GetFullPath(file).NormalizeSlashes(); + Line = line; + Column = column; + } + + override public string ToString() + { + if (File == "") + return ""; + return $"{File}({Line},{Column})"; + } + } +} diff --git a/Common/Utility/Utils.cs b/Common/Utility/Utils.cs index 87de0c6..cf08a90 100644 --- a/Common/Utility/Utils.cs +++ b/Common/Utility/Utils.cs @@ -59,5 +59,24 @@ public static void CreateDefinitionFile(string defFile, IEnumerable mang sb.AppendLine("; End of generated file."); File.WriteAllText(defFile, sb.ToString()); } + + public static void WritePrefixedString(this BinaryWriter writer, string str) { + byte[] bytes = Encoding.UTF8.GetBytes(str); + writer.Write(bytes.Length); + writer.Write(bytes); + } + + public static string ReadPrefixedString(this BinaryReader reader) { + int length = reader.ReadInt32(); + byte[] bytes = reader.ReadBytes(length); + return Encoding.UTF8.GetString(bytes); + } + + public static void Align(this BinaryWriter writer, int alignment = 16, byte pad = 0x00) { + long pos = writer.BaseStream.Position; + int padding = (int)((alignment - (pos % alignment)) % alignment); + for (int i = 0; i < padding; i++) + writer.Write(pad); + } } } diff --git a/ModuleTweaker/Amethyst.ModuleTweaker.csproj b/ModuleTweaker/Amethyst.ModuleTweaker.csproj index 90eab50..ea117d0 100644 --- a/ModuleTweaker/Amethyst.ModuleTweaker.csproj +++ b/ModuleTweaker/Amethyst.ModuleTweaker.csproj @@ -7,17 +7,17 @@ enable Amethyst.ModuleTweaker Amethyst.ModuleTweaker - 1.0.6 + 2.0.0 - - - - + + + + diff --git a/ModuleTweaker/Commands/MainCommand.cs b/ModuleTweaker/Commands/MainCommand.cs index 16dc34b..ae782d0 100644 --- a/ModuleTweaker/Commands/MainCommand.cs +++ b/ModuleTweaker/Commands/MainCommand.cs @@ -2,44 +2,65 @@ using Amethyst.Common.Models; using Amethyst.ModuleTweaker.Patching; using AsmResolver.PE.File; +using AsmResolver.PE.Imports; using CliFx; using CliFx.Attributes; using CliFx.Infrastructure; +using K4os.Hash.xxHash; using Newtonsoft.Json; +using System.Globalization; namespace Amethyst.ModuleTweaker.Commands { [Command(Description = "Patches or unpatches modules for runtime importing support.")] public class MainCommand : ICommand { - [CommandOption("module", 'm', Description = "The specified module path to patch.")] + [CommandOption("module", 'm', Description = "The specified module path to patch.", IsRequired = true)] public string ModulePath { get; set; } = null!; - [CommandOption("symbols", 's', Description = "Path to directory containing *.symbols.json to use for patching.")] + [CommandOption("symbols", 's', Description = "Path to directory containing *.symbols.json to use for patching.", IsRequired = true)] public string SymbolsPath { get; set; } = null!; + [CommandOption("output", 'o', Description = "Path to save temporary files, don't confuse with -m.")] + public string OutputPath { get; set; } = null!; + public ValueTask ExecuteAsync(IConsole console) { FileInfo module = new(ModulePath); - DirectoryInfo symbols = new(SymbolsPath); - if (module.Exists is false) - { - Logger.Warn("Couldn't patch module, specified module does not exist."); + DirectoryInfo symbolsDir = new(SymbolsPath); + if (module.Exists is false) { + Logger.Fatal("Couldn't patch module, specified module does not exist."); return default; } - if (symbols.Exists is false) - { - Logger.Warn("Couldn't patch module, specified symbols directory does not exist."); + if (symbolsDir.Exists is false) { + Logger.Fatal("Couldn't patch module, specified symbols directory does not exist."); return default; } + if (string.IsNullOrEmpty(OutputPath)) { + OutputPath = Path.GetFullPath(Path.Combine(SymbolsPath, "../")); + } + DirectoryInfo outDir = new(OutputPath); + + ulong ParseAddress(string? address) + { + if (string.IsNullOrEmpty(address)) + return 0x0; + if (address.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + address = address[2..]; + if (!ulong.TryParse(address, NumberStyles.HexNumber, null, out var addr)) + return 0x0; + return addr; + } + + SymbolFactory.Register(new SymbolType(1, "pe32+", "data"), () => new Patching.PE.V1.PEDataSymbol()); + SymbolFactory.Register(new SymbolType(1, "pe32+", "function"), () => new Patching.PE.V1.PEFunctionSymbol()); + HeaderFactory.Register(new HeaderType(1, "pe32+"), (args) => new Patching.PE.V1.PEImporterHeader()); + // Collect all symbol files and accumulate mangled names - IEnumerable symbolFiles = symbols.EnumerateFiles("*.json", SearchOption.AllDirectories); - HashSet methods = []; - HashSet variables = []; - HashSet vtables = []; - HashSet vfuncs = []; + IEnumerable symbolFiles = symbolsDir.EnumerateFiles("*.json", SearchOption.AllDirectories); + List symbols = []; foreach (var symbolFile in symbolFiles) { using var stream = symbolFile.OpenRead(); @@ -50,29 +71,48 @@ public ValueTask ExecuteAsync(IConsole console) switch (symbolJson.FormatVersion) { case 1: - foreach (var function in symbolJson.Functions) - { + foreach (var function in symbolJson.Functions) { if (string.IsNullOrEmpty(function.Name)) continue; - methods.Add(function); + symbols.Add(new Patching.PE.V1.PEFunctionSymbol { + Name = function.Name, + IsVirtual = false, + IsSignature = function.Signature is not null, + Address = ParseAddress(function.Address), + Signature = function.Signature ?? string.Empty + }); } - foreach (var variable in symbolJson.Variables) - { - if (string.IsNullOrEmpty(variable.Name)) + foreach (var vfunc in symbolJson.VirtualFunctions) { + if (string.IsNullOrEmpty(vfunc.Name)) continue; - variables.Add(variable); + symbols.Add(new Patching.PE.V1.PEFunctionSymbol { + Name = vfunc.Name, + IsVirtual = true, + VirtualIndex = vfunc.Index, + VirtualTable = vfunc.VirtualTable ?? "this", + IsDestructor = vfunc.IsVirtualDestructor, + HasStorage = vfunc.IsVirtualDestructor + }); } - foreach (var vtable in symbolJson.VirtualTables) - { - if (string.IsNullOrEmpty(vtable.Name)) + foreach (var variable in symbolJson.Variables) { + if (string.IsNullOrEmpty(variable.Name)) continue; - vtables.Add(vtable); + symbols.Add(new Patching.PE.V1.PEDataSymbol { + Name = variable.Name, + IsVirtualTable = false, + Address = ParseAddress(variable.Address), + IsVirtualTableAddress = variable.IsVirtualTableAddress, + HasStorage = variable.IsVirtualTableAddress + }); } - foreach (var vfunc in symbolJson.VirtualFunctions) - { - if (string.IsNullOrEmpty(vfunc.Name)) + foreach (var vtable in symbolJson.VirtualTables) { + if (string.IsNullOrEmpty(vtable.Name)) continue; - vfuncs.Add(vfunc); + symbols.Add(new Patching.PE.V1.PEDataSymbol { + Name = vtable.Name, + IsVirtualTable = true, + Address = ParseAddress(vtable.Address) + }); } break; } @@ -82,13 +122,35 @@ public ValueTask ExecuteAsync(IConsole console) try { // Patch the module - var file = PEFile.FromFile(ModulePath); - PEFileHelper helper = new(file); - if (helper.Patch(methods, variables, vtables, vfuncs)) + var bytes = File.ReadAllBytes(ModulePath); + ulong hash = XXH64.DigestOf(bytes); + if (File.Exists(Path.Combine(outDir.FullName, "module_hash.txt"))) { + var existingHash = File.ReadAllText(Path.Combine(outDir.FullName, "module_hash.txt")); + if (ulong.TryParse(existingHash, NumberStyles.HexNumber, null, out var existingHashValue)) { + if (existingHashValue == hash) { + Logger.Info("Module hash matches previous hash, skipping patch."); + return default; + } + } + } + + var peFile = PEFile.FromBytes(bytes); + if (peFile is null) { + Logger.Fatal("Failed to read module as a PE file."); + return default; + } + Logger.Info($"Loaded module '{ModulePath}' as PE file."); + var patcher = new Patching.PE.PEPatcher(peFile, symbols); + + if (patcher.Patch()) { - file.AlignSections(); - File.Copy(ModulePath, ModulePath + ".backup", true); - file.Write(ModulePath); + File.Copy(ModulePath, ModulePath + ".bak", true); + using var ms = new MemoryStream(); + peFile.Write(ms); + var newBytes = ms.ToArray(); + ulong newHash = XXH64.DigestOf(newBytes); + File.WriteAllBytes(ModulePath, newBytes); + File.WriteAllText(Path.Combine(outDir.FullName, "module_hash.txt"), newHash.ToString("X16")); } } catch (Exception ex) diff --git a/ModuleTweaker/Patching/AbstractHeader.cs b/ModuleTweaker/Patching/AbstractHeader.cs new file mode 100644 index 0000000..ec1986d --- /dev/null +++ b/ModuleTweaker/Patching/AbstractHeader.cs @@ -0,0 +1,69 @@ +using Amethyst.Common.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + // [Header Format] + // [8 bytes ] Magic "AME_RTI" + // [4 bytes ] Format Version (uint) + // [4 bytes ] Format Type length (N) + // [N bytes ] Format Type (UTF-8) + // [4 bytes ] Number of symbols (M) + // [M symbols] + // [Classes that inherit add more data here] + public abstract class AbstractHeader { + public const string MagicSignature = "AME_RTI"; + + public List Symbols { get; set; } = []; + + public abstract string FormatType { get; } + public abstract uint FormatVersion { get; } + + public virtual void WriteTo(BinaryWriter writer) { + writer.WritePrefixedString(MagicSignature); + writer.Write(FormatVersion); + writer.WritePrefixedString(FormatType); + writer.Write(Symbols.Count); + foreach (var sym in Symbols) + sym.WriteTo(writer); + } + + public virtual void ReadFrom(BinaryReader reader) { + string magic = reader.ReadPrefixedString(); + if (magic != MagicSignature) + throw new InvalidOperationException($"Invalid magic signature '{magic}', expected '{MagicSignature}'."); + uint ver = reader.ReadUInt32(); + if (ver != FormatVersion) + throw new InvalidOperationException($"Incompatible header version {ver}, expected {FormatVersion}."); + string fmt = reader.ReadPrefixedString(); + if (fmt != FormatType) + throw new InvalidOperationException($"Incompatible header format {fmt}, expected {FormatType}."); + int count = reader.ReadInt32(); + Symbols.Clear(); + for (int i = 0; i < count; i++) { + SymbolInfo info = AbstractSymbol.PeekInfo(reader); + AbstractSymbol sym = SymbolFactory.Create(info.Type); + sym.ReadFrom(reader); + Symbols.Add(sym); + } + } + + public static HeaderType PeekInfo(BinaryReader reader) { + long initialPos = reader.BaseStream.Position; + try { + string magic = reader.ReadPrefixedString(); + if (magic != MagicSignature) + throw new InvalidOperationException($"Invalid magic signature '{magic}', expected '{MagicSignature}'."); + uint ver = reader.ReadUInt32(); + string fmt = reader.ReadPrefixedString(); + return new(ver, fmt); + } + finally { + reader.BaseStream.Position = initialPos; + } + } + } +} diff --git a/ModuleTweaker/Patching/AbstractSymbol.cs b/ModuleTweaker/Patching/AbstractSymbol.cs new file mode 100644 index 0000000..9ed2455 --- /dev/null +++ b/ModuleTweaker/Patching/AbstractSymbol.cs @@ -0,0 +1,54 @@ +using Amethyst.Common.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public abstract class AbstractSymbol { + public string Name { get; set; } = string.Empty; + + public abstract string FormatType { get; } + public abstract uint FormatVersion { get; } + public abstract string Kind { get; } + public abstract bool IsShadowSymbol { get; } + + public virtual void WriteTo(BinaryWriter writer) { + writer.Write(FormatVersion); + writer.WritePrefixedString(FormatType); + writer.WritePrefixedString(Kind); + writer.WritePrefixedString(Name); + } + + public virtual void ReadFrom(BinaryReader reader) { + uint ver = reader.ReadUInt32(); + if (ver != FormatVersion) + throw new InvalidOperationException($"Incompatible symbol version {ver}, expected {FormatVersion}."); + string fmt = reader.ReadPrefixedString(); + if (fmt != FormatType) + throw new InvalidOperationException($"Incompatible symbol format {fmt}, expected {FormatType}."); + string kind = reader.ReadPrefixedString(); + if (kind != Kind) + throw new InvalidOperationException($"Incompatible symbol kind {kind}, expected {Kind}."); + Name = reader.ReadPrefixedString(); + } + + public override string ToString() { + return $"Symbol[v{FormatVersion}, {FormatType}, {Name}, {Kind}]"; + } + + public static SymbolInfo PeekInfo(BinaryReader reader) { + long initialPos = reader.BaseStream.Position; + try { + uint ver = reader.ReadUInt32(); + string fmt = reader.ReadPrefixedString(); + string kind = reader.ReadPrefixedString(); + string name = reader.ReadPrefixedString(); + return new(ver, fmt, kind, name); + } finally { + reader.BaseStream.Position = initialPos; + } + } + } +} diff --git a/ModuleTweaker/Patching/HeaderFactory.cs b/ModuleTweaker/Patching/HeaderFactory.cs new file mode 100644 index 0000000..68e436e --- /dev/null +++ b/ModuleTweaker/Patching/HeaderFactory.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public class HeaderFactory { + private static readonly Dictionary> sConstructors = []; + public static IReadOnlyDictionary> Constructors => sConstructors; + + public static void Register(HeaderType type, Func constructor) { + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(constructor); + if (sConstructors.ContainsKey(type)) + throw new InvalidOperationException($"A constructor for {type} is already registered."); + sConstructors[type] = constructor; + } + + public static AbstractHeader Create(HeaderType type, params object[] args) { + if (sConstructors.TryGetValue(type, out var constructor)) + return constructor(args); + throw new InvalidOperationException($"No constructor registered for {type}."); + } + } +} diff --git a/ModuleTweaker/Patching/HeaderType.cs b/ModuleTweaker/Patching/HeaderType.cs new file mode 100644 index 0000000..69ade01 --- /dev/null +++ b/ModuleTweaker/Patching/HeaderType.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public class HeaderType(uint version, string type) { + public uint Version { get; set; } = version; + public string Type { get; set; } = type; + + public override bool Equals(object? obj) { + if (obj is not HeaderType other) + return false; + return Version == other.Version && Type == other.Type; + } + + public override int GetHashCode() { + return HashCode.Combine(Version, Type); + } + + public override string ToString() { + return $"HeaderType[v{Version}, {Type}]"; + } + + public static bool operator ==(HeaderType? a, HeaderType? b) => + a?.Equals(b) ?? b is null; + + public static bool operator !=(HeaderType? a, HeaderType? b) => + !(a == b); + } +} diff --git a/ModuleTweaker/Patching/IPatcher.cs b/ModuleTweaker/Patching/IPatcher.cs new file mode 100644 index 0000000..71f5d7c --- /dev/null +++ b/ModuleTweaker/Patching/IPatcher.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public interface IPatcher { + bool IsPatched(); + bool RemoveSection(string name); + bool Patch(); + bool Unpatch(); + bool IsCustomSection(string name); + } +} diff --git a/ModuleTweaker/Patching/PE/AbstractPEImporterHeader.cs b/ModuleTweaker/Patching/PE/AbstractPEImporterHeader.cs new file mode 100644 index 0000000..01c605e --- /dev/null +++ b/ModuleTweaker/Patching/PE/AbstractPEImporterHeader.cs @@ -0,0 +1,29 @@ +namespace Amethyst.ModuleTweaker.Patching.PE { + // [Header Format] + // [Includes AbstractHeader layout] + // [4 bytes ] Old IDT RVA + // [4 bytes ] Old IDT Size + // [4 bytes ] Import Count + + public abstract class AbstractPEImporterHeader() : AbstractHeader { + public override string FormatType => "pe32+"; + + public uint OldIDT { get; set; } = 0; + public uint OldIDTSize { get; set; } = 0; + public uint ImportCount { get; set; } = 0; + + public override void ReadFrom(BinaryReader reader) { + base.ReadFrom(reader); + OldIDT = reader.ReadUInt32(); + OldIDTSize = reader.ReadUInt32(); + ImportCount = reader.ReadUInt32(); + } + + public override void WriteTo(BinaryWriter writer) { + base.WriteTo(writer); + writer.Write(OldIDT); + writer.Write(OldIDTSize); + writer.Write(ImportCount); + } + } +} diff --git a/ModuleTweaker/Patching/PE/AbstractPESymbol.cs b/ModuleTweaker/Patching/PE/AbstractPESymbol.cs new file mode 100644 index 0000000..934e51f --- /dev/null +++ b/ModuleTweaker/Patching/PE/AbstractPESymbol.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching.PE { + public abstract class AbstractPESymbol : AbstractSymbol { + public override string FormatType => "pe32+"; + public uint TargetOffset { get; set; } = 0; + public bool HasStorage { get; set; } = false; + public uint StorageOffset { get; set; } = 0; + + public override void ReadFrom(BinaryReader reader) { + base.ReadFrom(reader); + TargetOffset = reader.ReadUInt32(); + HasStorage = reader.ReadByte() != 0; + StorageOffset = reader.ReadUInt32(); + } + + public override void WriteTo(BinaryWriter writer) { + base.WriteTo(writer); + writer.Write(TargetOffset); + writer.Write((byte)(HasStorage ? 1 : 0)); + writer.Write(StorageOffset); + } + + public abstract void SetStorage(BinaryWriter writer); + } +} diff --git a/ModuleTweaker/Patching/Models/ImportDescriptor.cs b/ModuleTweaker/Patching/PE/ImportDescriptor.cs similarity index 82% rename from ModuleTweaker/Patching/Models/ImportDescriptor.cs rename to ModuleTweaker/Patching/PE/ImportDescriptor.cs index 7372f2c..86d2a6d 100644 --- a/ModuleTweaker/Patching/Models/ImportDescriptor.cs +++ b/ModuleTweaker/Patching/PE/ImportDescriptor.cs @@ -1,13 +1,6 @@ -using AsmResolver.IO; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; +using System.Runtime.InteropServices; -namespace Amethyst.ModuleTweaker.Patching.Models -{ +namespace Amethyst.ModuleTweaker.Patching.PE { [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct ImportDescriptor() { @@ -31,18 +24,18 @@ public bool IsZero } } - public static ImportDescriptor Read(ref BinaryStreamReader reader) + public static ImportDescriptor Read(BinaryReader reader) { Span bytes = stackalloc byte[(int)Size]; - if (reader.ReadBytes(bytes) != Size) + if (reader.Read(bytes) != Size) throw new ArgumentException("Not enough data to read ImportDescriptor."); return MemoryMarshal.Read(bytes); } - public void Write(BinaryStreamWriter writer) + public void Write(BinaryWriter writer) { Span bytes = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref this, 1)); - writer.WriteBytes(bytes); + writer.Write(bytes); } public override bool Equals(object? obj) diff --git a/ModuleTweaker/Patching/PE/PEPatcher.cs b/ModuleTweaker/Patching/PE/PEPatcher.cs new file mode 100644 index 0000000..caae6c6 --- /dev/null +++ b/ModuleTweaker/Patching/PE/PEPatcher.cs @@ -0,0 +1,228 @@ +using Amethyst.Common.Diagnostics; +using Amethyst.ModuleTweaker.Patching.PE.V1; +using Amethyst.ModuleTweaker.Utility; +using AsmResolver; +using AsmResolver.PE.File; +using AsmResolver.PE.File.Headers; +using System.Text; + +namespace Amethyst.ModuleTweaker.Patching.PE { + public class PEPatcher(PEFile file, List symbols) : IPatcher { + // Runtime Importer Header + public const string SectionRTIH = ".rtih"; // Runtime Importer Header + public const string SectionRTIS = ".rtis"; // Runtime Importer Storage + public const string SectionNIDT = ".nidt"; // New Import Directory Table + + public static HashSet CustomSections { get; } = [ + SectionRTIH, + SectionRTIS, + SectionNIDT + ]; + + public PEFile File { get; } = file; + public List Symbols { get; } = symbols; + + public bool IsCustomSection(string name) { + return CustomSections.Contains(name); + } + + public bool IsPatched() { + foreach (var section in File.Sections) { + if (IsCustomSection(section.Name)) + return true; + } + return false; + } + + public bool Patch() { + // Version 1 full PE-specific layout: + // .rtih - Runtime Importer Header + // [PEImporterHeader data] + + if (IsPatched()) { + if (!Unpatch()) + Logger.Fatal("Failed to unpatch existing patch, cannot re-patch."); + } + + Logger.Debug("Patching PE file..."); + var importDirectory = File.OptionalHeader.GetDataDirectory(DataDirectoryIndex.ImportDirectory); + if (!importDirectory.IsPresentInPE) { + Logger.Warn("PE file has no import directory, skipping patch."); + return false; + } + + // Read existing import descriptors + using var importReader = File.CreateDataDirectoryReader(importDirectory).ToReader(); + List importDescriptors = []; + uint targetIATRVA = 0; + uint targetILTRVA = 0; + while (true) { + var entry = ImportDescriptor.Read(importReader); + if (entry.IsZero) + break; + string name = File.CreateReaderAtRva(entry.Name).ReadAsciiString(); + if (name.StartsWith("Minecraft.Windows", StringComparison.OrdinalIgnoreCase)) { + targetIATRVA = entry.OriginalFirstThunk; + targetILTRVA = entry.FirstThunk; + } + importDescriptors.Add(entry); + } + + if (targetIATRVA == 0 || targetILTRVA == 0) { + Logger.Warn("PE file does not import from 'Minecraft.Windows', skipping patch."); + return false; + } + + // Map import names to their target RVAs + Dictionary importNameToTarget = []; + List symbolsToWrite = []; + var targetILTReader = File.CreateReaderAtRva(targetILTRVA); + var targetIATReader = File.CreateReaderAtRva(targetIATRVA); + uint index = 0; + while (true) { + ulong iltEntry = targetILTReader.ReadUInt64(); + if (iltEntry == 0) + break; + index++; + ulong iatEntry = targetIATReader.ReadUInt64(); + bool isOrdinal = (iltEntry & 0x8000000000000000) != 0; + if (isOrdinal) { + continue; + } + uint hintNameRVA = (uint)(iltEntry & 0x7FFFFFFFFFFFFFFF); + var hintNameReader = File.CreateReaderAtRva(hintNameRVA); + ushort hint = hintNameReader.ReadUInt16(); + string name = hintNameReader.ReadAsciiString(); + var symbol = Symbols.OfType().FirstOrDefault(s => s.Name == name); + if (symbol is null) + continue; + uint entryRVA = targetILTRVA + ((index - 1) * 8); + importNameToTarget[name] = entryRVA; + symbol.TargetOffset = entryRVA; + symbolsToWrite.Add(symbol); + Logger.Debug($"Mapping import {name} to target RVA 0x{entryRVA:X}..."); + } + + foreach (var s in Symbols.Where(s => s.IsShadowSymbol && !symbolsToWrite.Contains(s))) { + symbolsToWrite.Add(s); + Logger.Debug($"Mapping shadow symbol {s.Name}..."); + } + + uint rtisRealSize = 0; + // Create the RTIS section + { + PESection rtisSec = new(SectionRTIS, SectionFlags.ContentInitializedData | SectionFlags.MemoryRead | SectionFlags.MemoryWrite | SectionFlags.MemoryExecute); + using var ms = new MemoryStream(); + using var writer = new BinaryWriter(ms, Encoding.UTF8); + writer.Write(0ul); // Runtime state (8 bytes) + foreach (var sym in symbolsToWrite) { + if (sym is AbstractPESymbol peSym) { + if (peSym.HasStorage) { + peSym.SetStorage(writer); + Logger.Debug($"Assigned storage offset 0x{peSym.StorageOffset:X} to symbol {peSym.Name}..."); + } + } + } + byte[] msData = ms.ToArray(); + rtisRealSize = (uint)msData.Length; + var data = new DataSegment(msData); + rtisSec.Contents = data; + File.Sections.Add(rtisSec); + File.AlignSections(); + } + Logger.Info($"Generated storage for {symbolsToWrite.Count(s => (s is AbstractPESymbol peSym) && peSym.HasStorage)} symbols, total size 0x{rtisRealSize:X} bytes."); + + // Update storage offsets to be section-relative + uint rtisRVA = File.Sections.First(s => s.Name == SectionRTIS).Rva; + foreach (var sym in symbolsToWrite) { + if (sym is AbstractPESymbol peSym) { + if (peSym.HasStorage) { + // StorageOffset is RVO now, convert to RVA + peSym.StorageOffset += rtisRVA; + Logger.Debug($"Fixed up storage RVA to 0x{peSym.StorageOffset:X} for symbol {peSym.Name}..."); + } + } + } + + uint rtihRealSize = 0; + // Create the RTIH section + { + PESection rtihSec = new(SectionRTIH, SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); + using var ms = new MemoryStream(); + using var writer = new BinaryWriter(ms, Encoding.UTF8); + var header = new PEImporterHeader() { + Symbols = symbolsToWrite, + OldIDT = importDirectory.VirtualAddress, + OldIDTSize = importDirectory.Size, + ImportCount = index + }; + header.WriteTo(writer); + byte[] msData = ms.ToArray(); + rtihRealSize = (uint)msData.Length; + var data = new DataSegment(msData); + rtihSec.Contents = data; + File.Sections.Add(rtihSec); + File.AlignSections(); + } + Logger.Info($"Mapped {symbolsToWrite.Count} symbols, total size 0x{rtihRealSize:X} bytes."); + + // Create new NIDT section + { + PESection nidtSec = new(SectionNIDT, SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); + using var ms = new MemoryStream(); + using var writer = new BinaryWriter(ms, Encoding.UTF8); + foreach (var entry in importDescriptors) { + if (entry.OriginalFirstThunk == targetIATRVA || entry.FirstThunk == targetILTRVA) + continue; + entry.Write(writer); + } + var data = new DataSegment(ms.ToArray()); + nidtSec.Contents = data; + File.Sections.Add(nidtSec); + File.AlignSections(); + + // Update import directory to point to new NIDT + File.OptionalHeader.SetDataDirectory(DataDirectoryIndex.ImportDirectory, new DataDirectory(nidtSec.Rva, data.GetVirtualSize())); + } + + File.UpdateHeaders(); + Logger.Info("PE file patched successfully."); + return true; + } + + public bool RemoveSection(string name) { + var section = File.Sections.FirstOrDefault(s => s.Name == name); + if (section is not null) { + return File.Sections.Remove(section); + } + return false; + } + + public bool Unpatch() { + for (int i = File.Sections.Count - 1; i >= 0; i--) { + var section = File.Sections[i]!; + if (section.Name == SectionRTIH) { + using var reader = section.CreateReader().ToReader(); + var type = AbstractHeader.PeekInfo(reader); + var header = HeaderFactory.Create(type); + header.ReadFrom(reader); + + if (header is not AbstractPEImporterHeader peHeader) { + Logger.Warn($"RTIH section does not contain a valid PE Importer Header, cannot unpatch."); + return false; + } + + // Restore the old import directory + File.OptionalHeader.SetDataDirectory(DataDirectoryIndex.ImportDirectory, new DataDirectory(peHeader.OldIDT, peHeader.OldIDTSize)); + } + + if (IsCustomSection(section.Name)) { + Logger.Debug($"Removing custom section '{section.Name}'..."); + File.Sections.RemoveAt(i); + } + } + File.AlignSections(); + return true; + } + } +} diff --git a/ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs b/ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs new file mode 100644 index 0000000..af4e638 --- /dev/null +++ b/ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs @@ -0,0 +1,41 @@ +using Amethyst.Common.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching.PE.V1 { + public class PEDataSymbol : AbstractPESymbol { + public override uint FormatVersion => 1; + public override string Kind => "data"; + + public bool IsVirtualTableAddress { get; set; } = false; + public bool IsVirtualTable { get; set; } = false; + public ulong Address { get; set; } = 0x0; + + public override bool IsShadowSymbol => IsVirtualTable; + + public override void WriteTo(BinaryWriter writer) { + base.WriteTo(writer); + writer.Write((byte)(IsVirtualTableAddress ? 1 : 0)); + writer.Write((byte)(IsVirtualTable ? 1 : 0)); + writer.Write(Address); + } + + public override void ReadFrom(BinaryReader reader) { + base.ReadFrom(reader); + IsVirtualTableAddress = reader.ReadByte() != 0; + IsVirtualTable = reader.ReadByte() != 0; + Address = reader.ReadUInt64(); + } + + public override void SetStorage(BinaryWriter writer) { + if (!HasStorage) + throw new InvalidOperationException("Cannot set storage on a symbol without storage."); + writer.Align(8, 0x00); // Align to 8 bytes with zeros + StorageOffset = (uint)writer.BaseStream.Position; + writer.Write(new byte[8]); + } + } +} diff --git a/ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs b/ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs new file mode 100644 index 0000000..d4c7ca6 --- /dev/null +++ b/ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs @@ -0,0 +1,73 @@ +using Amethyst.Common.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching.PE.V1 { + public class PEFunctionSymbol : AbstractPESymbol { + public static readonly byte[] VirtualDestructorDeletingDisableBlock = [ + 0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, // mov rax, 0x1000000000000000 + 0x31, 0xD2, // xor edx, edx (sets delete flag to false) + 0xFF, 0xE0 // jmp rax + ]; + + public override uint FormatVersion => 1; + public override string Kind => "function"; + + public override bool IsShadowSymbol => false; + + public bool IsDestructor { get; set; } = false; + public bool IsVirtual { get; set; } = false; + public uint VirtualIndex { get; set; } = 0; + public string VirtualTable { get; set; } = string.Empty; + public bool IsSignature { get; set; } = false; + public string Signature { get; set; } = string.Empty; + public ulong Address { get; set; } = 0x0; + + public override void ReadFrom(BinaryReader reader) { + base.ReadFrom(reader); + IsDestructor = reader.ReadByte() != 0; + IsVirtual = reader.ReadByte() != 0; + if (IsVirtual) { + VirtualIndex = reader.ReadUInt32(); + VirtualTable = reader.ReadPrefixedString(); + } + else { + IsSignature = reader.ReadByte() != 0; + if (IsSignature) + Signature = reader.ReadPrefixedString(); + else + Address = reader.ReadUInt64(); + } + } + + public override void WriteTo(BinaryWriter writer) { + base.WriteTo(writer); + writer.Write((byte)(IsDestructor ? 1 : 0)); + writer.Write((byte)(IsVirtual ? 1 : 0)); + if (IsVirtual) { + writer.Write(VirtualIndex); + writer.WritePrefixedString(VirtualTable); + } + else { + writer.Write((byte)(IsSignature ? 1 : 0)); + if (IsSignature) + writer.WritePrefixedString(Signature); + else + writer.Write(Address); + } + } + + public override void SetStorage(BinaryWriter writer) { + if (!HasStorage) + throw new InvalidOperationException("Cannot set storage on a symbol without storage."); + if (!IsDestructor) + throw new InvalidOperationException("Cannot set storage on a non-destructor function symbol."); + writer.Align(16, 0x90); // Align to 16 bytes with NOPs + StorageOffset = (uint)writer.BaseStream.Position; + writer.Write(VirtualDestructorDeletingDisableBlock); + } + } +} diff --git a/ModuleTweaker/Patching/PE/V1/PEImporterHeader.cs b/ModuleTweaker/Patching/PE/V1/PEImporterHeader.cs new file mode 100644 index 0000000..23da440 --- /dev/null +++ b/ModuleTweaker/Patching/PE/V1/PEImporterHeader.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching.PE.V1 { + public class PEImporterHeader : AbstractPEImporterHeader { + public override uint FormatVersion => 1; + } +} diff --git a/ModuleTweaker/Patching/PEFileHelper.cs b/ModuleTweaker/Patching/PEFileHelper.cs deleted file mode 100644 index d7127a3..0000000 --- a/ModuleTweaker/Patching/PEFileHelper.cs +++ /dev/null @@ -1,446 +0,0 @@ -using Amethyst.Common.Diagnostics; -using Amethyst.Common.Models; -using Amethyst.ModuleTweaker.Patching.Models; -using AsmResolver; -using AsmResolver.IO; -using AsmResolver.PE.File; -using AsmResolver.PE.File.Headers; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using static System.Runtime.InteropServices.JavaScript.JSType; - -namespace Amethyst.ModuleTweaker.Patching -{ - public class PEFileHelper(PEFile file) - { - public const string StringTableName = ".strt"; - public const string FunctionDescriptorSectionName = ".fndt"; - public const string VirtualFunctionDescriptorSectionName = ".vfndt"; - public const string VariableDescriptorSectionName = ".vardt"; - public const string VirtualTableDescriptorSectionName = ".vtbdt"; - public const string NewImportDescriptorSectionName = ".idnew"; - public static HashSet SectionNames = [ - VariableDescriptorSectionName, - FunctionDescriptorSectionName, - StringTableName, - VirtualFunctionDescriptorSectionName, - VirtualTableDescriptorSectionName, - NewImportDescriptorSectionName - ]; - - public PEFile File { get; private set; } = file; - - public bool IsPatched() - { - foreach (var section in File.Sections) - { - if (IsCustomSectionName(section.Name)) - return true; - } - return false; - } - - public void RemoveSection(string name) - { - var section = File.Sections.FirstOrDefault(s => s.Name == name); - if (section is not null) - File.Sections.Remove(section); - } - - public void Unpatch() - { - if (!IsPatched()) - return; - Logger.Debug("Unpatching PE file..."); - for (int i = File.Sections.Count - 1; i >= 0; i--) - { - var section = File.Sections[i]; - if (section.Name == NewImportDescriptorSectionName) - { - BinaryStreamReader reader = section.CreateReader(); - uint originalIDTRva = reader.ReadUInt32(); - uint originalIDTSize = reader.ReadUInt32(); - - File.OptionalHeader.SetDataDirectory( - DataDirectoryIndex.ImportDirectory, - new(originalIDTRva, originalIDTSize) - ); - } - - if (IsCustomSectionName(section.Name)) - { - Logger.Debug($"Removing custom section '{section.Name}'..."); - File.Sections.RemoveAt(i); - } - } - File.AlignSections(); - } - - public bool Patch( - IEnumerable functions, - IEnumerable variables, - IEnumerable vtables, - IEnumerable vfunctions) - { - if (IsPatched()) - { - Logger.Debug("PE file is already patched, unpatching it first."); - Unpatch(); - } - - Logger.Debug("Patching PE file for runtime importing..."); - - // Get the import directory entries - var importDirectory = File.OptionalHeader.GetDataDirectory(DataDirectoryIndex.ImportDirectory); - if (!importDirectory.IsPresentInPE) - { - Logger.Warn("PE file has no import directory, no reason to patch it."); - return false; - } - - // Read existing import descriptors - List importDescriptors = []; - { - var importDirectoryReader = File.CreateDataDirectoryReader(importDirectory); - while (importDirectoryReader.CanRead(ImportDescriptor.Size)) - { - var descriptor = ImportDescriptor.Read(ref importDirectoryReader); - if (descriptor.IsZero) - break; - importDescriptors.Add(descriptor); - } - } - - // Find the import descriptor for "Minecraft.Windows.exe" - ImportDescriptor? minecraftWindowsImportDescriptor = null; - foreach (var descriptor in importDescriptors) - { - var nameRva = descriptor.Name; - var name = File.CreateReaderAtRva(nameRva).ReadAsciiString(); - if (name.StartsWith("Minecraft.Windows", StringComparison.OrdinalIgnoreCase)) - { - minecraftWindowsImportDescriptor = descriptor; - break; - } - } - - // If not found, no reason to patch - if (minecraftWindowsImportDescriptor is null) - { - Logger.Warn("PE file does not import from 'Minecraft.Windows.exe', no reason to patch it."); - return false; - } - - // Map all mangled names for "Minecraft.Windows.exe" functions to IAT offsets - uint iatRva = minecraftWindowsImportDescriptor.Value.FirstThunk; - uint iatSize = 0; - Dictionary iatIndices = []; - { - var reader = File.CreateReaderAtRva(minecraftWindowsImportDescriptor.Value.OriginalFirstThunk); - uint iatIndex = 0; - while (reader.CanRead(sizeof(ulong))) - { - ulong iltEntry = reader.ReadUInt64(); - if (iltEntry == 0) - break; - iatIndex++; - bool isOrdinal = (iltEntry & 0x8000000000000000) != 0; - if (isOrdinal) - continue; - - uint hintNameRva = (uint)(iltEntry & 0x7FFFFFFFFFFFFFFF); - var hintNameReader = File.CreateReaderAtRva(hintNameRva); - ushort hint = hintNameReader.ReadUInt16(); - string functionName = hintNameReader.ReadAsciiString(); - iatIndices[functionName] = iatIndex - 1; - } - iatSize = iatIndex; - } - - Dictionary stringToIndex = []; - { - // Create string table section - // Contains all strings (eg. mangled names, signatures) to be resolved at runtime - PESection section = new( - StringTableName, - SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); - - using var ms = new MemoryStream(); - var writer = new BinaryStreamWriter(ms); - uint countPosition = (uint)ms.Position; - writer.WriteUInt32(0u); // Placeholder for count - uint index = 0; - - // Write all function names - foreach (var method in functions) - { - writer.WriteBytes([.. Encoding.ASCII.GetBytes(method.Name), 0]); - stringToIndex[method.Name] = index++; - - } - - // Write all function signatures - foreach (var method in functions.Where(m => m.Signature is not null)) - { - writer.WriteBytes([.. Encoding.ASCII.GetBytes(method.Signature!), 0]); - stringToIndex[method.Signature!] = index++; - } - - // Write all variable names - foreach (var variable in variables) - { - writer.WriteBytes([.. Encoding.ASCII.GetBytes(variable.Name), 0]); - stringToIndex[variable.Name] = index++; - } - - // Write all virtual table names - foreach (var vtable in vtables) - { - writer.WriteBytes([.. Encoding.ASCII.GetBytes(vtable.Name), 0]); - stringToIndex[vtable.Name] = index++; - } - - // Write all virtual function names - foreach (var vfunc in vfunctions) - { - writer.WriteBytes([.. Encoding.ASCII.GetBytes(vfunc.Name), 0]); - stringToIndex[vfunc.Name] = index++; - } - - // Go back and write the count - ms.Seek(countPosition, SeekOrigin.Begin); - writer.WriteUInt32(index); - - section.Contents = new DataSegment(ms.ToArray()); - File.Sections.Add(section); - Logger.Info($"Added {index} strings."); - } - - // Create function descriptor table section - // Contains descriptors for all functions to be resolved at runtime - { - PESection section = new( - FunctionDescriptorSectionName, - SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); - using var ms = new MemoryStream(); - var writer = new BinaryStreamWriter(ms); - uint countPosition = (uint)ms.Position; - writer.WriteUInt32(0u); // Placeholder for count - writer.WriteUInt32(iatRva); - writer.WriteUInt32(iatSize); - uint count = 0; - foreach (var function in functions) - { - if (!iatIndices.TryGetValue(function.Name, out uint iatIndex)) - continue; - uint nameIndex = stringToIndex[function.Name]; - bool usesSignature = function.Signature is not null; - - writer.WriteUInt32(nameIndex); - writer.WriteUInt32(iatIndex); - writer.WriteByte((byte)(usesSignature ? 1 : 0)); - - if (usesSignature) - { - uint signatureIndex = stringToIndex[function.Signature!]; - writer.WriteUInt64(signatureIndex); - } - else - { - if (!ulong.TryParse(function.Address?.Replace("0x", "") ?? "0", out ulong address)) - writer.WriteUInt64(0x0); - else - writer.WriteUInt64(address); - } - - // uint: NameIndex - // uint: IATIndex - // byte: UsesSignature - // ulong: SignatureIndex or Address - count++; - Logger.Debug($"Tweaked: '{function.Name}'."); - } - - // Go back and write the count - ms.Seek(countPosition, SeekOrigin.Begin); - writer.WriteUInt32(count); - - section.Contents = new DataSegment(ms.ToArray()); - File.Sections.Add(section); - } - - // Create variable descriptor table section - // Contains descriptors for all variables to be resolved at runtime - { - PESection section = new( - VariableDescriptorSectionName, - SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); - using var ms = new MemoryStream(); - var writer = new BinaryStreamWriter(ms); - uint countPosition = (uint)ms.Position; - writer.WriteUInt32(0u); // Placeholder for count - writer.WriteUInt32(iatRva); - writer.WriteUInt32(iatSize); - uint count = 0; - foreach (var variable in variables) - { - if (!iatIndices.TryGetValue(variable.Name, out uint iatIndex)) - continue; - uint nameIndex = stringToIndex[variable.Name]; - - writer.WriteUInt32(nameIndex); - writer.WriteUInt32(iatIndex); - string address = variable.Address.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? variable.Address[2..] : variable.Address; - - try - { - ulong addressValue = ulong.Parse(address, NumberStyles.HexNumber); - writer.WriteUInt64(addressValue); - } - catch (Exception) - { - writer.WriteUInt64(0x0); - } - - // uint: NameIndex - // uint: IATIndex - // ulong: Address - count++; - Logger.Debug($"Tweaked: '{variable.Name}'."); - } - - // Go back and write the count - ms.Seek(countPosition, SeekOrigin.Begin); - writer.WriteUInt32(count); - - section.Contents = new DataSegment(ms.ToArray()); - File.Sections.Add(section); - } - - // Create virtual table descriptor table section - { - PESection section = new( - VirtualTableDescriptorSectionName, - SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); - using var ms = new MemoryStream(); - var writer = new BinaryStreamWriter(ms); - uint countPosition = (uint)ms.Position; - writer.WriteUInt32(0u); // Placeholder for count - uint count = 0; - foreach (var vtable in vtables) - { - uint nameIndex = stringToIndex[vtable.Name]; - - writer.WriteUInt32(nameIndex); - string address = vtable.Address.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? vtable.Address[2..] : vtable.Address; - - try - { - ulong addressValue = ulong.Parse(address, NumberStyles.HexNumber); - writer.WriteUInt64(addressValue); - } - catch (Exception) - { - writer.WriteUInt64(0x0); - } - - // uint: NameIndex - // ulong: Address - count++; - Logger.Debug($"Tweaked: '{vtable.Name}'."); - } - - // Go back and write the count - ms.Seek(countPosition, SeekOrigin.Begin); - writer.WriteUInt32(count); - - section.Contents = new DataSegment(ms.ToArray()); - File.Sections.Add(section); - } - - // Create virtual function descriptor table section - { - PESection section = new( - VirtualFunctionDescriptorSectionName, - SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); - using var ms = new MemoryStream(); - var writer = new BinaryStreamWriter(ms); - uint countPosition = (uint)ms.Position; - writer.WriteUInt32(0u); // Placeholder for count - writer.WriteUInt32(iatRva); - writer.WriteUInt32(iatSize); - uint count = 0; - foreach (var vfunc in vfunctions) - { - if (!iatIndices.TryGetValue(vfunc.Name, out uint iatIndex)) - continue; - if (!stringToIndex.TryGetValue(vfunc.VirtualTable, out uint vtableIndex)) - continue; - - uint nameIndex = stringToIndex[vfunc.Name]; - - writer.WriteUInt32(nameIndex); - writer.WriteUInt32(iatIndex); - writer.WriteUInt32(vtableIndex); - writer.WriteUInt32(vfunc.Index); - - // uint: NameIndex - // uint: IATIndex - // uint: VirtualTableNameIndex - // uint: FunctionIndex - count++; - Logger.Debug($"Tweaked: '{vfunc.Name}'."); - } - - // Go back and write the count - ms.Seek(countPosition, SeekOrigin.Begin); - writer.WriteUInt32(count); - - section.Contents = new DataSegment(ms.ToArray()); - File.Sections.Add(section); - } - - // Create new import descriptor table section - // Contains a copy of the original import descriptor table without the "Minecraft.Windows.exe" entry - { - PESection section = new( - NewImportDescriptorSectionName, - SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); - using var ms = new MemoryStream(); - var writer = new BinaryStreamWriter(ms); - - // Write original import directory RVA and size at the start - writer.WriteUInt32(importDirectory.VirtualAddress); - writer.WriteUInt32(importDirectory.Size); - - foreach (var descriptor in importDescriptors) - { - if (descriptor.Equals(minecraftWindowsImportDescriptor.Value)) - continue; - descriptor.Write(writer); - } - - // Write null descriptor at the end - ImportDescriptor.Empty.Write(writer); - section.Contents = new DataSegment(ms.ToArray()); - File.Sections.Add(section); - File.AlignSections(); - - // Update import directory to point to the new table - File.OptionalHeader.SetDataDirectory( - DataDirectoryIndex.ImportDirectory, - new(section.Rva + sizeof(uint) * 2, (uint)ms.Length)); - Logger.Debug("Removed import from 'Minecraft.Windows.exe'."); - } - Logger.Debug("Patching completed."); - return true; - } - - public static bool IsCustomSectionName(string name) => SectionNames.Contains(name); - } -} diff --git a/ModuleTweaker/Patching/SymbolFactory.cs b/ModuleTweaker/Patching/SymbolFactory.cs new file mode 100644 index 0000000..462c102 --- /dev/null +++ b/ModuleTweaker/Patching/SymbolFactory.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public static class SymbolFactory { + private static readonly Dictionary> sConstructors = []; + public static IReadOnlyDictionary> Constructors => sConstructors; + + public static void Register(SymbolType type, Func constructor) { + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(constructor); + if (sConstructors.ContainsKey(type)) + throw new InvalidOperationException($"A constructor for {type} is already registered."); + sConstructors[type] = constructor; + } + + public static AbstractSymbol Create(SymbolType type) { + if (sConstructors.TryGetValue(type, out var constructor)) + return constructor(); + throw new InvalidOperationException($"No constructor registered for {type}."); + } + } +} diff --git a/ModuleTweaker/Patching/SymbolInfo.cs b/ModuleTweaker/Patching/SymbolInfo.cs new file mode 100644 index 0000000..aad7eb6 --- /dev/null +++ b/ModuleTweaker/Patching/SymbolInfo.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public class SymbolInfo(uint version, string format, string kind, string name) { + public SymbolType Type { get; init; } = new(version, format, kind); + public string Name { get; set; } = name; + + public override bool Equals(object? obj) { + if (obj is not SymbolInfo other) + return false; + return Type == other.Type && + Name == other.Name; + } + + public override int GetHashCode() { + return HashCode.Combine(Type, Name); + } + + public override string ToString() { + return $"SymbolInfo[{Type}, {Name}]"; + } + + public static bool operator ==(SymbolInfo? a, SymbolInfo? b) => + a?.Equals(b) ?? b is null; + + public static bool operator !=(SymbolInfo? a, SymbolInfo? b) => + !(a == b); + } +} diff --git a/ModuleTweaker/Patching/SymbolType.cs b/ModuleTweaker/Patching/SymbolType.cs new file mode 100644 index 0000000..a02d381 --- /dev/null +++ b/ModuleTweaker/Patching/SymbolType.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public class SymbolType(uint version, string format, string kind) { + public uint Version { get; set; } = version; + public string Format { get; set; } = format; + public string Kind { get; set; } = kind; + + public override bool Equals(object? obj) { + if (obj is not SymbolType other) + return false; + return Version == other.Version && Format == other.Format && Kind == other.Kind; + } + + public override int GetHashCode() { + return HashCode.Combine(Version, Format, Kind); + } + + public override string ToString() { + return $"SymbolType[v{Version}, {Format}, {Kind}]"; + } + + public static bool operator ==(SymbolType? a, SymbolType? b) => + a?.Equals(b) ?? b is null; + + public static bool operator !=(SymbolType? a, SymbolType? b) => + !(a == b); + } +} diff --git a/ModuleTweaker/Properties/launchSettings.json b/ModuleTweaker/Properties/launchSettings.json index 8b4f1e0..7efb724 100644 --- a/ModuleTweaker/Properties/launchSettings.json +++ b/ModuleTweaker/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "ModuleTweaker": { "commandName": "Project", - "commandLineArgs": "-m \"C:\\Users\\bravo\\AppData\\Local\\Packages\\Microsoft.MinecraftUWP_8wekyb3d8bbwe\\LocalState\\games\\com.mojang\\amethyst\\mods\\AmethystRuntime@2.0.0\\AmethystRuntime.dll\"\r\n-s \"C:\\Users\\bravo\\Documents\\GitHub\\Amethyst\\generated\\symbols\"" + "commandLineArgs": "-m \"./input/Amethyst-Runtime.dll\"\r\n-s \"./output/symbols\"" } } } \ No newline at end of file diff --git a/ModuleTweaker/Utility/Utils.cs b/ModuleTweaker/Utility/Utils.cs new file mode 100644 index 0000000..5ed7089 --- /dev/null +++ b/ModuleTweaker/Utility/Utils.cs @@ -0,0 +1,19 @@ +using AsmResolver.IO; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Utility { + public static class Utils { + public static BinaryReader ToReader(this BinaryStreamReader reader) { + var ms = new MemoryStream(reader.ReadToEnd()); + return new BinaryReader(ms, Encoding.UTF8, false); + } + + public static BinaryWriter ToWriter(this BinaryStreamWriter writer) { + return new BinaryWriter(writer.BaseStream, Encoding.UTF8, false); + } + } +} diff --git a/SymbolGenerator/Commands/MainCommand.cs b/SymbolGenerator/Commands/MainCommand.cs index ad75716..5f3f237 100644 --- a/SymbolGenerator/Commands/MainCommand.cs +++ b/SymbolGenerator/Commands/MainCommand.cs @@ -149,7 +149,7 @@ public ValueTask ExecuteAsync(IConsole console) if (processed is null) continue; - if (processed.Target.Location is not { } location || location.File == "Unknown File") + if (processed.Target.Location is not { } location || location.File == "") { Logger.Warn($"Skipping annotation for '{processed.Target}' due to unknown location."); continue; @@ -168,7 +168,8 @@ public ValueTask ExecuteAsync(IConsole console) annotationsData[location.File].Add(new VariableSymbolModel { Name = mangled, - Address = vtable.Address + Address = vtable.Address, + IsVirtualTableAddress = true }); } diff --git a/SymbolGenerator/Parsing/ASTClass.cs b/SymbolGenerator/Parsing/ASTClass.cs index 6646fc6..c68d578 100644 --- a/SymbolGenerator/Parsing/ASTClass.cs +++ b/SymbolGenerator/Parsing/ASTClass.cs @@ -1,4 +1,5 @@ -using Amethyst.SymbolGenerator.Parsing.Annotations; +using Amethyst.Common.Utility; +using Amethyst.SymbolGenerator.Parsing.Annotations; namespace Amethyst.SymbolGenerator.Parsing { @@ -6,7 +7,7 @@ public class ASTClass : AbstractAnnotationTarget { public string? Name { get; set; } public string? Namespace { get; set; } - public override ASTCursorLocation? Location { get; set; } + public override CursorLocation? Location { get; set; } public ASTBaseSpecifier[] DirectBaseClasses { get; set; } = []; public ASTMethod[] Methods { get; set; } = []; public ASTVariable[] Variables { get; set; } = []; diff --git a/SymbolGenerator/Parsing/ASTCursorLocation.cs b/SymbolGenerator/Parsing/ASTCursorLocation.cs deleted file mode 100644 index 07ecd55..0000000 --- a/SymbolGenerator/Parsing/ASTCursorLocation.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Amethyst.SymbolGenerator.Parsing -{ - public record ASTCursorLocation(string File, uint Line, uint Column, uint Offset) - { - override public string ToString() - { - return $"{File}:{Line}:{Column}:{Offset}"; - } - } -} diff --git a/SymbolGenerator/Parsing/ASTMethod.cs b/SymbolGenerator/Parsing/ASTMethod.cs index 82a1335..42e123c 100644 --- a/SymbolGenerator/Parsing/ASTMethod.cs +++ b/SymbolGenerator/Parsing/ASTMethod.cs @@ -1,4 +1,5 @@ -using Amethyst.SymbolGenerator.Parsing.Annotations; +using Amethyst.Common.Utility; +using Amethyst.SymbolGenerator.Parsing.Annotations; namespace Amethyst.SymbolGenerator.Parsing { @@ -8,7 +9,7 @@ public class ASTMethod : AbstractAnnotationTarget public string MangledName { get; set; } = null!; public string? Namespace { get; set; } = null; public ASTClass? DeclaringClass { get; set; } = null; - public override ASTCursorLocation? Location { get; set; } = null; + public override CursorLocation? Location { get; set; } = null; public bool IsVirtual { get; set; } = false; public bool IsImported { get; set; } = false; public bool IsConstructor { get; set; } = false; diff --git a/SymbolGenerator/Parsing/ASTPrinter.cs b/SymbolGenerator/Parsing/ASTPrinter.cs index 7c6e648..060e333 100644 --- a/SymbolGenerator/Parsing/ASTPrinter.cs +++ b/SymbolGenerator/Parsing/ASTPrinter.cs @@ -9,7 +9,7 @@ public static void PrintVariable(ASTVariable variable, string inputDir) var kind = variable.IsFreeVariable ? "Free Variable" : "Variable"; Logger.Debug($"{kind}: {variable.FullName}, Imported: {variable.IsImported}, Static: {variable.IsStatic}"); if (variable.Location is not null && !string.IsNullOrEmpty(variable.Location.File)) - Logger.Debug($" at {Path.GetRelativePath(inputDir, variable.Location.File)}:{variable.Location.Line}:{variable.Location.Column}:{variable.Location.Offset}"); + Logger.Debug($" at {Path.GetRelativePath(inputDir, variable.Location.File)}:{variable.Location.Line}:{variable.Location.Column}"); } public static void PrintMethod(ASTMethod method, string inputDir) @@ -17,7 +17,7 @@ public static void PrintMethod(ASTMethod method, string inputDir) var kind = method.IsFreeFunction ? "Free Function" : "Method"; Logger.Debug($"{kind}: {method.FullName}, Virtual: {method.IsVirtual}, Imported: {method.IsImported}"); if (method.Location is not null && !string.IsNullOrEmpty(method.Location.File)) - Logger.Debug($" at {Path.GetRelativePath(inputDir, method.Location.File)}:{method.Location.Line}:{method.Location.Column}:{method.Location.Offset}"); + Logger.Debug($" at {Path.GetRelativePath(inputDir, method.Location.File)}:{method.Location.Line}:{method.Location.Column}"); } public static void PrintClass(ASTClass cls, string inputDir) @@ -31,7 +31,7 @@ public static void PrintClass(ASTClass cls, string inputDir) if (!string.IsNullOrEmpty(cls.Namespace)) Logger.Debug($" FullName: {cls.FullName}"); if (cls.Location != null) - Logger.Debug($" Location: {Path.GetRelativePath(inputDir, cls.Location.File)}:{cls.Location.Line}:{cls.Location.Column}:{cls.Location.Offset}"); + Logger.Debug($" Location: {Path.GetRelativePath(inputDir, cls.Location.File)}:{cls.Location.Line}:{cls.Location.Column}"); // Print methods foreach (var method in cls.Methods) diff --git a/SymbolGenerator/Parsing/ASTVariable.cs b/SymbolGenerator/Parsing/ASTVariable.cs index 76d79ad..fcb4592 100644 --- a/SymbolGenerator/Parsing/ASTVariable.cs +++ b/SymbolGenerator/Parsing/ASTVariable.cs @@ -1,4 +1,5 @@ -using Amethyst.SymbolGenerator.Parsing.Annotations; +using Amethyst.Common.Utility; +using Amethyst.SymbolGenerator.Parsing.Annotations; namespace Amethyst.SymbolGenerator.Parsing { @@ -8,7 +9,7 @@ public class ASTVariable : AbstractAnnotationTarget public string MangledName { get; set; } = string.Empty; public string? Namespace { get; set; } = null; public ASTClass? DeclaringClass { get; set; } = null; - public override ASTCursorLocation? Location { get; set; } = null; + public override CursorLocation? Location { get; set; } = null; public bool IsImported { get; set; } = false; public string? RawComment { get; set; } = null; public bool IsStatic { get; set; } = false; diff --git a/SymbolGenerator/Parsing/ASTVisitor.cs b/SymbolGenerator/Parsing/ASTVisitor.cs index a3db261..5cebe7d 100644 --- a/SymbolGenerator/Parsing/ASTVisitor.cs +++ b/SymbolGenerator/Parsing/ASTVisitor.cs @@ -1,5 +1,6 @@ using Amethyst.Common.Diagnostics; using Amethyst.Common.Extensions; +using Amethyst.Common.Utility; using ClangSharp.Interop; using System.Linq; @@ -9,7 +10,7 @@ public class ASTVisitor { private readonly Dictionary SpellingCache = []; private readonly Dictionary MangleCache = []; - private readonly Dictionary LocationCache = []; + private readonly Dictionary LocationCache = []; private readonly Dictionary RawCommentCache = []; private readonly Dictionary IsImportedCache = []; private readonly Dictionary FullNamespaceCache = []; @@ -64,7 +65,7 @@ public bool PrintErrors() return false; foreach (var diag in GetDiagnostics()) { - Action? log = diag.Severity switch + Action? log = diag.Severity switch { CXDiagnosticSeverity.CXDiagnostic_Error => Logger.Error, CXDiagnosticSeverity.CXDiagnostic_Fatal => Logger.Error, @@ -75,16 +76,16 @@ public bool PrintErrors() var location = diag.Location; location.GetFileLocation(out var file, out var line, out var column, out var offset); string filePath = file.ToString() ?? "Unknown file"; - string message = diag.Format(CXDiagnostic.DefaultDisplayOptions).ToString() ?? "Unknown diagnostic message"; - log($"{filePath}({line},{column}): {message}"); + string message = diag.Spelling.ToString() ?? "Unknown diagnostic message"; + log(message, new(filePath, line, column)); } return true; } #endregion - public string GetUsr(CXCursor cursor) + public static string GetUsr(CXCursor cursor) { - return cursor.Usr.ToString() + cursor.IsDefinition; + return cursor.Usr.ToString() + "@" + cursor.IsDefinition; } public string GetSpelling(CXCursor cursor) @@ -99,7 +100,7 @@ public string GetSpelling(CXCursor cursor) return spelling; } - public ASTCursorLocation? GetLocation(CXCursor cursor) + public CursorLocation? GetLocation(CXCursor cursor) { string usr = GetUsr(cursor); if (LocationCache.TryGetValue(usr, out var cached)) @@ -111,7 +112,7 @@ public string GetSpelling(CXCursor cursor) if (!string.IsNullOrEmpty(path)) path = Path.GetFullPath(path.ToString()).NormalizeSlashes(); - var location = new ASTCursorLocation(path, line, column, offset); + var location = new CursorLocation(path, line, column); LocationCache[usr] = location; return location; @@ -416,7 +417,7 @@ public ASTVariable[] GetVariables() if (ClassCache.TryGetValue(usr, out var cached)) return (CXChildVisitResult.CXChildVisit_Continue, cached); - ASTCursorLocation? location = GetLocation(cursor); + CursorLocation? location = GetLocation(cursor); // Collect members var (methodsCursors, variableCursors, classesCursors, baseCursors) = CollectClassMembers(cursor); diff --git a/SymbolGenerator/Parsing/Annotations/AbstractAnnotationTarget.cs b/SymbolGenerator/Parsing/Annotations/AbstractAnnotationTarget.cs index 33edaf3..5e83763 100644 --- a/SymbolGenerator/Parsing/Annotations/AbstractAnnotationTarget.cs +++ b/SymbolGenerator/Parsing/Annotations/AbstractAnnotationTarget.cs @@ -1,8 +1,10 @@ -namespace Amethyst.SymbolGenerator.Parsing.Annotations +using Amethyst.Common.Utility; + +namespace Amethyst.SymbolGenerator.Parsing.Annotations { public abstract class AbstractAnnotationTarget { - public virtual ASTCursorLocation? Location { get; set; } + public virtual CursorLocation? Location { get; set; } public HashSet Annotations { get; set; } = []; diff --git a/SymbolGenerator/Parsing/Annotations/Comments/CommentParser.cs b/SymbolGenerator/Parsing/Annotations/Comments/CommentParser.cs index 8674fa6..275f76a 100644 --- a/SymbolGenerator/Parsing/Annotations/Comments/CommentParser.cs +++ b/SymbolGenerator/Parsing/Annotations/Comments/CommentParser.cs @@ -1,11 +1,12 @@ -using Amethyst.SymbolGenerator.Parsing.Annotations.Comments; +using Amethyst.Common.Utility; +using Amethyst.SymbolGenerator.Parsing.Annotations.Comments; using System.Text.RegularExpressions; namespace Amethyst.SymbolGenerator.Parsing.Annotations { public static partial class CommentParser { - public static IEnumerable ParseAnnotations(AbstractAnnotationTarget target, string comment, ASTCursorLocation location) + public static IEnumerable ParseAnnotations(AbstractAnnotationTarget target, string comment, CursorLocation location) { using var sr = new StringReader(comment); string? line; diff --git a/SymbolGenerator/Parsing/Annotations/Comments/RawAnnotation.cs b/SymbolGenerator/Parsing/Annotations/Comments/RawAnnotation.cs index 0bfce13..4d2670f 100644 --- a/SymbolGenerator/Parsing/Annotations/Comments/RawAnnotation.cs +++ b/SymbolGenerator/Parsing/Annotations/Comments/RawAnnotation.cs @@ -1,6 +1,8 @@ -namespace Amethyst.SymbolGenerator.Parsing.Annotations.Comments +using Amethyst.Common.Utility; + +namespace Amethyst.SymbolGenerator.Parsing.Annotations.Comments { - public record RawAnnotation(string Tag, IEnumerable Arguments, ASTCursorLocation Location, AbstractAnnotationTarget Target) + public record RawAnnotation(string Tag, IEnumerable Arguments, CursorLocation Location, AbstractAnnotationTarget Target) { public override string ToString() { diff --git a/SymbolGenerator/Parsing/Annotations/Handlers/VirtualIndexAnnotationHandler.cs b/SymbolGenerator/Parsing/Annotations/Handlers/VirtualIndexAnnotationHandler.cs index 8df3411..a6cec96 100644 --- a/SymbolGenerator/Parsing/Annotations/Handlers/VirtualIndexAnnotationHandler.cs +++ b/SymbolGenerator/Parsing/Annotations/Handlers/VirtualIndexAnnotationHandler.cs @@ -48,7 +48,8 @@ public override ProcessedAnnotation Handle(RawAnnotation annotation) Index = ParameterPack.Index, VirtualTable = $"{target.DeclaringClass!.FullName}::vtable::'{ParameterPack.TargetVirtualTable}'", Inherit = ParameterPack.ShouldInherit, - Overrides = ParameterPack.ShouldInherit ? target.OverrideOf!.MangledName : null + Overrides = ParameterPack.ShouldInherit ? target.OverrideOf!.MangledName : null, + IsVirtualDestructor = target.IsDestructor }, Resolve ); diff --git a/SymbolGenerator/Properties/launchSettings.json b/SymbolGenerator/Properties/launchSettings.json index ff1e550..2a964cb 100644 --- a/SymbolGenerator/Properties/launchSettings.json +++ b/SymbolGenerator/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Generate Symbols": { "commandName": "Project", - "commandLineArgs": "-i \"./input/src\"\r\n-o \"./output\"\r\n-f \"mc\"\r\n-p \"win-client\"\r\n--\r\n-x c++\r\n-std=c++23\r\n-fms-extensions\r\n-fms-compatibility\r\n-fms-define-stdc\r\n-I\"./input/include\"\r\n-I\"./input/src\"" + "commandLineArgs": "-i \"./input/src\"\r\n-o \"./output\"\r\n-f \"mc\"\r\n-p \"win-client\"\r\n--\r\n-x c++\r\n-std=c++23\r\n\r\n-fms-extensions\r\n-fms-compatibility\r\n-fms-define-stdc\r\n-I\"./input/include\"\r\n-I\"./input/src\"" } } } \ No newline at end of file