diff --git a/src/ISTAlter/Core/Patch.cs b/src/ISTAlter/Core/Patch.cs index c44bb6d..e6afec6 100755 --- a/src/ISTAlter/Core/Patch.cs +++ b/src/ISTAlter/Core/Patch.cs @@ -32,9 +32,20 @@ public static void PatchISTA(IPatcherProvider patcherProvider, ISTAOptions.Patch var cts = new CancellationTokenSource(); var factory = new TaskFactory(new ConcurrencyTaskScheduler(options.MaxDegreeOfParallelism)); - var tasks = pendingPatchList.Select(item => factory.StartNew(() => PatchSingleFile(item, guiBasePath, indentLength, patcherProvider, options), cts.Token)); + var tasks = pendingPatchList.Select(item => factory.StartNew(() => PatchSingleFile(item, guiBasePath, indentLength, patcherProvider, options), cts.Token)).ToArray(); Task.WaitAll(tasks, cts.Token); + foreach (var patchedFileFullPath in tasks.Select(t => t.Result).OfType()) + { + if (PatchUtils.Anchor(patchedFileFullPath)) + { + continue; + } + + File.Delete(patchedFileFullPath); + Log.Debug("Patched file {PatchedFileFullPath} discarded on verification", patchedFileFullPath); + } + foreach (var line in BuildIndicator(patcherProvider.Patches)) { Log.Information("{Indent}{Line}", new string(' ', indentLength), line); @@ -68,7 +79,7 @@ public static void PatchISTA(IPatcherProvider patcherProvider, ISTAOptions.Patch skippedCount); } - private static void PatchSingleFile(string pendingPatchItem, string guiBasePath, int indentLength, IPatcherProvider patcherProvider, ISTAOptions.PatchOptions options) + private static string? PatchSingleFile(string pendingPatchItem, string guiBasePath, int indentLength, IPatcherProvider patcherProvider, ISTAOptions.PatchOptions options) { var pendingPatchItemFullPath = pendingPatchItem.StartsWith('!') ? Path.Join(options.TargetPath, pendingPatchItem.Trim('!')) @@ -83,7 +94,7 @@ private static void PatchSingleFile(string pendingPatchItem, string guiBasePath, if (!pendingPatchItemFullPath.StartsWith(expectedBasePath, StringComparison.OrdinalIgnoreCase)) { Log.Error("Path traversal attempt detected: {Item}", pendingPatchItem); - return; + return null; } var originalDirPath = Path.GetDirectoryName(pendingPatchItemFullPath); @@ -105,7 +116,7 @@ private static void PatchSingleFile(string pendingPatchItem, string guiBasePath, pendingPatchItem, indent, string.Concat(Enumerable.Repeat("*", patcherProvider.Patches.Count))); - return; + return null; } Directory.CreateDirectory(patchedDirPath); @@ -127,7 +138,7 @@ private static void PatchSingleFile(string pendingPatchItem, string guiBasePath, Log.Information("{Item}{Indent}[NO BACKUP]", pendingPatchItem, indent); } - return; + return null; } using var module = PatchUtils.LoadModule(pendingPatchItemFullPath); @@ -141,7 +152,7 @@ private static void PatchSingleFile(string pendingPatchItem, string guiBasePath, indent, string.Concat(Enumerable.Repeat("*", patcherProvider.Patches.Count)), patcherVersion); - return; + return null; } // Patch and print result @@ -193,7 +204,7 @@ private static void PatchSingleFile(string pendingPatchItem, string guiBasePath, if (!isPatched) { Log.Information("{Item}{Indent}{Result} [NOP]", pendingPatchItem, indent, resultStr); - return; + return null; } // Create backup only when patches will be saved @@ -227,6 +238,7 @@ private static void PatchSingleFile(string pendingPatchItem, string guiBasePath, Log.Debug("Patched file {PatchedFileFullPath} created", patchedFileFullPath); Log.Information("{Item}{Indent}{Result} [FNC: {PatchedFunctionCount:00}]", pendingPatchItem, indent, resultStr, patchedFunctionCount); + return patchedFileFullPath; } catch (Exception ex) { @@ -245,7 +257,7 @@ private static void PatchSingleFile(string pendingPatchItem, string guiBasePath, } } - return; + return null; static bool ShouldSkipPatch(string[] skipLibraries, string[] libraryList) { diff --git a/src/ISTAlter/Core/PatchUtils.Binding.cs b/src/ISTAlter/Core/PatchUtils.Binding.cs index 67bf0cd..a4f5a08 100644 --- a/src/ISTAlter/Core/PatchUtils.Binding.cs +++ b/src/ISTAlter/Core/PatchUtils.Binding.cs @@ -100,6 +100,24 @@ private static uint Scatter(byte[] data) return acc; } + internal static bool Anchor(string fileName) + { + using var module = LoadModule(fileName); + var typeDef = module.GetType("\u0042\u004d\u0057.Rheingold.CoreFramework.Interaction.Models.InteractionModel"); + if (typeDef == null) + { + return HavePatchedMark(module) != null; + } + + if (!IsStamped(module)) + { + return false; + } + + var getter = DnlibUtils.FindPropertyInClassAndBaseClasses(typeDef, "\u0054\u0069\u0074\u006c\u0065")?.GetMethod; + return getter != null && Reconcile(getter); + } + private sealed class AssemblyBindingState { public int Slots { get; private set; } diff --git a/src/ISTAlter/Core/PatchUtils.Optional.cs b/src/ISTAlter/Core/PatchUtils.Optional.cs index d9b86f8..4d25e92 100755 --- a/src/ISTAlter/Core/PatchUtils.Optional.cs +++ b/src/ISTAlter/Core/PatchUtils.Optional.cs @@ -968,16 +968,26 @@ instructions[i].Operand is IMethod m && [LibraryName("RheingoldxVM.dll")] public static int PatchSLP(ModuleDefMD module) { - return module.PatchFunction( + var version = module.Assembly.Version; + + var result = module.PatchFunction( "\u0042\u004d\u0057.Rheingold.xVM.SLP", "ScanDeviceFromAttrList", "(\u0042\u004d\u0057.Rheingold.xVM.SLPAttrRply,System.String[])\u0042\u004d\u0057.Rheingold.CoreFramework.DatabaseProvider.VCIDevice", - ReplaceDeviceType) + module.PatchFunction( + version < new Version("4.60") ? ReplaceDeviceType : RewriteIcomNextDispatch + ) + module.PatchFunction( "\u0042\u004d\u0057.Rheingold.xVM.SLP", "IsIcomUnsupported", "(System.Collections.Generic.Dictionary`2)System.Boolean", DnlibUtils.ReturnFalseMethod); + if (result == 0) + { + Log.Warning("{PatchName} found no applicable target in {Assembly}({Version})", nameof(PatchSLP), module.Assembly.Name, version); + } + + return result; + static void ReplaceDeviceType(MethodDef method) { const string targetOperand = "System.String System.Collections.Generic.Dictionary`2::get_Item(System.String)"; @@ -998,6 +1008,164 @@ static void ReplaceDeviceType(MethodDef method) } } } + + static void RewriteIcomNextDispatch(MethodDef method) + { + const string parseAttrListOperand = "System.Collections.Generic.Dictionary`2 \u0042\u004d\u0057.Rheingold.xVM.SLP::ParseAttrList(\u0042\u004d\u0057.Rheingold.xVM.SLPString)"; + if (method.FindInstruction(OpCodes.Call, parseAttrListOperand) == null) + { + Log.Warning("Required instructions not found, can not patch {Method}", method.FullName); + return; + } + + var widened = WidenIcomDispatch(method); + var gated = KeepRawStateForLegacyIcom(method); + if (widened || gated) + { + method.Body.SimplifyBranches(); + method.Body.OptimizeBranches(); + } + } + } + + // >= 4.60: legacy ICOM (A/A1/A2) report DevType="ICOM" while ISTA only dispatches DevType=="ICOM-Next". + // Widen the dispatch so DevType=="ICOM" also takes the ICOM-Next branch (DevTypeExt is never rewritten). + private static bool WidenIcomDispatch(MethodDef method) + { + const string opEqualityOperand = "System.Boolean System.String::op_Equality(System.String,System.String)"; + const string getItemOperand = "System.String System.Collections.Generic.Dictionary`2::get_Item(System.String)"; + + var instructions = method.Body.Instructions; + + // Anchor: the DevType=="ICOM-Next" dispatch comparison (ldstr / call op_Equality / brfalse else). + var icomNext = method.FindInstruction(OpCodes.Ldstr, "ICOM-Next"); + if (icomNext == null) + { + Log.Warning("Required instructions not found, can not patch {Method}", method.FullName); + return false; + } + + var idxNext = instructions.IndexOf(icomNext); + var dispatchEquals = instructions[idxNext + 1]; + var dispatchBranch = instructions[idxNext + 2]; + if (dispatchEquals.OpCode != OpCodes.Call || + !string.Equals((dispatchEquals.Operand as IMethod)?.FullName, opEqualityOperand, StringComparison.Ordinal) || + (dispatchBranch.OpCode != OpCodes.Brfalse && dispatchBranch.OpCode != OpCodes.Brfalse_S) || + dispatchBranch.Operand is not Instruction dispatchElseTarget) + { + Log.Warning("Required instructions not found, can not patch {Method}", method.FullName); + return false; + } + + var dispatchBodyStart = instructions[idxNext + 3]; + + // Reuse the target module's (mscorlib) members from existing operands; never via typeof() (the net + // patcher would bind those to System.Private.CoreLib, absent on ISTA's .NET Framework runtime). + var getItem = method.FindInstruction(OpCodes.Callvirt, getItemOperand)?.Operand as IMethod; + var stringEquals = dispatchEquals.Operand as IMethod; + if (getItem == null || stringEquals == null) + { + Log.Warning("Required instructions not found, can not patch {Method}", method.FullName); + return false; + } + + dispatchBranch.OpCode = OpCodes.Brtrue; + dispatchBranch.Operand = dispatchBodyStart; + Instruction[] widenDispatch = + [ + OpCodes.Ldloc_0.ToInstruction(), + OpCodes.Ldstr.ToInstruction("DevType"), + OpCodes.Callvirt.ToInstruction(getItem), + OpCodes.Ldstr.ToInstruction("ICOM"), + OpCodes.Call.ToInstruction(stringEquals), + OpCodes.Brtrue.ToInstruction(dispatchBodyStart), + OpCodes.Br.ToInstruction(dispatchElseTarget), + ]; + var widenAt = instructions.IndexOf(dispatchBranch) + 1; + for (var i = 0; i < widenDispatch.Length; i++) + { + instructions.Insert(widenAt + i, widenDispatch[i]); + } + + return true; + } + + // Keep the raw reported State for legacy ICOM A1/A2 only, identified by DevTypeExt ("ICOM_A1"/"ICOM_A2"), + // bypassing the firmware-update "14" downgrade. Genuine ICOM-Next ("ICOM_Next_A") and non-ICOM devices do + // not match and fall through to the original firmware/State logic, untouched. + private static bool KeepRawStateForLegacyIcom(MethodDef method) + { + const string opEqualityOperand = "System.Boolean System.String::op_Equality(System.String,System.String)"; + const string getItemOperand = "System.String System.Collections.Generic.Dictionary`2::get_Item(System.String)"; + const string containsKeyOperand = "System.Boolean System.Collections.Generic.Dictionary`2::ContainsKey(System.String)"; + + var instructions = method.Body.Instructions; + + // The State try block opens with "ICOM".Equals(vcidevice.DevType, ...); match on the handler's TryStart + // (not the first "ICOM" ldstr, which is the vcidevice.DevType = "ICOM" assignment in the dispatch branch). + var stateTry = method.Body.ExceptionHandlers.FirstOrDefault(eh => + eh.TryStart?.OpCode == OpCodes.Ldstr && string.Equals(eh.TryStart.Operand as string, "ICOM", StringComparison.Ordinal)); + var setState = instructions.FirstOrDefault(i => i.OpCode == OpCodes.Callvirt && string.Equals((i.Operand as IMethod)?.Name?.ToString(), "set_State", StringComparison.Ordinal))?.Operand as IMethod; + if (stateTry?.HandlerEnd is not { } leaveTarget || setState == null) + { + Log.Warning("Required instructions not found, can not patch {Method}", method.FullName); + return false; + } + + var icomEquals = stateTry.TryStart; + var vciLoad = instructions[instructions.IndexOf(icomEquals) + 1]; + var vciLocal = vciLoad.OpCode.Code switch + { + Code.Ldloc_0 => method.Body.Variables[0], + Code.Ldloc_1 => method.Body.Variables[1], + Code.Ldloc_2 => method.Body.Variables[2], + Code.Ldloc_3 => method.Body.Variables[3], + Code.Ldloc_S or Code.Ldloc => vciLoad.Operand as Local, + _ => null, + }; + var getItem = method.FindInstruction(OpCodes.Callvirt, getItemOperand)?.Operand as IMethod; + var containsKey = method.FindInstruction(OpCodes.Callvirt, containsKeyOperand)?.Operand as IMethod; + var stringEquals = method.FindInstruction(OpCodes.Call, opEqualityOperand)?.Operand as IMethod; + if (vciLocal == null || getItem == null || containsKey == null || stringEquals == null) + { + Log.Warning("Required instructions not found, can not patch {Method}", method.FullName); + return false; + } + + var setStateRaw = OpCodes.Ldloc.ToInstruction(vciLocal); + Instruction[] stateGate = + [ + OpCodes.Ldloc_0.ToInstruction(), + OpCodes.Ldstr.ToInstruction("DevTypeExt"), + OpCodes.Callvirt.ToInstruction(containsKey), + OpCodes.Brfalse.ToInstruction(icomEquals), + OpCodes.Ldloc_0.ToInstruction(), + OpCodes.Ldstr.ToInstruction("DevTypeExt"), + OpCodes.Callvirt.ToInstruction(getItem), + OpCodes.Ldstr.ToInstruction("ICOM_A1"), + OpCodes.Call.ToInstruction(stringEquals), + OpCodes.Brtrue.ToInstruction(setStateRaw), + OpCodes.Ldloc_0.ToInstruction(), + OpCodes.Ldstr.ToInstruction("DevTypeExt"), + OpCodes.Callvirt.ToInstruction(getItem), + OpCodes.Ldstr.ToInstruction("ICOM_A2"), + OpCodes.Call.ToInstruction(stringEquals), + OpCodes.Brfalse.ToInstruction(icomEquals), + setStateRaw, + OpCodes.Ldloc_0.ToInstruction(), + OpCodes.Ldstr.ToInstruction("State"), + OpCodes.Callvirt.ToInstruction(getItem), + OpCodes.Callvirt.ToInstruction(setState), + OpCodes.Leave.ToInstruction(leaveTarget), + ]; + var gateAt = instructions.IndexOf(icomEquals); + for (var i = 0; i < stateGate.Length; i++) + { + instructions.Insert(gateAt + i, stateGate[i]); + } + + stateTry.TryStart = stateGate[0]; + return true; } [MotorbikeClamp15Patch]