Merge review R4 fixes (#610): array writes require an exact element-type match (BadTypeMismatch, nothing sent); Galaxy encoder refuses uint[]/ulong[]; Modbus array writability bounded by maxRegistersPerWrite/coil cap; arrayLength >= 1 required; Galaxy arrays without a declared length stay read-only; arrays to scalar nodes refused

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 08:08:03 -04:00
14 changed files with 532 additions and 85 deletions
@@ -39,8 +39,13 @@ internal static class MxValueEncoder
case DateTimeOffset dto: return new MxValue { TimestampValue = Timestamp.FromDateTimeOffset(dto) };
case bool[] arr: return EncodeArray(arr, (mx, vs) => mx.BoolValues = ToBoolArray(vs));
case int[] arr: return EncodeArray(arr, (mx, vs) => mx.Int32Values = ToInt32Array(vs));
case long[] arr: return EncodeArray(arr, (mx, vs) => mx.Int64Values = ToInt64Array(vs));
// Exact-type guards: CLR array covariance makes `uint[] is int[]` and `ulong[] is long[]` true, so
// without them an unsigned array would be encoded by REINTERPRETING its bits (uint.MaxValue → -1) —
// unlike the range-checked scalar arms above. An unsigned array is refused instead (Gitea #610 review).
case int[] arr when arr.GetType() == typeof(int[]):
return EncodeArray(arr, (mx, vs) => mx.Int32Values = ToInt32Array(vs));
case long[] arr when arr.GetType() == typeof(long[]):
return EncodeArray(arr, (mx, vs) => mx.Int64Values = ToInt64Array(vs));
case float[] arr: return EncodeArray(arr, (mx, vs) => mx.FloatValues = ToFloatArray(vs));
case double[] arr: return EncodeArray(arr, (mx, vs) => mx.DoubleValues = ToDoubleArray(vs));
case string[] arr: return EncodeArray(arr, (mx, vs) => mx.StringValues = ToStringArray(vs));