mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-19 07:02:16 +08:00
eb40a59566
Drive the solution to zero warnings / zero errors with analyzers on, promote the consistently-fixed rules to error, and lock in coverage on the only line of new logic.
3902 lines
127 KiB
C#
3902 lines
127 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.Globalization;
|
|
using System.IO;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using Microsoft.Extensions.Logging;
|
|
#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER
|
|
using System.Buffers;
|
|
#endif
|
|
#pragma warning disable CS0618 // Type or member is obsolete
|
|
|
|
namespace Opc.Ua.PubSub.Encoding
|
|
{
|
|
/// <summary>
|
|
/// Writes objects to a JSON stream.
|
|
/// </summary>
|
|
internal class PubSubJsonEncoder : IEncoder
|
|
{
|
|
private const int kStreamWriterBufferSize = 1024;
|
|
private const string kQuotationColon = "\":";
|
|
private const char kComma = ',';
|
|
private const char kQuotation = '\"';
|
|
private const char kBackslash = '\\';
|
|
private const char kLeftCurlyBrace = '{';
|
|
private const char kRightCurlyBrace = '}';
|
|
private const char kLeftSquareBracket = '[';
|
|
private const char kRightSquareBracket = ']';
|
|
private static readonly UTF8Encoding s_utf8Encoding = new(false);
|
|
private const string kNull = "null";
|
|
private Stream? m_stream;
|
|
private MemoryStream? m_memoryStream;
|
|
private StreamWriter m_writer = null!;
|
|
private readonly Stack<string> m_namespaces = [];
|
|
private bool m_commaRequired;
|
|
private bool m_inVariantWithEncoding;
|
|
private ushort[]? m_namespaceMappings;
|
|
private ushort[]? m_serverMappings;
|
|
private uint m_nestingLevel;
|
|
private readonly bool m_topLevelIsArray;
|
|
private readonly ILogger m_logger = null!;
|
|
private bool m_levelOneSkipped;
|
|
private bool m_dontWriteClosing;
|
|
private readonly bool m_leaveOpen;
|
|
private bool m_forceNamespaceUri;
|
|
private bool m_forceNamespaceUriForIndex1;
|
|
private bool m_includeDefaultNumberValues;
|
|
private bool m_includeDefaultValues;
|
|
private bool m_encodeNodeIdAsString;
|
|
|
|
[Flags]
|
|
private enum EscapeOptions
|
|
{
|
|
None = 0,
|
|
Quotes = 1,
|
|
NoValueEscape = 2,
|
|
NoFieldNameEscape = 4
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the object with default values.
|
|
/// Selects the reversible or non reversible encoding.
|
|
/// </summary>
|
|
public PubSubJsonEncoder(IServiceMessageContext context, bool useReversibleEncoding)
|
|
: this(
|
|
context,
|
|
useReversibleEncoding
|
|
? PubSubJsonEncoding.Reversible
|
|
: PubSubJsonEncoding.NonReversible,
|
|
false,
|
|
null,
|
|
false)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the object with default values.
|
|
/// Selects the reversible or non reversible encoding.
|
|
/// </summary>
|
|
public PubSubJsonEncoder(
|
|
IServiceMessageContext context,
|
|
bool useReversibleEncoding,
|
|
bool topLevelIsArray = false,
|
|
Stream? stream = null,
|
|
bool leaveOpen = false,
|
|
int streamSize = kStreamWriterBufferSize)
|
|
: this(
|
|
context,
|
|
useReversibleEncoding
|
|
? PubSubJsonEncoding.Reversible
|
|
: PubSubJsonEncoding.NonReversible,
|
|
topLevelIsArray,
|
|
stream,
|
|
leaveOpen,
|
|
streamSize)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the object with default values.
|
|
/// Selects the reversible or non reversible encoding.
|
|
/// </summary>
|
|
public PubSubJsonEncoder(
|
|
IServiceMessageContext context,
|
|
bool useReversibleEncoding,
|
|
StreamWriter streamWriter,
|
|
bool topLevelIsArray = false)
|
|
: this(
|
|
context,
|
|
useReversibleEncoding
|
|
? PubSubJsonEncoding.Reversible
|
|
: PubSubJsonEncoding.NonReversible,
|
|
streamWriter,
|
|
topLevelIsArray)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the object with default values.
|
|
/// </summary>
|
|
public PubSubJsonEncoder(
|
|
IServiceMessageContext context,
|
|
PubSubJsonEncoding encoding,
|
|
bool topLevelIsArray = false,
|
|
Stream? stream = null,
|
|
bool leaveOpen = false,
|
|
int streamSize = kStreamWriterBufferSize)
|
|
: this(encoding)
|
|
{
|
|
Context = context;
|
|
m_logger = context.Telemetry.CreateLogger<PubSubJsonEncoder>();
|
|
m_stream = stream;
|
|
m_leaveOpen = leaveOpen;
|
|
m_topLevelIsArray = topLevelIsArray;
|
|
|
|
if (m_stream == null)
|
|
{
|
|
m_memoryStream = new MemoryStream();
|
|
m_writer = new StreamWriter(m_memoryStream, s_utf8Encoding, streamSize, false);
|
|
m_leaveOpen = false;
|
|
}
|
|
else
|
|
{
|
|
m_writer = new StreamWriter(m_stream, s_utf8Encoding, streamSize, m_leaveOpen);
|
|
}
|
|
|
|
InitializeWriter();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the object with default values.
|
|
/// </summary>
|
|
public PubSubJsonEncoder(
|
|
IServiceMessageContext context,
|
|
PubSubJsonEncoding encoding,
|
|
StreamWriter writer,
|
|
bool topLevelIsArray = false)
|
|
: this(encoding)
|
|
{
|
|
Context = context;
|
|
m_logger = context.Telemetry.CreateLogger<PubSubJsonEncoder>();
|
|
m_writer = writer;
|
|
m_topLevelIsArray = topLevelIsArray;
|
|
|
|
if (m_writer == null)
|
|
{
|
|
m_stream = new MemoryStream();
|
|
m_writer = new StreamWriter(m_stream, s_utf8Encoding, kStreamWriterBufferSize);
|
|
}
|
|
|
|
InitializeWriter();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets default values.
|
|
/// </summary>
|
|
private PubSubJsonEncoder(PubSubJsonEncoding encoding)
|
|
{
|
|
// defaults for JSON encoding
|
|
EncodingToUse = encoding;
|
|
if (encoding is PubSubJsonEncoding.Reversible or PubSubJsonEncoding.NonReversible)
|
|
{
|
|
// defaults for reversible and non reversible JSON encoding
|
|
// -- encode namespace index for reversible encoding / uri for non reversible
|
|
// -- do not include default values for reversible encoding
|
|
// -- include default values for non reversible encoding
|
|
m_forceNamespaceUri =
|
|
m_forceNamespaceUriForIndex1 =
|
|
m_includeDefaultValues =
|
|
encoding == PubSubJsonEncoding.NonReversible;
|
|
m_includeDefaultNumberValues = true;
|
|
m_encodeNodeIdAsString = false;
|
|
}
|
|
else
|
|
{
|
|
// defaults for compact and verbose JSON encoding, properties throw exception if modified
|
|
m_forceNamespaceUri = true;
|
|
m_forceNamespaceUriForIndex1 = true;
|
|
m_includeDefaultValues = encoding == PubSubJsonEncoding.Verbose;
|
|
m_includeDefaultNumberValues = encoding == PubSubJsonEncoding.Verbose;
|
|
m_encodeNodeIdAsString = true;
|
|
}
|
|
m_inVariantWithEncoding = IncludeDefaultValues;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize Writer.
|
|
/// </summary>
|
|
private void InitializeWriter()
|
|
{
|
|
if (m_topLevelIsArray)
|
|
{
|
|
m_writer.Write(kLeftSquareBracket);
|
|
}
|
|
else
|
|
{
|
|
m_writer.Write(kLeftCurlyBrace);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encodes a message in a stream.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"><paramref name="message"/> is <c>null</c>.</exception>
|
|
/// <exception cref="ArgumentNullException"><paramref name="buffer"/> is <c>null</c>.</exception>
|
|
/// <exception cref="ArgumentNullException"><paramref name="context"/> is <c>null</c>.</exception>
|
|
public static ArraySegment<byte> EncodeMessage(
|
|
IEncodeable message,
|
|
byte[] buffer,
|
|
IServiceMessageContext context)
|
|
{
|
|
if (message == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(message));
|
|
}
|
|
|
|
if (buffer == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(buffer));
|
|
}
|
|
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
using var stream = new MemoryStream(buffer, true);
|
|
using var encoder = new PubSubJsonEncoder(context, true, false, stream);
|
|
// encode message
|
|
encoder.EncodeMessage(message, message.TypeId);
|
|
int length = encoder.Close();
|
|
|
|
return new ArraySegment<byte>(buffer, 0, length);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void EncodeMessage<T>(T message, ExpandedNodeId encodeableTypeId)
|
|
where T : IEncodeable
|
|
{
|
|
if (EqualityComparer<T>.Default.Equals(message, default!))
|
|
{
|
|
throw new ArgumentNullException(nameof(message));
|
|
}
|
|
|
|
// convert the namespace uri to an index.
|
|
var typeId = ExpandedNodeId.ToNodeId(encodeableTypeId, Context.NamespaceUris);
|
|
|
|
// write the type id.
|
|
WriteNodeId("TypeId", typeId);
|
|
|
|
// write the message.
|
|
WriteEncodeable("Body", message, message.GetType());
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void EncodeMessage<T>(T message) where T : IEncodeable, new()
|
|
{
|
|
if (EqualityComparer<T>.Default.Equals(message, default!))
|
|
{
|
|
throw new ArgumentNullException(nameof(message));
|
|
}
|
|
|
|
// convert the namespace uri to an index.
|
|
var typeId = ExpandedNodeId.ToNodeId(message.TypeId, Context.NamespaceUris);
|
|
|
|
// write the type id.
|
|
WriteNodeId("TypeId", typeId);
|
|
|
|
// write the message.
|
|
WriteEncodeable("Body", message, message.GetType());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the tables used to map namespace and server uris during encoding.
|
|
/// </summary>
|
|
/// <param name="namespaceUris">The namespaces URIs referenced by the data being encoded.</param>
|
|
/// <param name="serverUris">The server URIs referenced by the data being encoded.</param>
|
|
public void SetMappingTables(NamespaceTable namespaceUris, StringTable serverUris)
|
|
{
|
|
m_namespaceMappings = null;
|
|
|
|
if (namespaceUris != null && Context.NamespaceUris != null)
|
|
{
|
|
m_namespaceMappings = namespaceUris.CreateMapping(Context.NamespaceUris, false);
|
|
}
|
|
|
|
m_serverMappings = null;
|
|
|
|
if (serverUris != null && Context.ServerUris != null)
|
|
{
|
|
m_serverMappings = serverUris.CreateMapping(Context.ServerUris, false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Completes writing and returns the JSON text.
|
|
/// </summary>
|
|
/// <exception cref="NotSupportedException">The underlying stream is not a <see cref="MemoryStream"/>.</exception>
|
|
public string CloseAndReturnText()
|
|
{
|
|
try
|
|
{
|
|
InternalClose(false);
|
|
if (m_memoryStream == null)
|
|
{
|
|
if (m_stream is MemoryStream memoryStream)
|
|
{
|
|
return System.Text.Encoding.UTF8.GetString(memoryStream.ToArray());
|
|
}
|
|
throw new NotSupportedException(
|
|
"Cannot get text from external stream. Use Close or MemoryStream instead.");
|
|
}
|
|
return System.Text.Encoding.UTF8.GetString(m_memoryStream.ToArray());
|
|
}
|
|
finally
|
|
{
|
|
m_writer?.Dispose();
|
|
m_writer = null!;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Completes writing and returns the text length.
|
|
/// The StreamWriter is disposed.
|
|
/// </summary>
|
|
public int Close()
|
|
{
|
|
return InternalClose(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Frees any unmanaged resources.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// An overrideable version of the Dispose.
|
|
/// </summary>
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
if (m_writer != null)
|
|
{
|
|
InternalClose(true);
|
|
m_writer = null!;
|
|
}
|
|
|
|
if (!m_leaveOpen)
|
|
{
|
|
m_memoryStream?.Dispose();
|
|
m_stream?.Dispose();
|
|
m_memoryStream = null;
|
|
m_stream = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public PubSubJsonEncoding EncodingToUse { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public bool SuppressArtifacts { get; set; }
|
|
|
|
/// <inheritdoc/>
|
|
public void PushStructure(string? fieldName)
|
|
{
|
|
m_nestingLevel++;
|
|
|
|
if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(fieldName))
|
|
{
|
|
EscapeString(fieldName);
|
|
m_writer.Write(kQuotationColon);
|
|
}
|
|
else if (!m_commaRequired)
|
|
{
|
|
if (m_nestingLevel == 1 && !m_topLevelIsArray)
|
|
{
|
|
m_levelOneSkipped = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
m_commaRequired = false;
|
|
m_writer.Write(kLeftCurlyBrace);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void PushArray(string? fieldName)
|
|
{
|
|
m_nestingLevel++;
|
|
|
|
if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(fieldName))
|
|
{
|
|
EscapeString(fieldName);
|
|
m_writer.Write(kQuotationColon);
|
|
}
|
|
else if (!m_commaRequired)
|
|
{
|
|
if (m_nestingLevel == 1 && !m_topLevelIsArray)
|
|
{
|
|
m_levelOneSkipped = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
m_commaRequired = false;
|
|
m_writer.Write(kLeftSquareBracket);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void PopStructure()
|
|
{
|
|
if (m_nestingLevel > 1 ||
|
|
m_topLevelIsArray ||
|
|
(m_nestingLevel == 1 && !m_levelOneSkipped))
|
|
{
|
|
m_writer.Write(kRightCurlyBrace);
|
|
m_commaRequired = true;
|
|
}
|
|
|
|
m_nestingLevel--;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void PopArray()
|
|
{
|
|
if (m_nestingLevel > 1 ||
|
|
m_topLevelIsArray ||
|
|
(m_nestingLevel == 1 && !m_levelOneSkipped))
|
|
{
|
|
m_writer.Write(kRightSquareBracket);
|
|
m_commaRequired = true;
|
|
}
|
|
|
|
m_nestingLevel--;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
[Obsolete(
|
|
"Non/Reversible encoding is deprecated. Use UsingAlternateEncoding instead to support new encoding types."
|
|
)]
|
|
public void UsingReversibleEncoding<T>(
|
|
Action<string, T> action,
|
|
string fieldName,
|
|
T value,
|
|
bool useReversibleEncoding)
|
|
{
|
|
PubSubJsonEncoding currentValue = EncodingToUse;
|
|
try
|
|
{
|
|
EncodingToUse = useReversibleEncoding
|
|
? PubSubJsonEncoding.Reversible
|
|
: PubSubJsonEncoding.NonReversible;
|
|
action(fieldName, value);
|
|
}
|
|
finally
|
|
{
|
|
EncodingToUse = currentValue;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void UsingAlternateEncoding<T>(
|
|
Action<string, T> action,
|
|
string fieldName,
|
|
T value,
|
|
PubSubJsonEncoding useEncodingType)
|
|
{
|
|
PubSubJsonEncoding currentValue = EncodingToUse;
|
|
try
|
|
{
|
|
EncodingToUse = useEncodingType;
|
|
action(fieldName, value);
|
|
}
|
|
finally
|
|
{
|
|
EncodingToUse = currentValue;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteSwitchField(uint switchField, out string? fieldName)
|
|
{
|
|
fieldName = null;
|
|
|
|
switch (EncodingToUse)
|
|
{
|
|
case PubSubJsonEncoding.Compact:
|
|
if (SuppressArtifacts)
|
|
{
|
|
return;
|
|
}
|
|
break;
|
|
case PubSubJsonEncoding.Reversible:
|
|
fieldName = "Value";
|
|
break;
|
|
case PubSubJsonEncoding.Verbose:
|
|
case PubSubJsonEncoding.NonReversible:
|
|
return;
|
|
default:
|
|
throw ServiceResultException.Unexpected(
|
|
$"Unexpected Encoding type {EncodingToUse}");
|
|
}
|
|
|
|
WriteUInt32("SwitchField", switchField);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteEncodingMask(uint encodingMask)
|
|
{
|
|
if ((!SuppressArtifacts && EncodingToUse == PubSubJsonEncoding.Compact) ||
|
|
EncodingToUse == PubSubJsonEncoding.Reversible)
|
|
{
|
|
WriteUInt32("EncodingMask", encodingMask);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public EncodingType EncodingType => EncodingType.Json;
|
|
|
|
/// <inheritdoc/>
|
|
public bool CanOmitFields => true;
|
|
|
|
/// <inheritdoc/>
|
|
public bool UseReversibleEncoding => EncodingToUse != PubSubJsonEncoding.NonReversible;
|
|
|
|
/// <summary>
|
|
/// The message context associated with the encoder.
|
|
/// </summary>
|
|
public IServiceMessageContext Context { get; } = null!;
|
|
|
|
/// <summary>
|
|
/// The Json encoder to encoder namespace URI instead of
|
|
/// namespace Index in NodeIds.
|
|
/// </summary>
|
|
public bool ForceNamespaceUri
|
|
{
|
|
get => m_forceNamespaceUri;
|
|
set => m_forceNamespaceUri = ThrowIfCompactOrVerbose(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The Json encoder to encode namespace URI for all
|
|
/// namespaces
|
|
/// </summary>
|
|
public bool ForceNamespaceUriForIndex1
|
|
{
|
|
get => m_forceNamespaceUriForIndex1;
|
|
set => m_forceNamespaceUriForIndex1 = ThrowIfCompactOrVerbose(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The Json encoder default value option.
|
|
/// </summary>
|
|
public bool IncludeDefaultValues
|
|
{
|
|
get => m_includeDefaultValues;
|
|
set => m_includeDefaultValues = ThrowIfCompactOrVerbose(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The Json encoder default value option for numbers.
|
|
/// </summary>
|
|
public bool IncludeDefaultNumberValues
|
|
{
|
|
get => m_includeDefaultNumberValues || m_includeDefaultValues;
|
|
set => m_includeDefaultNumberValues = ThrowIfCompactOrVerbose(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The Json encoder default encoding for NodeId as string or object.
|
|
/// </summary>
|
|
public bool EncodeNodeIdAsString
|
|
{
|
|
get => m_encodeNodeIdAsString;
|
|
set => m_encodeNodeIdAsString = ThrowIfCompactOrVerbose(value);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void PushNamespace(string namespaceUri)
|
|
{
|
|
m_namespaces.Push(namespaceUri);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void PopNamespace()
|
|
{
|
|
m_namespaces.Pop();
|
|
}
|
|
|
|
private static readonly char[] s_specialChars
|
|
= [kQuotation, kBackslash, '\n', '\r', '\t', '\b', '\f'];
|
|
|
|
private static readonly char[] s_substitution
|
|
= [kQuotation, kBackslash, 'n', 'r', 't', 'b', 'f'];
|
|
|
|
#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER
|
|
/// <summary>
|
|
/// Using a span to escape the string, write strings to stream writer if possible.
|
|
/// </summary>
|
|
private void EscapeString(ReadOnlySpan<char> value)
|
|
{
|
|
int lastOffset = 0;
|
|
|
|
m_writer.Write(kQuotation);
|
|
|
|
for (int i = 0; i < value.Length; i++)
|
|
{
|
|
bool found = false;
|
|
char ch = value[i];
|
|
|
|
for (int ii = 0; ii < s_specialChars.Length; ii++)
|
|
{
|
|
if (s_specialChars[ii] == ch)
|
|
{
|
|
WriteSpan(ref lastOffset, value, i);
|
|
m_writer.Write('\\');
|
|
m_writer.Write(s_substitution[ii]);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found && ch < 32)
|
|
{
|
|
WriteSpan(ref lastOffset, value, i);
|
|
m_writer.Write('\\');
|
|
m_writer.Write('u');
|
|
m_writer.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
|
|
}
|
|
}
|
|
|
|
if (lastOffset == 0)
|
|
{
|
|
m_writer.Write(value);
|
|
}
|
|
else
|
|
{
|
|
WriteSpan(ref lastOffset, value, value.Length);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void WriteSpan(ref int lastOffset, ReadOnlySpan<char> valueSpan, int index)
|
|
{
|
|
if (lastOffset < index - 2)
|
|
{
|
|
m_writer.Write(valueSpan[lastOffset..index]);
|
|
}
|
|
else
|
|
{
|
|
while (lastOffset < index)
|
|
{
|
|
m_writer.Write(valueSpan[lastOffset++]);
|
|
}
|
|
}
|
|
lastOffset = index + 1;
|
|
}
|
|
#else
|
|
/// <summary>
|
|
/// Escapes a string and writes it to the stream.
|
|
/// </summary>
|
|
private void EscapeString(string? value)
|
|
{
|
|
m_writer.Write(kQuotation);
|
|
|
|
foreach (char ch in value!)
|
|
{
|
|
bool found = false;
|
|
|
|
for (int ii = 0; ii < s_specialChars.Length; ii++)
|
|
{
|
|
if (s_specialChars[ii] == ch)
|
|
{
|
|
m_writer.Write(kBackslash);
|
|
m_writer.Write(s_substitution[ii]);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
if (ch < 32)
|
|
{
|
|
m_writer.Write(kBackslash);
|
|
m_writer.Write('u');
|
|
m_writer.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
|
|
continue;
|
|
}
|
|
m_writer.Write(ch);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
private void WriteSimpleFieldNull(string? fieldName)
|
|
{
|
|
if (string.IsNullOrEmpty(fieldName))
|
|
{
|
|
if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
m_writer.Write(kNull);
|
|
|
|
m_commaRequired = true;
|
|
}
|
|
}
|
|
|
|
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
|
|
private void WriteSimpleField(
|
|
string? fieldName,
|
|
string? value,
|
|
EscapeOptions options = EscapeOptions.None)
|
|
{
|
|
// unlike Span<byte>, Span<char> can not become null, handle the case here
|
|
if (value == null)
|
|
{
|
|
WriteSimpleFieldNull(fieldName);
|
|
return;
|
|
}
|
|
|
|
WriteSimpleFieldAsSpan(fieldName, value.AsSpan(), options);
|
|
}
|
|
|
|
private void WriteSimpleFieldAsSpan(
|
|
string? fieldName,
|
|
ReadOnlySpan<char> value,
|
|
EscapeOptions options)
|
|
{
|
|
if (!string.IsNullOrEmpty(fieldName))
|
|
{
|
|
if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
if ((options & EscapeOptions.NoFieldNameEscape) == EscapeOptions.NoFieldNameEscape)
|
|
{
|
|
m_writer.Write(kQuotation);
|
|
m_writer.Write(fieldName);
|
|
}
|
|
else
|
|
{
|
|
EscapeString(fieldName);
|
|
}
|
|
m_writer.Write(kQuotationColon);
|
|
}
|
|
else if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
if ((options & EscapeOptions.Quotes) == EscapeOptions.Quotes)
|
|
{
|
|
if ((options & EscapeOptions.NoValueEscape) == EscapeOptions.NoValueEscape)
|
|
{
|
|
m_writer.Write(kQuotation);
|
|
m_writer.Write(value);
|
|
}
|
|
else
|
|
{
|
|
EscapeString(value);
|
|
}
|
|
m_writer.Write(kQuotation);
|
|
}
|
|
else
|
|
{
|
|
m_writer.Write(value);
|
|
}
|
|
|
|
m_commaRequired = true;
|
|
}
|
|
#else
|
|
private void WriteSimpleField(
|
|
string? fieldName,
|
|
string? value,
|
|
EscapeOptions options = EscapeOptions.None)
|
|
{
|
|
if (!string.IsNullOrEmpty(fieldName))
|
|
{
|
|
if (value == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
if ((options & EscapeOptions.NoFieldNameEscape) == EscapeOptions.NoFieldNameEscape)
|
|
{
|
|
m_writer.Write(kQuotation);
|
|
m_writer.Write(fieldName);
|
|
}
|
|
else
|
|
{
|
|
EscapeString(fieldName);
|
|
}
|
|
m_writer.Write(kQuotationColon);
|
|
}
|
|
else if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
if (value != null)
|
|
{
|
|
if ((options & EscapeOptions.Quotes) == EscapeOptions.Quotes)
|
|
{
|
|
if ((options & EscapeOptions.NoValueEscape) == EscapeOptions.NoValueEscape)
|
|
{
|
|
m_writer.Write(kQuotation);
|
|
m_writer.Write(value);
|
|
}
|
|
else
|
|
{
|
|
EscapeString(value);
|
|
}
|
|
m_writer.Write(kQuotation);
|
|
}
|
|
else
|
|
{
|
|
m_writer.Write(value);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_writer.Write(kNull);
|
|
}
|
|
|
|
m_commaRequired = true;
|
|
}
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Writes a boolean to the stream.
|
|
/// </summary>
|
|
public void WriteBoolean(string? fieldName, bool value)
|
|
{
|
|
if (fieldName != null && !IncludeDefaultNumberValues && !value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (value)
|
|
{
|
|
WriteSimpleField(fieldName, "true");
|
|
}
|
|
else
|
|
{
|
|
WriteSimpleField(fieldName, "false");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a sbyte to the stream.
|
|
/// </summary>
|
|
public void WriteSByte(string? fieldName, sbyte value)
|
|
{
|
|
if (fieldName != null && !IncludeDefaultNumberValues && value == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
WriteSimpleField(fieldName, value.ToString(CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a byte to the stream.
|
|
/// </summary>
|
|
public void WriteByte(string? fieldName, byte value)
|
|
{
|
|
if (fieldName != null && !IncludeDefaultNumberValues && value == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
WriteSimpleField(fieldName, value.ToString(CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a short to the stream.
|
|
/// </summary>
|
|
public void WriteInt16(string? fieldName, short value)
|
|
{
|
|
if (fieldName != null && !IncludeDefaultNumberValues && value == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
WriteSimpleField(fieldName, value.ToString(CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a ushort to the stream.
|
|
/// </summary>
|
|
public void WriteUInt16(string? fieldName, ushort value)
|
|
{
|
|
if (fieldName != null && !IncludeDefaultNumberValues && value == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
WriteSimpleField(fieldName, value.ToString(CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an int to the stream.
|
|
/// </summary>
|
|
public void WriteInt32(string? fieldName, int value)
|
|
{
|
|
if (fieldName != null && !IncludeDefaultNumberValues && value == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
WriteSimpleField(fieldName, value.ToString(CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a uint to the stream.
|
|
/// </summary>
|
|
public void WriteUInt32(string? fieldName, uint value)
|
|
{
|
|
if (fieldName != null && !IncludeDefaultNumberValues && value == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
WriteSimpleField(fieldName, value.ToString(CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a long to the stream.
|
|
/// </summary>
|
|
public void WriteInt64(string? fieldName, long value)
|
|
{
|
|
if (fieldName != null && !IncludeDefaultNumberValues && value == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
WriteSimpleField(
|
|
fieldName,
|
|
value.ToString(CultureInfo.InvariantCulture),
|
|
EscapeOptions.Quotes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a ulong to the stream.
|
|
/// </summary>
|
|
public void WriteUInt64(string? fieldName, ulong value)
|
|
{
|
|
if (fieldName != null && !IncludeDefaultNumberValues && value == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
WriteSimpleField(
|
|
fieldName,
|
|
value.ToString(CultureInfo.InvariantCulture),
|
|
EscapeOptions.Quotes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a float to the stream.
|
|
/// </summary>
|
|
public void WriteFloat(string? fieldName, float value)
|
|
{
|
|
if (fieldName != null &&
|
|
!IncludeDefaultNumberValues &&
|
|
(value > -float.Epsilon) &&
|
|
(value < float.Epsilon))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (float.IsNaN(value))
|
|
{
|
|
WriteSimpleField(fieldName, "\"NaN\"");
|
|
}
|
|
else if (float.IsPositiveInfinity(value))
|
|
{
|
|
WriteSimpleField(fieldName, "\"Infinity\"");
|
|
}
|
|
else if (float.IsNegativeInfinity(value))
|
|
{
|
|
WriteSimpleField(fieldName, "\"-Infinity\"");
|
|
}
|
|
else
|
|
{
|
|
WriteSimpleField(fieldName, value.ToString("R", CultureInfo.InvariantCulture));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a double to the stream.
|
|
/// </summary>
|
|
public void WriteDouble(string? fieldName, double value)
|
|
{
|
|
if (fieldName != null &&
|
|
!IncludeDefaultNumberValues &&
|
|
(value > -double.Epsilon) &&
|
|
(value < double.Epsilon))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (double.IsNaN(value))
|
|
{
|
|
WriteSimpleField(fieldName, "\"NaN\"");
|
|
}
|
|
else if (double.IsPositiveInfinity(value))
|
|
{
|
|
WriteSimpleField(fieldName, "\"Infinity\"");
|
|
}
|
|
else if (double.IsNegativeInfinity(value))
|
|
{
|
|
WriteSimpleField(fieldName, "\"-Infinity\"");
|
|
}
|
|
else
|
|
{
|
|
WriteSimpleField(fieldName, value.ToString("R", CultureInfo.InvariantCulture));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a string to the stream.
|
|
/// </summary>
|
|
public void WriteString(string? fieldName, string? value)
|
|
{
|
|
if (fieldName != null && !IncludeDefaultValues && value == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
WriteSimpleField(fieldName, value, EscapeOptions.Quotes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a UTC date/time to the stream.
|
|
/// </summary>
|
|
public void WriteDateTime(string? fieldName, DateTimeUtc value)
|
|
{
|
|
WriteDateTime(fieldName, value, EscapeOptions.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a GUID to the stream.
|
|
/// </summary>
|
|
public void WriteGuid(string? fieldName, Uuid value)
|
|
{
|
|
if (fieldName != null && !IncludeDefaultValues && value == Uuid.Empty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
WriteSimpleField(
|
|
fieldName,
|
|
value.ToString(),
|
|
EscapeOptions.Quotes | EscapeOptions.NoValueEscape);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteByteString(string? fieldName, ByteString value)
|
|
{
|
|
WriteByteString(fieldName, value.ToArray(), 0, value.Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a byte string to the stream with a given index and count.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException">The byte string length exceeds the maximum allowed.</exception>
|
|
public void WriteByteString(string? fieldName, byte[] value, int index, int count)
|
|
{
|
|
if (fieldName != null && !IncludeDefaultValues && value == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (value == null)
|
|
{
|
|
WriteSimpleField(fieldName, kNull, EscapeOptions.NoValueEscape);
|
|
return;
|
|
}
|
|
|
|
// check the length.
|
|
if (Context.MaxByteStringLength > 0 && Context.MaxByteStringLength < count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
WriteSimpleField(
|
|
fieldName,
|
|
Convert.ToBase64String(value, index, count),
|
|
EscapeOptions.Quotes | EscapeOptions.NoValueEscape);
|
|
}
|
|
|
|
#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER
|
|
/// <summary>
|
|
/// Writes a byte string to the stream.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException">The byte string length exceeds the maximum allowed or encoding fails.</exception>
|
|
public void WriteByteString(string? fieldName, ReadOnlySpan<byte> value)
|
|
{
|
|
// == compares memory reference, comparing to empty means we compare to the default
|
|
// If null array is converted to span the span is default
|
|
bool isNull = value == ReadOnlySpan<byte>.Empty;
|
|
|
|
if (fieldName != null && !IncludeDefaultValues && isNull)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (isNull)
|
|
{
|
|
WriteSimpleField(fieldName, kNull, EscapeOptions.NoValueEscape);
|
|
return;
|
|
}
|
|
|
|
// check the length.
|
|
if (Context.MaxByteStringLength > 0 && Context.MaxByteStringLength < value.Length)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
if (value.Length > 0)
|
|
{
|
|
const int maxStackLimit = 1024;
|
|
int length = (value.Length + 2) / 3 * 4;
|
|
char[]? arrayPool = null;
|
|
Span<char> chars =
|
|
length <= maxStackLimit
|
|
? stackalloc char[length]
|
|
: (arrayPool = ArrayPool<char>.Shared.Rent(length)).AsSpan(0, length);
|
|
try
|
|
{
|
|
bool success = Convert.TryToBase64Chars(
|
|
value,
|
|
chars,
|
|
out int charsWritten,
|
|
Base64FormattingOptions.None);
|
|
if (success)
|
|
{
|
|
WriteSimpleFieldAsSpan(
|
|
fieldName,
|
|
chars[..charsWritten],
|
|
EscapeOptions.Quotes | EscapeOptions.NoValueEscape);
|
|
return;
|
|
}
|
|
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadEncodingError,
|
|
"Failed to convert ByteString to Base64");
|
|
}
|
|
finally
|
|
{
|
|
if (arrayPool != null)
|
|
{
|
|
ArrayPool<char>.Shared.Return(arrayPool);
|
|
}
|
|
}
|
|
}
|
|
|
|
WriteSimpleField(
|
|
fieldName,
|
|
string.Empty,
|
|
EscapeOptions.Quotes | EscapeOptions.NoValueEscape);
|
|
}
|
|
#endif
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteXmlElement(string? fieldName, XmlElement value)
|
|
{
|
|
if (fieldName != null && !IncludeDefaultValues && value.IsEmpty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (value.IsEmpty)
|
|
{
|
|
WriteSimpleField(fieldName, kNull, EscapeOptions.NoValueEscape);
|
|
return;
|
|
}
|
|
|
|
string? xml = value.OuterXml;
|
|
|
|
int count = xml!.Length;
|
|
|
|
if (Context.MaxStringLength > 0 && Context.MaxStringLength < count)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadEncodingLimitsExceeded,
|
|
"MaxStringLength {0} < {1}",
|
|
Context.MaxStringLength,
|
|
count);
|
|
}
|
|
|
|
WriteSimpleField(fieldName, xml, EscapeOptions.Quotes);
|
|
}
|
|
|
|
private void WriteNamespaceIndex(string? fieldName, ushort namespaceIndex)
|
|
{
|
|
if (namespaceIndex == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ((!UseReversibleEncoding || ForceNamespaceUri) &&
|
|
namespaceIndex > (ForceNamespaceUriForIndex1 ? 0 : 1))
|
|
{
|
|
string? uri = Context.NamespaceUris.GetString(namespaceIndex);
|
|
if (!string.IsNullOrEmpty(uri))
|
|
{
|
|
WriteSimpleField(fieldName, uri, EscapeOptions.Quotes);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (m_namespaceMappings != null && m_namespaceMappings.Length > namespaceIndex)
|
|
{
|
|
namespaceIndex = m_namespaceMappings[namespaceIndex];
|
|
}
|
|
|
|
if (namespaceIndex != 0)
|
|
{
|
|
WriteUInt16(fieldName, namespaceIndex);
|
|
}
|
|
}
|
|
|
|
private void WriteNodeIdContents(NodeId value, string? namespaceUri = null)
|
|
{
|
|
if (value.IdType > IdType.Numeric)
|
|
{
|
|
WriteInt32("IdType", (int)value.IdType);
|
|
}
|
|
if (value.TryGetValue(out uint numericId))
|
|
{
|
|
WriteUInt32("Id", numericId);
|
|
}
|
|
else if (value.TryGetValue(out string stringId))
|
|
{
|
|
WriteString("Id", stringId);
|
|
}
|
|
else if (value.TryGetValue(out Guid guidIdentifier))
|
|
{
|
|
WriteGuid("Id", guidIdentifier);
|
|
}
|
|
else if (value.TryGetValue(out ByteString opaqueId))
|
|
{
|
|
WriteByteString("Id", opaqueId);
|
|
}
|
|
else
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
$"Unexpected Node IdType {value.IdType}");
|
|
}
|
|
if (namespaceUri != null)
|
|
{
|
|
WriteString("Namespace", namespaceUri);
|
|
}
|
|
else
|
|
{
|
|
WriteNamespaceIndex("Namespace", value.NamespaceIndex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an NodeId to the stream.
|
|
/// </summary>
|
|
public void WriteNodeId(string? fieldName, NodeId value)
|
|
{
|
|
bool isNull = value.IsNull;
|
|
|
|
if (fieldName != null && isNull && !IncludeDefaultValues)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_encodeNodeIdAsString)
|
|
{
|
|
WriteSimpleField(
|
|
fieldName,
|
|
isNull ? string.Empty : value.Format(Context, ForceNamespaceUri),
|
|
EscapeOptions.Quotes);
|
|
return;
|
|
}
|
|
|
|
PushStructure(fieldName);
|
|
|
|
if (!isNull)
|
|
{
|
|
ushort namespaceIndex = value.NamespaceIndex;
|
|
if (ForceNamespaceUri && namespaceIndex > (ForceNamespaceUriForIndex1 ? 0 : 1))
|
|
{
|
|
string? namespaceUri = Context.NamespaceUris.GetString(namespaceIndex);
|
|
WriteNodeIdContents(value, namespaceUri);
|
|
}
|
|
else
|
|
{
|
|
WriteNodeIdContents(value);
|
|
}
|
|
}
|
|
|
|
PopStructure();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an ExpandedNodeId to the stream.
|
|
/// </summary>
|
|
public void WriteExpandedNodeId(string? fieldName, ExpandedNodeId value)
|
|
{
|
|
bool isNull = value.IsNull;
|
|
|
|
if (fieldName != null && isNull && !IncludeDefaultValues)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_encodeNodeIdAsString)
|
|
{
|
|
WriteSimpleField(
|
|
fieldName,
|
|
isNull ? string.Empty : value.Format(Context, ForceNamespaceUri),
|
|
EscapeOptions.Quotes);
|
|
return;
|
|
}
|
|
|
|
PushStructure(fieldName);
|
|
|
|
if (!isNull)
|
|
{
|
|
string? namespaceUri = value.NamespaceUri;
|
|
ushort namespaceIndex = value.InnerNodeId.NamespaceIndex;
|
|
if (ForceNamespaceUri &&
|
|
namespaceUri == null &&
|
|
namespaceIndex > (ForceNamespaceUriForIndex1 ? 0 : 1))
|
|
{
|
|
namespaceUri = Context.NamespaceUris.GetString(namespaceIndex);
|
|
}
|
|
WriteNodeIdContents(value.InnerNodeId, namespaceUri);
|
|
|
|
uint serverIndex = value.ServerIndex;
|
|
|
|
if (serverIndex >= 1)
|
|
{
|
|
if (EncodingToUse == PubSubJsonEncoding.NonReversible)
|
|
{
|
|
string? uri = Context.ServerUris.GetString(serverIndex);
|
|
|
|
if (!string.IsNullOrEmpty(uri))
|
|
{
|
|
WriteSimpleField(
|
|
"ServerUri",
|
|
uri,
|
|
EscapeOptions.Quotes | EscapeOptions.NoFieldNameEscape);
|
|
}
|
|
|
|
PopStructure();
|
|
return;
|
|
}
|
|
|
|
if (m_serverMappings != null && m_serverMappings.Length > serverIndex)
|
|
{
|
|
serverIndex = m_serverMappings[serverIndex];
|
|
}
|
|
|
|
if (serverIndex != 0)
|
|
{
|
|
WriteUInt32("ServerUri", serverIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
PopStructure();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an StatusCode to the stream.
|
|
/// </summary>
|
|
public void WriteStatusCode(string? fieldName, StatusCode value)
|
|
{
|
|
WriteStatusCode(fieldName, value, EscapeOptions.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a DiagnosticInfo to the stream.
|
|
/// </summary>
|
|
public void WriteDiagnosticInfo(string? fieldName, DiagnosticInfo? value)
|
|
{
|
|
WriteDiagnosticInfo(fieldName, value, 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an QualifiedName to the stream.
|
|
/// </summary>
|
|
public void WriteQualifiedName(string? fieldName, QualifiedName value)
|
|
{
|
|
bool isNull = value.IsNull;
|
|
|
|
if (fieldName != null && isNull && !IncludeDefaultValues)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_encodeNodeIdAsString)
|
|
{
|
|
WriteSimpleField(
|
|
fieldName,
|
|
isNull ? string.Empty : value.Format(Context, ForceNamespaceUri),
|
|
EscapeOptions.Quotes);
|
|
return;
|
|
}
|
|
|
|
PushStructure(fieldName);
|
|
|
|
if (!isNull)
|
|
{
|
|
WriteString("Name", value.Name);
|
|
WriteNamespaceIndex("Uri", value.NamespaceIndex);
|
|
}
|
|
|
|
PopStructure();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an LocalizedText to the stream.
|
|
/// </summary>
|
|
public void WriteLocalizedText(string? fieldName, LocalizedText value)
|
|
{
|
|
bool isNull = value.IsNullOrEmpty;
|
|
|
|
if (fieldName != null && isNull && !IncludeDefaultValues)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (EncodingToUse == PubSubJsonEncoding.NonReversible)
|
|
{
|
|
WriteSimpleField(
|
|
fieldName,
|
|
isNull ? string.Empty : value.Text,
|
|
EscapeOptions.Quotes);
|
|
return;
|
|
}
|
|
|
|
PushStructure(fieldName);
|
|
|
|
if (!isNull)
|
|
{
|
|
WriteSimpleField(
|
|
"Text",
|
|
value.Text,
|
|
EscapeOptions.Quotes | EscapeOptions.NoFieldNameEscape);
|
|
|
|
if (!string.IsNullOrEmpty(value.Locale))
|
|
{
|
|
WriteSimpleField(
|
|
"Locale",
|
|
value.Locale,
|
|
EscapeOptions.Quotes | EscapeOptions.NoFieldNameEscape);
|
|
}
|
|
}
|
|
|
|
PopStructure();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an Variant to the stream.
|
|
/// </summary>
|
|
public void WriteVariant(string? fieldName, Variant value)
|
|
{
|
|
bool isNull =
|
|
value.TypeInfo.IsUnknown ||
|
|
value.TypeInfo.BuiltInType == BuiltInType.Null ||
|
|
value.IsNull;
|
|
|
|
if (EncodingToUse is PubSubJsonEncoding.Compact or PubSubJsonEncoding.Verbose)
|
|
{
|
|
if (fieldName != null && isNull && EncodingToUse == PubSubJsonEncoding.Compact)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushStructure(fieldName);
|
|
|
|
if (!isNull)
|
|
{
|
|
WriteVariantIntoObject("Value", value);
|
|
}
|
|
|
|
PopStructure();
|
|
return;
|
|
}
|
|
|
|
if (fieldName != null && isNull && !IncludeDefaultValues)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CheckAndIncrementNestingLevel();
|
|
|
|
try
|
|
{
|
|
if (!isNull && EncodingToUse != PubSubJsonEncoding.NonReversible)
|
|
{
|
|
PushStructure(fieldName);
|
|
|
|
// encode enums as int32.
|
|
byte encodingByte = (byte)value.TypeInfo.BuiltInType;
|
|
|
|
if (value.TypeInfo.BuiltInType == BuiltInType.Enumeration)
|
|
{
|
|
encodingByte = (byte)BuiltInType.Int32;
|
|
}
|
|
|
|
if (!SuppressArtifacts)
|
|
{
|
|
WriteByte("Type", encodingByte);
|
|
}
|
|
|
|
fieldName = "Body";
|
|
}
|
|
|
|
if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(fieldName))
|
|
{
|
|
EscapeString(fieldName);
|
|
m_writer.Write(kQuotationColon);
|
|
}
|
|
|
|
WriteVariantContents(value.Value, value.TypeInfo);
|
|
|
|
if (!isNull && EncodingToUse != PubSubJsonEncoding.NonReversible)
|
|
{
|
|
if (value.Value is Matrix matrix)
|
|
{
|
|
WriteInt32Array("Dimensions", matrix.Dimensions);
|
|
}
|
|
|
|
PopStructure();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
m_nestingLevel--;
|
|
}
|
|
}
|
|
|
|
private void WriteVariantIntoObject(string? fieldName, Variant value)
|
|
{
|
|
object? boxed = value.AsBoxedObject();
|
|
if (boxed is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
CheckAndIncrementNestingLevel();
|
|
|
|
bool isNull =
|
|
value.TypeInfo.IsUnknown ||
|
|
value.TypeInfo.BuiltInType == BuiltInType.Null ||
|
|
value.IsNull;
|
|
|
|
if (!isNull)
|
|
{
|
|
byte encodingByte = (byte)value.TypeInfo.BuiltInType;
|
|
|
|
if (value.TypeInfo.BuiltInType == BuiltInType.Enumeration)
|
|
{
|
|
encodingByte = (byte)BuiltInType.Int32;
|
|
}
|
|
|
|
if (!SuppressArtifacts)
|
|
{
|
|
WriteByte("UaType", encodingByte);
|
|
}
|
|
}
|
|
|
|
if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(fieldName))
|
|
{
|
|
EscapeString(fieldName);
|
|
m_writer.Write(kQuotationColon);
|
|
m_commaRequired = false;
|
|
}
|
|
|
|
if (value.Value is Matrix matrix)
|
|
{
|
|
WriteVariantContents(value.Value, value.TypeInfo);
|
|
WriteInt32Array("Dimensions", matrix.Dimensions);
|
|
return;
|
|
}
|
|
|
|
WriteVariantContents(value.Value, value.TypeInfo);
|
|
}
|
|
finally
|
|
{
|
|
m_nestingLevel--;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an DataValue array to the stream.
|
|
/// </summary>
|
|
public void WriteDataValue(string? fieldName, DataValue value)
|
|
{
|
|
PushStructure(fieldName);
|
|
|
|
if (!value.WrappedValue.TypeInfo.IsUnknown &&
|
|
value.WrappedValue.TypeInfo.BuiltInType != BuiltInType.Null)
|
|
{
|
|
if (EncodingToUse is not PubSubJsonEncoding.Compact and not PubSubJsonEncoding.Verbose)
|
|
{
|
|
WriteVariant("Value", value.WrappedValue);
|
|
}
|
|
else
|
|
{
|
|
WriteVariantIntoObject("Value", value.WrappedValue);
|
|
}
|
|
}
|
|
|
|
if (value.StatusCode != StatusCodes.Good)
|
|
{
|
|
WriteStatusCode(
|
|
"StatusCode",
|
|
value.StatusCode,
|
|
EscapeOptions.NoFieldNameEscape);
|
|
}
|
|
|
|
if (value.SourceTimestamp != DateTimeUtc.MinValue)
|
|
{
|
|
WriteDateTime(
|
|
"SourceTimestamp",
|
|
value.SourceTimestamp,
|
|
EscapeOptions.NoFieldNameEscape);
|
|
|
|
if (value.SourcePicoseconds != 0)
|
|
{
|
|
WriteUInt16("SourcePicoseconds", value.SourcePicoseconds);
|
|
}
|
|
}
|
|
|
|
if (value.ServerTimestamp != DateTimeUtc.MinValue)
|
|
{
|
|
WriteDateTime(
|
|
"ServerTimestamp",
|
|
value.ServerTimestamp,
|
|
EscapeOptions.NoFieldNameEscape);
|
|
|
|
if (value.ServerPicoseconds != 0)
|
|
{
|
|
WriteUInt16("ServerPicoseconds", value.ServerPicoseconds);
|
|
}
|
|
}
|
|
|
|
PopStructure();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an ExtensionObject to the stream.
|
|
/// </summary>
|
|
public void WriteExtensionObject(string? fieldName, ExtensionObject value)
|
|
{
|
|
bool isNull = value.IsNull || value.Encoding == ExtensionObjectEncoding.None;
|
|
|
|
if (fieldName != null && isNull && !IncludeDefaultValues)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var encodeable = value.Body as IEncodeable;
|
|
|
|
if (encodeable != null && EncodingToUse == PubSubJsonEncoding.NonReversible)
|
|
{
|
|
// non reversible encoding, only the content of the Body field is encoded.
|
|
if (value.Body is IStructureTypeInfo structureType &&
|
|
structureType.StructureType == StructureType.Union)
|
|
{
|
|
if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(fieldName))
|
|
{
|
|
fieldName = "Value";
|
|
}
|
|
|
|
EscapeString(fieldName);
|
|
m_writer.Write(kQuotationColon);
|
|
encodeable.Encode(this);
|
|
return;
|
|
}
|
|
|
|
PushStructure(fieldName);
|
|
encodeable.Encode(this);
|
|
PopStructure();
|
|
return;
|
|
}
|
|
|
|
PushStructure(fieldName);
|
|
|
|
ExpandedNodeId typeId = !value.TypeId.IsNull
|
|
? value.TypeId
|
|
: encodeable?.TypeId ?? NodeId.Null;
|
|
var localTypeId = ExpandedNodeId.ToNodeId(typeId, Context.NamespaceUris);
|
|
|
|
if (EncodingToUse is PubSubJsonEncoding.Compact or PubSubJsonEncoding.Verbose)
|
|
{
|
|
if (encodeable != null)
|
|
{
|
|
if (!SuppressArtifacts && !localTypeId.IsNull)
|
|
{
|
|
WriteNodeId("UaTypeId", localTypeId);
|
|
}
|
|
|
|
encodeable.Encode(this);
|
|
}
|
|
else if (value.TryGetAsJson(out string? text))
|
|
{
|
|
if (!SuppressArtifacts && !localTypeId.IsNull)
|
|
{
|
|
WriteNodeId("UaTypeId", localTypeId);
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
m_writer.Write(text.Trim()[1..^1]);
|
|
}
|
|
else if (value.Encoding == ExtensionObjectEncoding.Binary)
|
|
{
|
|
if (!SuppressArtifacts && !localTypeId.IsNull)
|
|
{
|
|
WriteNodeId("UaTypeId", localTypeId);
|
|
}
|
|
|
|
WriteByte("UaEncoding", (byte)ExtensionObjectEncoding.Binary);
|
|
WriteByteString("UaBody", value.TryGetAsBinary(out ByteString b) ? b : default);
|
|
}
|
|
else if (value.Encoding == ExtensionObjectEncoding.Xml)
|
|
{
|
|
if (!SuppressArtifacts && !localTypeId.IsNull)
|
|
{
|
|
WriteNodeId("UaTypeId", localTypeId);
|
|
}
|
|
|
|
WriteByte("UaEncoding", (byte)ExtensionObjectEncoding.Xml);
|
|
WriteXmlElement("UaBody", value.TryGetAsXml(out XmlElement x) ? x : default);
|
|
}
|
|
|
|
PopStructure();
|
|
return;
|
|
}
|
|
|
|
WriteNodeId("TypeId", localTypeId);
|
|
|
|
if (encodeable != null)
|
|
{
|
|
WriteEncodeable("Body", encodeable, null!);
|
|
}
|
|
else if (value.TryGetAsJson(out string? text))
|
|
{
|
|
m_writer.Write(text!.Trim()[1..^1]);
|
|
}
|
|
else
|
|
{
|
|
WriteByte("Encoding", (byte)value.Encoding);
|
|
|
|
if (value.Encoding == ExtensionObjectEncoding.Binary)
|
|
{
|
|
WriteByteString("Body", value.TryGetAsBinary(out ByteString b) ? b : default);
|
|
}
|
|
else if (value.Encoding == ExtensionObjectEncoding.Xml)
|
|
{
|
|
WriteXmlElement("Body", value.TryGetAsXml(out XmlElement x) ? x : default);
|
|
}
|
|
else if (value.Encoding == ExtensionObjectEncoding.Json)
|
|
{
|
|
WriteSimpleField("Body", value.TryGetAsJson(out string? j) ? j! : default);
|
|
}
|
|
}
|
|
|
|
PopStructure();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an encodeable object to the stream.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException">The encoding would create invalid JSON or the nesting level is exceeded.</exception>
|
|
public void WriteEncodeable(string? fieldName, IEncodeable value, Type systemType)
|
|
{
|
|
bool isNull = value == null;
|
|
|
|
if (fieldName != null && isNull && !IncludeDefaultValues)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_nestingLevel == 0 &&
|
|
(m_commaRequired || m_topLevelIsArray) &&
|
|
(string.IsNullOrWhiteSpace(fieldName) ^ m_topLevelIsArray))
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadEncodingError,
|
|
"With Array as top level, encodeables with fieldname will create invalid json");
|
|
}
|
|
|
|
if (m_nestingLevel == 0 &&
|
|
!m_commaRequired &&
|
|
string.IsNullOrWhiteSpace(fieldName) &&
|
|
!m_topLevelIsArray)
|
|
{
|
|
m_writer.Flush();
|
|
if (m_writer.BaseStream.Length == 1) //Opening "{"
|
|
{
|
|
m_writer.BaseStream.Seek(0, SeekOrigin.Begin);
|
|
}
|
|
m_dontWriteClosing = true;
|
|
}
|
|
|
|
CheckAndIncrementNestingLevel();
|
|
|
|
try
|
|
{
|
|
PushStructure(fieldName);
|
|
|
|
value?.Encode(this);
|
|
|
|
PopStructure();
|
|
}
|
|
finally
|
|
{
|
|
m_nestingLevel--;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an enumerated value to the stream.
|
|
/// </summary>
|
|
public void WriteEnumerated(string? fieldName, Enum value)
|
|
{
|
|
int numeric = Convert.ToInt32(value, CultureInfo.InvariantCulture);
|
|
string numericString = numeric.ToString(CultureInfo.InvariantCulture);
|
|
|
|
if (EncodingToUse is PubSubJsonEncoding.Reversible or PubSubJsonEncoding.Compact)
|
|
{
|
|
WriteSimpleField(fieldName, numericString);
|
|
}
|
|
else
|
|
{
|
|
string valueString = value.ToString();
|
|
|
|
if (valueString == numericString)
|
|
{
|
|
WriteSimpleField(fieldName, numericString, EscapeOptions.Quotes);
|
|
}
|
|
else
|
|
{
|
|
WriteSimpleField(
|
|
fieldName,
|
|
Utils.Format("{0}_{1}", valueString!, numeric),
|
|
EscapeOptions.Quotes);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an enumerated EnumValue value to the stream.
|
|
/// </summary>
|
|
public void WriteEnumerated(string? fieldName, EnumValue value)
|
|
{
|
|
int numeric = value.Value;
|
|
string numericString = numeric.ToString(CultureInfo.InvariantCulture);
|
|
|
|
if (EncodingToUse is PubSubJsonEncoding.Reversible or PubSubJsonEncoding.Compact)
|
|
{
|
|
WriteSimpleField(fieldName, numericString);
|
|
}
|
|
else
|
|
{
|
|
string? valueString = value.Symbol;
|
|
|
|
if (string.IsNullOrEmpty(valueString) || valueString == numericString)
|
|
{
|
|
WriteSimpleField(fieldName, numericString, EscapeOptions.Quotes);
|
|
}
|
|
else
|
|
{
|
|
WriteSimpleField(
|
|
fieldName,
|
|
Utils.Format("{0}_{1}", valueString!, numeric),
|
|
EscapeOptions.Quotes);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a boolean array to the stream.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException">The array length exceeds the maximum allowed.</exception>
|
|
public void WriteBooleanArray(string? fieldName, ArrayOf<bool> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteBoolean(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteSByteArray(string? fieldName, ArrayOf<sbyte> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteSByte(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteByteArray(string? fieldName, ArrayOf<byte> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteByte(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteInt16Array(string? fieldName, ArrayOf<short> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteInt16(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteUInt16Array(string? fieldName, ArrayOf<ushort> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteUInt16(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteInt32Array(string? fieldName, ArrayOf<int> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteInt32(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteUInt32Array(string? fieldName, ArrayOf<uint> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteUInt32(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteInt64Array(string? fieldName, ArrayOf<long> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteInt64(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteUInt64Array(string? fieldName, ArrayOf<ulong> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteUInt64(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteFloatArray(string? fieldName, ArrayOf<float> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteFloat(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteDoubleArray(string? fieldName, ArrayOf<double> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteDouble(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteStringArray(string? fieldName, ArrayOf<string> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteString(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteDateTimeArray(string? fieldName, ArrayOf<DateTimeUtc> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
if (values[ii] <= DateTimeUtc.MinValue)
|
|
{
|
|
WriteSimpleFieldNull(null);
|
|
}
|
|
else
|
|
{
|
|
WriteDateTime(null, values[ii]);
|
|
}
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteGuidArray(string? fieldName, ArrayOf<Uuid> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteGuid(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteByteStringArray(string? fieldName, ArrayOf<ByteString> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteByteString(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteXmlElementArray(string? fieldName, ArrayOf<XmlElement> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteXmlElement(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteNodeIdArray(string? fieldName, ArrayOf<NodeId> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteNodeId(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteExpandedNodeIdArray(string? fieldName, ArrayOf<ExpandedNodeId> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteExpandedNodeId(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteStatusCodeArray(string? fieldName, ArrayOf<StatusCode> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
if (!UseReversibleEncoding && values[ii] == StatusCodes.Good)
|
|
{
|
|
WriteSimpleFieldNull(null);
|
|
}
|
|
else
|
|
{
|
|
WriteStatusCode(null, values[ii]);
|
|
}
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteDiagnosticInfoArray(string? fieldName, ArrayOf<DiagnosticInfo> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteDiagnosticInfo(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteQualifiedNameArray(string? fieldName, ArrayOf<QualifiedName> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteQualifiedName(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteLocalizedTextArray(string? fieldName, ArrayOf<LocalizedText> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteLocalizedText(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteVariantArray(string? fieldName, ArrayOf<Variant> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
if (values[ii] == Variant.Null)
|
|
{
|
|
WriteSimpleFieldNull(null);
|
|
continue;
|
|
}
|
|
|
|
WriteVariant(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteDataValueArray(string? fieldName, ArrayOf<DataValue> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteDataValue(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteExtensionObjectArray(string? fieldName, ArrayOf<ExtensionObject> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteExtensionObject(null, values[ii]);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an encodeable object array to the stream.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException">The array length exceeds the maximum allowed or the encoding would create invalid JSON.</exception>
|
|
public void WriteEncodeableArray(
|
|
string? fieldName,
|
|
ArrayOf<IEncodeable> values,
|
|
Type? systemType)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(fieldName) && m_nestingLevel == 0 && !m_topLevelIsArray)
|
|
{
|
|
m_writer.Flush();
|
|
if (m_writer.BaseStream.Length == 1) //Opening "{"
|
|
{
|
|
m_writer.BaseStream.Seek(0, SeekOrigin.Begin);
|
|
}
|
|
|
|
m_nestingLevel++;
|
|
PushArray(fieldName);
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteEncodeable(null, values[ii], systemType!);
|
|
}
|
|
|
|
PopArray();
|
|
m_dontWriteClosing = true;
|
|
m_nestingLevel--;
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(fieldName) &&
|
|
m_nestingLevel == 0 &&
|
|
m_topLevelIsArray)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadEncodingError,
|
|
"With Array as top level, encodeables array with fieldname will create invalid json");
|
|
}
|
|
else
|
|
{
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteEncodeable(null, values[ii], systemType!);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an enumerated value array to the stream.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException">The array length exceeds the maximum allowed or the array element type is invalid.</exception>
|
|
public void WriteEnumeratedArray(string? fieldName, Array? values, Type? systemType)
|
|
{
|
|
if (values == null || values.Length == 0)
|
|
{
|
|
WriteSimpleFieldNull(fieldName);
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Length)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
// encode each element in the array.
|
|
Type? arrayType = values.GetType().GetElementType();
|
|
if (arrayType!.IsEnum)
|
|
{
|
|
foreach (Enum value in values)
|
|
{
|
|
WriteEnumerated(null, value);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (arrayType != typeof(int))
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadEncodingError,
|
|
Utils.Format(
|
|
"Type '{0}' is not allowed in an Enumeration.",
|
|
arrayType.FullName!));
|
|
}
|
|
foreach (int value in values)
|
|
{
|
|
WriteEnumerated(null, new EnumValue(value));
|
|
}
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encode an array according to its valueRank and BuiltInType
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException">The encoding of the array fails due to invalid data or exceeded limits.</exception>
|
|
/// <exception cref="InvalidOperationException">The argument is not an array type.</exception>
|
|
public void WriteArray(
|
|
string fieldName,
|
|
object array,
|
|
int valueRank,
|
|
BuiltInType builtInType)
|
|
{
|
|
// write array.
|
|
if (valueRank == ValueRanks.OneDimension)
|
|
{
|
|
switch (builtInType)
|
|
{
|
|
case BuiltInType.Boolean:
|
|
WriteBooleanArray(fieldName, (bool[])array);
|
|
return;
|
|
case BuiltInType.SByte:
|
|
WriteSByteArray(fieldName, (sbyte[])array);
|
|
return;
|
|
case BuiltInType.Byte:
|
|
WriteByteArray(fieldName, (byte[])array);
|
|
return;
|
|
case BuiltInType.Int16:
|
|
WriteInt16Array(fieldName, (short[])array);
|
|
return;
|
|
case BuiltInType.UInt16:
|
|
WriteUInt16Array(fieldName, (ushort[])array);
|
|
return;
|
|
case BuiltInType.Int32:
|
|
WriteInt32Array(fieldName, (int[])array);
|
|
return;
|
|
case BuiltInType.UInt32:
|
|
WriteUInt32Array(fieldName, (uint[])array);
|
|
return;
|
|
case BuiltInType.Int64:
|
|
WriteInt64Array(fieldName, (long[])array);
|
|
return;
|
|
case BuiltInType.UInt64:
|
|
WriteUInt64Array(fieldName, (ulong[])array);
|
|
return;
|
|
case BuiltInType.Float:
|
|
WriteFloatArray(fieldName, (float[])array);
|
|
return;
|
|
case BuiltInType.Double:
|
|
WriteDoubleArray(fieldName, (double[])array);
|
|
return;
|
|
case BuiltInType.String:
|
|
WriteStringArray(fieldName, (string[])array);
|
|
return;
|
|
case BuiltInType.DateTime:
|
|
WriteDateTimeArray(fieldName, (DateTimeUtc[])array);
|
|
return;
|
|
case BuiltInType.Guid:
|
|
WriteGuidArray(fieldName, (Uuid[])array);
|
|
return;
|
|
case BuiltInType.ByteString:
|
|
WriteByteStringArray(fieldName, (ByteString[])array);
|
|
return;
|
|
case BuiltInType.XmlElement:
|
|
WriteXmlElementArray(fieldName, (XmlElement[])array);
|
|
return;
|
|
case BuiltInType.NodeId:
|
|
WriteNodeIdArray(fieldName, (NodeId[])array);
|
|
return;
|
|
case BuiltInType.ExpandedNodeId:
|
|
WriteExpandedNodeIdArray(fieldName, (ExpandedNodeId[])array);
|
|
return;
|
|
case BuiltInType.StatusCode:
|
|
WriteStatusCodeArray(fieldName, (StatusCode[])array);
|
|
return;
|
|
case BuiltInType.QualifiedName:
|
|
WriteQualifiedNameArray(fieldName, (QualifiedName[])array);
|
|
return;
|
|
case BuiltInType.LocalizedText:
|
|
WriteLocalizedTextArray(fieldName, (LocalizedText[])array);
|
|
return;
|
|
case BuiltInType.ExtensionObject:
|
|
WriteExtensionObjectArray(fieldName, (ExtensionObject[])array);
|
|
return;
|
|
case BuiltInType.DataValue:
|
|
WriteDataValueArray(fieldName, (DataValue[])array);
|
|
return;
|
|
case BuiltInType.DiagnosticInfo:
|
|
WriteDiagnosticInfoArray(fieldName, (DiagnosticInfo[])array);
|
|
return;
|
|
case BuiltInType.Enumeration:
|
|
if (array is null or Array)
|
|
{
|
|
WriteEnumeratedArray(
|
|
fieldName,
|
|
(Array?)array,
|
|
array?.GetType().GetElementType());
|
|
return;
|
|
}
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadEncodingError,
|
|
"Unexpected non Array type encountered while encoding an array of enumeration: {0}",
|
|
array.GetType());
|
|
case BuiltInType.Variant:
|
|
if (array is null or Variant[])
|
|
{
|
|
WriteVariantArray(fieldName, (Variant[])array!);
|
|
return;
|
|
}
|
|
|
|
// try to write IEncodeable Array
|
|
if (array is IEncodeable[] encodeableArray)
|
|
{
|
|
WriteEncodeableArray(
|
|
fieldName,
|
|
encodeableArray,
|
|
array.GetType().GetElementType()
|
|
?? throw new InvalidOperationException("Argument is not an array type."));
|
|
return;
|
|
}
|
|
|
|
if (array is object[] objects)
|
|
{
|
|
WriteObjectArray(fieldName, objects);
|
|
return;
|
|
}
|
|
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadEncodingError,
|
|
"Unexpected type encountered while encoding an array of Variants: {0}",
|
|
array.GetType());
|
|
case BuiltInType.Null:
|
|
case BuiltInType.Number:
|
|
case BuiltInType.Integer:
|
|
case BuiltInType.UInteger:
|
|
// try to write IEncodeable Array
|
|
if (array is null or IEncodeable[])
|
|
{
|
|
WriteEncodeableArray(
|
|
fieldName,
|
|
(IEncodeable[])array!,
|
|
array?.GetType().GetElementType()
|
|
?? throw new InvalidOperationException("Argument is not an array type."));
|
|
return;
|
|
}
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadEncodingError,
|
|
"Unexpected BuiltInType encountered while encoding an array: {0}",
|
|
builtInType);
|
|
default:
|
|
throw ServiceResultException.Unexpected(
|
|
$"Unexpected BuiltInType {builtInType}");
|
|
}
|
|
}
|
|
// write matrix.
|
|
else if (valueRank > ValueRanks.OneDimension)
|
|
{
|
|
if (array is not Matrix matrix)
|
|
{
|
|
if (array is Array multiArray && multiArray.Rank == valueRank)
|
|
{
|
|
matrix = new Matrix(multiArray, builtInType);
|
|
}
|
|
else
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadEncodingError,
|
|
"Unexpected array type encountered while encoding array: {0}",
|
|
array.GetType().Name);
|
|
}
|
|
}
|
|
|
|
if (EncodingToUse is PubSubJsonEncoding.Compact or PubSubJsonEncoding.Verbose)
|
|
{
|
|
WriteArrayDimensionMatrix(fieldName, builtInType, matrix);
|
|
}
|
|
else
|
|
{
|
|
int index = 0;
|
|
WriteStructureMatrix(fieldName, matrix, 0, ref index, matrix.TypeInfo);
|
|
}
|
|
return;
|
|
|
|
// field is omitted
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a raw value.
|
|
/// </summary>
|
|
public void WriteRawValue(FieldMetaData field, DataValue dv, DataSetFieldContentMask mask)
|
|
{
|
|
m_nestingLevel++;
|
|
|
|
try
|
|
{
|
|
if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
EscapeString(field.Name);
|
|
m_writer.Write(kQuotationColon);
|
|
m_commaRequired = false;
|
|
bool dimensionsInline = false;
|
|
|
|
if (mask is not DataSetFieldContentMask.None and not DataSetFieldContentMask.RawData)
|
|
{
|
|
m_writer.Write(kLeftCurlyBrace);
|
|
m_writer.Write(kQuotation);
|
|
m_writer.Write("Value");
|
|
m_writer.Write(kQuotationColon);
|
|
dimensionsInline = true;
|
|
}
|
|
|
|
if (mask == DataSetFieldContentMask.None && StatusCode.IsBad(dv.StatusCode))
|
|
{
|
|
dv = new DataValue(new Variant(dv.StatusCode));
|
|
}
|
|
|
|
WriteRawValueContents(field, dv, dimensionsInline);
|
|
|
|
if (mask is not DataSetFieldContentMask.None and not DataSetFieldContentMask.RawData)
|
|
{
|
|
if ((mask & DataSetFieldContentMask.StatusCode) != 0 &&
|
|
dv.StatusCode != StatusCodes.Good)
|
|
{
|
|
WriteStatusCode(nameof(dv.StatusCode), dv.StatusCode);
|
|
}
|
|
|
|
if ((mask & DataSetFieldContentMask.SourceTimestamp) != 0 &&
|
|
dv.SourceTimestamp != DateTimeUtc.MinValue)
|
|
{
|
|
WriteDateTime(nameof(dv.SourceTimestamp), dv.SourceTimestamp);
|
|
|
|
if (dv.SourcePicoseconds != 0)
|
|
{
|
|
WriteUInt16(nameof(dv.SourcePicoseconds), dv.SourcePicoseconds);
|
|
}
|
|
}
|
|
|
|
if ((mask & DataSetFieldContentMask.ServerTimestamp) != 0 &&
|
|
dv.ServerTimestamp != DateTimeUtc.MinValue)
|
|
{
|
|
WriteDateTime(nameof(dv.ServerTimestamp), dv.ServerTimestamp);
|
|
|
|
if (dv.ServerPicoseconds != 0)
|
|
{
|
|
WriteUInt16(nameof(dv.ServerPicoseconds), dv.ServerPicoseconds);
|
|
}
|
|
}
|
|
|
|
m_writer.Write(kRightCurlyBrace);
|
|
}
|
|
|
|
m_commaRequired = true;
|
|
}
|
|
finally
|
|
{
|
|
m_nestingLevel--;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteEncodeable<T>(string? fieldName, T value) where T : IEncodeable, new()
|
|
{
|
|
WriteEncodeable(fieldName, value, typeof(T));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteEncodeable<T>(string? fieldName, T value, ExpandedNodeId encodeableTypeId) where T : IEncodeable
|
|
{
|
|
WriteEncodeable(fieldName, value, typeof(T));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteEncodeableAsExtensionObject<T>(string? fieldName, T value) where T : IEncodeable
|
|
{
|
|
WriteExtensionObject(fieldName, new ExtensionObject(value));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteEnumerated<T>(string? fieldName, T value) where T : struct, Enum
|
|
{
|
|
WriteEnumerated(fieldName, (Enum)value);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteEncodeableArray<T>(string? fieldName, ArrayOf<T> values) where T : IEncodeable, new()
|
|
{
|
|
WriteEncodeableArray(fieldName, values.ConvertAll(d => (IEncodeable)d), typeof(T));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteEncodeableArray<T>(string? fieldName, ArrayOf<T> values, ExpandedNodeId encodeableTypeId) where T : IEncodeable
|
|
{
|
|
WriteEncodeableArray(fieldName, values.ConvertAll(d => (IEncodeable)d), typeof(T));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteEncodeableArrayAsExtensionObjects<T>(string? fieldName, ArrayOf<T> values) where T : IEncodeable
|
|
{
|
|
WriteExtensionObjectArray(fieldName, values.ConvertAll(d => new ExtensionObject(d)));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteEnumeratedArray<T>(string? fieldName, ArrayOf<T> values) where T : struct, Enum
|
|
{
|
|
WriteEnumeratedArray(fieldName, values.ToArray(), typeof(T));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteEnumeratedArray(string? fieldName, ArrayOf<EnumValue> values)
|
|
{
|
|
if (values.IsEmpty)
|
|
{
|
|
WriteSimpleFieldNull(fieldName);
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
// check the length.
|
|
if (Context.MaxArrayLength > 0 && Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
// encode each element in the array.
|
|
foreach (EnumValue value in values)
|
|
{
|
|
WriteEnumerated(null, value);
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteVariantValue(string? fieldName, Variant value)
|
|
{
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteEncodeableMatrix<T>(string? fieldName, MatrixOf<T> values) where T : IEncodeable, new()
|
|
{
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void WriteEncodeableMatrix<T>(string? fieldName, MatrixOf<T> values, ExpandedNodeId encodeableTypeId) where T : IEncodeable
|
|
{
|
|
}
|
|
|
|
private void WriteRawExtensionObject(object value)
|
|
{
|
|
if (value is ExtensionObject eo)
|
|
{
|
|
value = eo.Body!;
|
|
}
|
|
|
|
if (value is IEncodeable encodeable)
|
|
{
|
|
PushStructure(null);
|
|
encodeable.Encode(this);
|
|
PopStructure();
|
|
}
|
|
else
|
|
{
|
|
if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
m_writer.Write(kNull);
|
|
}
|
|
|
|
m_commaRequired = true;
|
|
}
|
|
|
|
private void WriteRawVariantArray(object value)
|
|
{
|
|
if (value is IList<Variant> list)
|
|
{
|
|
PushArray(null);
|
|
|
|
foreach (Variant ii in list)
|
|
{
|
|
if (!ii.IsNull)
|
|
{
|
|
Variant vt = ii;
|
|
PushStructure(null);
|
|
WriteVariantContents(vt.Value, vt.TypeInfo);
|
|
PopStructure();
|
|
}
|
|
else
|
|
{
|
|
if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
m_writer.Write(kNull);
|
|
}
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
else
|
|
{
|
|
m_writer.Write(kNull);
|
|
}
|
|
|
|
m_commaRequired = true;
|
|
}
|
|
|
|
private void WriteRawValueContents(FieldMetaData field, DataValue dv, bool dimensionsInline)
|
|
{
|
|
object? value = dv.WrappedValue.AsBoxedObject(Variant.BoxingBehavior.LegacyWithMatrix);
|
|
TypeInfo typeInfo = dv.WrappedValue.TypeInfo;
|
|
|
|
if (dv.WrappedValue == Variant.Null)
|
|
{
|
|
value = TypeInfo.GetDefaultValue((BuiltInType)field.BuiltInType, field.ValueRank);
|
|
typeInfo = new TypeInfo((BuiltInType)field.BuiltInType, field.ValueRank);
|
|
|
|
if (value != null)
|
|
{
|
|
WriteVariantContents(value, typeInfo);
|
|
}
|
|
else if (field.ValueRank >= 0)
|
|
{
|
|
m_writer.Write(kLeftSquareBracket);
|
|
m_writer.Write(kRightSquareBracket);
|
|
}
|
|
else if (field.BuiltInType == (byte)BuiltInType.ExtensionObject)
|
|
{
|
|
m_writer.Write(kLeftCurlyBrace);
|
|
m_writer.Write(kRightCurlyBrace);
|
|
}
|
|
else
|
|
{
|
|
m_writer.Write(kNull);
|
|
}
|
|
|
|
m_commaRequired = true;
|
|
return;
|
|
}
|
|
|
|
if (field.ValueRank == ValueRanks.Scalar)
|
|
{
|
|
if (field.BuiltInType == (byte)BuiltInType.ExtensionObject)
|
|
{
|
|
WriteRawExtensionObject(value!);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (value is Matrix matrix)
|
|
{
|
|
if (!dimensionsInline)
|
|
{
|
|
PushStructure(null);
|
|
}
|
|
|
|
PushArray(!dimensionsInline ? "Array" : null);
|
|
|
|
foreach (object ii in matrix.Elements)
|
|
{
|
|
if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
if (field.BuiltInType == (byte)BuiltInType.ExtensionObject)
|
|
{
|
|
m_commaRequired = false;
|
|
WriteRawExtensionObject(ii);
|
|
m_commaRequired = true;
|
|
continue;
|
|
}
|
|
else if (field.BuiltInType == (byte)BuiltInType.Variant)
|
|
{
|
|
m_commaRequired = false;
|
|
|
|
if (ii is Variant vt)
|
|
{
|
|
WriteVariant(null, vt);
|
|
}
|
|
else
|
|
{
|
|
m_writer.Write(kNull);
|
|
}
|
|
|
|
m_commaRequired = true;
|
|
continue;
|
|
}
|
|
|
|
WriteVariantContents(
|
|
ii,
|
|
new TypeInfo((BuiltInType)field.BuiltInType, ValueRanks.Scalar));
|
|
m_commaRequired = true;
|
|
}
|
|
|
|
PopArray();
|
|
WriteInt32Array("Dimensions", matrix.Dimensions);
|
|
if (!dimensionsInline)
|
|
{
|
|
PopStructure();
|
|
}
|
|
|
|
m_commaRequired = true;
|
|
return;
|
|
}
|
|
|
|
if (field.BuiltInType == (byte)BuiltInType.ExtensionObject &&
|
|
value is IList<ExtensionObject> list)
|
|
{
|
|
PushArray(null);
|
|
|
|
foreach (ExtensionObject element in list)
|
|
{
|
|
WriteRawExtensionObject(element);
|
|
}
|
|
|
|
PopArray();
|
|
m_commaRequired = true;
|
|
return;
|
|
}
|
|
|
|
if (field.BuiltInType == (byte)BuiltInType.Variant && value is IList<Variant>)
|
|
{
|
|
WriteRawVariantArray(value);
|
|
return;
|
|
}
|
|
}
|
|
|
|
WriteVariantContents(value, typeInfo);
|
|
|
|
if (EncodingToUse == PubSubJsonEncoding.Reversible)
|
|
{
|
|
if (dv.WrappedValue.AsBoxedObject(Variant.BoxingBehavior.LegacyWithMatrix) is Matrix matrix)
|
|
{
|
|
WriteInt32Array("Dimensions", matrix.Dimensions);
|
|
}
|
|
|
|
m_writer.Write(kRightCurlyBrace);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the contents of a Variant to the stream.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException">An unexpected built-in type is encountered.</exception>
|
|
public void WriteVariantContents(object? value, TypeInfo typeInfo)
|
|
{
|
|
bool inVariantWithEncoding = m_inVariantWithEncoding;
|
|
try
|
|
{
|
|
m_inVariantWithEncoding = true;
|
|
|
|
// check for null.
|
|
if (value == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_commaRequired = false;
|
|
|
|
// write scalar.
|
|
if (typeInfo.ValueRank < 0)
|
|
{
|
|
switch (typeInfo.BuiltInType)
|
|
{
|
|
case BuiltInType.Boolean:
|
|
WriteBoolean(null, (bool)value);
|
|
return;
|
|
case BuiltInType.SByte:
|
|
WriteSByte(null, (sbyte)value);
|
|
return;
|
|
case BuiltInType.Byte:
|
|
WriteByte(null, (byte)value);
|
|
return;
|
|
case BuiltInType.Int16:
|
|
WriteInt16(null, (short)value);
|
|
return;
|
|
case BuiltInType.UInt16:
|
|
WriteUInt16(null, (ushort)value);
|
|
return;
|
|
case BuiltInType.Int32:
|
|
WriteInt32(null, (int)value);
|
|
return;
|
|
case BuiltInType.UInt32:
|
|
WriteUInt32(null, (uint)value);
|
|
return;
|
|
case BuiltInType.Int64:
|
|
WriteInt64(null, (long)value);
|
|
return;
|
|
case BuiltInType.UInt64:
|
|
WriteUInt64(null, (ulong)value);
|
|
return;
|
|
case BuiltInType.Float:
|
|
WriteFloat(null, (float)value);
|
|
return;
|
|
case BuiltInType.Double:
|
|
WriteDouble(null, (double)value);
|
|
return;
|
|
case BuiltInType.String:
|
|
WriteString(null, (string)value);
|
|
return;
|
|
case BuiltInType.DateTime:
|
|
WriteDateTime(null, (DateTimeUtc)value);
|
|
return;
|
|
case BuiltInType.Guid:
|
|
WriteGuid(null, (Uuid)value);
|
|
return;
|
|
case BuiltInType.ByteString:
|
|
WriteByteString(null, (ByteString)value);
|
|
return;
|
|
case BuiltInType.XmlElement:
|
|
WriteXmlElement(null, (XmlElement)value);
|
|
return;
|
|
case BuiltInType.NodeId:
|
|
WriteNodeId(null, (NodeId)value);
|
|
return;
|
|
case BuiltInType.ExpandedNodeId:
|
|
WriteExpandedNodeId(null, (ExpandedNodeId)value);
|
|
return;
|
|
case BuiltInType.StatusCode:
|
|
WriteStatusCode(null, (StatusCode)value);
|
|
return;
|
|
case BuiltInType.QualifiedName:
|
|
WriteQualifiedName(null, (QualifiedName)value);
|
|
return;
|
|
case BuiltInType.LocalizedText:
|
|
WriteLocalizedText(null, (LocalizedText)value);
|
|
return;
|
|
case BuiltInType.ExtensionObject:
|
|
WriteExtensionObject(null, (ExtensionObject)value);
|
|
return;
|
|
case BuiltInType.DataValue:
|
|
WriteDataValue(null, (DataValue)value);
|
|
return;
|
|
case BuiltInType.Enumeration:
|
|
WriteEnumerated(null, (Enum)value);
|
|
return;
|
|
case BuiltInType.DiagnosticInfo:
|
|
WriteDiagnosticInfo(null, (DiagnosticInfo)value);
|
|
return;
|
|
case BuiltInType.Null:
|
|
case BuiltInType.Variant:
|
|
case BuiltInType.Number:
|
|
case BuiltInType.Integer:
|
|
case BuiltInType.UInteger:
|
|
// Should this not throw?
|
|
break;
|
|
default:
|
|
throw ServiceResultException.Unexpected(
|
|
$"Unexpected BuiltInType {typeInfo.BuiltInType}");
|
|
}
|
|
}
|
|
// write array
|
|
else if (typeInfo.ValueRank >= ValueRanks.OneDimension)
|
|
{
|
|
int valueRank = typeInfo.ValueRank;
|
|
if (EncodingToUse != PubSubJsonEncoding.NonReversible && value is Matrix matrix)
|
|
{
|
|
// linearize the matrix
|
|
value = matrix.Elements;
|
|
valueRank = ValueRanks.OneDimension;
|
|
}
|
|
WriteArray(null!, value, valueRank, typeInfo.BuiltInType);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
m_inVariantWithEncoding = inVariantWithEncoding;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a Variant array to the stream.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException">The array length exceeds the maximum allowed.</exception>
|
|
public void WriteObjectArray(string? fieldName, ArrayOf<object> values)
|
|
{
|
|
if (CheckForSimpleFieldNull(fieldName, values))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PushArray(fieldName);
|
|
|
|
if (!values.IsNull &&
|
|
Context.MaxArrayLength > 0 &&
|
|
Context.MaxArrayLength < values.Count)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded);
|
|
}
|
|
|
|
if (!values.IsNull)
|
|
{
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
WriteVariant("Variant", new Variant(values[ii]));
|
|
}
|
|
}
|
|
|
|
PopArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Push structure with an option to not escape a known fieldname.
|
|
/// </summary>
|
|
private void PushStructure(
|
|
string fieldName,
|
|
EscapeOptions escapeOptions = EscapeOptions.None)
|
|
{
|
|
m_nestingLevel++;
|
|
|
|
if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(fieldName))
|
|
{
|
|
if ((escapeOptions & EscapeOptions.NoFieldNameEscape) != 0)
|
|
{
|
|
m_writer.Write(kQuotation);
|
|
m_writer.Write(fieldName);
|
|
}
|
|
else
|
|
{
|
|
EscapeString(fieldName);
|
|
}
|
|
m_writer.Write(kQuotationColon);
|
|
}
|
|
else if (!m_commaRequired)
|
|
{
|
|
if (m_nestingLevel == 1 && !m_topLevelIsArray)
|
|
{
|
|
m_levelOneSkipped = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
m_commaRequired = false;
|
|
m_writer.Write(kLeftCurlyBrace);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an StatusCode to the stream.
|
|
/// </summary>
|
|
private void WriteStatusCode(
|
|
string? fieldName,
|
|
StatusCode value,
|
|
EscapeOptions escapeOptions)
|
|
{
|
|
bool isNull = value == StatusCodes.Good;
|
|
|
|
if (fieldName != null && isNull && !IncludeDefaultValues)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (EncodingToUse == PubSubJsonEncoding.Reversible)
|
|
{
|
|
WriteUInt32(fieldName, value.Code);
|
|
return;
|
|
}
|
|
|
|
PushStructure(fieldName!, escapeOptions);
|
|
|
|
if (!isNull)
|
|
{
|
|
WriteUInt32("Code", value.Code);
|
|
|
|
if (EncodingToUse is PubSubJsonEncoding.NonReversible or PubSubJsonEncoding.Verbose)
|
|
{
|
|
string? symbolicId = value.SymbolicId;
|
|
if (!string.IsNullOrEmpty(symbolicId))
|
|
{
|
|
WriteSimpleField(
|
|
"Symbol",
|
|
symbolicId,
|
|
EscapeOptions.Quotes | EscapeOptions.NoFieldNameEscape);
|
|
}
|
|
}
|
|
}
|
|
|
|
PopStructure();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a UTC date/time to the stream. Reduce escape overhead for fieldname.
|
|
/// </summary>
|
|
private void WriteDateTime(string? fieldName, DateTimeUtc value, EscapeOptions escapeOptions)
|
|
{
|
|
if (fieldName != null && !IncludeDefaultValues && value == DateTimeUtc.MinValue)
|
|
{
|
|
WriteSimpleFieldNull(fieldName);
|
|
return;
|
|
}
|
|
|
|
escapeOptions |= EscapeOptions.NoValueEscape;
|
|
if (value <= DateTimeUtc.MinValue)
|
|
{
|
|
WriteSimpleField(fieldName, "\"0001-01-01T00:00:00Z\"", escapeOptions);
|
|
}
|
|
else if (value >= DateTimeUtc.MaxValue)
|
|
{
|
|
WriteSimpleField(fieldName, "\"9999-12-31T23:59:59Z\"", escapeOptions);
|
|
}
|
|
else
|
|
{
|
|
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
|
|
Span<char> valueString = stackalloc char[DateTimeRoundTripKindLength];
|
|
ConvertUniversalTimeToString((DateTime)value, valueString, out int charsWritten);
|
|
WriteSimpleFieldAsSpan(
|
|
fieldName,
|
|
valueString[..charsWritten],
|
|
escapeOptions | EscapeOptions.Quotes);
|
|
#else
|
|
WriteSimpleField(
|
|
fieldName,
|
|
ConvertUniversalTimeToString((DateTime)value),
|
|
escapeOptions | EscapeOptions.Quotes);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if a simple field can be written.
|
|
/// </summary>
|
|
/// <typeparam name="T"></typeparam>
|
|
private bool CheckForSimpleFieldNull<T>(string? fieldName, ArrayOf<T> values)
|
|
{
|
|
// always include default values for non reversible/verbose
|
|
// include default values when encoding in a Variant
|
|
if (values.IsNull ||
|
|
(values.Count == 0 && !m_inVariantWithEncoding && !m_includeDefaultValues))
|
|
{
|
|
WriteSimpleFieldNull(fieldName);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called on properties which can only be modified for the deprecated encoding.
|
|
/// </summary>
|
|
/// <exception cref="NotSupportedException">The encoding type is <see cref="PubSubJsonEncoding.Compact"/> or <see cref="PubSubJsonEncoding.Verbose"/>.</exception>
|
|
private bool ThrowIfCompactOrVerbose(bool value)
|
|
{
|
|
if (EncodingToUse is PubSubJsonEncoding.Compact or PubSubJsonEncoding.Verbose)
|
|
{
|
|
throw new NotSupportedException(
|
|
$"This property can not be modified with {EncodingToUse} encoding.");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Completes writing and returns the text length.
|
|
/// </summary>
|
|
private int InternalClose(bool dispose)
|
|
{
|
|
if (m_writer == null)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (!m_dontWriteClosing)
|
|
{
|
|
if (m_topLevelIsArray)
|
|
{
|
|
m_writer.Write(kRightSquareBracket);
|
|
}
|
|
else
|
|
{
|
|
m_writer.Write(kRightCurlyBrace);
|
|
}
|
|
}
|
|
|
|
m_writer.Flush();
|
|
int length = (int)m_writer.BaseStream.Position;
|
|
if (dispose)
|
|
{
|
|
m_writer.Dispose();
|
|
m_writer = null!;
|
|
}
|
|
return length;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a DiagnosticInfo to the stream.
|
|
/// Ignores InnerDiagnosticInfo field if the nesting level
|
|
/// <see cref="DiagnosticInfo.MaxInnerDepth"/> is exceeded.
|
|
/// </summary>
|
|
private void WriteDiagnosticInfo(string? fieldName, DiagnosticInfo? value, int depth)
|
|
{
|
|
bool isNull = value == null || value.IsNullDiagnosticInfo;
|
|
|
|
if (fieldName != null && isNull && !IncludeDefaultValues)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (value == null)
|
|
{
|
|
WriteSimpleField(fieldName, kNull, EscapeOptions.NoValueEscape);
|
|
return;
|
|
}
|
|
|
|
CheckAndIncrementNestingLevel();
|
|
|
|
try
|
|
{
|
|
PushStructure(fieldName);
|
|
|
|
if (value.SymbolicId >= 0)
|
|
{
|
|
WriteSimpleField(
|
|
"SymbolicId",
|
|
value.SymbolicId.ToString(CultureInfo.InvariantCulture),
|
|
EscapeOptions.NoFieldNameEscape);
|
|
}
|
|
|
|
if (value.NamespaceUri >= 0)
|
|
{
|
|
WriteSimpleField(
|
|
"NamespaceUri",
|
|
value.NamespaceUri.ToString(CultureInfo.InvariantCulture),
|
|
EscapeOptions.NoFieldNameEscape);
|
|
}
|
|
|
|
if (value.Locale >= 0)
|
|
{
|
|
WriteSimpleField(
|
|
"Locale",
|
|
value.Locale.ToString(CultureInfo.InvariantCulture),
|
|
EscapeOptions.NoFieldNameEscape);
|
|
}
|
|
|
|
if (value.LocalizedText >= 0)
|
|
{
|
|
WriteSimpleField(
|
|
"LocalizedText",
|
|
value.LocalizedText.ToString(CultureInfo.InvariantCulture),
|
|
EscapeOptions.NoFieldNameEscape);
|
|
}
|
|
|
|
if (value.AdditionalInfo != null)
|
|
{
|
|
WriteSimpleField(
|
|
"AdditionalInfo",
|
|
value.AdditionalInfo,
|
|
EscapeOptions.Quotes | EscapeOptions.NoFieldNameEscape);
|
|
}
|
|
|
|
if (value.InnerStatusCode != StatusCodes.Good)
|
|
{
|
|
WriteStatusCode("InnerStatusCode", value.InnerStatusCode);
|
|
}
|
|
|
|
if (value.InnerDiagnosticInfo != null)
|
|
{
|
|
if (depth < DiagnosticInfo.MaxInnerDepth)
|
|
{
|
|
WriteDiagnosticInfo(
|
|
"InnerDiagnosticInfo",
|
|
value.InnerDiagnosticInfo,
|
|
depth + 1);
|
|
}
|
|
else
|
|
{
|
|
m_logger.LogWarning(
|
|
"InnerDiagnosticInfo dropped because nesting exceeds maximum of {MaxInnerDepth}.",
|
|
DiagnosticInfo.MaxInnerDepth);
|
|
}
|
|
}
|
|
|
|
PopStructure();
|
|
}
|
|
finally
|
|
{
|
|
m_nestingLevel--;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encode the Matrix as Dimensions/Array element.
|
|
/// Writes the matrix as a flattended array with dimensions.
|
|
/// Validates the dimensions and array size.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException">The number of elements does not match the dimensions.</exception>
|
|
private void WriteArrayDimensionMatrix(
|
|
string fieldName,
|
|
BuiltInType builtInType,
|
|
Matrix matrix)
|
|
{
|
|
// check if matrix is well formed
|
|
(bool valid, int sizeFromDimensions) = Matrix.ValidateDimensions(
|
|
true,
|
|
matrix.Dimensions,
|
|
Context.MaxArrayLength,
|
|
m_logger);
|
|
|
|
if (!valid || (sizeFromDimensions != matrix.Elements.Length))
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadEncodingError,
|
|
"The number of elements in the matrix does not match the dimensions.");
|
|
}
|
|
|
|
PushStructure(fieldName);
|
|
WriteInt32Array("Dimensions", matrix.Dimensions);
|
|
WriteArray("Array", matrix.Elements, 1, builtInType);
|
|
PopStructure();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write multi dimensional array in structure.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException">The number of elements does not match the dimensions.</exception>
|
|
/// <exception cref="InvalidOperationException">The matrix elements is not an array type.</exception>
|
|
private void WriteStructureMatrix(
|
|
string fieldName,
|
|
Matrix matrix,
|
|
int dim,
|
|
ref int index,
|
|
TypeInfo typeInfo)
|
|
{
|
|
// check if matrix is well formed
|
|
(bool valid, int sizeFromDimensions) = Matrix.ValidateDimensions(
|
|
true,
|
|
matrix.Dimensions,
|
|
Context.MaxArrayLength,
|
|
m_logger);
|
|
|
|
if (!valid || (sizeFromDimensions != matrix.Elements.Length))
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadEncodingError,
|
|
"The number of elements in the matrix does not match the dimensions.");
|
|
}
|
|
|
|
CheckAndIncrementNestingLevel();
|
|
|
|
try
|
|
{
|
|
int arrayLen = matrix.Dimensions[dim];
|
|
if (dim == matrix.Dimensions.Length - 1)
|
|
{
|
|
// Create a slice of values for the top dimension
|
|
var copy = Array.CreateInstance(
|
|
matrix.Elements.GetType().GetElementType()
|
|
?? throw new InvalidOperationException("Elements is not an array type."),
|
|
arrayLen);
|
|
Array.Copy(matrix.Elements, index, copy, 0, arrayLen);
|
|
// Write slice as value rank
|
|
if (m_commaRequired)
|
|
{
|
|
m_writer.Write(kComma);
|
|
}
|
|
WriteVariantContents(copy, TypeInfo.Create(typeInfo.BuiltInType, ValueRanks.OneDimension));
|
|
index += arrayLen;
|
|
}
|
|
else
|
|
{
|
|
PushArray(fieldName);
|
|
for (int i = 0; i < arrayLen; i++)
|
|
{
|
|
WriteStructureMatrix(null!, matrix, dim + 1, ref index, typeInfo);
|
|
}
|
|
PopArray();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
m_nestingLevel--;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test and increment the nesting level.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException">The maximum nesting level is exceeded.</exception>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void CheckAndIncrementNestingLevel()
|
|
{
|
|
if (m_nestingLevel > Context.MaxEncodingNestingLevels)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadEncodingLimitsExceeded,
|
|
"Maximum nesting level of {0} was exceeded",
|
|
Context.MaxEncodingNestingLevels);
|
|
}
|
|
m_nestingLevel++;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The length of the DateTime string encoded by "o"
|
|
/// </summary>
|
|
internal const int DateTimeRoundTripKindLength = 28;
|
|
|
|
/// <summary>
|
|
/// the index of the last digit which can be omitted if 0
|
|
/// </summary>
|
|
internal const int DateTimeRoundTripKindLastDigit = DateTimeRoundTripKindLength - 2;
|
|
|
|
/// <summary>
|
|
/// the index of the first digit which can be omitted (7 digits total)
|
|
/// </summary>
|
|
internal const int DateTimeRoundTripKindFirstDigit = DateTimeRoundTripKindLastDigit - 7;
|
|
|
|
/// <summary>
|
|
/// Write Utc time in the format "yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK".
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
|
|
internal static void ConvertUniversalTimeToString(
|
|
DateTime value,
|
|
Span<char> valueString,
|
|
out int charsWritten)
|
|
{
|
|
// Note: "o" is a shortcut for "yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK" and implicitly
|
|
// uses invariant culture and gregorian calendar, but executes up to 10 times faster.
|
|
// But in contrary to the explicit format string, trailing zeroes are not omitted!
|
|
if (value.Kind != DateTimeKind.Utc)
|
|
{
|
|
value.ToUniversalTime()
|
|
.TryFormat(valueString, out charsWritten, "o", CultureInfo.InvariantCulture);
|
|
}
|
|
else
|
|
{
|
|
value.TryFormat(valueString, out charsWritten, "o", CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
System.Diagnostics.Debug.Assert(charsWritten == DateTimeRoundTripKindLength);
|
|
|
|
// check if trailing zeroes can be omitted
|
|
int i = DateTimeRoundTripKindLastDigit;
|
|
while (i > DateTimeRoundTripKindFirstDigit)
|
|
{
|
|
if (valueString[i] != '0')
|
|
{
|
|
break;
|
|
}
|
|
i--;
|
|
}
|
|
|
|
if (i < DateTimeRoundTripKindLastDigit)
|
|
{
|
|
// check if the decimal point has to be removed too
|
|
if (i == DateTimeRoundTripKindFirstDigit)
|
|
{
|
|
i--;
|
|
}
|
|
valueString[i + 1] = 'Z';
|
|
charsWritten = i + 2;
|
|
}
|
|
}
|
|
#else
|
|
internal static string ConvertUniversalTimeToString(DateTime value)
|
|
{
|
|
// Note: "o" is a shortcut for "yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK" and implicitly
|
|
// uses invariant culture and gregorian calendar, but executes up to 10 times faster.
|
|
// But in contrary to the explicit format string, trailing zeroes are not omitted!
|
|
string valueString = value.ToUniversalTime().ToString("o");
|
|
|
|
// check if trailing zeroes can be omitted
|
|
int i = DateTimeRoundTripKindLastDigit;
|
|
while (i > DateTimeRoundTripKindFirstDigit)
|
|
{
|
|
if (valueString[i] != '0')
|
|
{
|
|
break;
|
|
}
|
|
i--;
|
|
}
|
|
|
|
if (i < DateTimeRoundTripKindLastDigit)
|
|
{
|
|
// check if the decimal point has to be removed too
|
|
if (i == DateTimeRoundTripKindFirstDigit)
|
|
{
|
|
i--;
|
|
}
|
|
valueString = valueString.Remove(i + 1, DateTimeRoundTripKindLastDigit - i);
|
|
}
|
|
|
|
return valueString;
|
|
}
|
|
#endif
|
|
}
|
|
}
|