Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions src/ISTAlter/Core/Patch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>())
{
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);
Expand Down Expand Up @@ -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('!'))
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
{
Expand All @@ -245,7 +257,7 @@ private static void PatchSingleFile(string pendingPatchItem, string guiBasePath,
}
}

return;
return null;

static bool ShouldSkipPatch(string[] skipLibraries, string[] libraryList)
{
Expand Down
18 changes: 18 additions & 0 deletions src/ISTAlter/Core/PatchUtils.Binding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
172 changes: 170 additions & 2 deletions src/ISTAlter/Core/PatchUtils.Optional.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.String,System.String>)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<System.String,System.String>::get_Item(System.String)";
Expand All @@ -998,6 +1008,164 @@ static void ReplaceDeviceType(MethodDef method)
}
}
}

static void RewriteIcomNextDispatch(MethodDef method)
{
const string parseAttrListOperand = "System.Collections.Generic.Dictionary`2<System.String,System.String> \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<System.String,System.String>::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<System.String,System.String>::get_Item(System.String)";
const string containsKeyOperand = "System.Boolean System.Collections.Generic.Dictionary`2<System.String,System.String>::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]
Expand Down
Loading