mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-27 04:59:55 +08:00
# 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.
3684 lines
122 KiB
C#
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++;
|
|
}
|
|
}
|
|
}
|