22using Amethyst . Common . Models ;
33using Amethyst . ModuleTweaker . Patching ;
44using AsmResolver . PE . File ;
5+ using AsmResolver . PE . Imports ;
56using CliFx ;
67using CliFx . Attributes ;
78using CliFx . Infrastructure ;
9+ using K4os . Hash . xxHash ;
810using Newtonsoft . Json ;
11+ using System . Globalization ;
912
1013namespace Amethyst . ModuleTweaker . Commands
1114{
1215 [ Command ( Description = "Patches or unpatches modules for runtime importing support." ) ]
1316 public class MainCommand : ICommand
1417 {
15- [ CommandOption ( "module" , 'm' , Description = "The specified module path to patch." ) ]
18+ [ CommandOption ( "module" , 'm' , Description = "The specified module path to patch." , IsRequired = true ) ]
1619 public string ModulePath { get ; set ; } = null ! ;
1720
18- [ CommandOption ( "symbols" , 's' , Description = "Path to directory containing *.symbols.json to use for patching." ) ]
21+ [ CommandOption ( "symbols" , 's' , Description = "Path to directory containing *.symbols.json to use for patching." , IsRequired = true ) ]
1922 public string SymbolsPath { get ; set ; } = null ! ;
2023
24+ [ CommandOption ( "output" , 'o' , Description = "Path to save temporary files, don't confuse with -m." ) ]
25+ public string OutputPath { get ; set ; } = null ! ;
26+
2127 public ValueTask ExecuteAsync ( IConsole console )
2228 {
2329 FileInfo module = new ( ModulePath ) ;
24- DirectoryInfo symbols = new ( SymbolsPath ) ;
25- if ( module . Exists is false )
26- {
27- Logger . Warn ( "Couldn't patch module, specified module does not exist." ) ;
30+ DirectoryInfo symbolsDir = new ( SymbolsPath ) ;
31+ if ( module . Exists is false ) {
32+ Logger . Fatal ( "Couldn't patch module, specified module does not exist." ) ;
2833 return default ;
2934 }
3035
31- if ( symbols . Exists is false )
32- {
33- Logger . Warn ( "Couldn't patch module, specified symbols directory does not exist." ) ;
36+ if ( symbolsDir . Exists is false ) {
37+ Logger . Fatal ( "Couldn't patch module, specified symbols directory does not exist." ) ;
3438 return default ;
3539 }
3640
41+ if ( string . IsNullOrEmpty ( OutputPath ) ) {
42+ OutputPath = Path . GetFullPath ( Path . Combine ( SymbolsPath , "../" ) ) ;
43+ }
44+ DirectoryInfo outDir = new ( OutputPath ) ;
45+
46+ ulong ParseAddress ( string ? address )
47+ {
48+ if ( string . IsNullOrEmpty ( address ) )
49+ return 0x0 ;
50+ if ( address . StartsWith ( "0x" , StringComparison . OrdinalIgnoreCase ) )
51+ address = address [ 2 ..] ;
52+ if ( ! ulong . TryParse ( address , NumberStyles . HexNumber , null , out var addr ) )
53+ return 0x0 ;
54+ return addr ;
55+ }
56+
57+ SymbolFactory . Register ( new SymbolType ( 1 , "pe32+" , "data" ) , ( ) => new Patching . PE . V1 . PEDataSymbol ( ) ) ;
58+ SymbolFactory . Register ( new SymbolType ( 1 , "pe32+" , "function" ) , ( ) => new Patching . PE . V1 . PEFunctionSymbol ( ) ) ;
59+ HeaderFactory . Register ( new HeaderType ( 1 , "pe32+" ) , ( args ) => new Patching . PE . V1 . PEImporterHeader ( ) ) ;
60+
3761 // Collect all symbol files and accumulate mangled names
38- IEnumerable < FileInfo > symbolFiles = symbols . EnumerateFiles ( "*.json" , SearchOption . AllDirectories ) ;
39- HashSet < FunctionSymbolModel > methods = [ ] ;
40- HashSet < VariableSymbolModel > variables = [ ] ;
41- HashSet < VirtualTableSymbolModel > vtables = [ ] ;
42- HashSet < VirtualFunctionSymbolModel > vfuncs = [ ] ;
62+ IEnumerable < FileInfo > symbolFiles = symbolsDir . EnumerateFiles ( "*.json" , SearchOption . AllDirectories ) ;
63+ List < AbstractSymbol > symbols = [ ] ;
4364 foreach ( var symbolFile in symbolFiles )
4465 {
4566 using var stream = symbolFile . OpenRead ( ) ;
@@ -50,29 +71,48 @@ public ValueTask ExecuteAsync(IConsole console)
5071 switch ( symbolJson . FormatVersion )
5172 {
5273 case 1 :
53- foreach ( var function in symbolJson . Functions )
54- {
74+ foreach ( var function in symbolJson . Functions ) {
5575 if ( string . IsNullOrEmpty ( function . Name ) )
5676 continue ;
57- methods . Add ( function ) ;
77+ symbols . Add ( new Patching . PE . V1 . PEFunctionSymbol {
78+ Name = function . Name ,
79+ IsVirtual = false ,
80+ IsSignature = function . Signature is not null ,
81+ Address = ParseAddress ( function . Address ) ,
82+ Signature = function . Signature ?? string . Empty
83+ } ) ;
5884 }
59- foreach ( var variable in symbolJson . Variables )
60- {
61- if ( string . IsNullOrEmpty ( variable . Name ) )
85+ foreach ( var vfunc in symbolJson . VirtualFunctions ) {
86+ if ( string . IsNullOrEmpty ( vfunc . Name ) )
6287 continue ;
63- variables . Add ( variable ) ;
88+ symbols . Add ( new Patching . PE . V1 . PEFunctionSymbol {
89+ Name = vfunc . Name ,
90+ IsVirtual = true ,
91+ VirtualIndex = vfunc . Index ,
92+ VirtualTable = vfunc . VirtualTable ?? "this" ,
93+ IsDestructor = vfunc . IsVirtualDestructor ,
94+ HasStorage = vfunc . IsVirtualDestructor
95+ } ) ;
6496 }
65- foreach ( var vtable in symbolJson . VirtualTables )
66- {
67- if ( string . IsNullOrEmpty ( vtable . Name ) )
97+ foreach ( var variable in symbolJson . Variables ) {
98+ if ( string . IsNullOrEmpty ( variable . Name ) )
6899 continue ;
69- vtables . Add ( vtable ) ;
100+ symbols . Add ( new Patching . PE . V1 . PEDataSymbol {
101+ Name = variable . Name ,
102+ IsVirtualTable = false ,
103+ Address = ParseAddress ( variable . Address ) ,
104+ IsVirtualTableAddress = variable . IsVirtualTableAddress ,
105+ HasStorage = variable . IsVirtualTableAddress
106+ } ) ;
70107 }
71- foreach ( var vfunc in symbolJson . VirtualFunctions )
72- {
73- if ( string . IsNullOrEmpty ( vfunc . Name ) )
108+ foreach ( var vtable in symbolJson . VirtualTables ) {
109+ if ( string . IsNullOrEmpty ( vtable . Name ) )
74110 continue ;
75- vfuncs . Add ( vfunc ) ;
111+ symbols . Add ( new Patching . PE . V1 . PEDataSymbol {
112+ Name = vtable . Name ,
113+ IsVirtualTable = true ,
114+ Address = ParseAddress ( vtable . Address )
115+ } ) ;
76116 }
77117 break ;
78118 }
@@ -82,13 +122,35 @@ public ValueTask ExecuteAsync(IConsole console)
82122 try
83123 {
84124 // Patch the module
85- var file = PEFile . FromFile ( ModulePath ) ;
86- PEFileHelper helper = new ( file ) ;
87- if ( helper . Patch ( methods , variables , vtables , vfuncs ) )
125+ var bytes = File . ReadAllBytes ( ModulePath ) ;
126+ ulong hash = XXH64 . DigestOf ( bytes ) ;
127+ if ( File . Exists ( Path . Combine ( outDir . FullName , "module_hash.txt" ) ) ) {
128+ var existingHash = File . ReadAllText ( Path . Combine ( outDir . FullName , "module_hash.txt" ) ) ;
129+ if ( ulong . TryParse ( existingHash , NumberStyles . HexNumber , null , out var existingHashValue ) ) {
130+ if ( existingHashValue == hash ) {
131+ Logger . Info ( "Module hash matches previous hash, skipping patch." ) ;
132+ return default ;
133+ }
134+ }
135+ }
136+
137+ var peFile = PEFile . FromBytes ( bytes ) ;
138+ if ( peFile is null ) {
139+ Logger . Fatal ( "Failed to read module as a PE file." ) ;
140+ return default ;
141+ }
142+ Logger . Info ( $ "Loaded module '{ ModulePath } ' as PE file.") ;
143+ var patcher = new Patching . PE . PEPatcher ( peFile , symbols ) ;
144+
145+ if ( patcher . Patch ( ) )
88146 {
89- file . AlignSections ( ) ;
90- File . Copy ( ModulePath , ModulePath + ".backup" , true ) ;
91- file . Write ( ModulePath ) ;
147+ File . Copy ( ModulePath , ModulePath + ".bak" , true ) ;
148+ using var ms = new MemoryStream ( ) ;
149+ peFile . Write ( ms ) ;
150+ var newBytes = ms . ToArray ( ) ;
151+ ulong newHash = XXH64 . DigestOf ( newBytes ) ;
152+ File . WriteAllBytes ( ModulePath , newBytes ) ;
153+ File . WriteAllText ( Path . Combine ( outDir . FullName , "module_hash.txt" ) , newHash . ToString ( "X16" ) ) ;
92154 }
93155 }
94156 catch ( Exception ex )
0 commit comments