feat(patch-engine): locate patches structurally instead of by signature

Anchor each patch on a stable string (API endpoint, IPC channel, method name)
and walk the delimiter structure via JsCursor to the edit site, reading
minified identifiers out of the located region.

Move injected JavaScript into WandEnhancer/Patches/*.js as embedded resources.
Declare patches as PatchEntry rows in EnhancerConfig with CandidateFileNames,
SearchHints and optional CapabilityHints.

Recognise keyword-preceded regex literals in JsCursor so `return/re/.test(x)`
no longer desynchronises the scan.
Require the remote setValue anchor to match exactly once; Wand ships sibling
call sites for other sources.
Require both backup halves in IsPatched so a partial backup no longer blocks
patch and restore at the same time.
Chain inner exceptions when unpack or pack fails.
Rename Common to ProcessTerminator and Utils.Extensions to WeModInstalls.
This commit is contained in:
kitbyte
2026-08-29 16:56:09 +03:00
parent 20956c3228
commit 6716da5c80
21 changed files with 1249 additions and 486 deletions
@@ -0,0 +1,82 @@
using System;
using System.IO;
using System.Linq;
using WandEnhancer.Core.Js;
using WandEnhancer.Models;
using WandEnhancer.View.MainWindow;
namespace WandEnhancer.Core
{
internal sealed class JavaScriptPatchApplier
{
private readonly Action<string, ELogType> _logger;
public JavaScriptPatchApplier(Action<string, ELogType> logger)
{
_logger = logger;
}
public string Apply(string fileName, string source, EnhancerConfig.PatchEntry patch, EPatchType patchType, out bool patchApplied)
{
patchApplied = false;
if (patch.Applied || !CanSearchFile(fileName, patch))
{
return source;
}
patch.CapabilityDetected |= ContainsAny(source, patch.CapabilityHints);
if (!ContainsAny(source, patch.SearchHints))
{
return source;
}
string label = FormatLabel(patchType, patch);
JsEdit[] edits;
try
{
edits = patch.Locate(new JsCursor(source));
}
catch (Exception e)
{
throw new Exception($"[ENHANCER] [{label}] {e.Message}. The version may not be supported.", e);
}
if (edits == null || edits.Length == 0)
{
return source;
}
_logger($"[ENHANCER] [{label}] Found target in: {Path.GetFileName(fileName)}", ELogType.Info);
foreach (var edit in edits.OrderByDescending(edit => edit.Start))
{
source = edit.ApplyTo(source);
}
_logger($"[ENHANCER] [{label}] Patch applied", ELogType.Success);
patch.Applied = true;
patchApplied = true;
return source;
}
public static string FormatLabel(EPatchType patchType, EnhancerConfig.PatchEntry patch)
{
return string.IsNullOrEmpty(patch.Name) ? patchType.ToString() : $"{patchType} -> {patch.Name}";
}
public static bool CanSearchFile(string filePath, EnhancerConfig.PatchEntry patch)
{
if (patch.CandidateFileNames == null || patch.CandidateFileNames.Length == 0)
{
return true;
}
string fileName = Path.GetFileName(filePath);
return patch.CandidateFileNames.Any(candidate => fileName.Equals(candidate, StringComparison.OrdinalIgnoreCase));
}
private static bool ContainsAny(string source, string[] hints)
{
return hints != null && hints.Any(hint => source.IndexOf(hint, StringComparison.Ordinal) >= 0);
}
}
}