/* ======================================================================== * 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 { /// /// Writes objects to a JSON stream. /// 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 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 } /// /// Initializes the object with default values. /// Selects the reversible or non reversible encoding. /// public PubSubJsonEncoder(IServiceMessageContext context, bool useReversibleEncoding) : this( context, useReversibleEncoding ? PubSubJsonEncoding.Reversible : PubSubJsonEncoding.NonReversible, false, null, false) { } /// /// Initializes the object with default values. /// Selects the reversible or non reversible encoding. /// 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) { } /// /// Initializes the object with default values. /// Selects the reversible or non reversible encoding. /// public PubSubJsonEncoder( IServiceMessageContext context, bool useReversibleEncoding, StreamWriter streamWriter, bool topLevelIsArray = false) : this( context, useReversibleEncoding ? PubSubJsonEncoding.Reversible : PubSubJsonEncoding.NonReversible, streamWriter, topLevelIsArray) { } /// /// Initializes the object with default values. /// 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(); 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(); } /// /// Initializes the object with default values. /// public PubSubJsonEncoder( IServiceMessageContext context, PubSubJsonEncoding encoding, StreamWriter writer, bool topLevelIsArray = false) : this(encoding) { Context = context; m_logger = context.Telemetry.CreateLogger(); m_writer = writer; m_topLevelIsArray = topLevelIsArray; if (m_writer == null) { m_stream = new MemoryStream(); m_writer = new StreamWriter(m_stream, s_utf8Encoding, kStreamWriterBufferSize); } InitializeWriter(); } /// /// Sets default values. /// 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; } /// /// Initialize Writer. /// private void InitializeWriter() { if (m_topLevelIsArray) { m_writer.Write(kLeftSquareBracket); } else { m_writer.Write(kLeftCurlyBrace); } } /// /// Encodes a message in a stream. /// /// is null. /// is null. /// is null. public static ArraySegment 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(buffer, 0, length); } /// public void EncodeMessage(T message, ExpandedNodeId encodeableTypeId) where T : IEncodeable { if (EqualityComparer.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()); } /// public void EncodeMessage(T message) where T : IEncodeable, new() { if (EqualityComparer.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()); } /// /// Initializes the tables used to map namespace and server uris during encoding. /// /// The namespaces URIs referenced by the data being encoded. /// The server URIs referenced by the data being encoded. 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); } } /// /// Completes writing and returns the JSON text. /// /// The underlying stream is not a . 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!; } } /// /// Completes writing and returns the text length. /// The StreamWriter is disposed. /// public int Close() { return InternalClose(true); } /// /// Frees any unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// An overrideable version of the Dispose. /// 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; } } } /// public PubSubJsonEncoding EncodingToUse { get; private set; } /// public bool SuppressArtifacts { get; set; } /// 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); } /// 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); } /// public void PopStructure() { if (m_nestingLevel > 1 || m_topLevelIsArray || (m_nestingLevel == 1 && !m_levelOneSkipped)) { m_writer.Write(kRightCurlyBrace); m_commaRequired = true; } m_nestingLevel--; } /// public void PopArray() { if (m_nestingLevel > 1 || m_topLevelIsArray || (m_nestingLevel == 1 && !m_levelOneSkipped)) { m_writer.Write(kRightSquareBracket); m_commaRequired = true; } m_nestingLevel--; } /// [Obsolete( "Non/Reversible encoding is deprecated. Use UsingAlternateEncoding instead to support new encoding types." )] public void UsingReversibleEncoding( Action action, string fieldName, T value, bool useReversibleEncoding) { PubSubJsonEncoding currentValue = EncodingToUse; try { EncodingToUse = useReversibleEncoding ? PubSubJsonEncoding.Reversible : PubSubJsonEncoding.NonReversible; action(fieldName, value); } finally { EncodingToUse = currentValue; } } /// public void UsingAlternateEncoding( Action action, string fieldName, T value, PubSubJsonEncoding useEncodingType) { PubSubJsonEncoding currentValue = EncodingToUse; try { EncodingToUse = useEncodingType; action(fieldName, value); } finally { EncodingToUse = currentValue; } } /// 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); } /// public void WriteEncodingMask(uint encodingMask) { if ((!SuppressArtifacts && EncodingToUse == PubSubJsonEncoding.Compact) || EncodingToUse == PubSubJsonEncoding.Reversible) { WriteUInt32("EncodingMask", encodingMask); } } /// public EncodingType EncodingType => EncodingType.Json; /// public bool CanOmitFields => true; /// public bool UseReversibleEncoding => EncodingToUse != PubSubJsonEncoding.NonReversible; /// /// The message context associated with the encoder. /// public IServiceMessageContext Context { get; } = null!; /// /// The Json encoder to encoder namespace URI instead of /// namespace Index in NodeIds. /// public bool ForceNamespaceUri { get => m_forceNamespaceUri; set => m_forceNamespaceUri = ThrowIfCompactOrVerbose(value); } /// /// The Json encoder to encode namespace URI for all /// namespaces /// public bool ForceNamespaceUriForIndex1 { get => m_forceNamespaceUriForIndex1; set => m_forceNamespaceUriForIndex1 = ThrowIfCompactOrVerbose(value); } /// /// The Json encoder default value option. /// public bool IncludeDefaultValues { get => m_includeDefaultValues; set => m_includeDefaultValues = ThrowIfCompactOrVerbose(value); } /// /// The Json encoder default value option for numbers. /// public bool IncludeDefaultNumberValues { get => m_includeDefaultNumberValues || m_includeDefaultValues; set => m_includeDefaultNumberValues = ThrowIfCompactOrVerbose(value); } /// /// The Json encoder default encoding for NodeId as string or object. /// public bool EncodeNodeIdAsString { get => m_encodeNodeIdAsString; set => m_encodeNodeIdAsString = ThrowIfCompactOrVerbose(value); } /// public void PushNamespace(string namespaceUri) { m_namespaces.Push(namespaceUri); } /// 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 /// /// Using a span to escape the string, write strings to stream writer if possible. /// private void EscapeString(ReadOnlySpan 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 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 /// /// Escapes a string and writes it to the stream. /// 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, Span 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 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 /// /// Writes a boolean to the stream. /// public void WriteBoolean(string? fieldName, bool value) { if (fieldName != null && !IncludeDefaultNumberValues && !value) { return; } if (value) { WriteSimpleField(fieldName, "true"); } else { WriteSimpleField(fieldName, "false"); } } /// /// Writes a sbyte to the stream. /// public void WriteSByte(string? fieldName, sbyte value) { if (fieldName != null && !IncludeDefaultNumberValues && value == 0) { return; } WriteSimpleField(fieldName, value.ToString(CultureInfo.InvariantCulture)); } /// /// Writes a byte to the stream. /// public void WriteByte(string? fieldName, byte value) { if (fieldName != null && !IncludeDefaultNumberValues && value == 0) { return; } WriteSimpleField(fieldName, value.ToString(CultureInfo.InvariantCulture)); } /// /// Writes a short to the stream. /// public void WriteInt16(string? fieldName, short value) { if (fieldName != null && !IncludeDefaultNumberValues && value == 0) { return; } WriteSimpleField(fieldName, value.ToString(CultureInfo.InvariantCulture)); } /// /// Writes a ushort to the stream. /// public void WriteUInt16(string? fieldName, ushort value) { if (fieldName != null && !IncludeDefaultNumberValues && value == 0) { return; } WriteSimpleField(fieldName, value.ToString(CultureInfo.InvariantCulture)); } /// /// Writes an int to the stream. /// public void WriteInt32(string? fieldName, int value) { if (fieldName != null && !IncludeDefaultNumberValues && value == 0) { return; } WriteSimpleField(fieldName, value.ToString(CultureInfo.InvariantCulture)); } /// /// Writes a uint to the stream. /// public void WriteUInt32(string? fieldName, uint value) { if (fieldName != null && !IncludeDefaultNumberValues && value == 0) { return; } WriteSimpleField(fieldName, value.ToString(CultureInfo.InvariantCulture)); } /// /// Writes a long to the stream. /// public void WriteInt64(string? fieldName, long value) { if (fieldName != null && !IncludeDefaultNumberValues && value == 0) { return; } WriteSimpleField( fieldName, value.ToString(CultureInfo.InvariantCulture), EscapeOptions.Quotes); } /// /// Writes a ulong to the stream. /// public void WriteUInt64(string? fieldName, ulong value) { if (fieldName != null && !IncludeDefaultNumberValues && value == 0) { return; } WriteSimpleField( fieldName, value.ToString(CultureInfo.InvariantCulture), EscapeOptions.Quotes); } /// /// Writes a float to the stream. /// 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)); } } /// /// Writes a double to the stream. /// 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)); } } /// /// Writes a string to the stream. /// public void WriteString(string? fieldName, string? value) { if (fieldName != null && !IncludeDefaultValues && value == null) { return; } WriteSimpleField(fieldName, value, EscapeOptions.Quotes); } /// /// Writes a UTC date/time to the stream. /// public void WriteDateTime(string? fieldName, DateTimeUtc value) { WriteDateTime(fieldName, value, EscapeOptions.None); } /// /// Writes a GUID to the stream. /// public void WriteGuid(string? fieldName, Uuid value) { if (fieldName != null && !IncludeDefaultValues && value == Uuid.Empty) { return; } WriteSimpleField( fieldName, value.ToString(), EscapeOptions.Quotes | EscapeOptions.NoValueEscape); } /// public void WriteByteString(string? fieldName, ByteString value) { WriteByteString(fieldName, value.ToArray(), 0, value.Length); } /// /// Writes a byte string to the stream with a given index and count. /// /// The byte string length exceeds the maximum allowed. 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 /// /// Writes a byte string to the stream. /// /// The byte string length exceeds the maximum allowed or encoding fails. public void WriteByteString(string? fieldName, ReadOnlySpan 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.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 chars = length <= maxStackLimit ? stackalloc char[length] : (arrayPool = ArrayPool.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.Shared.Return(arrayPool); } } } WriteSimpleField( fieldName, string.Empty, EscapeOptions.Quotes | EscapeOptions.NoValueEscape); } #endif /// 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); } } /// /// Writes an NodeId to the stream. /// 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(); } /// /// Writes an ExpandedNodeId to the stream. /// 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(); } /// /// Writes an StatusCode to the stream. /// public void WriteStatusCode(string? fieldName, StatusCode value) { WriteStatusCode(fieldName, value, EscapeOptions.None); } /// /// Writes a DiagnosticInfo to the stream. /// public void WriteDiagnosticInfo(string? fieldName, DiagnosticInfo? value) { WriteDiagnosticInfo(fieldName, value, 0); } /// /// Writes an QualifiedName to the stream. /// 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(); } /// /// Writes an LocalizedText to the stream. /// 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(); } /// /// Writes an Variant to the stream. /// 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--; } } /// /// Writes an DataValue array to the stream. /// 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(); } /// /// Writes an ExtensionObject to the stream. /// 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(); } /// /// Writes an encodeable object to the stream. /// /// The encoding would create invalid JSON or the nesting level is exceeded. 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--; } } /// /// Writes an enumerated value to the stream. /// 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); } } } /// /// Writes an enumerated EnumValue value to the stream. /// 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); } } } /// /// Writes a boolean array to the stream. /// /// The array length exceeds the maximum allowed. public void WriteBooleanArray(string? fieldName, ArrayOf 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(); } /// public void WriteSByteArray(string? fieldName, ArrayOf 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(); } /// public void WriteByteArray(string? fieldName, ArrayOf 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(); } /// public void WriteInt16Array(string? fieldName, ArrayOf 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(); } /// public void WriteUInt16Array(string? fieldName, ArrayOf 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(); } /// public void WriteInt32Array(string? fieldName, ArrayOf 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(); } /// public void WriteUInt32Array(string? fieldName, ArrayOf 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(); } /// public void WriteInt64Array(string? fieldName, ArrayOf 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(); } /// public void WriteUInt64Array(string? fieldName, ArrayOf 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(); } /// public void WriteFloatArray(string? fieldName, ArrayOf 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(); } /// public void WriteDoubleArray(string? fieldName, ArrayOf 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(); } /// public void WriteStringArray(string? fieldName, ArrayOf 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(); } /// public void WriteDateTimeArray(string? fieldName, ArrayOf 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(); } /// public void WriteGuidArray(string? fieldName, ArrayOf 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(); } /// public void WriteByteStringArray(string? fieldName, ArrayOf 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(); } /// public void WriteXmlElementArray(string? fieldName, ArrayOf 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(); } /// public void WriteNodeIdArray(string? fieldName, ArrayOf 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(); } /// public void WriteExpandedNodeIdArray(string? fieldName, ArrayOf 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(); } /// public void WriteStatusCodeArray(string? fieldName, ArrayOf 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(); } /// public void WriteDiagnosticInfoArray(string? fieldName, ArrayOf 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(); } /// public void WriteQualifiedNameArray(string? fieldName, ArrayOf 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(); } /// public void WriteLocalizedTextArray(string? fieldName, ArrayOf 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(); } /// public void WriteVariantArray(string? fieldName, ArrayOf 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(); } /// public void WriteDataValueArray(string? fieldName, ArrayOf 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(); } /// public void WriteExtensionObjectArray(string? fieldName, ArrayOf 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(); } /// /// Writes an encodeable object array to the stream. /// /// The array length exceeds the maximum allowed or the encoding would create invalid JSON. public void WriteEncodeableArray( string? fieldName, ArrayOf 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(); } } /// /// Writes an enumerated value array to the stream. /// /// The array length exceeds the maximum allowed or the array element type is invalid. 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(); } /// /// Encode an array according to its valueRank and BuiltInType /// /// The encoding of the array fails due to invalid data or exceeded limits. /// The argument is not an array type. 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 } } /// /// Writes a raw value. /// 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--; } } /// public void WriteEncodeable(string? fieldName, T value) where T : IEncodeable, new() { WriteEncodeable(fieldName, value, typeof(T)); } /// public void WriteEncodeable(string? fieldName, T value, ExpandedNodeId encodeableTypeId) where T : IEncodeable { WriteEncodeable(fieldName, value, typeof(T)); } /// public void WriteEncodeableAsExtensionObject(string? fieldName, T value) where T : IEncodeable { WriteExtensionObject(fieldName, new ExtensionObject(value)); } /// public void WriteEnumerated(string? fieldName, T value) where T : struct, Enum { WriteEnumerated(fieldName, (Enum)value); } /// public void WriteEncodeableArray(string? fieldName, ArrayOf values) where T : IEncodeable, new() { WriteEncodeableArray(fieldName, values.ConvertAll(d => (IEncodeable)d), typeof(T)); } /// public void WriteEncodeableArray(string? fieldName, ArrayOf values, ExpandedNodeId encodeableTypeId) where T : IEncodeable { WriteEncodeableArray(fieldName, values.ConvertAll(d => (IEncodeable)d), typeof(T)); } /// public void WriteEncodeableArrayAsExtensionObjects(string? fieldName, ArrayOf values) where T : IEncodeable { WriteExtensionObjectArray(fieldName, values.ConvertAll(d => new ExtensionObject(d))); } /// public void WriteEnumeratedArray(string? fieldName, ArrayOf values) where T : struct, Enum { WriteEnumeratedArray(fieldName, values.ToArray(), typeof(T)); } /// public void WriteEnumeratedArray(string? fieldName, ArrayOf 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(); } /// public void WriteVariantValue(string? fieldName, Variant value) { } /// public void WriteEncodeableMatrix(string? fieldName, MatrixOf values) where T : IEncodeable, new() { } /// public void WriteEncodeableMatrix(string? fieldName, MatrixOf 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 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 list) { PushArray(null); foreach (ExtensionObject element in list) { WriteRawExtensionObject(element); } PopArray(); m_commaRequired = true; return; } if (field.BuiltInType == (byte)BuiltInType.Variant && value is IList) { 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); } } /// /// Writes the contents of a Variant to the stream. /// /// An unexpected built-in type is encountered. 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; } } /// /// Writes a Variant array to the stream. /// /// The array length exceeds the maximum allowed. public void WriteObjectArray(string? fieldName, ArrayOf 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(); } /// /// Push structure with an option to not escape a known fieldname. /// 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); } /// /// Writes an StatusCode to the stream. /// 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(); } /// /// Writes a UTC date/time to the stream. Reduce escape overhead for fieldname. /// 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 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 } } /// /// Returns true if a simple field can be written. /// /// private bool CheckForSimpleFieldNull(string? fieldName, ArrayOf 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; } /// /// Called on properties which can only be modified for the deprecated encoding. /// /// The encoding type is or . 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; } /// /// Completes writing and returns the text length. /// 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; } /// /// Writes a DiagnosticInfo to the stream. /// Ignores InnerDiagnosticInfo field if the nesting level /// is exceeded. /// 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--; } } /// /// Encode the Matrix as Dimensions/Array element. /// Writes the matrix as a flattended array with dimensions. /// Validates the dimensions and array size. /// /// The number of elements does not match the dimensions. 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(); } /// /// Write multi dimensional array in structure. /// /// The number of elements does not match the dimensions. /// The matrix elements is not an array type. 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--; } } /// /// Test and increment the nesting level. /// /// The maximum nesting level is exceeded. [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++; } /// /// The length of the DateTime string encoded by "o" /// internal const int DateTimeRoundTripKindLength = 28; /// /// the index of the last digit which can be omitted if 0 /// internal const int DateTimeRoundTripKindLastDigit = DateTimeRoundTripKindLength - 2; /// /// the index of the first digit which can be omitted (7 digits total) /// internal const int DateTimeRoundTripKindFirstDigit = DateTimeRoundTripKindLastDigit - 7; /// /// Write Utc time in the format "yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK". /// [MethodImpl(MethodImplOptions.AggressiveInlining)] #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER internal static void ConvertUniversalTimeToString( DateTime value, Span 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 } }