fix(commons): resolve Commons-013,014 — integral JSON index handling, distinguish Malformed vs Legacy OPC UA config

This commit is contained in:
Joseph Doherty
2026-05-17 03:18:17 -04:00
parent 21856a4be7
commit a78c3bcb6f
5 changed files with 190 additions and 18 deletions

View File

@@ -6,6 +6,8 @@ namespace ScadaLink.Commons.Types;
/// <summary>
/// Wraps a JsonElement as a dynamic object for convenient property access in scripts.
/// Supports property access (obj.name), indexing (obj.items[0]), and ToString().
/// Array indexing accepts any integral index type (int, long, short, byte, ...), so an
/// index derived from another wrapped JSON number — which Wrap surfaces as long — works.
/// </summary>
/// <remarks>
/// The element passed to the constructor is <see cref="JsonElement.Clone()">cloned</see>
@@ -39,13 +41,16 @@ public class DynamicJsonElement : DynamicObject
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object? result)
{
// Accept any integral index, not just int. DynamicJsonElement surfaces JSON
// numbers as long (see Wrap), so an index computed from another wrapped value
// (e.g. obj.items[obj.count - 1]) arrives as a long; byte/short widen too.
if (_element.ValueKind == JsonValueKind.Array &&
indexes.Length == 1 && indexes[0] is int index)
indexes.Length == 1 && TryGetIntegralIndex(indexes[0], out var index))
{
var arrayLength = _element.GetArrayLength();
if (index >= 0 && index < arrayLength)
{
result = Wrap(_element[index]);
result = Wrap(_element[(int)index]);
return true;
}
}
@@ -53,6 +58,28 @@ public class DynamicJsonElement : DynamicObject
return false;
}
private static bool TryGetIntegralIndex(object? value, out long index)
{
switch (value)
{
case int i: index = i; return true;
case long l: index = l; return true;
case short s: index = s; return true;
case byte b: index = b; return true;
case sbyte sb: index = sb; return true;
case ushort us: index = us; return true;
case uint ui: index = ui; return true;
case ulong ul when ul <= long.MaxValue: index = (long)ul; return true;
// Whole-valued floating-point indices are accepted too: arithmetic on
// wrapped JSON numbers can yield a double/decimal even for an integer result.
case double d when d >= long.MinValue && d <= long.MaxValue && d == Math.Floor(d):
index = (long)d; return true;
case decimal m when m == Math.Floor(m):
index = (long)m; return true;
default: index = 0; return false;
}
}
public override bool TryConvert(ConvertBinder binder, out object? result)
{
// Conversion to object (or dynamic): never null out a present value. Return the
@@ -102,11 +129,23 @@ public class DynamicJsonElement : DynamicObject
JsonValueKind.Object => new DynamicJsonElement(element),
JsonValueKind.Array => new DynamicJsonElement(element),
JsonValueKind.String => element.GetString(),
JsonValueKind.Number => element.TryGetInt64(out var l) ? l : element.GetDouble(),
JsonValueKind.Number => WrapNumber(element),
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null,
_ => element.GetRawText()
};
}
private static object WrapNumber(JsonElement element)
{
// An integral JSON number must box as long, not double. A ternary
// (TryGetInt64 ? long : double) would unify both branches to double and
// silently widen the long, so an integral index read back out of the wrapper
// (e.g. obj.items[obj.count - 1]) would arrive as a double. Box each case
// with its own typed return so the runtime type is preserved.
if (element.TryGetInt64(out var l))
return l;
return element.GetDouble();
}
}