fix: reject unsupported upstream matrix payloads per item

This commit is contained in:
Joseph Doherty
2026-11-10 10:20:36 -05:00
parent 3dcb035f89
commit 7314f159a0
2 changed files with 22 additions and 12 deletions
@@ -8,7 +8,12 @@ public static class UpstreamValueMapping
/// <summary>Materializes scalar and one-dimensional array values without flattening matrices.</summary>
/// <param name="value">The upstream value.</param>
/// <returns>The independent CLR payload.</returns>
public static object? ToClr(Variant value) => Detach(value.AsBoxedObject(Variant.BoxingBehavior.Legacy));
public static object? ToClr(Variant value)
{
if (value.TypeInfo.IsMatrix)
throw new NotSupportedException("Matrix payloads are not supported by the OPC UA driver.");
return Detach(value.AsBoxedObject(Variant.BoxingBehavior.Legacy));
}
private static object? Detach(object? value) => value switch
{
@@ -548,7 +548,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
// G-2b: resolve any secret:-prefixed Password / UserCertificatePassword through the
// shared secret store ONCE, here in the async connect flow, just before the credentials
// are consumed. _options stays the raw config (with secret: refs) — only this
// connect-scoped local carries plaintext, and only for the duration of the connect.
// session identity carries the resolved credential while activation/signing needs it.
// Resolving on every InitializeAsync (a full reconnect calls it) picks up rotations.
// Both credential consumers — the Username password below and the Certificate password
// in BuildCertificateIdentity — flow through BuildUserIdentity(resolvedOptions), so a
@@ -1474,11 +1474,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
// Preserve the upstream StatusCode verbatim — including Bad codes per
// §8's cascading-quality rule. Also preserve SourceTimestamp so downstream
// clients can detect stale upstream data.
results[r] = new DataValueSnapshot(
Value: UpstreamValueMapping.ToClr(dv.WrappedValue),
StatusCode: dv.StatusCode.Code,
SourceTimestampUtc: dv.SourceTimestamp == DateTime.MinValue ? null : dv.SourceTimestamp.ToDateTime(),
ServerTimestampUtc: dv.ServerTimestamp == DateTime.MinValue ? now : dv.ServerTimestamp.ToDateTime());
results[r] = ToSnapshot(dv, now);
}
RecordDeviceAnswered(generation, now);
}
@@ -2104,6 +2100,19 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
PublishNotification(handle, RefOf(item), dv);
}
private static DataValueSnapshot ToSnapshot(DataValue value, DateTime now)
{
// Matrix payloads have no driver-domain contract. SDK 2's Legacy boxing materializes
// them into CLR matrices, so refuse that unsupported shape before boxing. 1D arrays
// retain their deployed pass-through behavior, including scalar-shaped authored tags.
var unsupportedShape = value.WrappedValue.TypeInfo.IsMatrix;
return new DataValueSnapshot(
Value: unsupportedShape ? null : UpstreamValueMapping.ToClr(value.WrappedValue),
StatusCode: unsupportedShape ? StatusCodes.BadTypeMismatch.Code : value.StatusCode.Code,
SourceTimestampUtc: value.SourceTimestamp == DateTimeUtc.MinValue ? null : value.SourceTimestamp.ToDateTime(),
ServerTimestampUtc: value.ServerTimestamp == DateTimeUtc.MinValue ? now : value.ServerTimestamp.ToDateTime());
}
private static string RefOf(MonitoredItem item) => (item.Handle as string) ?? item.DisplayName ?? string.Empty;
private void PublishNotification(OpcUaSubscriptionHandle handle, string fullRef, DataValue dv)
@@ -2113,11 +2122,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
// a delivered notification is liveness evidence — the subscription path never calls ReadAsync, so
// without this a busy subscription-only driver's LastSuccessfulRead stayed at its connect time.
RecordLivenessEvidence();
var snapshot = new DataValueSnapshot(
Value: UpstreamValueMapping.ToClr(dv.WrappedValue),
StatusCode: dv.StatusCode.Code,
SourceTimestampUtc: dv.SourceTimestamp == DateTime.MinValue ? null : dv.SourceTimestamp.ToDateTime(),
ServerTimestampUtc: dv.ServerTimestamp == DateTime.MinValue ? DateTime.UtcNow : dv.ServerTimestamp.ToDateTime());
var snapshot = ToSnapshot(dv, DateTime.UtcNow);
// Review of 3b88056f: the replay check and the publish are one critical section shared with the recovery and
// trailing reads, so an older value can never be delivered after a newer one (see _deliveryLock).
lock (_deliveryLock)