Files
Marc Schier 0000513cde Pluggable user-identity provider model (#3807)
# Description

Introduces a pluggable user-identity provider model covering all OPC UA
Part 6 §6.5 mechanisms (Anonymous, UserName, X509, IssuedToken/JWT) on
both client and server, with full
`Microsoft.Extensions.DependencyInjection` integration, plus the Part 12
§9 `AuthorizationService` two-phase token-request flow **including
refresh-token rotation**, the Part 12 §8 KeyCredential Push model, and a
clean provider model for OPC 10000-12 §7.2 `ApplicationSelfAdmin`. The
work is split across phase-by-phase commits so it can be reviewed
independently; nothing here changes wire-level behaviour or forces
existing applications to migrate.

### Documentation

All under `Docs/`:

- `IdentityProviders.md` — full developer guide: three-layer model,
recipes, configuration reference, "what gets auto-wired" matrix, JWS
algorithm table, identity-augmenters section, **"Implementing your own
provider"** how-to subsections for Entra / OIDC / Windows / AspNetCore
(sibling files merged in per review feedback).
- `DependencyInjection.md` — `Identity` sections with `appsettings.json`
examples, identity-extension matrix, `WithAuthorizationService` +
`WithKeyCredentialPush` + `AddIdentityAugmenter` extensions documented.
- `RoleBasedUserManagement.md` — claim-criteria callout describing
`IIdentityClaims` probe surface.
- `AuthorizationService.md` — Architecture diagram +
`IAccessTokenProvider` conceptual narrative restored from master; modern
`StartRequestToken` / `FinishRequestToken` flow leads; refresh-token
recipe added.
- `KeyCredentialService.md` — Push model + Pull vs Push vs Bridge
decision matrix + Hybrid (Push + Pull) section; explicit experimental
warning for the bridge.
- `GDS.md` — KeyCredential Push, `StartRequestToken`,
`GdsApplicationSelfAdminProvider`, **shipped Refresh-token support with
rotation policy and audit semantics** all documented.
- `MigrationGuide.md` — User Identity Providers section listing
`[Obsolete]` markers + mechanical recipes; SelfAdmin migration via
`IIdentityAugmenter`; `EnableRefreshTokens` default note.
- `README.md` index entries.

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [x] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.
2026-06-02 01:14:54 +02:00

3684 lines
122 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
#pragma warning disable CS0618 // Type or member is obsolete
namespace Opc.Ua.PubSub.Encoding
{
/// <summary>
/// Reads objects from a JSON stream.
/// </summary>
internal class PubSubJsonDecoder : IDecoder
{
/// <summary>
/// The name of the Root array if the json is defined as an array
/// </summary>
public const string RootArrayName = "___root_array___";
/// <summary>
/// If TRUE then the NamespaceUris and ServerUris tables are updated with new URIs read from the JSON stream.
/// </summary>
public bool UpdateNamespaceTable { get; set; }
private JsonTextReader m_reader;
private readonly ILogger m_logger;
private readonly Dictionary<string, object> m_root;
private readonly Stack<object> m_stack;
private ushort[]? m_namespaceMappings;
private ushort[]? m_serverMappings;
private uint m_nestingLevel;
/// <summary>
/// JSON encoded value of: “9999-12-31T23:59:59Z”
/// </summary>
private readonly DateTime m_dateTimeMaxJsonValue = new(3155378975990000000);
private enum JTokenNullObject
{
Undefined = 0,
Object = 1,
Array = 2
}
/// <summary>
/// Create a JSON decoder to decode a string.
/// </summary>
/// <param name="json">The JSON encoded string.</param>
/// <param name="context">The service message context to use.</param>
public PubSubJsonDecoder(string json, IServiceMessageContext context)
{
Context = context ?? throw new ArgumentNullException(nameof(context));
m_logger = context.Telemetry.CreateLogger<PubSubJsonDecoder>();
m_nestingLevel = 0;
m_reader = new JsonTextReader(new StringReader(json));
m_root = ReadObject();
m_stack = new Stack<object>();
m_stack.Push(m_root);
}
/// <summary>
/// Create a JSON decoder to decode a <see cref="Type"/>from a <see cref="JsonTextReader"/>.
/// </summary>
/// <param name="systemType">The system type of the encoded JSON stream.</param>
/// <param name="reader">The text reader.</param>
/// <param name="context">The service message context to use.</param>
public PubSubJsonDecoder(Type systemType, JsonTextReader reader, IServiceMessageContext context)
{
Context = context;
m_logger = context.Telemetry.CreateLogger<PubSubJsonDecoder>();
m_nestingLevel = 0;
m_reader = reader;
m_root = ReadObject();
m_stack = new Stack<object>();
m_stack.Push(m_root);
}
/// <summary>
/// Decodes a message from a buffer.
/// </summary>
/// <typeparam name="T">The type of the message to read</typeparam>
public static T DecodeMessage<T>(
byte[] buffer,
IServiceMessageContext context) where T : IEncodeable
{
return DecodeMessage<T>(new ArraySegment<byte>(buffer), context);
}
/// <summary>
/// Decodes a message from a buffer.
/// </summary>
/// <exception cref="ArgumentNullException">
/// <paramref name="context"/> is <c>null</c>.</exception>
/// <exception cref="ServiceResultException"></exception>
/// <typeparam name="T">The type of the message to read</typeparam>
public static T DecodeMessage<T>(
ArraySegment<byte> buffer,
IServiceMessageContext context) where T : IEncodeable
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// check that the max message size was not exceeded.
if (context.MaxMessageSize > 0 && context.MaxMessageSize < buffer.Count)
{
throw ServiceResultException.Create(
StatusCodes.BadEncodingLimitsExceeded,
"MaxMessageSize {0} < {1}",
context.MaxMessageSize,
buffer.Count);
}
using var decoder = new PubSubJsonDecoder(
System.Text.Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count),
context);
return decoder.DecodeMessage<T>();
}
/// <inheritdoc/>
public T DecodeMessage<T>() where T : IEncodeable
{
ArrayOf<string?> namespaceUris = ReadStringArray("NamespaceUris");
ArrayOf<string?> serverUris = ReadStringArray("ServerUris");
if (!namespaceUris.IsEmpty || !serverUris.IsEmpty)
{
NamespaceTable namespaces =
namespaceUris.IsEmpty
? Context.NamespaceUris
: new NamespaceTable(namespaceUris.ToArray()!);
StringTable servers =
serverUris.IsEmpty
? Context.ServerUris
: new StringTable(serverUris.ToArray()!);
SetMappingTables(namespaces, servers);
}
// read the node id.
NodeId typeId = ReadNodeId("TypeId");
// convert to absolute node id.
var absoluteId = NodeId.ToExpandedNodeId(typeId, Context.NamespaceUris);
// Read the message.
return ReadEncodeable<T>("Body", absoluteId);
}
/// <inheritdoc/>
public void SetMappingTables(NamespaceTable namespaceUris, StringTable serverUris)
{
m_namespaceMappings = null;
if (namespaceUris != null && Context.NamespaceUris != null)
{
ushort[] namespaceMappings = new ushort[namespaceUris.Count];
for (uint ii = 0; ii < namespaceUris.Count; ii++)
{
string uri = namespaceUris.GetString(ii)!;
if (UpdateNamespaceTable)
{
namespaceMappings[ii] = Context.NamespaceUris.GetIndexOrAppend(uri);
}
else
{
int index = Context.NamespaceUris.GetIndex(namespaceUris.GetString(ii)!);
namespaceMappings[ii] = index >= 0 ? (ushort)index : ushort.MaxValue;
}
}
m_namespaceMappings = namespaceMappings;
}
m_serverMappings = null;
if (serverUris != null && Context.ServerUris != null)
{
ushort[] serverMappings = new ushort[serverUris.Count];
for (uint ii = 0; ii < serverUris.Count; ii++)
{
string uri = serverUris.GetString(ii)!;
if (UpdateNamespaceTable)
{
serverMappings[ii] = Context.ServerUris.GetIndexOrAppend(uri);
}
else
{
int index = Context.ServerUris.GetIndex(serverUris.GetString(ii)!);
serverMappings[ii] = index >= 0 ? (ushort)index : ushort.MaxValue;
}
}
m_serverMappings = serverMappings;
}
}
/// <inheritdoc/>
public void Close()
{
m_reader.Close();
}
/// <summary>
/// Closes the stream used for reading.
/// </summary>
public void Close(bool checkEof)
{
if (checkEof && m_reader.TokenType != JsonToken.EndObject)
{
while (m_reader.Read() && m_reader.TokenType != JsonToken.EndObject)
{
}
}
m_reader.Close();
}
/// <inheritdoc/>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// An overrideable version of the Dispose.
/// </summary>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
(m_reader as IDisposable)?.Dispose();
m_reader = null!;
}
}
/// <inheritdoc/>
public EncodingType EncodingType => EncodingType.Json;
/// <inheritdoc/>
public bool HasField(string? fieldName)
{
if (string.IsNullOrEmpty(fieldName) || m_stack.Count == 0)
{
return true;
}
return m_stack.Peek() is Dictionary<string, object> context &&
context.ContainsKey(fieldName!);
}
/// <inheritdoc/>
public IServiceMessageContext Context { get; }
/// <inheritdoc/>
public void PushNamespace(string namespaceUri)
{
}
/// <inheritdoc/>
public void PopNamespace()
{
}
/// <inheritdoc/>
public bool ReadField(string? fieldName, out object token)
{
token = null!;
if (string.IsNullOrEmpty(fieldName))
{
token = m_stack.Peek();
return true;
}
return (m_stack.Peek() is Dictionary<string, object> context) &&
context.TryGetValue(fieldName!, out token!);
}
/// <inheritdoc/>
public bool ReadBoolean(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return false;
}
bool? value = token as bool?;
if (value == null)
{
return false;
}
return (bool)token;
}
/// <inheritdoc/>
public sbyte ReadSByte(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return 0;
}
long? value = token as long?;
if (value == null)
{
return 0;
}
if (value is < sbyte.MinValue or > sbyte.MaxValue)
{
return 0;
}
return (sbyte)value;
}
/// <inheritdoc/>
public byte ReadByte(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return 0;
}
long? value = token as long?;
if (value == null)
{
return 0;
}
if (value is < byte.MinValue or > byte.MaxValue)
{
return 0;
}
return (byte)value;
}
/// <inheritdoc/>
public short ReadInt16(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return 0;
}
long? value = token as long?;
if (value == null)
{
return 0;
}
if (value is < short.MinValue or > short.MaxValue)
{
return 0;
}
return (short)value;
}
/// <inheritdoc/>
public ushort ReadUInt16(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return 0;
}
long? value = token as long?;
if (value == null)
{
return 0;
}
if (value is < ushort.MinValue or > ushort.MaxValue)
{
return 0;
}
return (ushort)value;
}
/// <inheritdoc/>
public int ReadInt32(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return 0;
}
long? value = token as long?;
if (value == null)
{
return ReadEnumeratedString<int>(token, int.TryParse);
}
if (value is < int.MinValue or > int.MaxValue)
{
return 0;
}
return (int)value;
}
/// <inheritdoc/>
public uint ReadUInt32(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return 0;
}
long? value = token as long?;
if (value == null)
{
return ReadEnumeratedString<uint>(token, uint.TryParse);
}
if (value is < uint.MinValue or > uint.MaxValue)
{
return 0;
}
return (uint)value;
}
/// <inheritdoc/>
public long ReadInt64(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return 0;
}
long? value = token as long?;
if (value == null)
{
if (token is not string text ||
!long.TryParse(
text,
NumberStyles.Integer,
CultureInfo.InvariantCulture,
out long number))
{
return 0;
}
return number;
}
return (long)value;
}
/// <inheritdoc/>
public ulong ReadUInt64(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return 0;
}
long? value = token as long?;
if (value == null)
{
if (token is not string text ||
!ulong.TryParse(
text,
NumberStyles.Integer,
CultureInfo.InvariantCulture,
out ulong number))
{
return 0;
}
return number;
}
if (value < 0)
{
return 0;
}
return (ulong)value;
}
/// <inheritdoc/>
public float ReadFloat(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return 0;
}
double? value = token as double?;
if (value == null)
{
string? text = token as string;
if (text == null ||
!float.TryParse(
text,
NumberStyles.Float,
CultureInfo.InvariantCulture,
out float number))
{
if (text != null)
{
if (string.Equals(text, "Infinity", StringComparison.OrdinalIgnoreCase))
{
return float.PositiveInfinity;
}
else if (string.Equals(
text,
"-Infinity",
StringComparison.OrdinalIgnoreCase))
{
return float.NegativeInfinity;
}
else if (string.Equals(text, "NaN", StringComparison.OrdinalIgnoreCase))
{
return float.NaN;
}
}
long? integer = token as long?;
if (integer == null)
{
return 0;
}
return (float)integer;
}
return number;
}
float floatValue = (float)value;
if (floatValue is >= float.MinValue and <= float.MaxValue)
{
return (float)value;
}
return 0;
}
/// <inheritdoc/>
public double ReadDouble(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return 0;
}
double? value = token as double?;
if (value == null)
{
string? text = token as string;
if (text == null ||
!double.TryParse(
text,
NumberStyles.Float,
CultureInfo.InvariantCulture,
out double number))
{
if (text != null)
{
if (string.Equals(text, "Infinity", StringComparison.OrdinalIgnoreCase))
{
return double.PositiveInfinity;
}
else if (string.Equals(
text,
"-Infinity",
StringComparison.OrdinalIgnoreCase))
{
return double.NegativeInfinity;
}
else if (string.Equals(text, "NaN", StringComparison.OrdinalIgnoreCase))
{
return double.NaN;
}
}
long? integer = token as long?;
if (integer == null)
{
return 0;
}
return (double)integer;
}
return number;
}
return (double)value;
}
/// <inheritdoc/>
public string? ReadString(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return null;
}
if (token is not string value)
{
return null;
}
if (Context.MaxStringLength > 0 && Context.MaxStringLength < value.Length)
{
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
}
return value;
}
/// <inheritdoc/>
public DateTimeUtc ReadDateTime(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return DateTimeUtc.MinValue;
}
var value = token as DateTime?;
if (value != null)
{
return value.Value >= m_dateTimeMaxJsonValue ? DateTimeUtc.MaxValue : value.Value;
}
if (token is string text)
{
try
{
var result = XmlConvert.ToDateTime(text, XmlDateTimeSerializationMode.Utc);
return result >= m_dateTimeMaxJsonValue ? DateTimeUtc.MaxValue : result;
}
catch (FormatException fe)
{
throw ServiceResultException.Create(
StatusCodes.BadDecodingError,
"Failed to decode DateTime: {0}",
fe.Message);
}
}
return DateTimeUtc.MinValue;
}
/// <inheritdoc/>
public Uuid ReadGuid(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return Uuid.Empty;
}
if (token is not string value)
{
return Uuid.Empty;
}
try
{
return Uuid.Parse(value);
}
catch (FormatException fe)
{
throw ServiceResultException.Create(
StatusCodes.BadDecodingError,
"Failed to create Guid: {0}",
fe.Message);
}
}
/// <inheritdoc/>
public ByteString ReadByteString(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return default;
}
if (token is JTokenNullObject)
{
return default;
}
if (token is not string value)
{
return ByteString.Empty;
}
byte[] bytes = SafeConvertFromBase64String(value);
if (Context.MaxByteStringLength > 0 && Context.MaxByteStringLength < bytes.Length)
{
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
}
return ByteString.From(bytes);
}
/// <inheritdoc/>
public XmlElement ReadXmlElement(string? fieldName)
{
if (!ReadField(fieldName, out object token) || token is not string value)
{
return XmlElement.Empty;
}
return (XmlElement)value;
}
/// <inheritdoc/>
public NodeId ReadNodeId(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return NodeId.Null;
}
if (token is string text)
{
NodeId nodeId;
try
{
nodeId = NodeId.Parse(
Context,
text,
new NodeIdParsingOptions
{
UpdateTables = UpdateNamespaceTable,
NamespaceMappings = m_namespaceMappings,
ServerMappings = m_serverMappings
});
}
catch
{
// fallback on error. this allows the application to sort out the problem.
nodeId = new NodeId(text, 0);
}
return nodeId;
}
if (token is not Dictionary<string, object> value)
{
return NodeId.Null;
}
IdType idType = IdType.Numeric;
ushort namespaceIndex = 0;
try
{
m_stack.Push(value);
if (value.ContainsKey("IdType"))
{
idType = (IdType)ReadInt32("IdType");
}
if (ReadField("Namespace", out object namespaceToken))
{
long? index = namespaceToken as long?;
if (index == null)
{
if (namespaceToken is string namespaceUri)
{
namespaceIndex = ToNamespaceIndex(namespaceUri);
}
}
else if (index.Value is >= 0 and < ushort.MaxValue)
{
namespaceIndex = ToNamespaceIndex(index.Value);
}
}
if (value.ContainsKey("Id"))
{
switch (idType)
{
case IdType.Numeric:
return new NodeId(ReadUInt32("Id"), namespaceIndex);
case IdType.Opaque:
return new NodeId(ReadByteString("Id"), namespaceIndex);
case IdType.String:
return new NodeId(ReadString("Id")!, namespaceIndex);
case IdType.Guid:
return new NodeId(ReadGuid("Id"), namespaceIndex);
default:
throw ServiceResultException.Unexpected(
"Unexpected IdType value: {0}", idType);
}
}
return DefaultNodeId(idType, namespaceIndex);
}
finally
{
m_stack.Pop();
}
}
/// <inheritdoc/>
public ExpandedNodeId ReadExpandedNodeId(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return ExpandedNodeId.Null;
}
if (token is string text)
{
try
{
return ExpandedNodeId.Parse(
Context,
text,
new NodeIdParsingOptions
{
UpdateTables = UpdateNamespaceTable,
NamespaceMappings = m_namespaceMappings,
ServerMappings = m_serverMappings
});
}
catch
{
// fallback on error. this allows the application to sort out the problem.
_ = new NodeId(text, 0);
}
}
if (token is not Dictionary<string, object> value)
{
return ExpandedNodeId.Null;
}
IdType idType = IdType.Numeric;
ushort namespaceIndex = 0;
string? namespaceUri = null;
uint serverIndex = 0;
try
{
m_stack.Push(value);
if (value.ContainsKey("IdType"))
{
idType = (IdType)ReadInt32("IdType");
}
if (ReadField("Namespace", out object namespaceToken))
{
long? index = namespaceToken as long?;
if (index == null)
{
namespaceUri = namespaceToken as string;
}
else if (index.Value is >= 0 and < ushort.MaxValue)
{
namespaceIndex = ToNamespaceIndex(index.Value);
}
}
if (ReadField("ServerUri", out object serverUriToken))
{
long? index = serverUriToken as long?;
if (index == null)
{
serverIndex = ToServerIndex((string)serverUriToken);
}
else if (index.Value is >= 0 and < uint.MaxValue)
{
serverIndex = ToServerIndex(index.Value);
}
}
if (namespaceUri != null)
{
namespaceIndex = ToNamespaceIndex(namespaceUri);
if (ushort.MaxValue != namespaceIndex)
{
namespaceUri = null;
}
else
{
namespaceIndex = 0;
}
}
if (value.ContainsKey("Id"))
{
switch (idType)
{
case IdType.Numeric:
return new ExpandedNodeId(
ReadUInt32("Id"),
namespaceIndex,
namespaceUri,
serverIndex);
case IdType.Opaque:
return new ExpandedNodeId(
ReadByteString("Id"),
namespaceIndex,
namespaceUri,
serverIndex);
case IdType.String:
return new ExpandedNodeId(
ReadString("Id")!,
namespaceIndex,
namespaceUri,
serverIndex);
case IdType.Guid:
return new ExpandedNodeId(
ReadGuid("Id"),
namespaceIndex,
namespaceUri,
serverIndex);
default:
throw ServiceResultException.Unexpected(
"Unexpected IdType value: {0}", idType);
}
}
return new ExpandedNodeId(
DefaultNodeId(idType, namespaceIndex),
namespaceUri,
serverIndex);
}
finally
{
m_stack.Pop();
}
}
/// <inheritdoc/>
public StatusCode ReadStatusCode(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
// the status code was not found
return StatusCodes.Good;
}
if (token is long code)
{
return (StatusCode)(uint)code;
}
bool wasPush = PushStructure(fieldName);
try
{
// try to read the non reversible Code
if (ReadField("Code", out token))
{
return (StatusCode)ReadUInt32("Code");
}
// read the uint code
return ReadUInt32(null);
}
finally
{
if (wasPush)
{
Pop();
}
}
}
/// <inheritdoc/>
public DiagnosticInfo? ReadDiagnosticInfo(string? fieldName)
{
return ReadDiagnosticInfo(fieldName, 0);
}
/// <inheritdoc/>
public QualifiedName ReadQualifiedName(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return QualifiedName.Null;
}
if (token is string text)
{
QualifiedName qn;
try
{
qn = QualifiedName.Parse(Context, text, UpdateNamespaceTable);
if (qn.NamespaceIndex != 0)
{
ushort ns = ToNamespaceIndex(qn.NamespaceIndex);
if (ns != qn.NamespaceIndex)
{
qn = new QualifiedName(qn.Name, ns);
}
}
}
catch (Exception)
{
// fallback on error. this allows the application to sort out the problem.
qn = new QualifiedName(text, 0);
}
return qn;
}
if (token is not Dictionary<string, object> value)
{
return QualifiedName.Null;
}
ushort namespaceIndex = 0;
string? name = null;
try
{
m_stack.Push(value);
if (value.ContainsKey("Name"))
{
name = ReadString("Name");
}
if (ReadField("Uri", out object namespaceToken))
{
long? index = namespaceToken as long?;
if (index == null)
{
if (namespaceToken is string namespaceUri)
{
namespaceIndex = ToNamespaceIndex(namespaceUri);
}
}
else if (index.Value is >= 0 and < ushort.MaxValue)
{
namespaceIndex = ToNamespaceIndex(index.Value);
}
}
}
finally
{
m_stack.Pop();
}
return new QualifiedName(name, namespaceIndex);
}
/// <inheritdoc/>
public LocalizedText ReadLocalizedText(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return LocalizedText.Null;
}
string? locale = null;
string? text = null;
if (token is not Dictionary<string, object> value)
{
// read non reversible encoding
text = token as string;
if (text != null)
{
return new LocalizedText(text);
}
return LocalizedText.Null;
}
try
{
m_stack.Push(value);
if (value.ContainsKey("Locale"))
{
locale = ReadString("Locale");
}
if (value.ContainsKey("Text"))
{
text = ReadString("Text");
}
}
finally
{
m_stack.Pop();
}
return new LocalizedText(locale, text);
}
private Variant ReadVariantFromObject(
string valueName,
BuiltInType builtInType,
Dictionary<string, object> value)
{
if (value.TryGetValue(valueName, out object? innerValue))
{
if (innerValue is List<object> elements)
{
if (elements.Any(e => e is List<object> innerInner))
{
var elements2 = new List<object>();
var dimensions = new List<int>();
Type? systemType = null;
ExpandedNodeId encodeableTypeId = default;
if (builtInType is BuiltInType.Enumeration or BuiltInType.Variant or BuiltInType.Null)
{
DetermineIEncodeableSystemType(ref systemType, encodeableTypeId);
}
// decode structured matrix
ReadMatrixPart(
valueName,
elements,
builtInType,
ref elements2,
ref dimensions,
0,
systemType,
encodeableTypeId);
switch (builtInType)
{
case BuiltInType.Boolean:
return elements2.Cast<bool>().ToMatrixOf([.. dimensions]);
case BuiltInType.SByte:
return elements2.Cast<sbyte>().ToMatrixOf([.. dimensions]);
case BuiltInType.Byte:
return elements2.Cast<byte>().ToMatrixOf([.. dimensions]);
case BuiltInType.Int16:
return elements2.Cast<short>().ToMatrixOf([.. dimensions]);
case BuiltInType.UInt16:
return elements2.Cast<ushort>().ToMatrixOf([.. dimensions]);
case BuiltInType.Int32:
return elements2.Cast<int>().ToMatrixOf([.. dimensions]);
case BuiltInType.UInt32:
return elements2.Cast<uint>().ToMatrixOf([.. dimensions]);
case BuiltInType.Int64:
return elements2.Cast<long>().ToMatrixOf([.. dimensions]);
case BuiltInType.UInt64:
return elements2.Cast<ulong>().ToMatrixOf([.. dimensions]);
case BuiltInType.Float:
return elements2.Cast<float>().ToMatrixOf([.. dimensions]);
case BuiltInType.Double:
return elements2.Cast<double>().ToMatrixOf([.. dimensions]);
case BuiltInType.String:
return elements2.Cast<string>().ToMatrixOf([.. dimensions]);
case BuiltInType.DateTime:
return elements2.Cast<DateTimeUtc>().ToMatrixOf([.. dimensions]);
case BuiltInType.Guid:
return elements2.Cast<Uuid>().ToMatrixOf([.. dimensions]);
case BuiltInType.ByteString:
return elements2.Cast<ByteString>().ToMatrixOf([.. dimensions]);
case BuiltInType.XmlElement:
return elements2.Cast<XmlElement>().ToMatrixOf([.. dimensions]);
case BuiltInType.NodeId:
return elements2.Cast<NodeId>().ToMatrixOf([.. dimensions]);
case BuiltInType.ExpandedNodeId:
return elements2.Cast<ExpandedNodeId>().ToMatrixOf([.. dimensions]);
case BuiltInType.StatusCode:
return elements2.Cast<StatusCode>().ToMatrixOf([.. dimensions]);
case BuiltInType.QualifiedName:
return elements2.Cast<QualifiedName>().ToMatrixOf([.. dimensions]);
case BuiltInType.LocalizedText:
return elements2.Cast<LocalizedText>().ToMatrixOf([.. dimensions]);
case BuiltInType.DataValue:
return elements2.Cast<DataValue>().ToMatrixOf([.. dimensions]);
case BuiltInType.Enumeration:
return Variant.From(EnumValue.From(elements2.Cast<int>().ToMatrixOf([.. dimensions])));
case BuiltInType.Variant:
{
if (DetermineIEncodeableSystemType(ref systemType, encodeableTypeId))
{
var newElements = Array.CreateInstance(systemType!, elements.Count);
for (int i = 0; i < elements.Count; i++)
{
newElements.SetValue(
Convert.ChangeType(
elements[i],
systemType!,
CultureInfo.InvariantCulture),
i);
}
return new Variant(new Matrix(newElements, builtInType, [.. dimensions]));
}
return elements2.Cast<Variant>().ToMatrixOf([.. dimensions]);
}
case BuiltInType.ExtensionObject:
return elements2.Cast<ExtensionObject>().ToMatrixOf([.. dimensions]);
case BuiltInType.Null:
case BuiltInType.Number:
case BuiltInType.Integer:
case BuiltInType.UInteger:
if (DetermineIEncodeableSystemType(ref systemType, encodeableTypeId))
{
var newElements = Array.CreateInstance(systemType!, elements.Count);
for (int i = 0; i < elements.Count; i++)
{
newElements.SetValue(
Convert.ChangeType(
elements[i],
systemType!,
CultureInfo.InvariantCulture),
i);
}
return new Variant(new Matrix(newElements, builtInType, [.. dimensions]));
}
throw ServiceResultException.Create(
StatusCodes.BadDecodingError,
"Cannot decode unknown type in Array object with BuiltInType: {0}.",
builtInType);
default:
throw ServiceResultException.Unexpected(
$"Unexpected BuiltInType {builtInType}");
}
}
Variant array = ReadVariantArrayBody(valueName, builtInType);
if (value.ContainsKey("Dimensions"))
{
int[] dimensions = ReadInt32Array("Dimensions").ToArray()!;
if (array.Value is not Array arrayValue)
{
throw new ServiceResultException(
StatusCodes.BadDecodingError,
"Variant array body is missing or not an array.");
}
try
{
return new Variant(
new Matrix(arrayValue, builtInType, dimensions));
}
catch (ArgumentException e)
{
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded, e);
}
catch (Exception e)
{
throw new ServiceResultException(StatusCodes.BadDecodingError, e);
}
}
return array;
}
return ReadVariantBody(valueName, builtInType);
}
return Variant.Null;
}
/// <inheritdoc/>
public Variant ReadVariant(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return Variant.Null;
}
if (token is not Dictionary<string, object> value)
{
return Variant.Null;
}
CheckAndIncrementNestingLevel();
try
{
m_stack.Push(value);
BuiltInType builtInType = value.ContainsKey("UaType")
? (BuiltInType)ReadByte("UaType")
: (BuiltInType)ReadByte("Type");
if (value.ContainsKey("Value"))
{
return ReadVariantFromObject("Value", builtInType, value);
}
return ReadVariantFromObject("Body", builtInType, value);
}
finally
{
m_nestingLevel--;
m_stack.Pop();
}
}
/// <inheritdoc/>
public DataValue ReadDataValue(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return default;
}
if (token is not Dictionary<string, object> value)
{
return default;
}
var dv = new DataValue();
try
{
m_stack.Push(value);
if (value.ContainsKey("UaType"))
{
var builtInType = (BuiltInType)ReadByte("UaType");
dv = dv.WithWrappedValue(ReadVariantFromObject("Value", builtInType, value));
}
else
{
dv = dv.WithWrappedValue(ReadVariant("Value"));
}
dv = dv.WithStatus(ReadStatusCode("StatusCode"))
.WithSourceTimestamp(ReadDateTime("SourceTimestamp"));
dv = dv.WithSourcePicoseconds(
dv.SourceTimestamp != DateTimeUtc.MinValue
? ReadUInt16("SourcePicoseconds")
: (ushort)0);
dv = dv.WithServerTimestamp(ReadDateTime("ServerTimestamp"));
dv = dv.WithServerPicoseconds(
dv.ServerTimestamp != DateTimeUtc.MinValue
? ReadUInt16("ServerPicoseconds")
: (ushort)0);
}
finally
{
m_stack.Pop();
}
return dv;
}
/// <inheritdoc/>
public ExtensionObject ReadExtensionObject(string? fieldName)
{
ExtensionObject extension = ExtensionObject.Null;
if (!ReadField(fieldName, out object token))
{
return extension;
}
if ((token is not Dictionary<string, object> value) || (value.Count == 0))
{
return extension;
}
try
{
m_stack.Push(value);
bool inlineValues = true;
ExpandedNodeId typeId = ReadExpandedNodeId("UaTypeId");
if (typeId.IsNull)
{
typeId = ReadExpandedNodeId("TypeId");
inlineValues = false;
}
ExpandedNodeId absoluteId = typeId.IsAbsolute
? typeId
: NodeId.ToExpandedNodeId(typeId.InnerNodeId, Context.NamespaceUris);
if (!typeId.IsNull && absoluteId.IsNull)
{
m_logger.LogWarning(
"Cannot de-serialized extension objects if the NamespaceUri is not in the NamespaceTable: Type = {Type}",
typeId);
}
else
{
typeId = absoluteId;
}
ExtensionObjectEncoding encoding = 0;
string encodingFieldName = inlineValues ? "UaEncoding" : "Encoding";
encoding = (ExtensionObjectEncoding)ReadByte(encodingFieldName);
if (value.ContainsKey(encodingFieldName))
{
encoding = (ExtensionObjectEncoding)ReadByte(encodingFieldName);
if (encoding == ExtensionObjectEncoding.None)
{
return extension;
}
}
if (encoding == ExtensionObjectEncoding.Binary)
{
ByteString bytes = ReadByteString(inlineValues ? "UaBody" : "Body");
return new ExtensionObject(typeId, bytes);
}
if (encoding == ExtensionObjectEncoding.Xml)
{
XmlElement xml = ReadXmlElement(inlineValues ? "UaBody" : "Body");
if (xml.IsEmpty)
{
return extension;
}
return new ExtensionObject(typeId, xml);
}
if (encoding == ExtensionObjectEncoding.Json)
{
string? json = ReadString(inlineValues ? "UaBody" : "Body");
if (string.IsNullOrEmpty(json))
{
return extension;
}
return new ExtensionObject(typeId, json!);
}
Type? systemType = Context.Factory.GetSystemType(typeId);
if (systemType != null)
{
IEncodeable? encodeable = null;
if (inlineValues)
{
encodeable = Activator.CreateInstance(systemType) as IEncodeable
?? throw new ServiceResultException(
StatusCodes.BadDecodingError,
Utils.Format(
"Type does not support IEncodeable interface: '{0}'",
systemType.FullName!));
encodeable.Decode(this);
}
else
{
encodeable = ReadEncodeable("Body", systemType, typeId);
if (encodeable == null)
{
return extension;
}
}
return new ExtensionObject(typeId, encodeable);
}
using var ostrm = new MemoryStream();
using (var stream = new StreamWriter(ostrm))
using (var writer = new JsonTextWriter(stream))
{
EncodeAsJson(writer, token);
}
// Close the writer before retrieving the data
return new ExtensionObject(typeId, ByteString.From(ostrm.ToArray()));
}
finally
{
m_stack.Pop();
}
}
/// <inheritdoc/>
public IEncodeable? ReadEncodeable(
string? fieldName,
Type systemType,
ExpandedNodeId encodeableTypeId = default)
{
if (systemType == null)
{
throw new ArgumentNullException(nameof(systemType));
}
if (!ReadField(fieldName, out object token))
{
return null;
}
if (Activator.CreateInstance(systemType) is not IEncodeable value)
{
throw new ServiceResultException(
StatusCodes.BadDecodingError,
Utils.Format(
"Type does not support IEncodeable interface: '{0}'",
systemType.FullName!));
}
if (!encodeableTypeId.IsNull)
{
// set type identifier for custom complex data types before decode.
if (value is IComplexTypeInstance complexTypeInstance)
{
complexTypeInstance.TypeId = encodeableTypeId;
}
}
CheckAndIncrementNestingLevel();
try
{
m_stack.Push(token);
value.Decode(this);
}
finally
{
m_stack.Pop();
m_nestingLevel--;
}
return value;
}
/// <inheritdoc/>
public Enum ReadEnumerated(string? fieldName, Type enumType)
{
if (enumType == null)
{
throw new ArgumentNullException(nameof(enumType));
}
if (!ReadField(fieldName, out object token))
{
return (Enum)Enum.ToObject(enumType, 0);
}
if (token is long code)
{
return (Enum)Enum.ToObject(enumType, code);
}
if (token is string text)
{
int index = text.LastIndexOf('_');
if (index > 0 && long.TryParse(text[(index + 1)..], out code))
{
return (Enum)Enum.ToObject(enumType, code);
}
}
return (Enum)Enum.ToObject(enumType, 0);
}
/// <inheritdoc/>
public ArrayOf<bool> ReadBooleanArray(string? fieldName)
{
var values = new List<bool>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
values.Add(ReadBoolean(null));
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<sbyte> ReadSByteArray(string? fieldName)
{
var values = new List<sbyte>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
values.Add(ReadSByte(null));
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<byte> ReadByteArray(string? fieldName)
{
var values = new List<byte>();
string? value = ReadString(fieldName);
if (value != null)
{
return SafeConvertFromBase64String(value);
}
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
values.Add(ReadByte(null));
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<short> ReadInt16Array(string? fieldName)
{
var values = new List<short>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
values.Add(ReadInt16(null));
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<ushort> ReadUInt16Array(string? fieldName)
{
var values = new List<ushort>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
values.Add(ReadUInt16(null));
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<int> ReadInt32Array(string? fieldName)
{
var values = new List<int>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
values.Add(ReadInt32(null));
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<uint> ReadUInt32Array(string? fieldName)
{
var values = new List<uint>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
values.Add(ReadUInt32(null));
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<long> ReadInt64Array(string? fieldName)
{
var values = new List<long>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
values.Add(ReadInt64(null));
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<ulong> ReadUInt64Array(string? fieldName)
{
var values = new List<ulong>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
values.Add(ReadUInt64(null));
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<float> ReadFloatArray(string? fieldName)
{
var values = new List<float>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
values.Add(ReadFloat(null));
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<double> ReadDoubleArray(string? fieldName)
{
var values = new List<double>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
values.Add(ReadDouble(null));
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<string?> ReadStringArray(string? fieldName)
{
var values = new List<string?>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
values.Add(ReadString(null));
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<DateTimeUtc> ReadDateTimeArray(string? fieldName)
{
var values = new List<DateTimeUtc>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
values.Add(ReadDateTime(null));
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<Uuid> ReadGuidArray(string? fieldName)
{
var values = new List<Uuid>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
Uuid element = ReadGuid(null);
values.Add(element);
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<ByteString> ReadByteStringArray(string? fieldName)
{
var values = new List<ByteString>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
ByteString element = ReadByteString(null);
values.Add(element);
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<XmlElement> ReadXmlElementArray(string? fieldName)
{
var values = new List<XmlElement>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
XmlElement element = ReadXmlElement(null);
values.Add(element);
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<NodeId> ReadNodeIdArray(string? fieldName)
{
var values = new List<NodeId>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
NodeId element = ReadNodeId(null);
values.Add(element);
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<ExpandedNodeId> ReadExpandedNodeIdArray(string? fieldName)
{
var values = new List<ExpandedNodeId>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
ExpandedNodeId element = ReadExpandedNodeId(null);
values.Add(element);
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<StatusCode> ReadStatusCodeArray(string? fieldName)
{
var values = new List<StatusCode>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
StatusCode element = ReadStatusCode(null);
values.Add(element);
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<DiagnosticInfo?> ReadDiagnosticInfoArray(string? fieldName)
{
var values = new List<DiagnosticInfo?>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
DiagnosticInfo? element = ReadDiagnosticInfo(null);
values.Add(element);
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<QualifiedName> ReadQualifiedNameArray(string? fieldName)
{
var values = new List<QualifiedName>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
QualifiedName element = ReadQualifiedName(null);
values.Add(element);
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<LocalizedText> ReadLocalizedTextArray(string? fieldName)
{
var values = new List<LocalizedText>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
LocalizedText element = ReadLocalizedText(null);
values.Add(element);
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<Variant> ReadVariantArray(string? fieldName)
{
var values = new List<Variant>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
Variant element = ReadVariant(null);
values.Add(element);
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<DataValue> ReadDataValueArray(string? fieldName)
{
var values = new List<DataValue>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
DataValue element = ReadDataValue(null);
values.Add(element);
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public ArrayOf<ExtensionObject> ReadExtensionObjectArray(string? fieldName)
{
var values = new List<ExtensionObject>();
if (!ReadArrayField(fieldName, out List<object> token))
{
return values;
}
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
ExtensionObject element = ReadExtensionObject(null);
values.Add(element);
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public Array ReadEncodeableArray(
string? fieldName,
Type systemType,
ExpandedNodeId encodeableTypeId = default)
{
if (systemType == null)
{
throw new ArgumentNullException(nameof(systemType));
}
if (!ReadArrayField(fieldName, out List<object> token))
{
return Array.CreateInstance(systemType, 0);
}
var values = Array.CreateInstance(systemType, token.Count);
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
IEncodeable? element = ReadEncodeable(null, systemType, encodeableTypeId);
values.SetValue(element, ii);
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public Array ReadEnumeratedArray(string? fieldName, Type enumType)
{
if (enumType == null)
{
throw new ArgumentNullException(nameof(enumType));
}
if (!ReadArrayField(fieldName, out List<object> token))
{
return Array.CreateInstance(enumType, 0);
}
var values = Array.CreateInstance(enumType, token.Count);
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
Enum? element = ReadEnumerated(null, enumType);
values.SetValue(element, ii);
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public T ReadEncodeable<T>(string? fieldName, ExpandedNodeId encodeableTypeId) where T : IEncodeable
{
return (T)ReadEncodeable(fieldName, typeof(T), encodeableTypeId)!;
}
/// <inheritdoc/>
public T ReadEncodeable<T>(string? fieldName) where T : IEncodeable, new()
{
return (T)ReadEncodeable(fieldName, typeof(T))!;
}
/// <inheritdoc/>
public T ReadEncodeableAsExtensionObject<T>(string? fieldName) where T : IEncodeable
{
return ReadExtensionObject(fieldName).TryGetValue(out T? value) ? value! : default!;
}
/// <inheritdoc/>
public T ReadEnumerated<T>(string? fieldName) where T : struct, Enum
{
return (T)ReadEnumerated(fieldName, typeof(T));
}
/// <inheritdoc/>
public EnumValue ReadEnumerated(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return default;
}
if (token is long code)
{
return (EnumValue)(int)code;
}
if (token is string text)
{
int index = text.LastIndexOf('_');
if (index > 0 && long.TryParse(text[(index + 1)..], out code))
{
return new EnumValue((int)code, text[..index]);
}
}
return default;
}
/// <inheritdoc/>
public ArrayOf<T> ReadEncodeableArray<T>(string? fieldName) where T : IEncodeable, new()
{
return ArrayOf.From<T>(ReadEncodeableArray(fieldName, typeof(T)));
}
/// <inheritdoc/>
public ArrayOf<T> ReadEncodeableArray<T>(string? fieldName, ExpandedNodeId encodeableTypeId) where T : IEncodeable
{
return ArrayOf.From<T>(ReadEncodeableArray(fieldName, typeof(T), encodeableTypeId));
}
/// <inheritdoc/>
public ArrayOf<T> ReadEncodeableArrayAsExtensionObjects<T>(string? fieldName) where T : IEncodeable
{
ArrayOf<ExtensionObject> array = ReadExtensionObjectArray(fieldName);
return array.GetStructuresOf<T>();
}
/// <inheritdoc/>
public MatrixOf<T> ReadEncodeableMatrix<T>(string? fieldName, ExpandedNodeId encodeableTypeId) where T : IEncodeable
{
return default;
}
/// <inheritdoc/>
public ArrayOf<T> ReadEnumeratedArray<T>(string? fieldName) where T : struct, Enum
{
return ArrayOf.From<T>(ReadEnumeratedArray(fieldName, typeof(T)));
}
/// <inheritdoc/>
public ArrayOf<EnumValue> ReadEnumeratedArray(string? fieldName)
{
if (!ReadArrayField(fieldName, out List<object> token))
{
return default;
}
var values = new EnumValue[token.Count];
for (int ii = 0; ii < token.Count; ii++)
{
try
{
m_stack.Push(token[ii]);
values[ii] = ReadEnumerated(null);
}
finally
{
m_stack.Pop();
}
}
return values;
}
/// <inheritdoc/>
public Variant ReadVariantValue(string? fieldName, TypeInfo typeInfo)
{
return default;
}
/// <inheritdoc/>
public Array? ReadArray(
string fieldName,
int valueRank,
BuiltInType builtInType,
Type? systemType = null,
ExpandedNodeId encodeableTypeId = default)
{
if (valueRank == ValueRanks.OneDimension)
{
switch (builtInType)
{
case BuiltInType.Boolean:
return ReadBooleanArray(fieldName).ToArray();
case BuiltInType.SByte:
return ReadSByteArray(fieldName).ToArray();
case BuiltInType.Byte:
return ReadByteArray(fieldName).ToArray();
case BuiltInType.Int16:
return ReadInt16Array(fieldName).ToArray();
case BuiltInType.UInt16:
return ReadUInt16Array(fieldName).ToArray();
case BuiltInType.Enumeration:
DetermineIEncodeableSystemType(ref systemType, encodeableTypeId);
if (systemType?.IsEnum == true)
{
return ReadEnumeratedArray(fieldName, systemType);
}
goto case BuiltInType.Int32;
case BuiltInType.Int32:
return ReadInt32Array(fieldName).ToArray();
case BuiltInType.UInt32:
return ReadUInt32Array(fieldName).ToArray();
case BuiltInType.Int64:
return ReadInt64Array(fieldName).ToArray();
case BuiltInType.UInt64:
return ReadUInt64Array(fieldName).ToArray();
case BuiltInType.Float:
return ReadFloatArray(fieldName).ToArray();
case BuiltInType.Double:
return ReadDoubleArray(fieldName).ToArray();
case BuiltInType.String:
return ReadStringArray(fieldName).ToArray();
case BuiltInType.DateTime:
return ReadDateTimeArray(fieldName).ToArray();
case BuiltInType.Guid:
return ReadGuidArray(fieldName).ToArray();
case BuiltInType.ByteString:
return ReadByteStringArray(fieldName).ToArray();
case BuiltInType.XmlElement:
return ReadXmlElementArray(fieldName).ToArray();
case BuiltInType.NodeId:
return ReadNodeIdArray(fieldName).ToArray();
case BuiltInType.ExpandedNodeId:
return ReadExpandedNodeIdArray(fieldName).ToArray();
case BuiltInType.StatusCode:
return ReadStatusCodeArray(fieldName).ToArray();
case BuiltInType.QualifiedName:
return ReadQualifiedNameArray(fieldName).ToArray();
case BuiltInType.LocalizedText:
return ReadLocalizedTextArray(fieldName).ToArray();
case BuiltInType.DataValue:
return ReadDataValueArray(fieldName).ToArray();
case BuiltInType.Variant:
if (DetermineIEncodeableSystemType(ref systemType, encodeableTypeId))
{
return ReadEncodeableArray(fieldName, systemType!, encodeableTypeId);
}
return ReadVariantArray(fieldName).ToArray();
case BuiltInType.ExtensionObject:
return ReadExtensionObjectArray(fieldName).ToArray();
case BuiltInType.DiagnosticInfo:
return ReadDiagnosticInfoArray(fieldName).ToArray();
case BuiltInType.Null:
case BuiltInType.Number:
case BuiltInType.Integer:
case BuiltInType.UInteger:
if (DetermineIEncodeableSystemType(ref systemType, encodeableTypeId))
{
return ReadEncodeableArray(fieldName, systemType!, encodeableTypeId);
}
throw new ServiceResultException(
StatusCodes.BadDecodingError,
Utils.Format(
"Cannot decode unknown type in Array object with BuiltInType: {0}.",
builtInType));
default:
throw ServiceResultException.Unexpected($"Unexpected BuiltInType {builtInType}");
}
}
else if (valueRank >= ValueRanks.TwoDimensions)
{
if (!ReadField(fieldName, out object token))
{
return null;
}
if (token is Dictionary<string, object> value)
{
m_stack.Push(value);
int[] dimensions2;
if (value.ContainsKey("Dimensions"))
{
dimensions2 = ReadInt32Array("Dimensions").ToArray()!;
}
else
{
dimensions2 = [];
}
Array? array2 = ReadArray("Array", 1, builtInType, systemType, encodeableTypeId);
m_stack.Pop();
try
{
var matrix2 = new Matrix(array2!, builtInType, dimensions2!);
return matrix2.ToArray();
}
catch (ArgumentException e)
{
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded, e);
}
catch (Exception e)
{
throw new ServiceResultException(StatusCodes.BadDecodingError, e);
}
}
if (token is not List<object> array)
{
return null;
}
var elements = new List<object>();
var dimensions = new List<int>();
if (builtInType is BuiltInType.Enumeration or BuiltInType.Variant or BuiltInType.Null)
{
DetermineIEncodeableSystemType(ref systemType, encodeableTypeId);
}
ReadMatrixPart(
fieldName,
array,
builtInType,
ref elements,
ref dimensions,
0,
systemType,
encodeableTypeId);
if (dimensions.Count == 0)
{
// for an empty element create the empty dimension array
dimensions = new int[valueRank].ToList();
}
else if (dimensions.Count < ValueRanks.TwoDimensions)
{
throw ServiceResultException.Create(
StatusCodes.BadDecodingError,
"The ValueRank {0} of the decoded array doesn't match the desired ValueRank {1}.",
dimensions.Count,
valueRank);
}
Matrix matrix;
try
{
switch (builtInType)
{
case BuiltInType.Boolean:
matrix = new Matrix(
elements.Cast<bool>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.SByte:
matrix = new Matrix(
elements.Cast<sbyte>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.Byte:
matrix = new Matrix(
elements.Cast<byte>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.Int16:
matrix = new Matrix(
elements.Cast<short>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.UInt16:
matrix = new Matrix(
elements.Cast<ushort>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.Int32:
matrix = new Matrix(
elements.Cast<int>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.UInt32:
matrix = new Matrix(
elements.Cast<uint>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.Int64:
matrix = new Matrix(
elements.Cast<long>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.UInt64:
matrix = new Matrix(
elements.Cast<ulong>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.Float:
matrix = new Matrix(
elements.Cast<float>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.Double:
matrix = new Matrix(
elements.Cast<double>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.String:
matrix = new Matrix(
elements.Cast<string>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.DateTime:
matrix = new Matrix(
elements.Cast<DateTimeUtc>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.Guid:
matrix = new Matrix(
elements.Cast<Uuid>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.ByteString:
matrix = new Matrix(
elements.Cast<ByteString>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.XmlElement:
matrix = new Matrix(
elements.Cast<XmlElement>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.NodeId:
matrix = new Matrix(
elements.Cast<NodeId>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.ExpandedNodeId:
matrix = new Matrix(
elements.Cast<ExpandedNodeId>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.StatusCode:
matrix = new Matrix(
elements.Cast<StatusCode>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.QualifiedName:
matrix = new Matrix(
elements.Cast<QualifiedName>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.LocalizedText:
matrix = new Matrix(
elements.Cast<LocalizedText>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.DataValue:
matrix = new Matrix(
elements.Cast<DataValue>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.Enumeration:
{
if (systemType?.IsEnum == true)
{
var newElements = Array.CreateInstance(systemType!, elements.Count);
int ii = 0;
foreach (object element in elements)
{
newElements.SetValue(
Convert.ChangeType(
element,
systemType!,
CultureInfo.InvariantCulture),
ii++);
}
matrix = new Matrix(newElements, builtInType, [.. dimensions]);
}
else
{
matrix = new Matrix(
elements.Cast<int>().ToArray(),
builtInType,
[.. dimensions]);
}
break;
}
case BuiltInType.Variant:
{
if (DetermineIEncodeableSystemType(ref systemType, encodeableTypeId))
{
var newElements = Array.CreateInstance(systemType!, elements.Count);
for (int i = 0; i < elements.Count; i++)
{
newElements.SetValue(
Convert.ChangeType(
elements[i],
systemType!,
CultureInfo.InvariantCulture),
i);
}
matrix = new Matrix(newElements, builtInType, [.. dimensions]);
break;
}
matrix = new Matrix(
elements.Cast<Variant>().ToArray(),
builtInType,
[.. dimensions]);
break;
}
case BuiltInType.ExtensionObject:
matrix = new Matrix(
elements.Cast<ExtensionObject>().ToArray(),
builtInType,
[.. dimensions]);
break;
case BuiltInType.Null:
case BuiltInType.Number:
case BuiltInType.Integer:
case BuiltInType.UInteger:
if (DetermineIEncodeableSystemType(ref systemType, encodeableTypeId))
{
var newElements = Array.CreateInstance(systemType!, elements.Count);
for (int i = 0; i < elements.Count; i++)
{
newElements.SetValue(
Convert.ChangeType(
elements[i],
systemType!,
CultureInfo.InvariantCulture),
i);
}
matrix = new Matrix(newElements, builtInType, [.. dimensions]);
break;
}
throw ServiceResultException.Create(
StatusCodes.BadDecodingError,
"Cannot decode unknown type in Array object with BuiltInType: {0}.",
builtInType);
case BuiltInType.DiagnosticInfo:
throw ServiceResultException.Create(
StatusCodes.BadDecodingError,
"Cannot decode unknown type in Array object with BuiltInType: {0}.",
builtInType);
default:
throw ServiceResultException.Unexpected(
$"Unexpected BuiltInType {builtInType}");
}
}
catch (ArgumentException e)
{
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded, e);
}
catch (Exception e)
{
throw new ServiceResultException(StatusCodes.BadDecodingError, e);
}
return matrix.ToArray();
}
return null;
}
/// <inheritdoc/>
public uint ReadSwitchField(IList<string> switches, out string? fieldName)
{
fieldName = null;
if (m_stack.Peek() is Dictionary<string, object> context)
{
long index = -1;
if (context.ContainsKey("SwitchField"))
{
index = ReadUInt32("SwitchField");
}
if (switches == null)
{
return 0;
}
if (index >= switches.Count)
{
return (uint)index;
}
if (index >= 0)
{
if (!context.ContainsKey("Value"))
{
fieldName = switches[(int)(index - 1)];
}
else
{
fieldName = "Value";
}
return (uint)index;
}
foreach (KeyValuePair<string, object> ii in context)
{
if (ii.Key == "UaTypeId")
{
continue;
}
index = switches.IndexOf(ii.Key);
if (index >= 0)
{
fieldName = ii.Key;
return (uint)(index + 1);
}
}
}
return 0;
}
/// <inheritdoc/>
public uint ReadEncodingMask(IList<string> masks)
{
if (m_stack.Peek() is Dictionary<string, object> context)
{
if (context.ContainsKey("EncodingMask"))
{
return ReadUInt32("EncodingMask");
}
uint mask = 0;
if (masks == null)
{
return 0;
}
foreach (string fieldName in masks)
{
if (context.ContainsKey(fieldName))
{
int index = masks.IndexOf(fieldName);
if (index >= 0)
{
mask |= (uint)(1 << index);
}
}
}
return mask;
}
return 0;
}
/// <summary>
/// Push the specified structure on the Read Stack
/// </summary>
/// <param name="fieldName">The name of the object that shall be placed on the Read Stack</param>
/// <returns>true if successful</returns>
public bool PushStructure(string? fieldName)
{
if (!ReadField(fieldName, out object token))
{
return false;
}
if (token != null)
{
m_stack.Push(token);
return true;
}
return false;
}
/// <inheritdoc/>
public bool PushArray(string? fieldName, int index)
{
if (!ReadArrayField(fieldName, out List<object> token))
{
return false;
}
if (index < token.Count)
{
m_stack.Push(token[index]);
return true;
}
return false;
}
/// <inheritdoc/>
public void Pop()
{
m_stack.Pop();
}
private ushort ToNamespaceIndex(string uri)
{
int index = Context.NamespaceUris.GetIndex(uri);
if (index < 0)
{
if (!UpdateNamespaceTable)
{
return ushort.MaxValue;
}
index = Context.NamespaceUris.GetIndexOrAppend(uri);
}
return (ushort)index;
}
private ushort ToNamespaceIndex(long index)
{
if (m_namespaceMappings == null || index <= 0)
{
return (ushort)index;
}
if (index < 0 || index >= m_namespaceMappings.Length)
{
throw new ServiceResultException(
StatusCodes.BadDecodingError,
$"No mapping for NamespaceIndex={index}.");
}
return m_namespaceMappings[index];
}
private ushort ToServerIndex(string uri)
{
int index = Context.ServerUris.GetIndex(uri);
if (index < 0)
{
if (!UpdateNamespaceTable)
{
return ushort.MaxValue;
}
index = Context.ServerUris.GetIndexOrAppend(uri);
}
return (ushort)index;
}
private ushort ToServerIndex(long index)
{
if (m_serverMappings == null || index <= 0)
{
return (ushort)index;
}
if (index < 0 || index >= m_serverMappings.Length)
{
throw new ServiceResultException(
StatusCodes.BadDecodingError,
$"No mapping for ServerIndex(={index}.");
}
return m_serverMappings[index];
}
/// <summary>
/// Helper to provide the TryParse method when reading an enumerated string.
/// </summary>
/// <typeparam name="T"></typeparam>
private delegate bool TryParseHandler<T>(
string s,
NumberStyles numberStyles,
CultureInfo cultureInfo,
out T result);
/// <summary>
/// Helper to read an enumerated string in an extension object.
/// </summary>
/// <typeparam name="T">The number type which was encoded.</typeparam>
/// <returns>The parsed number or 0.</returns>
private static T ReadEnumeratedString<T>(object token, TryParseHandler<T> handler)
where T : struct
{
T number = default;
if (token is string text)
{
bool retry = false;
do
{
if (handler?.Invoke(
text,
NumberStyles.Integer,
CultureInfo.InvariantCulture,
out number) == false)
{
int lastIndex = text.LastIndexOf('_');
if (lastIndex != -1)
{
text = text[(lastIndex + 1)..];
retry = true;
}
}
} while (retry);
}
return number;
}
/// <summary>
/// Reads a DiagnosticInfo from the stream.
/// Limits the InnerDiagnosticInfos to the specified depth.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
private DiagnosticInfo? ReadDiagnosticInfo(string? fieldName, int depth)
{
if (!ReadField(fieldName, out object token))
{
return null;
}
if (token is not Dictionary<string, object> value)
{
return null;
}
if (depth >= DiagnosticInfo.MaxInnerDepth)
{
throw ServiceResultException.Create(
StatusCodes.BadEncodingLimitsExceeded,
"Maximum nesting level of InnerDiagnosticInfo was exceeded");
}
CheckAndIncrementNestingLevel();
try
{
m_stack.Push(value);
var di = new DiagnosticInfo();
bool hasDiagnosticInfo = false;
if (value.ContainsKey("SymbolicId"))
{
di.SymbolicId = ReadInt32("SymbolicId");
hasDiagnosticInfo = true;
}
if (value.ContainsKey("NamespaceUri"))
{
di.NamespaceUri = ReadInt32("NamespaceUri");
hasDiagnosticInfo = true;
}
if (value.ContainsKey("Locale"))
{
di.Locale = ReadInt32("Locale");
hasDiagnosticInfo = true;
}
if (value.ContainsKey("LocalizedText"))
{
di.LocalizedText = ReadInt32("LocalizedText");
hasDiagnosticInfo = true;
}
if (value.ContainsKey("AdditionalInfo"))
{
di.AdditionalInfo = ReadString("AdditionalInfo");
hasDiagnosticInfo = true;
}
if (value.ContainsKey("InnerStatusCode"))
{
di.InnerStatusCode = ReadStatusCode("InnerStatusCode");
hasDiagnosticInfo = true;
}
if (value.ContainsKey("InnerDiagnosticInfo") &&
depth < DiagnosticInfo.MaxInnerDepth)
{
di.InnerDiagnosticInfo = ReadDiagnosticInfo("InnerDiagnosticInfo", depth + 1);
hasDiagnosticInfo = true;
}
return hasDiagnosticInfo ? di : null;
}
finally
{
m_nestingLevel--;
m_stack.Pop();
}
}
/// <summary>
/// Get the system type from the type factory if not specified by caller.
/// </summary>
/// <param name="systemType">The reference to the system type, or null</param>
/// <param name="encodeableTypeId">The encodeable type id of the system type.</param>
/// <returns>If the system type is assignable to <see cref="IEncodeable"/> </returns>
private bool DetermineIEncodeableSystemType(
ref Type? systemType,
ExpandedNodeId encodeableTypeId)
{
if (!encodeableTypeId.IsNull && systemType == null)
{
systemType = Context.Factory.GetSystemType(encodeableTypeId);
}
return typeof(IEncodeable).IsAssignableFrom(systemType);
}
/// <summary>
/// Read the body of a Variant as a BuiltInType
/// </summary>
/// <exception cref="ServiceResultException"></exception>
private Variant ReadVariantBody(string? fieldName, BuiltInType type)
{
switch (type)
{
case BuiltInType.Boolean:
return new Variant(ReadBoolean(fieldName));
case BuiltInType.SByte:
return new Variant(ReadSByte(fieldName));
case BuiltInType.Byte:
return new Variant(ReadByte(fieldName));
case BuiltInType.Int16:
return new Variant(ReadInt16(fieldName));
case BuiltInType.UInt16:
return new Variant(ReadUInt16(fieldName));
case BuiltInType.Int32:
return new Variant(ReadInt32(fieldName));
case BuiltInType.UInt32:
return new Variant(ReadUInt32(fieldName));
case BuiltInType.Int64:
return new Variant(ReadInt64(fieldName));
case BuiltInType.UInt64:
return new Variant(ReadUInt64(fieldName));
case BuiltInType.Float:
return new Variant(ReadFloat(fieldName));
case BuiltInType.Double:
return new Variant(ReadDouble(fieldName));
case BuiltInType.String:
return new Variant(ReadString(fieldName)!);
case BuiltInType.ByteString:
return new Variant(ReadByteString(fieldName));
case BuiltInType.DateTime:
return new Variant(ReadDateTime(fieldName));
case BuiltInType.Guid:
return new Variant(ReadGuid(fieldName));
case BuiltInType.NodeId:
return new Variant(ReadNodeId(fieldName));
case BuiltInType.ExpandedNodeId:
return new Variant(ReadExpandedNodeId(fieldName));
case BuiltInType.QualifiedName:
return new Variant(ReadQualifiedName(fieldName));
case BuiltInType.LocalizedText:
return new Variant(ReadLocalizedText(fieldName));
case BuiltInType.StatusCode:
return new Variant(ReadStatusCode(fieldName));
case BuiltInType.XmlElement:
return new Variant(ReadXmlElement(fieldName));
case BuiltInType.ExtensionObject:
return new Variant(ReadExtensionObject(fieldName));
case BuiltInType.Variant:
return new Variant(ReadVariant(fieldName));
case BuiltInType.DataValue:
return new Variant(ReadDataValue(fieldName));
case BuiltInType.DiagnosticInfo:
case BuiltInType.Null:
case BuiltInType.Number:
case BuiltInType.Integer:
case BuiltInType.UInteger:
case BuiltInType.Enumeration:
return Variant.Null;
default:
throw ServiceResultException.Unexpected($"Unexpected BuiltInType {type}");
}
}
/// <summary>
/// Read the Body of a Variant as an Array
/// </summary>
/// <exception cref="ServiceResultException"></exception>
private Variant ReadVariantArrayBody(string? fieldName, BuiltInType type)
{
switch (type)
{
case BuiltInType.Boolean:
return new Variant(ReadBooleanArray(fieldName));
case BuiltInType.SByte:
return new Variant(ReadSByteArray(fieldName));
case BuiltInType.Byte:
return new Variant(ReadByteArray(fieldName));
case BuiltInType.Int16:
return new Variant(ReadInt16Array(fieldName));
case BuiltInType.UInt16:
return new Variant(ReadUInt16Array(fieldName));
case BuiltInType.Int32:
return new Variant(ReadInt32Array(fieldName));
case BuiltInType.UInt32:
return new Variant(ReadUInt32Array(fieldName));
case BuiltInType.Int64:
return new Variant(ReadInt64Array(fieldName));
case BuiltInType.UInt64:
return new Variant(ReadUInt64Array(fieldName));
case BuiltInType.Float:
return new Variant(ReadFloatArray(fieldName));
case BuiltInType.Double:
return new Variant(ReadDoubleArray(fieldName));
case BuiltInType.String:
return new Variant((ArrayOf<string>)ReadStringArray(fieldName)!);
case BuiltInType.ByteString:
return new Variant(ReadByteStringArray(fieldName));
case BuiltInType.DateTime:
return new Variant(ReadDateTimeArray(fieldName));
case BuiltInType.Guid:
return new Variant(ReadGuidArray(fieldName));
case BuiltInType.NodeId:
return new Variant(ReadNodeIdArray(fieldName));
case BuiltInType.ExpandedNodeId:
return new Variant(ReadExpandedNodeIdArray(fieldName));
case BuiltInType.QualifiedName:
return new Variant(ReadQualifiedNameArray(fieldName));
case BuiltInType.LocalizedText:
return new Variant(ReadLocalizedTextArray(fieldName));
case BuiltInType.StatusCode:
return new Variant(ReadStatusCodeArray(fieldName));
case BuiltInType.XmlElement:
return new Variant(ReadXmlElementArray(fieldName));
case BuiltInType.ExtensionObject:
return new Variant(ReadExtensionObjectArray(fieldName));
case BuiltInType.Variant:
return new Variant(ReadVariantArray(fieldName));
case BuiltInType.DataValue:
return new Variant(ReadDataValueArray(fieldName));
case BuiltInType.DiagnosticInfo:
case BuiltInType.Null:
case BuiltInType.Number:
case BuiltInType.Integer:
case BuiltInType.UInteger:
case BuiltInType.Enumeration:
return Variant.Null;
default:
throw ServiceResultException.Unexpected($"Unexpected BuiltInType {type}");
}
}
/// <summary>
/// Reads the content of an Array from json stream
/// </summary>
private List<object> ReadArray()
{
CheckAndIncrementNestingLevel();
try
{
var elements = new List<object>();
while (m_reader.Read() && m_reader.TokenType != JsonToken.EndArray)
{
switch (m_reader.TokenType)
{
case JsonToken.Comment:
break;
case JsonToken.Null:
elements.Add(JTokenNullObject.Array);
break;
case JsonToken.Date:
case JsonToken.Boolean:
case JsonToken.Integer:
case JsonToken.Float:
case JsonToken.String:
elements.Add(m_reader.Value!);
break;
case JsonToken.StartArray:
elements.Add(ReadArray());
break;
case JsonToken.StartObject:
elements.Add(ReadObject());
break;
case JsonToken.None:
case JsonToken.StartConstructor:
case JsonToken.PropertyName:
case JsonToken.Raw:
case JsonToken.Undefined:
case JsonToken.EndObject:
case JsonToken.EndArray:
case JsonToken.EndConstructor:
case JsonToken.Bytes:
break;
default:
Debug.Fail($"Unexpected token type in array: {m_reader.TokenType}");
break;
}
}
return elements;
}
finally
{
m_nestingLevel--;
}
}
/// <summary>
/// Reads an object from the json stream
/// </summary>
/// <exception cref="ServiceResultException"></exception>
private Dictionary<string, object> ReadObject()
{
var fields = new Dictionary<string, object>();
try
{
while (m_reader.Read() && m_reader.TokenType != JsonToken.EndObject)
{
if (m_reader.TokenType == JsonToken.StartArray)
{
fields[RootArrayName] = ReadArray();
}
else if (m_reader.TokenType == JsonToken.PropertyName)
{
string name = (string)m_reader.Value!;
if (m_reader.Read() && m_reader.TokenType != JsonToken.EndObject)
{
switch (m_reader.TokenType)
{
case JsonToken.Comment:
break;
case JsonToken.Null:
fields[name!] = JTokenNullObject.Object;
break;
case JsonToken.Date:
case JsonToken.Bytes:
case JsonToken.Boolean:
case JsonToken.Integer:
case JsonToken.Float:
case JsonToken.String:
fields[name!] = m_reader.Value!;
break;
case JsonToken.StartArray:
fields[name!] = ReadArray();
break;
case JsonToken.StartObject:
fields[name!] = ReadObject();
break;
case JsonToken.None:
case JsonToken.StartConstructor:
case JsonToken.PropertyName:
case JsonToken.Raw:
case JsonToken.Undefined:
case JsonToken.EndObject:
case JsonToken.EndArray:
case JsonToken.EndConstructor:
break;
default:
Debug.Fail($"Unexpected token type in array: {m_reader.TokenType}");
break;
}
}
}
}
}
catch (JsonReaderException jre)
{
throw ServiceResultException.Create(
StatusCodes.BadDecodingError,
"Error reading JSON object: {0}",
jre.Message);
}
return fields;
}
/// <summary>
/// Read the Matrix part (simple array or array of arrays)
/// </summary>
private void ReadMatrixPart(
string? fieldName,
List<object>? currentArray,
BuiltInType builtInType,
ref List<object> elements,
ref List<int> dimensions,
int level,
Type? systemType,
ExpandedNodeId encodeableTypeId)
{
CheckAndIncrementNestingLevel();
try
{
if (currentArray?.Count > 0)
{
bool hasInnerArray = false;
for (int ii = 0; ii < currentArray.Count; ii++)
{
if (ii == 0 && dimensions.Count <= level)
{
// remember dimension length
dimensions.Add(currentArray.Count);
}
if (currentArray[ii] is List<object>)
{
hasInnerArray = true;
PushArray(fieldName, ii);
ReadMatrixPart(
null,
currentArray[ii] as List<object>,
builtInType,
ref elements,
ref dimensions,
level + 1,
systemType,
encodeableTypeId);
Pop();
}
else
{
break; // do not continue reading array of array
}
}
if (!hasInnerArray)
{
// read array from one dimension
Array? part = ReadArray(
null!,
ValueRanks.OneDimension,
builtInType,
systemType,
encodeableTypeId);
if (part != null && part.Length > 0)
{
// add part elements to final list
foreach (object item in part)
{
elements.Add(item);
}
}
}
}
}
finally
{
m_nestingLevel--;
}
}
/// <summary>
/// Get Default value for NodeId for diferent IdTypes
/// </summary>
/// <returns>new NodeId</returns>
/// <exception cref="ServiceResultException"></exception>
private static NodeId DefaultNodeId(IdType idType, ushort namespaceIndex)
{
switch (idType)
{
case IdType.Numeric:
return new NodeId(0U, namespaceIndex);
case IdType.Opaque:
return new NodeId(ByteString.Empty, namespaceIndex);
case IdType.String:
return new NodeId(string.Empty, namespaceIndex);
case IdType.Guid:
return new NodeId(Guid.Empty, namespaceIndex);
default:
throw ServiceResultException.Unexpected(
"Unexpected IdType value: {0}", idType);
}
}
private void EncodeAsJson(JsonTextWriter writer, object value)
{
try
{
if (value is Dictionary<string, object> map)
{
EncodeAsJson(writer, map);
return;
}
if (value is List<object> list)
{
writer.WriteStartArray();
foreach (object element in list)
{
EncodeAsJson(writer, element);
}
writer.WriteEndArray();
return;
}
writer.WriteValue(value);
}
catch (JsonWriterException jwe)
{
throw ServiceResultException.Create(
StatusCodes.BadDecodingError,
"Unable to encode ExtensionObject Body as Json: {0}",
jwe.Message);
}
}
private void EncodeAsJson(JsonTextWriter writer, Dictionary<string, object> value)
{
writer.WriteStartObject();
foreach (KeyValuePair<string, object> field in value)
{
writer.WritePropertyName(field.Key);
EncodeAsJson(writer, field.Value);
}
writer.WriteEndObject();
}
private bool ReadArrayField(string? fieldName, out List<object> array)
{
array = null!;
if (!ReadField(fieldName, out object token))
{
return false;
}
array = (token as List<object>)!;
if (array == null)
{
return false;
}
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < array.Count)
{
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
}
return true;
}
/// <summary>
/// Safe Convert function which throws a BadDecodingError if unsuccessful.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
private static byte[] SafeConvertFromBase64String(string s)
{
try
{
return Convert.FromBase64String(s);
}
catch (FormatException fe)
{
throw ServiceResultException.Create(
StatusCodes.BadDecodingError,
"Error decoding base64 string: {0}",
fe.Message);
}
}
/// <summary>
/// Test and increment the nesting level.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
private void CheckAndIncrementNestingLevel()
{
if (m_nestingLevel > Context.MaxEncodingNestingLevels)
{
throw ServiceResultException.Create(
StatusCodes.BadEncodingLimitsExceeded,
"Maximum nesting level of {0} was exceeded",
Context.MaxEncodingNestingLevels);
}
m_nestingLevel++;
}
}
}