diff --git a/CHANGELOG.md b/CHANGELOG.md index ed8ad9b5..a3be26b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the AZOM plugin are documented here. +## [1.5.7] + +### Changed +- **Fixed antivirus false positive.** Windows Defender incorrectly flagged 1.5.6 as a virus. + The plugin was never unsafe — SimHub 9.12 LED compatibility built code on the fly at startup, + which Defender's heuristics mistake for malware. Now supports all versions without runtime + code generation. + ## [1.5.6] ### Fixed diff --git a/Devices/SimHubLedCompat.cs b/Devices/SimHubLedCompat.cs index fb21660b..1d09c762 100644 --- a/Devices/SimHubLedCompat.cs +++ b/Devices/SimHubLedCompat.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; -using System.Linq.Expressions; using System.Reflection; using BA63Driver.Mapper; @@ -24,6 +23,13 @@ namespace MozaPlugin.Devices /// at type-load time by name and signature, so each host binds the overload its own /// interface declares. The constructor half is handled here, because a direct /// new bakes one signature into the IL. + /// + /// The constructor is invoked late-bound via + /// rather than through a compiled expression tree. Both bind by parameter name and + /// degrade the same way, but Expression.Compile() emits IL at runtime, and a + /// plugin DLL that generates code trips Defender's ML heuristics (Wacatac.H!ml) — + /// see docs/DEVELOPMENT.md. Cost is one boxed argument array per state build, on the + /// LED display path only. /// internal static class SimHubLedCompat { @@ -33,13 +39,68 @@ internal static class SimHubLedCompat /// internal static readonly Func NoOverrides = () => Array.Empty(); - private delegate LedDeviceState StateFactory( - Color[] ledsState, Color[] buttonsState, Color[] encodersState, - Color[] matrixState, Color[] rawState, Color[] overrideState, - double rpmBrightness, double buttonsBrightness, - double encodersBrightness, double matrixBrightness); + // Our ten logical arguments, in the order CreateState takes them. The map below + // is expressed as indices into this list. + private static readonly string[] OurArgNames = + { + "ledsState", "buttonsState", "encodersState", "matrixState", "rawState", + "overrideState", "rpmBrightness", "buttonsBrightness", "encodersBrightness", + "matrixBrightness", + }; + + private static readonly ConstructorInfo s_ctor; + + // One entry per host constructor parameter: an index into OurArgNames, or -1 to + // take the constant in s_fallbacks at the same position. + private static readonly int[] s_argMap; + private static readonly object?[] s_fallbacks; + + static SimHubLedCompat() + { + // Widest public constructor: the one carrying every channel the host knows. + s_ctor = typeof(LedDeviceState) + .GetConstructors(BindingFlags.Public | BindingFlags.Instance) + .OrderByDescending(c => c.GetParameters().Length) + .First(); + + var ours = new Dictionary(StringComparer.Ordinal); + for (int i = 0; i < OurArgNames.Length; i++) ours[OurArgNames[i]] = i; - private static readonly StateFactory s_create = BuildStateFactory(); + var pars = s_ctor.GetParameters(); + s_argMap = new int[pars.Length]; + s_fallbacks = new object?[pars.Length]; + + var ourTypes = new[] + { + typeof(Color[]), typeof(Color[]), typeof(Color[]), typeof(Color[]), + typeof(Color[]), typeof(Color[]), typeof(double), typeof(double), + typeof(double), typeof(double), + }; + + for (int i = 0; i < pars.Length; i++) + { + var p = pars[i]; + if (p.Name != null && ours.TryGetValue(p.Name, out int ourIdx) + && ourTypes[ourIdx] == p.ParameterType) + { + s_argMap[i] = ourIdx; + continue; + } + + // A parameter we don't recognise takes its own declared default, so a + // future constructor change degrades to "that channel is empty" rather + // than failing to load. + s_argMap[i] = -1; + s_fallbacks[i] = p.HasDefaultValue + ? p.DefaultValue + : DefaultOf(p.ParameterType); + } + } + + // Boxed default for any type, without emitting code: a fresh one-element array + // is zero-initialised, so reading element 0 boxes default(T). + private static object? DefaultOf(Type t) + => t.IsValueType ? Array.CreateInstance(t, 1).GetValue(0) : null; /// /// Build a against whatever constructor the loaded @@ -50,59 +111,28 @@ internal static LedDeviceState CreateState( Color[] matrixState, Color[] rawState, Color[] overrideState, double rpmBrightness = 1.0, double buttonsBrightness = 1.0, double encodersBrightness = 1.0, double matrixBrightness = 1.0) - => s_create(ledsState, buttonsState, encodersState, matrixState, rawState, - overrideState, rpmBrightness, buttonsBrightness, - encodersBrightness, matrixBrightness); - - /// - /// Compile a constructor call once, binding our arguments to the host's parameters - /// by name. A parameter we don't recognise takes its own declared default (or - /// default(T)), so a future constructor change degrades to "that channel is - /// empty" rather than failing to load. - /// - private static StateFactory BuildStateFactory() { - var ledsState = Expression.Parameter(typeof(Color[]), "ledsState"); - var buttonsState = Expression.Parameter(typeof(Color[]), "buttonsState"); - var encodersState = Expression.Parameter(typeof(Color[]), "encodersState"); - var matrixState = Expression.Parameter(typeof(Color[]), "matrixState"); - var rawState = Expression.Parameter(typeof(Color[]), "rawState"); - var overrideState = Expression.Parameter(typeof(Color[]), "overrideState"); - var rpmBrightness = Expression.Parameter(typeof(double), "rpmBrightness"); - var buttonsBrightness = Expression.Parameter(typeof(double), "buttonsBrightness"); - var encodersBrightness = Expression.Parameter(typeof(double), "encodersBrightness"); - var matrixBrightness = Expression.Parameter(typeof(double), "matrixBrightness"); - - var ours = new ParameterExpression[] + var map = s_argMap; + var args = new object?[map.Length]; + for (int i = 0; i < map.Length; i++) { - ledsState, buttonsState, encodersState, matrixState, rawState, overrideState, - rpmBrightness, buttonsBrightness, encodersBrightness, matrixBrightness, - }; - var byName = ours.ToDictionary(p => p.Name!, StringComparer.Ordinal); - - // Widest public constructor: the one carrying every channel the host knows. - var ctor = typeof(LedDeviceState) - .GetConstructors(BindingFlags.Public | BindingFlags.Instance) - .OrderByDescending(c => c.GetParameters().Length) - .First(); - - var args = new List(); - foreach (var p in ctor.GetParameters()) - { - if (p.Name != null && byName.TryGetValue(p.Name, out var ours2) - && ours2.Type == p.ParameterType) + switch (map[i]) { - args.Add(ours2); - continue; + case 0: args[i] = ledsState; break; + case 1: args[i] = buttonsState; break; + case 2: args[i] = encodersState; break; + case 3: args[i] = matrixState; break; + case 4: args[i] = rawState; break; + case 5: args[i] = overrideState; break; + case 6: args[i] = rpmBrightness; break; + case 7: args[i] = buttonsBrightness; break; + case 8: args[i] = encodersBrightness; break; + case 9: args[i] = matrixBrightness; break; + default: args[i] = s_fallbacks[i]; break; } - - object? fallback = p.HasDefaultValue ? p.DefaultValue - : p.ParameterType.IsValueType ? Activator.CreateInstance(p.ParameterType) - : null; - args.Add(Expression.Constant(fallback, p.ParameterType)); } - return Expression.Lambda(Expression.New(ctor, args), ours).Compile(); + return (LedDeviceState)s_ctor.Invoke(args); } } }