diff --git a/.azurepipelines/signlistDebug.txt b/.azurepipelines/signlistDebug.txt
index e1871d3bf..5c9998add 100644
--- a/.azurepipelines/signlistDebug.txt
+++ b/.azurepipelines/signlistDebug.txt
@@ -1,3 +1,4 @@
+Stack\Opc.Ua.Core\bin\Debug\netstandard2.0\Opc.Ua.Core.dll
Stack\Opc.Ua.Core\bin\Debug\netstandard2.1\Opc.Ua.Core.dll
Stack\Opc.Ua.Core\bin\Debug\net48\Opc.Ua.Core.dll
Stack\Opc.Ua.Core\bin\Debug\net6.0\Opc.Ua.Core.dll
@@ -30,6 +31,7 @@ Libraries\Opc.Ua.Gds.Server.Common\bin\Debug\netstandard2.1\Opc.Ua.Gds.Server.Co
Libraries\Opc.Ua.Gds.Server.Common\bin\Debug\net48\Opc.Ua.Gds.Server.Common.dll
Libraries\Opc.Ua.Gds.Server.Common\bin\Debug\net6.0\Opc.Ua.Gds.Server.Common.dll
Libraries\Opc.Ua.Gds.Server.Common\bin\Debug\net8.0\Opc.Ua.Gds.Server.Common.dll
+Libraries\Opc.Ua.Security.Certificates\bin\Debug\netstandard2.0\Opc.Ua.Security.Certificates.dll
Libraries\Opc.Ua.Security.Certificates\bin\Debug\netstandard2.1\Opc.Ua.Security.Certificates.dll
Libraries\Opc.Ua.Security.Certificates\bin\Debug\net48\Opc.Ua.Security.Certificates.dll
Libraries\Opc.Ua.Security.Certificates\bin\Debug\net6.0\Opc.Ua.Security.Certificates.dll
diff --git a/.azurepipelines/signlistRelease.txt b/.azurepipelines/signlistRelease.txt
index 0251c5798..8a844e347 100644
--- a/.azurepipelines/signlistRelease.txt
+++ b/.azurepipelines/signlistRelease.txt
@@ -1,3 +1,4 @@
+Stack\Opc.Ua.Core\bin\Release\netstandard2.0\Opc.Ua.Core.dll
Stack\Opc.Ua.Core\bin\Release\netstandard2.1\Opc.Ua.Core.dll
Stack\Opc.Ua.Core\bin\Release\net48\Opc.Ua.Core.dll
Stack\Opc.Ua.Core\bin\Release\net6.0\Opc.Ua.Core.dll
@@ -30,6 +31,7 @@ Libraries\Opc.Ua.Gds.Server.Common\bin\Release\netstandard2.1\Opc.Ua.Gds.Server.
Libraries\Opc.Ua.Gds.Server.Common\bin\Release\net48\Opc.Ua.Gds.Server.Common.dll
Libraries\Opc.Ua.Gds.Server.Common\bin\Release\net6.0\Opc.Ua.Gds.Server.Common.dll
Libraries\Opc.Ua.Gds.Server.Common\bin\Release\net8.0\Opc.Ua.Gds.Server.Common.dll
+Libraries\Opc.Ua.Security.Certificates\bin\Release\netstandard2.0\Opc.Ua.Security.Certificates.dll
Libraries\Opc.Ua.Security.Certificates\bin\Release\netstandard2.1\Opc.Ua.Security.Certificates.dll
Libraries\Opc.Ua.Security.Certificates\bin\Release\net48\Opc.Ua.Security.Certificates.dll
Libraries\Opc.Ua.Security.Certificates\bin\Release\net6.0\Opc.Ua.Security.Certificates.dll
diff --git a/.azurepipelines/test.yml b/.azurepipelines/test.yml
index 089b20227..9a285c095 100644
--- a/.azurepipelines/test.yml
+++ b/.azurepipelines/test.yml
@@ -22,10 +22,11 @@ jobs:
inputs:
targetType: filePath
filePath: ./.azurepipelines/get-matrix.ps1
- arguments: -FileName azure-pipelines.yml -AgentTable ${{ parameters.agents }}
+ arguments: -FileName *.Tests.csproj -AgentTable ${{ parameters.agents }}
- job: testall${{ parameters.jobnamesuffix }}
displayName: Tests (${{ parameters.framework }})
dependsOn: testprep${{ parameters.jobnamesuffix }}
+ timeoutInMinutes: 120
strategy:
matrix: $[dependencies.testprep${{ parameters.jobnamesuffix }}.outputs['testmatrix.jobMatrix'] ]
variables:
@@ -62,12 +63,12 @@ jobs:
displayName: Restore ${{ parameters.configuration }}
inputs:
command: restore
- projects: '**/*.Tests.csproj'
+ projects: $(file)
arguments: '${{ variables.DotCliCommandline }} --configuration ${{ parameters.configuration }}'
- task: DotNetCoreCLI@2
displayName: Test ${{ parameters.configuration }}
- timeoutInMinutes: 60
+ timeoutInMinutes: 30
inputs:
command: test
- projects: '**/*.Tests.csproj'
+ projects: $(file)
arguments: '--no-restore ${{ variables.DotCliCommandline }} --configuration ${{ parameters.configuration }}'
diff --git a/.editorconfig b/.editorconfig
index 12e1f7923..1db323f25 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -336,6 +336,12 @@ dotnet_diagnostic.IDE1005.severity =
# CA1305: Specify IFormatProvider
dotnet_diagnostic.CA1305.severity = warning
+# CA1819: Properties should not return arrays
+dotnet_diagnostic.CA1819.severity = silent
+
+# CA1721: The property name is confusing given the existence of another method with the same name.
+dotnet_diagnostic.CA1721.severity = silent
+
# exclude generated code
[**/Generated/*.cs]
generated_code = true
diff --git a/Docs/NugetREADME.md b/Docs/NugetREADME.md
new file mode 100644
index 000000000..311df01d7
--- /dev/null
+++ b/Docs/NugetREADME.md
@@ -0,0 +1,36 @@
+# OPC UA .NET Standard stack documentation
+
+## Overview
+
+The OPC UA .NET Standard stack enables you to build multi-platform OPC UA Applications with rich functionality including:
+ - Server
+ - Client
+ - PubSub
+
+
+### For more information and license terms, see the projects official [Website](http://opcfoundation.github.io/UA-.NETStandard).
+
+## Getting started
+
+The reference [Client](https://github.com/OPCFoundation/UA-.NETStandard/tree/master/Applications/ConsoleReferenceClient) & [Server](https://github.com/OPCFoundation/UA-.NETStandard/tree/master/Applications/ReferenceServer) projects provide a starting point in implementing your own application.
+
+
+
+
+## Packages Overview
+
+Caution, there are multiple packages available with different functional scopes, for a detailed overview take a look at the [Information about the different Packages](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/PlatformBuild.md#further-information-on-the-supported-nuget-packages).
+
+## Additional documentation
+
+Additional information about the OPC UA .NET Standard stack is available on the GitHub Repo of the project in the [detailed Documentation](https://github.com/OPCFoundation/UA-.NETStandard/tree/master/Docs#opc-ua-net-standard-stack-documentation).
\ No newline at end of file
diff --git a/Docs/PlatformBuild.md b/Docs/PlatformBuild.md
index 82412d898..634aeba15 100644
--- a/Docs/PlatformBuild.md
+++ b/Docs/PlatformBuild.md
@@ -50,17 +50,17 @@ The following platform is deprecated but can still be built and tested:
To reduce the ci build overhead and the number of tests to be run in Visual Studio, only the tagged versions (* and **) are part of a qualifying ci build to pass a pull request.
All other platforms are only tested in weekly scheduled or manual ci builds.
-By default, in Visual Studio only the platforms tagged with (*) are tested. In order to test the other platforms in a command line window or in VS, there is a custom build variable defined to target a specific build. E.g. to target a .NETStandard2.1 build, the test runners are compiled with .NET 6.0 but the class libraries target only netstandard2.1, to force the use of that target.
+By default, in Visual Studio only the platforms tagged with (*) are tested. In order to test the other platforms in a command line window or in VS, there is a custom build variable defined to target a specific build. E.g. to target a .NETStandard2.0 build, the test runners are compiled with .NET 6.0 but the class libraries target only netstandard2.0, to force the use of that target.
Another option is to test run such a custom target in a command window with a batch file [CustomTest.bat](../Tests/customtest.bat) which is provided to clean up, restore the project and to run the tests. To run the custom tests in Visual Studio a section in [target.props](../targets.props) needs to be uncommented and the target platform value must be set.
```xml
- netstandard2.1
+ netstandard2.0
```
diff --git a/Libraries/Opc.Ua.Client/NodeCache.cs b/Libraries/Opc.Ua.Client/NodeCache.cs
index 66b6d2eb8..00dddacd2 100644
--- a/Libraries/Opc.Ua.Client/NodeCache.cs
+++ b/Libraries/Opc.Ua.Client/NodeCache.cs
@@ -32,6 +32,7 @@
using System.Linq;
using System.Reflection;
using System.Threading;
+using Opc.Ua.Redaction;
namespace Opc.Ua.Client
{
@@ -131,7 +132,7 @@ public INode Find(ExpandedNodeId nodeId)
}
catch (Exception e)
{
- Utils.LogError("Could not fetch node from server: NodeId={0}, Reason='{1}'.", nodeId, e.Message);
+ Utils.LogError("Could not fetch node from server: NodeId={0}, Reason='{1}'.", nodeId, Redact.Create(e));
// m_nodes[nodeId] = null;
return null;
}
@@ -856,7 +857,7 @@ public Node FetchNode(ExpandedNodeId nodeId)
}
catch (Exception e)
{
- Utils.LogError("Could not fetch references for valid node with NodeId = {0}. Error = {1}", nodeId, e.Message);
+ Utils.LogError("Could not fetch references for valid node with NodeId = {0}. Error = {1}", nodeId, Redact.Create(e));
}
InternalWriteLockedAttach(source);
diff --git a/Libraries/Opc.Ua.Client/NodeCacheAsync.cs b/Libraries/Opc.Ua.Client/NodeCacheAsync.cs
index a7da676d7..ebd8b1ea2 100644
--- a/Libraries/Opc.Ua.Client/NodeCacheAsync.cs
+++ b/Libraries/Opc.Ua.Client/NodeCacheAsync.cs
@@ -34,6 +34,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Opc.Ua.Redaction;
namespace Opc.Ua.Client
{
@@ -80,7 +81,7 @@ public async Task FindAsync(ExpandedNodeId nodeId, CancellationToken ct =
}
catch (Exception e)
{
- Utils.LogError("Could not fetch node from server: NodeId={0}, Reason='{1}'.", nodeId, e.Message);
+ Utils.LogError("Could not fetch node from server: NodeId={0}, Reason='{1}'.", nodeId, Redact.Create(e));
// m_nodes[nodeId] = null;
return null;
}
@@ -263,7 +264,7 @@ public async Task FetchNodeAsync(ExpandedNodeId nodeId, CancellationToken
}
catch (Exception e)
{
- Utils.LogError("Could not fetch references for valid node with NodeId = {0}. Error = {1}", nodeId, e.Message);
+ Utils.LogError("Could not fetch references for valid node with NodeId = {0}. Error = {1}", nodeId, Redact.Create(e));
}
InternalWriteLockedAttach(source);
diff --git a/Libraries/Opc.Ua.Client/SessionReconnectHandler.cs b/Libraries/Opc.Ua.Client/SessionReconnectHandler.cs
index 0de7799d8..9ec0ac072 100644
--- a/Libraries/Opc.Ua.Client/SessionReconnectHandler.cs
+++ b/Libraries/Opc.Ua.Client/SessionReconnectHandler.cs
@@ -30,6 +30,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
+using Opc.Ua.Redaction;
namespace Opc.Ua.Client
{
@@ -339,7 +340,7 @@ await DoReconnectAsync().ConfigureAwait(false))
}
catch (Exception exception)
{
- Utils.LogError(exception, "Unexpected error during reconnect.");
+ Utils.LogError("Unexpected error during reconnect: {0}", Redact.Create(exception));
}
// schedule the next reconnect.
@@ -510,17 +511,17 @@ await endpoint.UpdateFromServerAsync(
{
m_reconnectPeriod = m_baseReconnectPeriod;
}
- Utils.LogError("Could not reconnect due to failed security check. Request endpoint update from server. {0}", sre.Message);
+ Utils.LogError("Could not reconnect due to failed security check. Request endpoint update from server. {0}", Redact.Create(sre));
}
else
{
- Utils.LogError("Could not reconnect the Session. {0}", sre.Message);
+ Utils.LogError("Could not reconnect the Session. {0}", Redact.Create(sre));
}
return false;
}
catch (Exception exception)
{
- Utils.LogError("Could not reconnect the Session. {0}", exception.Message);
+ Utils.LogError("Could not reconnect the Session. {0}", Redact.Create(exception));
return false;
}
finally
diff --git a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs
index da509a13e..64dba21e1 100644
--- a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs
+++ b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs
@@ -999,7 +999,7 @@ private static async Task AddToTrustedStoreAsync(ApplicationConfiguration config
}
catch (Exception e)
{
- Utils.LogError(e, "Could not add certificate to trusted peer store.");
+ Utils.LogError("Could not add certificate to trusted peer store: {0}", Redaction.Redact.Create(e));
}
}
diff --git a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs
index 982915e96..51ede019f 100644
--- a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs
+++ b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs
@@ -1,5 +1,5 @@
/* ========================================================================
- * Copyright (c) 2005-2020 The OPC Foundation, Inc. All rights reserved.
+ * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
@@ -35,6 +35,7 @@
using System.Text;
using System.Threading.Tasks;
using Opc.Ua.Gds.Server.Database;
+using Opc.Ua.Gds.Server.Diagnostics;
using Opc.Ua.Server;
namespace Opc.Ua.Gds.Server
@@ -508,6 +509,12 @@ private ServiceResult OnRegisterApplication(
applicationId = m_database.RegisterApplication(application);
+ if (applicationId != null)
+ {
+ object[] inputArguments = new object[] { application, applicationId };
+ Server.ReportApplicationRegistrationChangedAuditEvent(context, objectId, method, inputArguments);
+ }
+
return ServiceResult.Good;
}
@@ -530,6 +537,9 @@ private ServiceResult OnUpdateApplication(
m_database.RegisterApplication(application);
+ object[] inputArguments = new object[] { application };
+ Server.ReportApplicationRegistrationChangedAuditEvent(context, objectId, method, inputArguments);
+
return ServiceResult.Good;
}
@@ -539,7 +549,7 @@ private ServiceResult OnUnregisterApplication(
NodeId objectId,
NodeId applicationId)
{
- AuthorizationHelper.HasAuthorization(context, AuthorizationHelper.DiscoveryAdmin);
+ AuthorizationHelper.HasAuthorization(context, AuthorizationHelper.DiscoveryAdminOrSelfAdmin);
Utils.LogInfo("OnUnregisterApplication: {0}", applicationId.ToString());
@@ -564,6 +574,9 @@ private ServiceResult OnUnregisterApplication(
m_database.UnregisterApplication(applicationId);
+ object[] inputArguments = new object[] { applicationId };
+ Server.ReportApplicationRegistrationChangedAuditEvent(context, objectId, method, inputArguments);
+
return ServiceResult.Good;
}
@@ -825,6 +838,9 @@ private ServiceResult OnStartNewKeyPairRequest(
string privateKeyPassword,
ref NodeId requestId)
{
+ object[] inputArguments = new object[] { applicationId, certificateGroupId, certificateTypeId, subjectName, domainNames, privateKeyFormat, privateKeyPassword };
+ Server.ReportCertificateRequestedAuditEvent(context, objectId, method, inputArguments, certificateGroupId, certificateTypeId);
+
AuthorizationHelper.HasAuthorization(context, AuthorizationHelper.CertificateAuthorityAdminOrSelfAdmin, applicationId); ;
var application = m_database.GetApplication(applicationId);
@@ -1183,6 +1199,9 @@ out privateKeyPassword
m_request.AcceptRequest(requestId, signedCertificate);
+ object[] inputArguments = new object[] { applicationId, requestId, signedCertificate, privateKey, issuerCertificates };
+ Server.ReportCertificateDeliveredAuditEvent(context, objectId, method, inputArguments);
+
return ServiceResult.Good;
}
diff --git a/Libraries/Opc.Ua.Gds.Server.Common/Diagnostics/AuditEvents.cs b/Libraries/Opc.Ua.Gds.Server.Common/Diagnostics/AuditEvents.cs
new file mode 100644
index 000000000..9834cc16a
--- /dev/null
+++ b/Libraries/Opc.Ua.Gds.Server.Common/Diagnostics/AuditEvents.cs
@@ -0,0 +1,198 @@
+/* ========================================================================
+ * Copyright (c) 2005-2024 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 Opc.Ua.Server;
+
+namespace Opc.Ua.Gds.Server.Diagnostics
+{
+ public static class AuditEvents
+ {
+ ///
+ /// Raise CertificateDeliveredAudit event
+ ///
+ /// The server which reports audit events.
+ /// The current system context.
+ /// The id of the object used for the method
+ /// The method that triggered the audit event.
+ /// The input arguments used to call the method that triggered the audit event.
+ public static void ReportCertificateDeliveredAuditEvent(
+ this IAuditEventServer server,
+ ISystemContext systemContext,
+ NodeId objectId,
+ MethodState method,
+ object[] inputArguments)
+ {
+ try
+ {
+ CertificateDeliveredAuditEventState e = new CertificateDeliveredAuditEventState(null);
+
+ TranslationInfo message = new TranslationInfo(
+ "CertificateUpdateRequestedAuditEvent",
+ "en-US",
+ "CertificateUpdateRequestedAuditEvent.");
+
+ e.Initialize(
+ systemContext,
+ null,
+ EventSeverity.Min,
+ new LocalizedText(message),
+ true,
+ DateTime.UtcNow); // initializes Status, ActionTimeStamp, ServerId, ClientAuditEntryId, ClientUserId
+
+ e.SetChildValue(systemContext, Ua.BrowseNames.SourceNode, objectId, false);
+ e.SetChildValue(systemContext, Ua.BrowseNames.SourceName, "Method/UpdateCertificate", false);
+ e.SetChildValue(systemContext, Ua.BrowseNames.LocalTime, Utils.GetTimeZoneInfo(), false);
+
+ e.SetChildValue(systemContext, Ua.BrowseNames.MethodId, method?.NodeId, false);
+ e.SetChildValue(systemContext, Ua.BrowseNames.InputArguments, inputArguments, false);
+
+ server?.ReportAuditEvent(systemContext, e);
+ }
+ catch (Exception ex)
+ {
+ Utils.LogError(ex, "Error while reporting CertificateDeliveredAuditEventState event.");
+ }
+ }
+
+ ///
+ /// Raise CertificateDeliveredAudit event
+ ///
+ /// The server which reports audit events.
+ /// The current system context.
+ /// The id of the object used for the method
+ /// The method that triggered the audit event.
+ /// The input arguments used to call the method that triggered the audit event.
+ /// The id of the certificate group
+ /// the certificate ype id
+ /// The exception resulted after executing the StartNewKeyPairRequest StartNewSigningRequest method. If null, the operation was successfull.
+ public static void ReportCertificateRequestedAuditEvent(
+ this IAuditEventServer server,
+ ISystemContext systemContext,
+ NodeId objectId,
+ MethodState method,
+ object[] inputArguments,
+ NodeId certificateGroupId,
+ NodeId certificateTypeId,
+ Exception exception = null)
+ {
+ try
+ {
+ CertificateRequestedAuditEventState e = new CertificateRequestedAuditEventState(null);
+
+ TranslationInfo message = null;
+ if (exception == null)
+ {
+ message = new TranslationInfo(
+ "CertificateRequestedAuditEvent",
+ "en-US",
+ "CertificateRequestedAuditEvent.");
+ }
+ else
+ {
+ message = new TranslationInfo(
+ "CertificateRequestedAuditEvent",
+ "en-US",
+ $"CertificateRequestedAuditEvent - Exception: {exception.Message}.");
+ }
+
+ e.Initialize(
+ systemContext,
+ null,
+ EventSeverity.Min,
+ new LocalizedText(message),
+ exception == null,
+ DateTime.UtcNow); // initializes Status, ActionTimeStamp, ServerId, ClientAuditEntryId, ClientUserId
+
+ e.SetChildValue(systemContext, Ua.BrowseNames.SourceNode, objectId, false);
+ e.SetChildValue(systemContext, Ua.BrowseNames.SourceName, "Method/UpdateCertificate", false);
+ e.SetChildValue(systemContext, Ua.BrowseNames.LocalTime, Utils.GetTimeZoneInfo(), false);
+
+ e.SetChildValue(systemContext, Ua.BrowseNames.MethodId, method?.NodeId, false);
+ e.SetChildValue(systemContext, Ua.BrowseNames.InputArguments, inputArguments, false);
+
+ e.SetChildValue(systemContext, BrowseNames.CertificateGroup, certificateGroupId, false);
+ e.SetChildValue(systemContext, BrowseNames.CertificateType, certificateTypeId, false);
+
+ server?.ReportAuditEvent(systemContext, e);
+ }
+ catch (Exception ex)
+ {
+ Utils.LogError(ex, "Error while reporting CertificateDeliveredAuditEventState event.");
+ }
+ }
+
+ ///
+ /// Raise ApplicationRegistrationChanged event
+ ///
+ /// The server which reports audit events.
+ /// The current system context.
+ /// The id of the object used for register Application method
+ /// The method that triggered the audit event.
+ /// The input arguments used to call the method that triggered the audit event.
+ public static void ReportApplicationRegistrationChangedAuditEvent(
+ this IAuditEventServer server,
+ ISystemContext systemContext,
+ NodeId objectId,
+ MethodState method,
+ object[] inputArguments)
+ {
+ try
+ {
+ ApplicationRegistrationChangedAuditEventState e = new ApplicationRegistrationChangedAuditEventState(null);
+
+ var message = new TranslationInfo(
+ "ApplicationRegistrationChangedAuditEvent",
+ "en-US",
+ "ApplicationRegistrationChangedAuditEvent.");
+
+ e.Initialize(
+ systemContext,
+ null,
+ EventSeverity.Min,
+ new LocalizedText(message),
+ true,
+ DateTime.UtcNow); // initializes Status, ActionTimeStamp, ServerId, ClientAuditEntryId, ClientUserId
+
+ e.SetChildValue(systemContext, Ua.BrowseNames.SourceNode, objectId, false);
+ e.SetChildValue(systemContext, Ua.BrowseNames.SourceName, "Method/UpdateCertificate", false);
+ e.SetChildValue(systemContext, Ua.BrowseNames.LocalTime, Utils.GetTimeZoneInfo(), false);
+
+ e.SetChildValue(systemContext, Ua.BrowseNames.MethodId, method?.NodeId, false);
+ e.SetChildValue(systemContext, Ua.BrowseNames.InputArguments, inputArguments, false);
+
+ server?.ReportAuditEvent(systemContext, e);
+ }
+ catch (Exception ex)
+ {
+ Utils.LogError(ex, "Error while reporting CertificateDeliveredAuditEventState event.");
+ }
+ }
+ }
+}
diff --git a/Libraries/Opc.Ua.Gds.Server.Common/RoleBasedUserManagement/AuthorizationHelper.cs b/Libraries/Opc.Ua.Gds.Server.Common/RoleBasedUserManagement/AuthorizationHelper.cs
index 228591e5a..a60920293 100644
--- a/Libraries/Opc.Ua.Gds.Server.Common/RoleBasedUserManagement/AuthorizationHelper.cs
+++ b/Libraries/Opc.Ua.Gds.Server.Common/RoleBasedUserManagement/AuthorizationHelper.cs
@@ -41,6 +41,7 @@ internal static class AuthorizationHelper
{
internal static List AuthenticatedUser { get; } = new List { Role.AuthenticatedUser };
internal static List DiscoveryAdmin { get; } = new List { GdsRole.DiscoveryAdmin };
+ internal static List DiscoveryAdminOrSelfAdmin { get; } = new List { GdsRole.DiscoveryAdmin, GdsRole.ApplicationSelfAdmin };
internal static List AuthenticatedUserOrSelfAdmin { get; } = new List { Role.AuthenticatedUser, GdsRole.ApplicationSelfAdmin };
internal static List CertificateAuthorityAdminOrSelfAdmin { get; } = new List { GdsRole.CertificateAuthorityAdmin, GdsRole.ApplicationSelfAdmin };
internal static List CertificateAuthorityAdmin { get; } = new List { GdsRole.CertificateAuthorityAdmin };
diff --git a/Libraries/Opc.Ua.Security.Certificates/Opc.Ua.Security.Certificates.csproj b/Libraries/Opc.Ua.Security.Certificates/Opc.Ua.Security.Certificates.csproj
index ce5446355..47b533cc3 100644
--- a/Libraries/Opc.Ua.Security.Certificates/Opc.Ua.Security.Certificates.csproj
+++ b/Libraries/Opc.Ua.Security.Certificates/Opc.Ua.Security.Certificates.csproj
@@ -2,7 +2,7 @@
Opc.Ua.Security.Certificates
- $(LibTargetFrameworks)
+ $(LibCoreTargetFrameworks)
OPCFoundation.NetStandard.Opc.Ua.Security.Certificates
Opc.Ua.Security.Certificates
OPC UA Security X509 Certificates Class Library
diff --git a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs
index 5973b952c..0350b5522 100644
--- a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs
+++ b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs
@@ -350,6 +350,7 @@ private ServiceResult UpdateCertificate(
object[] inputArguments = new object[] { certificateGroupId, certificateTypeId, certificate, issuerCertificates, privateKeyFormat, privateKey };
X509Certificate2 newCert = null;
+ Server.ReportCertificateUpdateRequestedAuditEvent(context, objectId, method, inputArguments);
try
{
if (certificate == null)
diff --git a/Libraries/Opc.Ua.Server/Configuration/TrustList.cs b/Libraries/Opc.Ua.Server/Configuration/TrustList.cs
index 3c16d62d5..202f014ec 100644
--- a/Libraries/Opc.Ua.Server/Configuration/TrustList.cs
+++ b/Libraries/Opc.Ua.Server/Configuration/TrustList.cs
@@ -302,6 +302,8 @@ private ServiceResult CloseAndUpdate(
uint fileHandle,
ref bool restartRequired)
{
+ object[] inputParameters = new object[] { fileHandle };
+ m_node.ReportTrustListUpdateRequestedAuditEvent(context, objectId, "Method/CloseAndUpdate", method.NodeId, inputParameters);
HasSecureWriteAccess(context);
ServiceResult result = StatusCodes.Good;
@@ -416,7 +418,6 @@ private ServiceResult CloseAndUpdate(
restartRequired = false;
// report the TrustListUpdatedAuditEvent
- object[] inputParameters = new object[] { fileHandle };
m_node.ReportTrustListUpdatedAuditEvent(context, objectId, "Method/CloseAndUpdate", method.NodeId, inputParameters, result.StatusCode);
return result;
@@ -429,6 +430,8 @@ private ServiceResult AddCertificate(
byte[] certificate,
bool isTrustedCertificate)
{
+ object[] inputParameters = new object[] { certificate, isTrustedCertificate };
+ m_node.ReportTrustListUpdateRequestedAuditEvent(context, objectId, "Method/AddCertificate", method.NodeId, inputParameters);
HasSecureWriteAccess(context);
ServiceResult result = StatusCodes.Good;
@@ -471,7 +474,6 @@ private ServiceResult AddCertificate(
}
// report the TrustListUpdatedAuditEvent
- object[] inputParameters = new object[] { certificate, isTrustedCertificate };
m_node.ReportTrustListUpdatedAuditEvent(context, objectId, "Method/AddCertificate", method.NodeId, inputParameters, result.StatusCode);
return result;
@@ -485,6 +487,9 @@ private ServiceResult RemoveCertificate(
string thumbprint,
bool isTrustedCertificate)
{
+ object[] inputParameters = new object[] { thumbprint };
+ m_node.ReportTrustListUpdateRequestedAuditEvent(context, objectId, "Method/RemoveCertificate", method.NodeId, inputParameters);
+
HasSecureWriteAccess(context);
ServiceResult result = StatusCodes.Good;
lock (m_lock)
@@ -548,7 +553,6 @@ private ServiceResult RemoveCertificate(
}
// report the TrustListUpdatedAuditEvent
- object[] inputParameters = new object[] { thumbprint };
m_node.ReportTrustListUpdatedAuditEvent(context, objectId, "Method/RemoveCertificate", method.NodeId, inputParameters, result.StatusCode);
return result;
diff --git a/Libraries/Opc.Ua.Server/Diagnostics/AuditEvents.cs b/Libraries/Opc.Ua.Server/Diagnostics/AuditEvents.cs
index 073f10396..07497f9a4 100644
--- a/Libraries/Opc.Ua.Server/Diagnostics/AuditEvents.cs
+++ b/Libraries/Opc.Ua.Server/Diagnostics/AuditEvents.cs
@@ -1010,7 +1010,7 @@ public static void ReportAuditTransferSubscriptionEvent(
///
/// The server which reports audit events.
/// The current system context.
- /// The id of the object ued for update certificate method
+ /// The id of the object used for update certificate method
/// The method that triggered the audit event.
/// The input arguments used to call the method that triggered the audit event.
/// The id of the certificate group
@@ -1072,6 +1072,55 @@ public static void ReportCertificateUpdatedAuditEvent(
}
}
+ ///
+ /// Raise CertificateUpdateRequestedAudit event
+ ///
+ /// The server which reports audit events.
+ /// The current system context.
+ /// The id of the object used for update certificate method
+ /// The method that triggered the audit event.
+ /// The input arguments used to call the method that triggered the audit event.
+ public static void ReportCertificateUpdateRequestedAuditEvent(
+ this IAuditEventServer server,
+ ISystemContext systemContext,
+ NodeId objectId,
+ MethodState method,
+ object[] inputArguments)
+ {
+ try
+ {
+ CertificateUpdateRequestedAuditEventState e = new CertificateUpdateRequestedAuditEventState(null);
+
+ TranslationInfo message = new TranslationInfo(
+ "CertificateUpdateRequestedAuditEvent",
+ "en-US",
+ "CertificateUpdateRequestedAuditEvent.");
+
+
+
+ e.Initialize(
+ systemContext,
+ null,
+ EventSeverity.Min,
+ new LocalizedText(message),
+ true,
+ DateTime.UtcNow); // initializes Status, ActionTimeStamp, ServerId, ClientAuditEntryId, ClientUserId
+
+ e.SetChildValue(systemContext, BrowseNames.SourceNode, objectId, false);
+ e.SetChildValue(systemContext, BrowseNames.SourceName, "Method/UpdateCertificate", false);
+ e.SetChildValue(systemContext, BrowseNames.LocalTime, Utils.GetTimeZoneInfo(), false);
+
+ e.SetChildValue(systemContext, BrowseNames.MethodId, method?.NodeId, false);
+ e.SetChildValue(systemContext, BrowseNames.InputArguments, inputArguments, false);
+
+ server?.ReportAuditEvent(systemContext, e);
+ }
+ catch (Exception ex)
+ {
+ Utils.LogError(ex, "Error while reporting CertificateUpdateRequestedAuditEvent event.");
+ }
+ }
+
///
/// Report the AuditAddNodesEvent
///
@@ -1470,6 +1519,55 @@ public static void ReportTrustListUpdatedAuditEvent(
Utils.LogError(ex, "Error while reporting ReportTrustListUpdatedAuditEvent event.");
}
}
+
+ ///
+ /// Reports an TrustListUpdatedAudit event.
+ ///
+ /// The trustlist node.
+ /// The current system context
+ /// The object id where the truest list update methods was called
+ /// The source name string
+ /// The id of the method that was called
+ /// The input parameters of the called method
+ public static void ReportTrustListUpdateRequestedAuditEvent(
+ this TrustListState node,
+ ISystemContext systemContext,
+ NodeId objectId,
+ string sourceName,
+ NodeId methodId,
+ object[] inputParameters)
+ {
+ try
+ {
+ TrustListUpdateRequestedAuditEventState e = new TrustListUpdateRequestedAuditEventState(null);
+
+ TranslationInfo message = new TranslationInfo(
+ "TrustListUpdateRequestedAuditEvent",
+ "en-US",
+ $"TrustListUpdateRequestedAuditEvent.");
+
+ e.Initialize(
+ systemContext,
+ null,
+ EventSeverity.Min,
+ new LocalizedText(message),
+ true,
+ DateTime.UtcNow); // initializes Status, ActionTimeStamp, ServerId, ClientAuditEntryId, ClientUserId
+
+ e.SetChildValue(systemContext, BrowseNames.SourceNode, objectId, false);
+ e.SetChildValue(systemContext, BrowseNames.SourceName, sourceName, false);
+ e.SetChildValue(systemContext, BrowseNames.LocalTime, Utils.GetTimeZoneInfo(), false);
+
+ e.SetChildValue(systemContext, BrowseNames.MethodId, methodId, false);
+ e.SetChildValue(systemContext, BrowseNames.InputArguments, inputParameters, false);
+
+ node?.ReportEvent(systemContext, e);
+ }
+ catch (Exception ex)
+ {
+ Utils.LogError(ex, "Error while reporting TrustListUpdateRequestedAuditEvent event.");
+ }
+ }
#endregion Report Audit Events
#region Private helpers
diff --git a/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj b/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj
index f91439637..2ce01329f 100644
--- a/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj
+++ b/Stack/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj
@@ -49,7 +49,7 @@
-
+
diff --git a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj
index 0c5fa65e7..4874bd731 100644
--- a/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj
+++ b/Stack/Opc.Ua.Core/Opc.Ua.Core.csproj
@@ -2,7 +2,7 @@
$(DefineConstants);NET_STANDARD;NET_STANDARD_ASYNC
- $(LibTargetFrameworks)
+ $(LibCoreTargetFrameworks)
Opc.Ua.Core
OPCFoundation.NetStandard.Opc.Ua.Core
Opc.Ua
diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateStoreIdentifier.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateStoreIdentifier.cs
index a08e22083..403ecc924 100644
--- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateStoreIdentifier.cs
+++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateStoreIdentifier.cs
@@ -189,18 +189,19 @@ public virtual ICertificateStore OpenStore()
store.Open(this.StorePath);
return store;
}
-
///
/// Returns an object to access the store containing the certificates.
///
///
/// Opens an instance of the store which contains public keys.
///
+ /// location of the store
+ /// Indicates whether NO private keys are found in the store. Default true.
/// A disposable instance of the .
- public static ICertificateStore OpenStore(string path)
+ public static ICertificateStore OpenStore(string path, bool noPrivateKeys = true)
{
ICertificateStore store = CertificateStoreIdentifier.CreateStore(CertificateStoreIdentifier.DetermineStoreType(path));
- store.Open(path);
+ store.Open(path, noPrivateKeys);
return store;
}
#endregion
diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs
index 9de96a07e..2b445d6fb 100644
--- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs
+++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs
@@ -20,6 +20,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
+using Opc.Ua.Redaction;
using Opc.Ua.Security.Certificates;
namespace Opc.Ua
@@ -385,7 +386,7 @@ public ushort MinimumCertificateKeySize
if (m_minimumCertificateKeySize != value)
{
m_minimumCertificateKeySize = value;
- ResetValidatedCertificates();
+ InternalResetValidatedCertificates();
}
}
finally
@@ -411,14 +412,13 @@ public bool UseValidatedCertificates
if (m_useValidatedCertificates != value)
{
m_useValidatedCertificates = value;
- ResetValidatedCertificates();
+ InternalResetValidatedCertificates();
}
}
finally
{
m_semaphore.Release();
}
-
}
}
@@ -1481,13 +1481,13 @@ public void ValidateDomains(X509Certificate2 serverCertificate, ConfiguredEndpoi
{
if (serverValidation)
{
- Utils.LogError(message, endpointUrl.DnsSafeHost);
+ Utils.LogError(message, Redact.Create(endpointUrl));
}
else
{
// write the invalid certificate to rejected store if specified.
Utils.LogCertificate(LogLevel.Error, "Certificate rejected. Reason={0}.",
- serverCertificate, serviceResult != null ? serviceResult.ToString() : "Unknown Error");
+ serverCertificate, Redact.Create(serviceResult));
SaveCertificate(serverCertificate);
}
diff --git a/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs b/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs
index 4042b2545..b897ba1e2 100644
--- a/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs
+++ b/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs
@@ -19,6 +19,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
using System.Text;
using System.Threading.Tasks;
using Opc.Ua.Security.Certificates;
+using Opc.Ua.Redaction;
namespace Opc.Ua
{
@@ -479,7 +480,7 @@ public async Task LoadPrivateKey(string thumbprint, string sub
// found a certificate, but some error occurred
if (certificateFound)
{
- Utils.LogError(Utils.TraceMasks.Security, "The private key for the certificate with subject {0} failed to import.", subjectName);
+ Utils.LogError(Utils.TraceMasks.Security, "The private key for the certificate with subject {0} failed to import.", Redact.Create(subjectName));
if (importException != null)
{
Utils.LogError(importException, "Certificate import failed.");
diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs b/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs
index 93d61973e..8c4a53147 100644
--- a/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs
+++ b/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs
@@ -10,7 +10,12 @@
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
+#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER
+#define STREAM_WITH_SPAN_SUPPORT
+#endif
+
using System;
+using System.Buffers;
using System.IO;
using System.Runtime.CompilerServices;
@@ -63,6 +68,37 @@ public ArraySegmentStream(
SetCurrentBuffer(0);
}
+
+ ///
+ /// Creates a writeable stream that creates buffers as necessary.
+ ///
+ /// The buffer manager.
+ public ArraySegmentStream(BufferManager bufferManager)
+ : this(bufferManager, bufferManager.MaxSuggestedBufferSize, 0, bufferManager.MaxSuggestedBufferSize)
+ {
+ }
+ #endregion
+
+ #region IDisposable
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ if (m_buffers != null && m_bufferManager != null)
+ {
+ for (int ii = 0; ii < m_buffers.Count; ii++)
+ {
+ m_bufferManager.ReturnBuffer(m_buffers[ii].Array, "ArraySegmentStream.Dispose");
+ }
+ m_buffers.Clear();
+ m_buffers = null;
+ }
+ m_bufferManager = null;
+ }
+
+ base.Dispose(disposing);
+ }
#endregion
#region Public Methods
@@ -81,44 +117,77 @@ public BufferCollection GetBuffers(string owner)
buffers.Add(new ArraySegment(m_buffers[ii].Array, m_buffers[ii].Offset, GetBufferCount(ii)));
}
- m_buffers.Clear();
+ ClearBuffers();
return buffers;
}
+
+ ///
+ /// Returns sequence of the buffers stored in the stream.
+ ///
+ ///
+ /// The buffers ownership is transferred to the sequence,
+ /// the stream can be disposed.
+ /// The new owner is responisble to dispose the sequence after use.
+ ///
+ public BufferSequence GetSequence(string owner)
+ {
+ if (m_buffers.Count == 0)
+ {
+ return new BufferSequence(m_bufferManager, owner, null, ReadOnlySequence.Empty);
+ }
+
+ int endIndex = GetBufferCount(0);
+ var firstSegment = new BufferSegment(m_buffers[0].Array, m_buffers[0].Offset, endIndex);
+ m_bufferManager.TransferBuffer(m_buffers[0].Array, owner);
+ BufferSegment nextSegment = firstSegment;
+ for (int ii = 1; ii < m_buffers.Count; ii++)
+ {
+ m_bufferManager.TransferBuffer(m_buffers[ii].Array, owner);
+ endIndex = GetBufferCount(ii);
+ nextSegment = nextSegment.Append(m_buffers[ii].Array, m_buffers[ii].Offset, endIndex);
+ }
+
+ var sequence = new ReadOnlySequence(firstSegment, 0, nextSegment, endIndex);
+
+ ClearBuffers();
+
+ return new BufferSequence(m_bufferManager, owner, firstSegment, sequence);
+ }
#endregion
#region Overridden Methods
- ///
+ ///
public override bool CanRead
{
- get { return true; }
+ get { return m_buffers != null; }
}
- ///
+ ///
public override bool CanSeek
{
- get { return true; }
+ get { return m_buffers != null; }
}
- ///
+ ///
public override bool CanWrite
{
- get { return true; }
+ get { return m_buffers != null; }
}
- ///
+ ///
public override void Flush()
{
// nothing to do.
}
- ///
+ ///
public override long Length
{
get { return GetAbsoluteLength(); }
}
- ///
+ ///
public override long Position
{
get
@@ -132,7 +201,7 @@ public override long Position
}
}
- ///
+ ///
public override int ReadByte()
{
do
@@ -148,7 +217,11 @@ public override int ReadByte()
// copy the bytes requested.
if (bytesLeft > 0)
{
+#if STREAM_WITH_SPAN_SUPPORT
+ return m_currentBuffer[m_currentPosition++];
+#else
return m_currentBuffer.Array[m_currentBuffer.Offset + m_currentPosition++];
+#endif
}
// move to next buffer.
@@ -157,7 +230,54 @@ public override int ReadByte()
} while (true);
}
- ///
+#if STREAM_WITH_SPAN_SUPPORT
+ ///
+ /// Helper to benchmark the performance of the stream.
+ ///
+ internal int ReadMemoryStream(Span buffer) => base.Read(buffer);
+
+ ///
+ public override int Read(Span buffer)
+ {
+ int count = buffer.Length;
+ int offset = 0;
+ int bytesRead = 0;
+
+ while (count > 0)
+ {
+ // check for end of stream.
+ if (m_currentBuffer.Array == null)
+ {
+ return bytesRead;
+ }
+
+ int bytesLeft = GetBufferCount(m_bufferIndex) - m_currentPosition;
+
+ // copy the bytes requested.
+ if (bytesLeft > count)
+ {
+ m_currentBuffer.AsSpan(m_currentPosition, count).CopyTo(buffer.Slice(offset));
+ bytesRead += count;
+ m_currentPosition += count;
+ return bytesRead;
+ }
+
+ // copy the bytes available and move to next buffer.
+ m_currentBuffer.AsSpan(m_currentPosition, bytesLeft).CopyTo(buffer.Slice(offset));
+ bytesRead += bytesLeft;
+
+ offset += bytesLeft;
+ count -= bytesLeft;
+
+ // move to next buffer.
+ SetCurrentBuffer(m_bufferIndex + 1);
+ }
+
+ return bytesRead;
+ }
+#endif
+
+ ///
public override int Read(byte[] buffer, int offset, int count)
{
int bytesRead = 0;
@@ -195,7 +315,7 @@ public override int Read(byte[] buffer, int offset, int count)
return bytesRead;
}
- ///
+ ///
public override long Seek(long offset, SeekOrigin origin)
{
switch (origin)
@@ -223,6 +343,13 @@ public override long Seek(long offset, SeekOrigin origin)
throw new IOException("Cannot seek beyond the beginning of the stream.");
}
+ // special case
+ if (offset == 0)
+ {
+ SetCurrentBuffer(0);
+ return 0;
+ }
+
int position = (int)offset;
if (position >= GetAbsolutePosition())
@@ -247,13 +374,13 @@ public override long Seek(long offset, SeekOrigin origin)
throw new IOException("Cannot seek beyond the end of the stream.");
}
- ///
+ ///
public override void SetLength(long value)
{
throw new NotSupportedException();
}
- ///
+ ///
public override void WriteByte(byte value)
{
do
@@ -266,16 +393,12 @@ public override void WriteByte(byte value)
// copy the byte requested.
if (bytesLeft >= 1)
{
+#if STREAM_WITH_SPAN_SUPPORT
+ m_currentBuffer[m_currentPosition] = value;
+#else
m_currentBuffer.Array[m_currentBuffer.Offset + m_currentPosition] = value;
- m_currentPosition++;
-
- if (m_bufferIndex == m_buffers.Count - 1)
- {
- if (m_endOfLastBuffer < m_currentPosition)
- {
- m_endOfLastBuffer = m_currentPosition;
- }
- }
+#endif
+ UpdateCurrentPosition(1);
return;
}
@@ -286,7 +409,47 @@ public override void WriteByte(byte value)
} while (true);
}
- ///
+#if STREAM_WITH_SPAN_SUPPORT
+ ///
+ /// Helper to benchmark the performance of the stream.
+ ///
+ internal void WriteMemoryStream(ReadOnlySpan buffer) => base.Write(buffer);
+
+ ///
+ public override void Write(ReadOnlySpan buffer)
+ {
+ int count = buffer.Length;
+ int offset = 0;
+ while (count > 0)
+ {
+ // check for end of stream.
+ CheckEndOfStream();
+
+ int bytesLeft = m_currentBuffer.Count - m_currentPosition;
+
+ // copy the bytes requested.
+ if (bytesLeft >= count)
+ {
+ buffer.Slice(offset, count).CopyTo(m_currentBuffer.AsSpan(m_currentPosition));
+
+ UpdateCurrentPosition(count);
+
+ return;
+ }
+
+ // copy the bytes available and move to next buffer.
+ buffer.Slice(offset, bytesLeft).CopyTo(m_currentBuffer.AsSpan(m_currentPosition));
+
+ offset += bytesLeft;
+ count -= bytesLeft;
+
+ // move to next buffer.
+ SetCurrentBuffer(m_bufferIndex + 1);
+ }
+ }
+#endif
+
+ ///
public override void Write(byte[] buffer, int offset, int count)
{
while (count > 0)
@@ -301,15 +464,7 @@ public override void Write(byte[] buffer, int offset, int count)
{
Array.Copy(buffer, offset, m_currentBuffer.Array, m_currentPosition + m_currentBuffer.Offset, count);
- m_currentPosition += count;
-
- if (m_bufferIndex == m_buffers.Count - 1)
- {
- if (m_endOfLastBuffer < m_currentPosition)
- {
- m_endOfLastBuffer = m_currentPosition;
- }
- }
+ UpdateCurrentPosition(count);
return;
}
@@ -325,9 +480,14 @@ public override void Write(byte[] buffer, int offset, int count)
}
}
- ///
+ ///
public override byte[] ToArray()
{
+ if (m_buffers == null)
+ {
+ throw new ObjectDisposedException(nameof(ArraySegmentStream));
+ }
+
int absoluteLength = GetAbsoluteLength();
if (absoluteLength == 0)
{
@@ -353,6 +513,23 @@ public override byte[] ToArray()
#endregion
#region Private Methods
+ ///
+ /// Update the current buffer count.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void UpdateCurrentPosition(int count)
+ {
+ m_currentPosition += count;
+
+ if (m_bufferIndex == m_buffers.Count - 1)
+ {
+ if (m_endOfLastBuffer < m_currentPosition)
+ {
+ m_endOfLastBuffer = m_currentPosition;
+ }
+ }
+ }
+
///
/// Sets the current buffer.
///
@@ -446,6 +623,17 @@ private void CheckEndOfStream()
SetCurrentBuffer(m_buffers.Count - 1);
}
}
+
+ ///
+ /// Clears the buffers and resets the state variables.
+ ///
+ private void ClearBuffers()
+ {
+ m_buffers.Clear();
+ m_bufferIndex = 0;
+ m_endOfLastBuffer = 0;
+ SetCurrentBuffer(0);
+ }
#endregion
#region Private Fields
diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/BufferManager.cs b/Stack/Opc.Ua.Core/Stack/Bindings/BufferManager.cs
index 1e3a50502..730b2b708 100644
--- a/Stack/Opc.Ua.Core/Stack/Bindings/BufferManager.cs
+++ b/Stack/Opc.Ua.Core/Stack/Bindings/BufferManager.cs
@@ -125,6 +125,7 @@ public BufferManager(string name, int maxBufferSize)
? ArrayPool.Shared
: ArrayPool.Create(maxBufferSize + kCookieLength, 4);
m_maxBufferSize = maxBufferSize;
+ m_maxSuggestedBufferSize = DetermineSuggestedBufferSize(maxBufferSize);
}
#endregion
@@ -336,11 +337,54 @@ public void ReturnBuffer(byte[] buffer, string owner)
#endif
m_arrayPool.Return(buffer);
}
+
+ ///
+ /// Returns the suggested max rent size for data in the buffers.
+ ///
+ /// The max buffer size configured.
+ private int DetermineSuggestedBufferSize(int maxBufferSize)
+ {
+ int bufferArrayPoolSize = RoundUpToPowerOfTwo(maxBufferSize);
+ int maxDataRentSize = RoundUpToPowerOfTwo(maxBufferSize + kCookieLength);
+ if (bufferArrayPoolSize != maxDataRentSize)
+ {
+ Utils.LogWarning("BufferManager: Max buffer size {0} + cookie length {1} may waste memory because it allocates buffers in the next bucket!", maxBufferSize, kCookieLength);
+ return bufferArrayPoolSize - kCookieLength;
+ }
+ return maxBufferSize;
+ }
+
+ ///
+ /// Helper to round up to the next power of two.
+ ///
+ private int RoundUpToPowerOfTwo(int value)
+ {
+ int result = 1;
+
+ while (result < value && result != 0)
+ {
+ result <<= 1;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Returns the max size of data in the buffers.
+ ///
+ ///
+ /// Due to the underlying implementation of the ArrayPool,
+ /// the actual buffer size may be larger than this value.
+ /// To avoid memory waste, use this value as a guideline
+ /// for the maximum buffer size when taking buffers.
+ ///
+ public int MaxSuggestedBufferSize => m_maxSuggestedBufferSize;
#endregion
#region Private Fields
private readonly string m_name;
private readonly int m_maxBufferSize;
+ private readonly int m_maxSuggestedBufferSize;
#if TRACE_MEMORY
private int m_buffersTaken = 0;
#endif
@@ -348,7 +392,7 @@ public void ReturnBuffer(byte[] buffer, string owner)
private const byte kCookieLocked = 0xa5;
private const byte kCookieUnlocked = 0x5a;
#if TRACK_MEMORY
- const byte kCookieLength = 5;
+ private const byte kCookieLength = 5;
class Allocation
{
public int Id;
diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/BufferSegment.cs b/Stack/Opc.Ua.Core/Stack/Bindings/BufferSegment.cs
new file mode 100644
index 000000000..06750fb5d
--- /dev/null
+++ b/Stack/Opc.Ua.Core/Stack/Bindings/BufferSegment.cs
@@ -0,0 +1,70 @@
+/* ========================================================================
+ * Copyright (c) 2005-2024 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.Buffers;
+
+namespace Opc.Ua.Bindings
+{
+ ///
+ /// Helper to build a ReadOnlySequence from a set of buffers.
+ ///
+ public sealed class BufferSegment : ReadOnlySequenceSegment
+ {
+ ///
+ /// Returns the base array of the buffer.
+ ///
+ public byte[] Array() => m_array;
+
+ ///
+ /// Constructor for a buffer segment.
+ ///
+ public BufferSegment(byte[] array, int offset, int length)
+ {
+ Memory = new ReadOnlyMemory(array, offset, length);
+ m_array = array;
+ }
+
+ ///
+ /// Appends a buffer to the sequence.
+ ///
+ public BufferSegment Append(byte[] array, int offset, int length)
+ {
+ var segment = new BufferSegment(array, offset, length) {
+ RunningIndex = RunningIndex + Memory.Length
+ };
+ Next = segment;
+ return segment;
+ }
+
+ #region Private Fields
+ private byte[] m_array;
+ #endregion
+ }
+}
diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/BufferSequence.cs b/Stack/Opc.Ua.Core/Stack/Bindings/BufferSequence.cs
new file mode 100644
index 000000000..81a647101
--- /dev/null
+++ b/Stack/Opc.Ua.Core/Stack/Bindings/BufferSequence.cs
@@ -0,0 +1,77 @@
+/* ========================================================================
+ * Copyright (c) 2005-2024 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.Buffers;
+
+namespace Opc.Ua.Bindings
+{
+ ///
+ /// A class to hold a sequence of buffers until disposed.
+ ///
+ public sealed class BufferSequence : IDisposable
+ {
+ ///
+ /// The constructor to create the sequence of buffers.
+ ///
+ public BufferSequence(BufferManager bufferManager, string owner, BufferSegment firstSegment, ReadOnlySequence sequence)
+ {
+ m_bufferManager = bufferManager;
+ m_owner = owner;
+ m_firstSegment = firstSegment;
+ m_sequence = sequence;
+ }
+
+ ///
+ public void Dispose()
+ {
+ BufferSegment segment = m_firstSegment;
+ while (segment != null)
+ {
+ m_bufferManager.ReturnBuffer(segment.Array(), m_owner);
+ segment = (BufferSegment)segment.Next;
+ }
+ m_sequence = ReadOnlySequence.Empty;
+ m_firstSegment = null;
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Returns a sequence which can be used to access the buffers.
+ ///
+ public ReadOnlySequence Sequence => m_sequence;
+
+ #region Private
+ private BufferManager m_bufferManager;
+ private BufferSegment m_firstSegment;
+ private ReadOnlySequence m_sequence;
+ private string m_owner;
+ #endregion
+ }
+}
diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs b/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs
index 03eb1036d..29cf6b273 100644
--- a/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs
+++ b/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs
@@ -198,7 +198,7 @@ public static ConfiguredEndpointCollection Load(Stream istrm)
}
catch (Exception e)
{
- Utils.LogError(e, "Unexpected error loading ConfiguredEndpoints.");
+ Utils.LogError("Unexpected error loading ConfiguredEndpoints: {0}", Redaction.Redact.Create(e));
throw;
}
}
diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs
index 5043920cc..602d9aeaf 100644
--- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs
+++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs
@@ -130,7 +130,7 @@ public static bool IsValid(uint messageType)
}
uint chunkTypeMask = messageType & ChunkTypeMask;
- if ((chunkTypeMask != Final) && (chunkTypeMask != Intermediate))
+ if ((chunkTypeMask != Final) && (chunkTypeMask != Intermediate) && (chunkTypeMask != Abort))
{
return false;
}
diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServiceHost.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServiceHost.cs
index 46d2b3ca7..717053026 100644
--- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServiceHost.cs
+++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServiceHost.cs
@@ -128,7 +128,7 @@ public List CreateServiceHost(
}
else
{
- Utils.LogError("Failed to create endpoint {0} because the transport profile is unsupported.", uri);
+ Utils.LogError("Failed to create endpoint {0} because the transport profile is unsupported.", Redaction.Redact.Create(uri));
}
}
diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs
index a36c06144..cc7fc6f67 100644
--- a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs
+++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs
@@ -283,41 +283,31 @@ public void SetMappingTables(NamespaceTable namespaceUris, StringTable serverUri
///
public string CloseAndReturnText()
{
- Close();
- if (m_memoryStream == null)
+ try
{
- if (m_stream is MemoryStream memoryStream)
+ InternalClose(false);
+ if (m_memoryStream == null)
{
- return Encoding.UTF8.GetString(memoryStream.ToArray());
+ if (m_stream is MemoryStream memoryStream)
+ {
+ return Encoding.UTF8.GetString(memoryStream.ToArray());
+ }
+ throw new NotSupportedException("Cannot get text from external stream. Use Close or MemoryStream instead.");
}
- throw new NotSupportedException("Cannot get text from external stream. Use Close or MemoryStream instead.");
+ return Encoding.UTF8.GetString(m_memoryStream.ToArray());
+ }
+ finally
+ {
+ m_writer?.Dispose();
+ m_writer = null;
}
- return Encoding.UTF8.GetString(m_memoryStream.ToArray());
}
///
/// Completes writing and returns the text length.
+ /// The StreamWriter is disposed.
///
- public int Close()
- {
- if (!m_dontWriteClosing)
- {
- if (m_topLevelIsArray)
- {
- m_writer.Write(s_rightSquareBracket);
- }
- else
- {
- m_writer.Write(s_rightCurlyBrace);
- }
- }
-
- m_writer.Flush();
- int length = (int)m_writer.BaseStream.Position;
- m_writer.Dispose();
- m_writer = null;
- return length;
- }
+ public int Close() => InternalClose(true);
#endregion
#region IDisposable Members
@@ -339,7 +329,7 @@ protected virtual void Dispose(bool disposing)
{
if (m_writer != null)
{
- Close();
+ InternalClose(true);
m_writer = null;
}
@@ -2580,6 +2570,33 @@ public void WriteArray(string fieldName, object array, int valueRank, BuiltInTyp
#endregion
#region Private Methods
+ ///
+ /// Completes writing and returns the text length.
+ ///
+ private int InternalClose(bool dispose)
+ {
+ if (!m_dontWriteClosing)
+ {
+ if (m_topLevelIsArray)
+ {
+ m_writer.Write(s_rightSquareBracket);
+ }
+ else
+ {
+ m_writer.Write(s_rightCurlyBrace);
+ }
+ }
+
+ 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
diff --git a/Stack/Opc.Ua.Core/Types/Redaction/IRedactionStrategy.cs b/Stack/Opc.Ua.Core/Types/Redaction/IRedactionStrategy.cs
new file mode 100644
index 000000000..eb7529f8b
--- /dev/null
+++ b/Stack/Opc.Ua.Core/Types/Redaction/IRedactionStrategy.cs
@@ -0,0 +1,46 @@
+/* ========================================================================
+ * Copyright (c) 2005-2024 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/
+ * ======================================================================*/
+
+namespace Opc.Ua.Redaction
+{
+ ///
+ /// Represents a redaction (censoring) strategy.
+ ///
+ ///
+ /// Use redaction to hide sensitive data in log messages, exception messages, etc.
+ /// To set your redaction strategy see .
+ ///
+ public interface IRedactionStrategy
+ {
+ ///
+ /// Returns a string representation of without sensitive data.
+ ///
+ string Redact(object value);
+ }
+}
diff --git a/Stack/Opc.Ua.Core/Types/Redaction/Redact.cs b/Stack/Opc.Ua.Core/Types/Redaction/Redact.cs
new file mode 100644
index 000000000..9ad19126f
--- /dev/null
+++ b/Stack/Opc.Ua.Core/Types/Redaction/Redact.cs
@@ -0,0 +1,48 @@
+/* ========================================================================
+ * Copyright (c) 2005-2024 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/
+ * ======================================================================*/
+
+namespace Opc.Ua.Redaction
+{
+ ///
+ /// Helper class for redacting sensitive data.
+ ///
+ public static class Redact
+ {
+ ///
+ /// Creates a wrapper to hold .
+ /// When is called on the resulting wrapper
+ /// it will invoke the redaction strategy to censor the
+ /// sensitive data held by .
+ ///
+ public static RedactionWrapper Create(T value)
+ {
+ return new RedactionWrapper(value);
+ }
+ }
+}
diff --git a/Stack/Opc.Ua.Core/Types/Redaction/RedactionStrategies.cs b/Stack/Opc.Ua.Core/Types/Redaction/RedactionStrategies.cs
new file mode 100644
index 000000000..d5485128f
--- /dev/null
+++ b/Stack/Opc.Ua.Core/Types/Redaction/RedactionStrategies.cs
@@ -0,0 +1,89 @@
+/* ========================================================================
+ * Copyright (c) 2005-2024 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;
+
+namespace Opc.Ua.Redaction
+{
+ ///
+ /// Collection of the redaction strategies.
+ /// The redaction (censoring) can be applied to log message parameters, exception messages, and other sensitive data.
+ ///
+ /// To use a strategy wrap the value with , e.g.
+ ///
+ /// Utils.LogDebug("The password is {0}", RedactionWrapper.Create(password));
+ /// Utils.LogError("An exception occurred: {0}", RedactionWrapper.Create(exception));
+ ///
+ ///
+ ///
+ /// The redaction is off by default and can be enabled by implementing
+ /// and setting it via .
+ ///
+ public static partial class RedactionStrategies
+ {
+ private static readonly IRedactionStrategy s_fallbackStrategy = new FallbackRedactionStrategy();
+
+ ///
+ /// Gets the current redaction strategy.
+ ///
+ internal static IRedactionStrategy CurrentStrategy { get; private set; } = s_fallbackStrategy;
+
+ ///
+ /// Sets the current redaction strategy.
+ ///
+ public static void SetStrategy(IRedactionStrategy strategy)
+ {
+ if (strategy == null)
+ {
+ throw new ArgumentNullException(nameof(strategy));
+ }
+
+ CurrentStrategy = strategy;
+ }
+
+ ///
+ /// Resets the fallback strategy to the default (empty) implementation.
+ ///
+ public static void ResetStrategy()
+ {
+ CurrentStrategy = s_fallbackStrategy;
+ }
+
+ ///
+ /// Fallback for when no other strategy was set. It returns the string representation of the value.
+ ///
+ private class FallbackRedactionStrategy : IRedactionStrategy
+ {
+ public string Redact(object value)
+ {
+ return value?.ToString() ?? "null";
+ }
+ }
+ }
+}
diff --git a/Stack/Opc.Ua.Core/Types/Redaction/RedactionWrapper.cs b/Stack/Opc.Ua.Core/Types/Redaction/RedactionWrapper.cs
new file mode 100644
index 000000000..ea73aff97
--- /dev/null
+++ b/Stack/Opc.Ua.Core/Types/Redaction/RedactionWrapper.cs
@@ -0,0 +1,54 @@
+/* ========================================================================
+ * Copyright (c) 2005-2024 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/
+ * ======================================================================*/
+
+namespace Opc.Ua.Redaction
+{
+ ///
+ /// Wraps the supplied value and applies the redaction strategy when converting to string.
+ ///
+ /// Type of the supplied value.
+ public class RedactionWrapper
+ {
+ private readonly T m_value;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RedactionWrapper(T value)
+ {
+ m_value = value;
+ }
+
+ ///
+ public override string ToString()
+ {
+ return RedactionStrategies.CurrentStrategy.Redact(m_value);
+ }
+ }
+}
diff --git a/Stack/Opc.Ua.Core/Types/Redaction/SimpleRedactionStrategy.cs b/Stack/Opc.Ua.Core/Types/Redaction/SimpleRedactionStrategy.cs
new file mode 100644
index 000000000..44a70a234
--- /dev/null
+++ b/Stack/Opc.Ua.Core/Types/Redaction/SimpleRedactionStrategy.cs
@@ -0,0 +1,144 @@
+/* ========================================================================
+ * Copyright (c) 2005-2024 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.Text;
+using Opc.Ua.Redaction;
+
+namespace Opc.Ua.Types.Redaction
+{
+ ///
+ /// Sample implementation of the redaction strategy.
+ ///
+ public class SimpleRedactionStrategy : IRedactionStrategy
+ {
+ private const int kDefaultMinLength = 8;
+ private const int kDefaultMaxLength = 16;
+ private const char kReplacementChar = '*';
+
+ private readonly int m_minLength;
+ private readonly int m_maxLength;
+
+
+ ///
+ /// Creates a new instance of the with default lengths.
+ ///
+ public SimpleRedactionStrategy() : this(kDefaultMinLength, kDefaultMaxLength)
+ { }
+
+ ///
+ /// Creates a new instance of the with default lengths.
+ ///
+ public SimpleRedactionStrategy(int minLength, int maxLength)
+ {
+ if (minLength < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(minLength), "Must be a non-negative number");
+ }
+
+ if (maxLength < -1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(maxLength), "Must be a non-negative number, or -1");
+ }
+
+ m_minLength = minLength;
+ m_maxLength = maxLength;
+ }
+
+ ///
+ public string Redact(object value)
+ {
+ if (value == null)
+ {
+ return "null";
+ }
+
+ if (value is Uri uri)
+ {
+ return RedactUri(uri);
+ }
+
+ if (value is UriBuilder uriBuilder)
+ {
+ return RedactUri(uriBuilder.Uri);
+ }
+
+ if (value is Exception exception)
+ {
+ return RedactException(exception);
+ }
+
+ string valueString = value.ToString();
+
+ if (valueString.Length < m_minLength)
+ {
+ return new string(kReplacementChar, m_minLength);
+ }
+
+ if (m_maxLength == -1 || valueString.Length <= m_maxLength)
+ {
+ return new string(kReplacementChar, valueString.Length);
+ }
+
+ return new string(kReplacementChar, m_maxLength);
+ }
+
+ private string RedactException(Exception exception)
+ {
+ return "An exception of type " + exception.GetType() + " was redacted because it may contain sensitive information.";
+ }
+
+ private string RedactUri(Uri uri)
+ {
+ if (!uri.IsWellFormedOriginalString() || !uri.IsAbsoluteUri || string.IsNullOrEmpty(uri.Host))
+ {
+ return Redact(uri.ToString());
+ }
+
+ string redactedHost = Redact(uri.Host);
+
+ StringBuilder sb = new StringBuilder()
+ .Append(uri.Scheme)
+ .Append(Uri.SchemeDelimiter)
+ .Append(redactedHost);
+
+ if (!uri.IsDefaultPort)
+ {
+ sb.Append(':').Append(uri.Port);
+ }
+
+ if (uri.LocalPath != null && !string.Equals(uri.LocalPath, "/", StringComparison.Ordinal))
+ {
+ sb.Append(uri.LocalPath);
+ }
+
+ return sb.ToString();
+ }
+ }
+}
diff --git a/Stack/Opc.Ua.Core/Types/Utils/LoggerUtils.cs b/Stack/Opc.Ua.Core/Types/Utils/LoggerUtils.cs
index 9dadcc904..47d315300 100644
--- a/Stack/Opc.Ua.Core/Types/Utils/LoggerUtils.cs
+++ b/Stack/Opc.Ua.Core/Types/Utils/LoggerUtils.cs
@@ -44,6 +44,7 @@
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.Extensions.Logging;
+using Opc.Ua.Redaction;
namespace Opc.Ua
{
@@ -161,7 +162,7 @@ public static void LogCertificate(LogLevel logLevel, EventId eventId, string mes
{
allArgs[i] = args[i];
}
- allArgs[argsLength] = certificate.Subject;
+ allArgs[argsLength] = Redact.Create(certificate.Subject);
allArgs[argsLength + 1] = certificate.Thumbprint;
Log(logLevel, eventId, builder.ToString(), allArgs);
}
diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj
index 7aafcb134..bb2402572 100644
--- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj
+++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj
@@ -14,7 +14,7 @@
all
runtime; build; native; contentfiles; analyzers
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs
index 7cf43a05c..75562af6f 100644
--- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs
+++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs
@@ -276,6 +276,7 @@ public void FillStructWithValues(BaseComplexType structType, bool randomValues)
///
protected void EncodeDecodeComplexType(
IServiceMessageContext encoderContext,
+ MemoryStreamType memoryStreamType,
EncodingType encoderType,
StructureType structureType,
ExpandedNodeId nodeId,
@@ -291,7 +292,7 @@ object data
TestContext.Out.WriteLine(expected);
byte[] buffer;
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType))
{
using (IEncoder encoder = CreateEncoder(encoderType, encoderContext, encoderStream, typeof(DataValue)))
{
diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs
index acdc60650..916e27fd3 100644
--- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs
+++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs
@@ -30,6 +30,7 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
+using Opc.Ua.Core.Tests.Types.Encoders;
namespace Opc.Ua.Client.ComplexTypes.Tests.Types
{
@@ -86,6 +87,7 @@ public class ComplexTypesEncoderTests : ComplexTypesCommon
[Theory]
[Category("ComplexTypes")]
public void ReEncodeComplexType(
+ MemoryStreamType memoryStreamType,
EncodingType encoderType,
StructureType structureType
)
@@ -96,7 +98,7 @@ StructureType structureType
object emittedType = Activator.CreateInstance(complexType);
var baseType = emittedType as BaseComplexType;
FillStructWithValues(baseType, true);
- EncodeDecodeComplexType(EncoderContext, encoderType, structureType, nodeId, emittedType);
+ EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, structureType, nodeId, emittedType);
}
///
@@ -106,6 +108,7 @@ StructureType structureType
[Theory]
[Category("ComplexTypes")]
public void ReEncodeStructureWithOptionalFieldsComplexType(
+ MemoryStreamType memoryStreamType,
EncodingType encoderType,
StructureFieldParameter structureFieldParameter
)
@@ -118,17 +121,17 @@ StructureFieldParameter structureFieldParameter
var builtInType = structureFieldParameter.BuiltInType;
TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} is the only value.");
baseType[structureFieldParameter.Name] = DataGenerator.GetRandom(builtInType);
- EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType);
+ EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType);
TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} is null.");
baseType[structureFieldParameter.Name] = null;
- EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType);
+ EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType);
TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} is null, all other fields have random values.");
FillStructWithValues(baseType, true);
baseType[structureFieldParameter.Name] = null;
- EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType);
+ EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType);
TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} has random value.");
baseType[structureFieldParameter.Name] = DataGenerator.GetRandom(builtInType);
- EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType);
+ EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType);
}
///
@@ -138,6 +141,7 @@ StructureFieldParameter structureFieldParameter
[Theory]
[Category("ComplexTypes")]
public void ReEncodeUnionComplexType(
+ MemoryStreamType memoryStreamType,
EncodingType encoderType,
StructureFieldParameter structureFieldParameter
)
@@ -150,10 +154,10 @@ StructureFieldParameter structureFieldParameter
var builtInType = structureFieldParameter.BuiltInType;
TestContext.Out.WriteLine($"Union Field: {structureFieldParameter.BuiltInType} is random.");
baseType[structureFieldParameter.Name] = DataGenerator.GetRandom(builtInType);
- EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.Union, nodeId, emittedType);
+ EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.Union, nodeId, emittedType);
TestContext.Out.WriteLine($"Union Field: {structureFieldParameter.BuiltInType} is null.");
baseType[structureFieldParameter.Name] = null;
- EncodeDecodeComplexType(EncoderContext, encoderType, StructureType.Union, nodeId, emittedType);
+ EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.Union, nodeId, emittedType);
}
#endregion Test Methods
}
diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs
index b73e471d1..5f12fea60 100644
--- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs
+++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs
@@ -120,7 +120,8 @@ public class ComplexTypesJsonEncoderTests : ComplexTypesCommon
///
[Theory]
public void JsonEncodeStructureRev(
- JsonValidationData jsonValidationData)
+ JsonValidationData jsonValidationData
+ )
{
ExpandedNodeId nodeId;
Type complexType;
@@ -131,6 +132,7 @@ public void JsonEncodeStructureRev(
ExtensionObject extensionObject = CreateExtensionObject(StructureType.Structure, nodeId, emittedType);
EncodeJsonComplexTypeVerifyResult(
jsonValidationData.BuiltInType,
+ MemoryStreamType.MemoryStream,
extensionObject,
true,
jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible,
@@ -142,7 +144,8 @@ public void JsonEncodeStructureRev(
///
[Theory]
public void JsonEncodeStructureNonRev(
- JsonValidationData jsonValidationData)
+ JsonValidationData jsonValidationData
+ )
{
ExpandedNodeId nodeId;
Type complexType;
@@ -153,6 +156,7 @@ public void JsonEncodeStructureNonRev(
ExtensionObject extensionObject = CreateExtensionObject(StructureType.Structure, nodeId, emittedType);
EncodeJsonComplexTypeVerifyResult(
jsonValidationData.BuiltInType,
+ MemoryStreamType.ArraySegmentStream,
extensionObject,
false,
jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible,
@@ -165,7 +169,8 @@ public void JsonEncodeStructureNonRev(
///
[Theory]
public void JsonEncodeOptionalFieldsRev(
- JsonValidationData jsonValidationData)
+ JsonValidationData jsonValidationData
+ )
{
ExpandedNodeId nodeId;
Type complexType;
@@ -176,6 +181,7 @@ public void JsonEncodeOptionalFieldsRev(
ExtensionObject extensionObject = CreateExtensionObject(StructureType.StructureWithOptionalFields, nodeId, emittedType);
EncodeJsonComplexTypeVerifyResult(
jsonValidationData.BuiltInType,
+ MemoryStreamType.ArraySegmentStream,
extensionObject,
true,
jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible,
@@ -199,6 +205,7 @@ public void JsonEncodeOptionalFieldsNonRev(
ExtensionObject extensionObject = CreateExtensionObject(StructureType.StructureWithOptionalFields, nodeId, emittedType);
EncodeJsonComplexTypeVerifyResult(
jsonValidationData.BuiltInType,
+ MemoryStreamType.RecyclableMemoryStream,
extensionObject,
false,
jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible,
@@ -210,7 +217,8 @@ public void JsonEncodeOptionalFieldsNonRev(
///
[Theory]
public void JsonEncodeUnionRev(
- JsonValidationData jsonValidationData)
+ JsonValidationData jsonValidationData
+ )
{
ExpandedNodeId nodeId;
Type complexType;
@@ -221,6 +229,7 @@ public void JsonEncodeUnionRev(
ExtensionObject extensionObject = CreateExtensionObject(StructureType.Union, nodeId, emittedType);
EncodeJsonComplexTypeVerifyResult(
jsonValidationData.BuiltInType,
+ MemoryStreamType.ArraySegmentStream,
extensionObject,
true,
jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible,
@@ -232,7 +241,8 @@ public void JsonEncodeUnionRev(
///
[Theory]
public void JsonEncodeUnionNonRev(
- JsonValidationData jsonValidationData)
+ JsonValidationData jsonValidationData
+ )
{
ExpandedNodeId nodeId;
Type complexType;
@@ -243,6 +253,7 @@ public void JsonEncodeUnionNonRev(
ExtensionObject extensionObject = CreateExtensionObject(StructureType.Union, nodeId, emittedType);
EncodeJsonComplexTypeVerifyResult(
jsonValidationData.BuiltInType,
+ MemoryStreamType.ArraySegmentStream,
extensionObject,
false,
jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible,
@@ -253,6 +264,7 @@ public void JsonEncodeUnionNonRev(
#region Private Methods
protected void EncodeJsonComplexTypeVerifyResult(
BuiltInType builtInType,
+ MemoryStreamType memoryStreamType,
ExtensionObject data,
bool useReversibleEncoding,
string expected,
@@ -269,7 +281,7 @@ bool topLevelIsArray
_ = PrettifyAndValidateJson(expected);
byte[] buffer;
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType))
{
using (IEncoder encoder = CreateEncoder(
EncodingType.Json, EncoderContext, encoderStream,
diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs
index 9bc2e73c0..3b822c94e 100644
--- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs
+++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs
@@ -36,6 +36,7 @@
using System.Text;
using System.Threading.Tasks;
using NUnit.Framework;
+using Opc.Ua.Core.Tests.Types.Encoders;
namespace Opc.Ua.Client.ComplexTypes.Tests.Types
{
@@ -148,7 +149,7 @@ public void Add(string name, NodeId typeId)
/// Test the functionality to create a custom complex type.
///
[Theory]
- public async Task CreateMockTypeAsync(EncodingType encodingType)
+ public async Task CreateMockTypeAsync(EncodingType encodingType, MemoryStreamType memoryStreamType)
{
var mockResolver = new MockResolver();
@@ -255,7 +256,7 @@ public async Task CreateMockTypeAsync(EncodingType encodingType)
};
byte[] buffer;
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType))
{
using (IEncoder encoder = CreateEncoder(EncodingType.Json, encoderContext, encoderStream, carType))
{
@@ -267,7 +268,7 @@ public async Task CreateMockTypeAsync(EncodingType encodingType)
_ = PrettifyAndValidateJson(Encoding.UTF8.GetString(buffer));
// test encoder/decoder
- EncodeDecodeComplexType(encoderContext, encodingType, StructureType.Structure, nodeId, car);
+ EncodeDecodeComplexType(encoderContext, memoryStreamType, encodingType, StructureType.Structure, nodeId, car);
// Test extracting type definition
@@ -281,7 +282,7 @@ public async Task CreateMockTypeAsync(EncodingType encodingType)
/// Test the functionality to create a custom complex type.
///
[Theory]
- public async Task CreateMockArrayTypeAsync(EncodingType encodingType)
+ public async Task CreateMockArrayTypeAsync(EncodingType encodingType, MemoryStreamType memoryStreamType)
{
var mockResolver = new MockResolver();
@@ -430,7 +431,7 @@ public async Task CreateMockArrayTypeAsync(EncodingType encodingType)
};
byte[] buffer;
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType))
{
using (IEncoder encoder = CreateEncoder(EncodingType.Json, encoderContext, encoderStream, arraysTypes, false))
{
@@ -442,7 +443,7 @@ public async Task CreateMockArrayTypeAsync(EncodingType encodingType)
_ = PrettifyAndValidateJson(Encoding.UTF8.GetString(buffer));
// test encoder/decoder
- EncodeDecodeComplexType(encoderContext, encodingType, StructureType.Structure, dataTypeNode.NodeId, arrays);
+ EncodeDecodeComplexType(encoderContext, memoryStreamType, encodingType, StructureType.Structure, dataTypeNode.NodeId, arrays);
// Test extracting type definition
@@ -456,7 +457,7 @@ public async Task CreateMockArrayTypeAsync(EncodingType encodingType)
/// Create a complex type with a single scalar or array type, with default and random values .
///
[Theory]
- public async Task CreateMockSingleTypeAsync(EncodingType encodingType, TestType typeDescription, bool randomValues, TestRanks testRank)
+ public async Task CreateMockSingleTypeAsync(EncodingType encodingType, MemoryStreamType memoryStreamType, TestType typeDescription, bool randomValues, TestRanks testRank)
{
SetRepeatedRandomSeed();
@@ -611,7 +612,7 @@ public async Task CreateMockSingleTypeAsync(EncodingType encodingType, TestType
};
byte [] buffer;
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType))
{
using (IEncoder encoder = CreateEncoder(EncodingType.Json, encoderContext, encoderStream, arraysTypes, false))
{
@@ -623,7 +624,7 @@ public async Task CreateMockSingleTypeAsync(EncodingType encodingType, TestType
_ = PrettifyAndValidateJson(Encoding.UTF8.GetString(buffer));
// test encoder/decoder
- EncodeDecodeComplexType(encoderContext, encodingType, StructureType.Structure, dataTypeNode.NodeId, testType);
+ EncodeDecodeComplexType(encoderContext, memoryStreamType, encodingType, StructureType.Structure, dataTypeNode.NodeId, testType);
// Test extracting type definition
diff --git a/Tests/Opc.Ua.Client.Tests/ClientTest.cs b/Tests/Opc.Ua.Client.Tests/ClientTest.cs
index 34d440575..3cd771984 100644
--- a/Tests/Opc.Ua.Client.Tests/ClientTest.cs
+++ b/Tests/Opc.Ua.Client.Tests/ClientTest.cs
@@ -614,13 +614,16 @@ public async Task ReconnectSessionOnAlternateChannel(bool closeChannel)
sre = Assert.Throws(() => session1.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType)));
// TODO: Both channel should return BadSecureChannelClosed
- if (endpoint.EndpointUrl.ToString().StartsWith(Utils.UriSchemeOpcTcp, StringComparison.Ordinal))
+ if (!(StatusCodes.BadSecureChannelClosed == sre.StatusCode))
{
- Assert.AreEqual(StatusCodes.BadSessionIdInvalid, sre.StatusCode, sre.Message);
- }
- else
- {
- Assert.AreEqual(StatusCodes.BadUnknownResponse, sre.StatusCode, sre.Message);
+ if (endpoint.EndpointUrl.ToString().StartsWith(Utils.UriSchemeOpcTcp, StringComparison.Ordinal))
+ {
+ Assert.AreEqual(StatusCodes.BadSessionIdInvalid, sre.StatusCode, sre.Message);
+ }
+ else
+ {
+ Assert.AreEqual(StatusCodes.BadUnknownResponse, sre.StatusCode, sre.Message);
+ }
}
}
diff --git a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj
index ab4d2a849..f4aa4733f 100644
--- a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj
+++ b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj
@@ -16,7 +16,7 @@
all
runtime; build; native; contentfiles; analyzers
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj
index 01092c681..ad10318d1 100644
--- a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj
+++ b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj
@@ -15,7 +15,7 @@
all
runtime; build; native; contentfiles; analyzers
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj
index b75d78987..5cfd6d209 100644
--- a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj
+++ b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj
@@ -18,7 +18,7 @@
all
runtime; build; native; contentfiles; analyzers
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManager.cs b/Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManagerBenchmarks.cs
similarity index 98%
rename from Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManager.cs
rename to Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManagerBenchmarks.cs
index 719b4d9bc..998f4d7ea 100644
--- a/Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManager.cs
+++ b/Tests/Opc.Ua.Core.Tests/Types/Bindings/BufferManagerBenchmarks.cs
@@ -43,7 +43,7 @@ namespace Opc.Ua.Core.Tests.Stack.Bindings
public class BufferManagerBenchmarks
{
//[Params(8192, 65535, 1024 * 1024 - 1)]
- public int BufferSize { get; set; } = 65535;
+ public int BufferSize { get; set; } = TcpMessageLimits.DefaultMaxBufferSize;
//[Params( /*8,*/ 64, 256, 1024)]
public int Allocations { get; set; } = 256;
diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryDecoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryDecoderBenchmarks.cs
new file mode 100644
index 000000000..a4ea14784
--- /dev/null
+++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryDecoderBenchmarks.cs
@@ -0,0 +1,239 @@
+/* ========================================================================
+ * Copyright (c) 2005-2024 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.IO;
+using BenchmarkDotNet.Attributes;
+using NUnit.Framework;
+using Opc.Ua.Bindings;
+
+namespace Opc.Ua.Core.Tests.Types.Encoders
+{
+ [TestFixture, Category("BinaryEncoder")]
+ [SetCulture("en-us"), SetUICulture("en-us")]
+ [NonParallelizable]
+ [MemoryDiagnoser]
+ [DisassemblyDiagnoser]
+ public class BinaryDecoderBenchmarks : EncoderBenchmarks
+ {
+ ///
+ /// Test decoding with internal memory stream.
+ ///
+ [Test]
+ public void BinaryDecoderInternalMemoryStreamTest()
+ {
+ using (var binaryDecoder = new BinaryDecoder(m_encodedByteArray, m_context))
+ {
+ TestDecoding(binaryDecoder);
+ }
+ }
+
+ ///
+ /// Test decoding with ArrayPool memory stream.
+ ///
+ [Test]
+ public void BinaryDecoderArraySegmentStreamTest()
+ {
+ using (var memoryStream = new ArraySegmentStream(m_encodedBufferList))
+ using (var binaryDecoder = new BinaryDecoder(memoryStream, m_context))
+ {
+ TestDecoding(binaryDecoder);
+ }
+ }
+
+ ///
+ /// Test decoding with ArrayPool memory stream that has no span support.
+ ///
+ [Test]
+ public void BinaryDecoderArraySegmentStreamNoSpanTest()
+ {
+#if NET6_0_OR_GREATER && ECC_SUPPORT
+ using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_encodedBufferList))
+#else
+ using (var arraySegmentStream = new ArraySegmentStream(m_encodedBufferList))
+#endif
+ using (var binaryDecoder = new BinaryDecoder(arraySegmentStream, m_context))
+ {
+ TestDecoding(binaryDecoder);
+ }
+ }
+
+ ///
+ /// Benchmark decoding with memory stream.
+ ///
+ [Benchmark(Baseline = true)]
+ [Test]
+ public void BinaryDecoderMemoryStream()
+ {
+ using (var memoryStream = new MemoryStream(m_encodedByteArray))
+ {
+ BinaryDecoder_Stream(memoryStream);
+ }
+ }
+
+ ///
+ /// Benchmark decoding with array segment memory stream.
+ ///
+ [Benchmark]
+ [Test]
+ public void BinaryDecoderArraySegmentStream()
+ {
+ using (var arraySegmentStream = new ArraySegmentStream(m_encodedBufferList))
+ {
+ BinaryDecoder_Stream(arraySegmentStream);
+ }
+ }
+
+ ///
+ /// Benchmark decoding with array segment memory stream without span support.
+ ///
+ [Benchmark]
+ [Test]
+ public void BinaryDecoderArraySegmentStreamNoSpan()
+ {
+#if NET6_0_OR_GREATER && ECC_SUPPORT
+ using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_encodedBufferList))
+#else
+ using (var arraySegmentStream = new ArraySegmentStream(m_encodedBufferList))
+#endif
+ {
+ BinaryDecoder_Stream(arraySegmentStream);
+ }
+ }
+
+ #region Private Methods
+ private void TestStreamDecode(MemoryStream memoryStream)
+ {
+ using (var binaryDecoder = new BinaryDecoder(memoryStream, m_context))
+ {
+ TestDecoding(binaryDecoder);
+ TestDecoding(binaryDecoder);
+ binaryDecoder.Close();
+ }
+ }
+
+ private void BinaryDecoder_Stream(MemoryStream memoryStream)
+ {
+ using (var binaryDecoder = new BinaryDecoder(memoryStream, m_context))
+ {
+ TestDecoding(binaryDecoder);
+ TestDecoding(binaryDecoder);
+ binaryDecoder.Close();
+ }
+ }
+ #endregion
+
+ #region Test Setup
+ [OneTimeSetUp]
+ public new void OneTimeSetUp()
+ {
+ base.OneTimeSetUp();
+ InitializeEncodedTestData();
+ }
+
+ [OneTimeTearDown]
+ public new void OneTimeTearDown()
+ {
+ base.OneTimeTearDown();
+ }
+ #endregion
+
+ #region Benchmark Setup
+ ///
+ /// Set up some variables for benchmarks.
+ ///
+ [GlobalSetup]
+ public new void GlobalSetup()
+ {
+ base.GlobalSetup();
+ InitializeEncodedBenchmarkData();
+ }
+
+ ///
+ /// Tear down benchmark variables.
+ ///
+ [GlobalCleanup]
+ public new void GlobalCleanup()
+ {
+ base.GlobalCleanup();
+ }
+ #endregion
+
+ #region Private Methods
+ ///
+ /// Initialize encoded data.
+ ///
+ private void InitializeEncodedTestData()
+ {
+ using (var memoryStream = new MemoryStream(StreamBufferSize))
+ using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true))
+ {
+ TestEncoding(binaryEncoder);
+ TestEncoding(binaryEncoder);
+ m_encodedByteArray = binaryEncoder.CloseAndReturnBuffer();
+ }
+
+ using (var memoryStream = new ArraySegmentStream(m_bufferManager))
+ using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true))
+ {
+ TestEncoding(binaryEncoder);
+ TestEncoding(binaryEncoder);
+ binaryEncoder.Close();
+ m_encodedBufferList = memoryStream.GetBuffers("writer");
+ }
+ }
+
+ ///
+ /// Initialize encoded data.
+ ///
+ private void InitializeEncodedBenchmarkData()
+ {
+ using (var memoryStream = new MemoryStream(StreamBufferSize))
+ using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true))
+ {
+ TestEncoding(binaryEncoder);
+ TestEncoding(binaryEncoder);
+ m_encodedByteArray = binaryEncoder.CloseAndReturnBuffer();
+ }
+
+ using (var memoryStream = new ArraySegmentStream(m_bufferManager))
+ using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true))
+ {
+ TestEncoding(binaryEncoder);
+ TestEncoding(binaryEncoder);
+ binaryEncoder.Close();
+ m_encodedBufferList = memoryStream.GetBuffers("writer");
+ }
+ }
+ #endregion
+
+ private byte[] m_encodedByteArray;
+ private BufferCollection m_encodedBufferList;
+
+ }
+}
diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs
index ce10178cd..fff30f452 100644
--- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs
+++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/BinaryEncoderBenchmarks.cs
@@ -1,5 +1,5 @@
/* ========================================================================
- * Copyright (c) 2005-2023 The OPC Foundation, Inc. All rights reserved.
+ * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
@@ -27,11 +27,10 @@
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
-using System;
-using System.Collections.Generic;
using System.IO;
using System.Text;
using BenchmarkDotNet.Attributes;
+using Microsoft.IO;
using NUnit.Framework;
using Opc.Ua.Bindings;
@@ -42,67 +41,88 @@ namespace Opc.Ua.Core.Tests.Types.Encoders
[NonParallelizable]
[MemoryDiagnoser]
[DisassemblyDiagnoser]
- public class BinaryEncoderBenchmarks
+ public class BinaryEncoderBenchmarks : EncoderBenchmarks
{
- const int kBufferSize = 4096;
-
- [Params(1, 2, 8, 64)]
- public int PayLoadSize { get; set; } = 64;
-
///
- /// Benchmark encoding with internal memory stream.
+ /// Test encoding with internal memory stream.
///
- [Benchmark]
[Test]
- public void BinaryEncoderConstructor2()
+ public void BinaryEncoderInternalMemoryStreamTest()
{
using (var binaryEncoder = new BinaryEncoder(m_context))
{
TestEncoding(binaryEncoder);
- _ = binaryEncoder.CloseAndReturnBuffer();
+ var result = binaryEncoder.CloseAndReturnBuffer();
+ Assert.NotNull(result);
}
}
+
///
- /// Benchmark encoding with ArrayPool memory stream.
+ /// Test encoding with internal memory stream,
+ /// uses reflection to get array from memory stream.
///
- [Benchmark]
[Test]
- public void BinaryEncoderConstructor3()
+ public void BinaryEncoderConstructorStreamwriterReflection2()
{
- using (var stream = new ArraySegmentStream(m_bufferManager, kBufferSize, 0, kBufferSize))
+ using (var memoryStream = new MemoryStream(StreamBufferSize))
+ using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true))
{
- using (var binaryEncoder = new BinaryEncoder(stream, m_context, false))
- {
- TestEncoding(binaryEncoder);
- _ = binaryEncoder.CloseAndReturnBuffer();
- }
+ TestEncoding(binaryEncoder);
+ var result = binaryEncoder.CloseAndReturnBuffer();
+ Assert.NotNull(result);
}
}
///
- /// Benchmark encoding with memory stream kept open.
+ /// Test encoding with ArrayPool memory stream.
///
- [Benchmark]
- [Test]
- public void BinaryEncoderConstructorStreamwriter2()
+ [Theory]
+ public void BinaryEncoderArraySegmentStreamTest(bool toArray)
{
- using (IEncoder binaryEncoder = new BinaryEncoder(m_memoryStream, m_context, true))
+ using (var memoryStream = new ArraySegmentStream(m_bufferManager))
{
- TestEncoding(binaryEncoder);
- int length = binaryEncoder.Close();
- var result = Encoding.UTF8.GetString(m_memoryStream.ToArray());
+ TestStreamEncode(memoryStream, toArray);
}
}
///
- /// Encoding test with memory stream kept open.
+ /// Test encoding with memory stream kept open.
///
- [Benchmark]
+ [Theory]
+ public void BinaryEncoderMemoryStreamTest(bool toArray)
+ {
+ using (var memoryStream = new MemoryStream(StreamBufferSize))
+ {
+ TestStreamEncode(memoryStream, toArray);
+ }
+ }
+
+ ///
+ /// Test encoding with recyclable memory stream kept open.
+ ///
+ [Theory]
+ public void BinaryEncoderRecyclableMemoryStream(bool toArray)
+ {
+ using (var memoryStream = new RecyclableMemoryStream(m_memoryManager))
+ {
+ TestStreamEncode(memoryStream, toArray);
+ }
+ }
+
+ ///
+ /// Benchmark encoding with memory stream kept open.
+ ///
+ [Benchmark(Baseline = true)]
[Test]
- public void BinaryEncoderStreamLeaveOpenMemoryStream()
+ public void BinaryEncoderMemoryStream()
{
- BinaryEncoder_StreamLeaveOpen(m_memoryStream);
+ using (var memoryStream = new MemoryStream(StreamBufferSize))
+ {
+ BinaryEncoder_StreamLeaveOpen(memoryStream);
+ // get buffer for write
+ _ = memoryStream.ToArray();
+ }
}
///
@@ -110,99 +130,117 @@ public void BinaryEncoderStreamLeaveOpenMemoryStream()
///
[Benchmark]
[Test]
- public void BinaryEncoderStreamLeaveOpenRecyclableMemoryStream()
+ public void BinaryEncoderRecyclableMemoryStream()
{
- BinaryEncoder_StreamLeaveOpen(m_recyclableMemoryStream);
+ using (var recyclableMemoryStream = new RecyclableMemoryStream(m_memoryManager))
+ {
+ BinaryEncoder_StreamLeaveOpen(recyclableMemoryStream);
+ // get buffers for write
+ _ = recyclableMemoryStream.GetReadOnlySequence();
+ }
}
///
- /// Encoding test with memory stream kept open.
+ /// Benchmark encoding with array segment memory stream kept open.
///
[Benchmark]
[Test]
- public void BinaryEncoderStreamLeaveOpenArraySegmentStream()
+ public void BinaryEncoderArraySegmentStream()
{
- BinaryEncoder_StreamLeaveOpen(m_arraySegmentStream);
+ using (var arraySegmentStream = new ArraySegmentStream(m_bufferManager))
+ {
+ BinaryEncoder_StreamLeaveOpen(arraySegmentStream);
+ // get buffers and return them to buffer manager
+ var buffers = arraySegmentStream.GetBuffers("writer");
+ foreach (var buffer in buffers)
+ {
+ m_bufferManager.ReturnBuffer(buffer.Array, "testreturn");
+ }
+ }
}
///
- /// Benchmark encoding with memory stream kept open,
- /// use internal reflection to get string from memory stream.
+ /// Benchmark encoding with array segment memory stream kept open,
+ /// to compare the version without span support.
///
[Benchmark]
[Test]
- public void BinaryEncoderConstructorStreamwriterReflection2()
+ public void BinaryEncoderArraySegmentStreamNoSpan()
{
- using (var binaryEncoder = new BinaryEncoder(m_memoryStream, m_context, true))
+#if NET6_0_OR_GREATER && ECC_SUPPORT
+ using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_bufferManager))
+#else
+ using (var arraySegmentStream = new ArraySegmentStream(m_bufferManager))
+#endif
{
- TestEncoding(binaryEncoder);
- var result = binaryEncoder.CloseAndReturnBuffer();
+ BinaryEncoder_StreamLeaveOpen(arraySegmentStream);
+ // get buffers and return them to buffer manager
+ var buffers = arraySegmentStream.GetBuffers("writer");
+ foreach (var buffer in buffers)
+ {
+ m_bufferManager.ReturnBuffer(buffer.Array, "testreturn");
+ }
}
}
#region Private Methods
- private void BinaryEncoder_StreamLeaveOpen(MemoryStream stream)
+ private void TestStreamEncode(MemoryStream memoryStream, bool toArray)
+ {
+ using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true))
+ {
+ TestEncoding(binaryEncoder);
+ _ = binaryEncoder.Close();
+ }
+ using (var binaryEncoder = new BinaryEncoder(memoryStream, m_context, true))
+ {
+ TestEncoding(binaryEncoder);
+ if (toArray)
+ {
+ int length = binaryEncoder.Close();
+ Assert.AreEqual(length, memoryStream.Position);
+ var result = memoryStream.ToArray();
+ Assert.NotNull(result);
+ Assert.AreEqual(length, result.Length);
+ }
+ else
+ {
+ var result = binaryEncoder.CloseAndReturnBuffer();
+ Assert.NotNull(result);
+ }
+ }
+ }
+
+ private int BinaryEncoder_StreamLeaveOpen(MemoryStream memoryStream, bool testResult = false)
{
int length1;
int length2;
- m_memoryStream.Position = 0;
- using (IEncoder encoder = new BinaryEncoder(stream, m_context, true))
+ using (var encoder = new BinaryEncoder(memoryStream, m_context, true))
{
TestEncoding(encoder);
length1 = encoder.Close();
}
- using (IEncoder encoder = new BinaryEncoder(stream, m_context, true))
+ using (var encoder = new BinaryEncoder(memoryStream, m_context, true))
{
TestEncoding(encoder);
length2 = encoder.Close();
}
- Assert.AreEqual(length1 * 2, length2);
- var result = Encoding.UTF8.GetString(stream.ToArray());
- Assert.NotNull(result);
- Assert.AreEqual(length2, stream.Position);
- }
-
- private void TestEncoding(IEncoder encoder)
- {
- int payLoadSize = PayLoadSize;
- encoder.WriteByte("Byte", 0);
- while (--payLoadSize > 0)
+ if (testResult)
{
- encoder.WriteBoolean("Boolean", true);
- encoder.WriteUInt64("UInt64", 1234566890);
- encoder.WriteString("String", "The quick brown fox...");
- encoder.WriteNodeId("NodeId", s_nodeId);
- encoder.WriteInt32Array("Array", s_list);
+ Assert.AreEqual(length1 * 2, length2);
+ var result = Encoding.UTF8.GetString(memoryStream.ToArray());
+ Assert.NotNull(result);
+ Assert.AreEqual(length2, memoryStream.Position);
}
+ return length1 + length2;
}
#endregion
#region Test Setup
[OneTimeSetUp]
- public void OneTimeSetUp()
- {
- // for validating benchmark tests
- m_context = new ServiceMessageContext();
- m_memoryStream = new MemoryStream();
- m_memoryManager = new Microsoft.IO.RecyclableMemoryStreamManager();
- m_recyclableMemoryStream = new Microsoft.IO.RecyclableMemoryStream(m_memoryManager);
- m_bufferManager = new BufferManager(nameof(BinaryEncoder), kBufferSize);
- m_arraySegmentStream = new ArraySegmentStream(m_bufferManager, kBufferSize, 0, kBufferSize);
- }
+ public new void OneTimeSetUp() => base.OneTimeSetUp();
[OneTimeTearDown]
- public void OneTimeTearDown()
- {
- m_context = null;
- m_memoryStream.Dispose();
- m_memoryStream = null;
- m_recyclableMemoryStream.Dispose();
- m_recyclableMemoryStream = null;
- m_memoryManager = null;
- m_bufferManager = null;
- m_arraySegmentStream.Dispose();
- m_arraySegmentStream = null;
- }
+ public new void OneTimeTearDown() => base.OneTimeTearDown();
#endregion
#region Benchmark Setup
@@ -210,44 +248,13 @@ public void OneTimeTearDown()
/// Set up some variables for benchmarks.
///
[GlobalSetup]
- public void GlobalSetup()
- {
- // for validating benchmark tests
- m_context = new ServiceMessageContext();
- m_memoryStream = new MemoryStream();
- m_memoryManager = new Microsoft.IO.RecyclableMemoryStreamManager();
- m_recyclableMemoryStream = new Microsoft.IO.RecyclableMemoryStream(m_memoryManager);
- m_bufferManager = new BufferManager(nameof(BinaryEncoder), kBufferSize);
- m_arraySegmentStream = new ArraySegmentStream(m_bufferManager, kBufferSize, 0, kBufferSize);
- }
+ public new void GlobalSetup() => base.GlobalSetup();
///
/// Tear down benchmark variables.
///
[GlobalCleanup]
- public void GlobalCleanup()
- {
- m_context = null;
- m_memoryStream.Dispose();
- m_memoryStream = null;
- m_recyclableMemoryStream.Dispose();
- m_recyclableMemoryStream = null;
- m_memoryManager = null;
- m_bufferManager = null;
- m_arraySegmentStream.Dispose();
- m_arraySegmentStream = null;
- }
- #endregion
-
- #region Private Fields
- private static NodeId s_nodeId = new NodeId(1234);
- private static IList s_list = new List() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
- private IServiceMessageContext m_context;
- private MemoryStream m_memoryStream;
- private Microsoft.IO.RecyclableMemoryStreamManager m_memoryManager;
- private Microsoft.IO.RecyclableMemoryStream m_recyclableMemoryStream;
- private BufferManager m_bufferManager;
- private ArraySegmentStream m_arraySegmentStream;
+ public new void GlobalCleanup() => base.GlobalCleanup();
#endregion
}
}
diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs
index b61557856..9adbfdee2 100644
--- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs
+++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs
@@ -57,6 +57,7 @@ public class EncodeableTypesTests : EncoderCommon
[Category("EncodeableTypes")]
public void ActivateEncodeableType(
EncodingType encoderType,
+ MemoryStreamType memoryStreamType,
Type systemType
)
{
@@ -68,13 +69,14 @@ Type systemType
Assert.AreNotEqual(testObject.TypeId, testObject.BinaryEncodingId);
Assert.AreNotEqual(testObject.TypeId, testObject.XmlEncodingId);
Assert.AreNotEqual(testObject.BinaryEncodingId, testObject.XmlEncodingId);
- EncodeDecode(encoderType, BuiltInType.ExtensionObject, new ExtensionObject(testObject.TypeId, testObject));
+ EncodeDecode(encoderType, BuiltInType.ExtensionObject, memoryStreamType, new ExtensionObject(testObject.TypeId, testObject));
}
[Theory]
[Category("EncodeableTypes")]
public void ActivateEncodeableTypeArray(
EncodingType encoderType,
+ MemoryStreamType memoryStreamType,
Type systemType
)
{
@@ -95,7 +97,7 @@ Type systemType
BuiltInType builtInType = BuiltInType.Variant;
byte[] buffer;
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType))
{
using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, systemType))
{
@@ -140,6 +142,7 @@ Type systemType
[Category("EncodeableTypes")]
public void ActivateEncodeableTypeMatrix(
EncodingType encoderType,
+ MemoryStreamType memoryStreamType,
bool encodeAsMatrix,
Type systemType
)
@@ -167,7 +170,7 @@ Type systemType
Matrix matrix = new Matrix(array, builtInType, dimensions);
byte[] buffer;
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType))
{
using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, systemType))
{
diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderBenchmarks.cs
new file mode 100644
index 000000000..4f834b320
--- /dev/null
+++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderBenchmarks.cs
@@ -0,0 +1,201 @@
+/* ========================================================================
+ * Copyright (c) 2005-2024 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/
+ * ======================================================================*/
+
+#pragma warning disable CA5394 // Do not use insecure randomness
+
+using System;
+using System.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+using Microsoft.IO;
+using Opc.Ua.Bindings;
+
+namespace Opc.Ua.Core.Tests.Types.Encoders
+{
+ ///
+ /// Common code for encoder/decoder benchmarks.
+ ///
+ public class EncoderBenchmarks
+ {
+ public const int StreamBufferSize = 4096;
+ public const int DataValueCount = 10;
+
+ public EncoderBenchmarks()
+ {
+ m_random = new Random(0x62541);
+ m_nodeId = new NodeId((uint)m_random.Next(50000));
+ m_list = new List();
+ for (int i = 0; i < DataValueCount; i++)
+ {
+ m_list.Add(m_random.Next());
+ }
+ m_values = new List();
+ DateTime now = new DateTime(2024, 03, 01, 06, 05, 59, DateTimeKind.Utc);
+ now += TimeSpan.FromTicks(456789);
+ for (int i = 0; i < DataValueCount; i++)
+ {
+ m_values.Add(new DataValue(new Variant((m_random.NextDouble() - 0.5) * 1000.0), m_random.NextDouble() > 0.1 ? StatusCodes.Good : StatusCodes.BadDataLost, now, now));
+ }
+ }
+
+ [Params(64, 1024)]
+ public int PayLoadSize { get; set; } = 64;
+
+ #region Private Methods
+ protected void TestEncoding(IEncoder encoder)
+ {
+ var now = DateTime.UtcNow;
+ int payLoadSize = PayLoadSize;
+ encoder.WriteInt32("PayloadSize", payLoadSize);
+ while (payLoadSize-- > 0)
+ {
+ encoder.WriteBoolean("Boolean", true);
+ encoder.WriteByte("Byte", 123);
+ encoder.WriteUInt16("UInt16", 1234);
+ encoder.WriteUInt32("UInt32", 123456);
+ encoder.WriteUInt64("UInt64", 1234566890);
+ encoder.WriteSByte("Int8", -123);
+ encoder.WriteInt16("Int16", -1234);
+ encoder.WriteInt32("Int32", -123456);
+ encoder.WriteInt64("Int64", -1234566890);
+ encoder.WriteFloat("Float", 123.456f);
+ encoder.WriteDouble("Double", 123456.789);
+ encoder.WriteDateTime("DateTime", now);
+ encoder.WriteString("String", "The quick brown fox jumps over the lazy dog.");
+ encoder.WriteNodeId("NodeId", m_nodeId);
+ encoder.WriteNodeId("ExpandedNodeId", m_nodeId);
+ encoder.WriteInt32Array("Array", m_list);
+ encoder.WriteDataValueArray("DataValues", m_values);
+ }
+ }
+
+ protected void TestDecoding(IDecoder decoder)
+ {
+ var payLoadSize = decoder.ReadInt32("PayloadSize");
+ while (payLoadSize-- > 0)
+ {
+ _ = decoder.ReadBoolean("Boolean");
+ _ = decoder.ReadByte("Byte");
+ _ = decoder.ReadUInt16("UInt16");
+ _ = decoder.ReadUInt32("UInt32");
+ _ = decoder.ReadUInt64("UInt64");
+ _ = decoder.ReadSByte("Int8");
+ _ = decoder.ReadInt16("Int16");
+ _ = decoder.ReadInt32("Int32");
+ _ = decoder.ReadInt64("Int64");
+ _ = decoder.ReadFloat("Float");
+ _ = decoder.ReadDouble("Double");
+ _ = decoder.ReadDateTime("DateTime");
+ _ = decoder.ReadString("String");
+ _ = decoder.ReadNodeId("NodeId");
+ _ = decoder.ReadNodeId("ExpandedNodeId");
+ _ = decoder.ReadInt32Array("Array");
+ _ = decoder.ReadDataValueArray("DataValues");
+ }
+ }
+ #endregion
+
+ #region Test Setup
+ public void OneTimeSetUp()
+ {
+ // for validating benchmark tests
+ m_context = new ServiceMessageContext();
+ m_memoryManager = new RecyclableMemoryStreamManager(new RecyclableMemoryStreamManager.Options { BlockSize = StreamBufferSize });
+ m_bufferManager = new BufferManager(nameof(BinaryEncoder), StreamBufferSize);
+ }
+
+ public void OneTimeTearDown()
+ {
+ m_context = null;
+ m_memoryManager = null;
+ m_bufferManager = null;
+ }
+ #endregion
+
+ #region Benchmark Setup
+ ///
+ /// Set up some variables for benchmarks.
+ ///
+ public void GlobalSetup()
+ {
+ // for validating benchmark tests
+ m_context = new ServiceMessageContext();
+ m_memoryManager = new RecyclableMemoryStreamManager(new RecyclableMemoryStreamManager.Options { BlockSize = StreamBufferSize });
+ m_bufferManager = new BufferManager(nameof(BinaryEncoder), StreamBufferSize);
+ }
+
+ ///
+ /// Tear down benchmark variables.
+ ///
+ public void GlobalCleanup()
+ {
+ m_context = null;
+ m_memoryManager = null;
+ m_bufferManager = null;
+ }
+ #endregion
+
+ #region Protected Fields
+ protected Random m_random;
+ protected NodeId m_nodeId = new NodeId(1234);
+ protected List m_list;
+ protected List m_values;
+ protected IServiceMessageContext m_context;
+ protected RecyclableMemoryStreamManager m_memoryManager;
+ protected BufferManager m_bufferManager;
+ #endregion
+ }
+
+#if NET6_0_OR_GREATER && ECC_SUPPORT
+ ///
+ /// Helper class to test ArraySegmentStream without Span support.
+ ///
+ public class ArraySegmentStreamNoSpan : ArraySegmentStream
+ {
+ public ArraySegmentStreamNoSpan(BufferManager bufferManager)
+ : base(bufferManager)
+ {
+ }
+
+ public ArraySegmentStreamNoSpan(BufferCollection buffers)
+ : base(buffers)
+ {
+ }
+
+ public override int Read(Span buffer)
+ {
+ return base.ReadMemoryStream(buffer);
+ }
+
+ public override void Write(ReadOnlySpan buffer)
+ {
+ base.WriteMemoryStream(buffer);
+ }
+ }
+#endif
+}
diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs
index c50b97908..3ef4f09e1 100644
--- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs
+++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs
@@ -37,13 +37,25 @@
using System.Text;
using System.Threading;
using System.Xml;
+using Microsoft.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
+using Opc.Ua.Bindings;
using Opc.Ua.Test;
namespace Opc.Ua.Core.Tests.Types.Encoders
{
+ ///
+ /// Supported memory stream types.
+ ///
+ public enum MemoryStreamType
+ {
+ MemoryStream,
+ ArraySegmentStream,
+ RecyclableMemoryStream
+ }
+
///
/// Base class for the encoder tests.
///
@@ -55,12 +67,16 @@ public class EncoderCommon
protected const int kRandomStart = 4840;
protected const int kRandomRepeats = 100;
protected const int kMaxArrayLength = 1024 * 64;
+ protected const int kTestBlockSize = 0x1000;
protected const string kApplicationUri = "uri:localhost:opcfoundation.org:EncoderCommon";
protected RandomSource RandomSource { get; private set; }
protected DataGenerator DataGenerator { get; private set; }
protected IServiceMessageContext Context { get; private set; }
protected NamespaceTable NameSpaceUris { get; private set; }
protected StringTable ServerUris { get; private set; }
+ protected BufferManager BufferManager { get; private set; }
+ protected RecyclableMemoryStreamManager RecyclableMemoryManager { get; private set; }
+
#region Test Setup
[OneTimeSetUp]
@@ -74,6 +90,8 @@ protected void OneTimeSetUp()
NameSpaceUris.GetIndexOrAppend(kApplicationUri);
NameSpaceUris.GetIndexOrAppend(Namespaces.OpcUaGds);
ServerUris = new StringTable();
+ BufferManager = new BufferManager(nameof(EncoderCommon), kTestBlockSize);
+ RecyclableMemoryManager = new RecyclableMemoryStreamManager(new RecyclableMemoryStreamManager.Options { BlockSize = kTestBlockSize });
}
[OneTimeTearDown]
@@ -135,6 +153,7 @@ protected void SetRandomSeed(int randomSeed)
protected string EncodeDataValue(
EncodingType encoderType,
BuiltInType builtInType,
+ MemoryStreamType memoryStreamType,
object data,
bool useReversibleEncoding = true
)
@@ -146,7 +165,7 @@ protected string EncodeDataValue(
TestContext.Out.WriteLine("Expected:");
TestContext.Out.WriteLine(expected);
Assert.IsNotNull(expected, "Expected DataValue is Null, " + encodeInfo);
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType))
{
using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue), useReversibleEncoding))
{
@@ -164,6 +183,7 @@ protected string EncodeDataValue(
protected void EncodeDecodeDataValue(
EncodingType encoderType,
BuiltInType builtInType,
+ MemoryStreamType memoryStreamType,
object data
)
{
@@ -176,7 +196,7 @@ object data
TestContext.Out.WriteLine(expected);
byte[] buffer;
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType))
{
using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue)))
{
@@ -217,6 +237,7 @@ object data
protected void EncodeDecode(
EncodingType encoderType,
BuiltInType builtInType,
+ MemoryStreamType memoryStreamType,
object expected
)
{
@@ -227,7 +248,7 @@ object expected
TestContext.Out.WriteLine(expected);
byte[] buffer;
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType))
{
using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type))
{
@@ -272,6 +293,7 @@ object expected
///
protected void EncodeJsonVerifyResult(
BuiltInType builtInType,
+ MemoryStreamType memoryStreamType,
object data,
bool useReversibleEncoding,
string expected,
@@ -300,7 +322,7 @@ bool includeDefaults
bool includeDefaultNumbers = isNumber ? includeDefaults : true;
byte[] buffer;
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType))
{
using (IEncoder encoder = CreateEncoder(EncodingType.Json, Context, encoderStream, typeof(DataValue),
useReversibleEncoding, topLevelIsArray, includeDefaultValues, includeDefaultNumbers))
@@ -379,16 +401,18 @@ protected string PrettifyAndValidateJson(string json)
using (var stringWriter = new StringWriter())
using (var stringReader = new StringReader(json))
{
- var jsonReader = new JsonTextReader(stringReader);
- var jsonWriter = new JsonTextWriter(stringWriter) {
+ using (var jsonReader = new JsonTextReader(stringReader))
+ using (var jsonWriter = new JsonTextWriter(stringWriter) {
FloatFormatHandling = FloatFormatHandling.String,
Formatting = Newtonsoft.Json.Formatting.Indented,
Culture = System.Globalization.CultureInfo.InvariantCulture
- };
- jsonWriter.WriteToken(jsonReader);
- string formattedJson = stringWriter.ToString();
- TestContext.Out.WriteLine(formattedJson);
- return formattedJson;
+ })
+ {
+ jsonWriter.WriteToken(jsonReader);
+ string formattedJson = stringWriter.ToString();
+ TestContext.Out.WriteLine(formattedJson);
+ return formattedJson;
+ }
}
}
catch (Exception ex)
@@ -399,6 +423,27 @@ protected string PrettifyAndValidateJson(string json)
return json;
}
+ ///
+ /// Returns various implementations of a memory stream.
+ ///
+ ///
+ /// A MemoryStream
+ ///
+ protected MemoryStream CreateEncoderMemoryStream(MemoryStreamType memoryStreamType)
+ {
+ switch (memoryStreamType)
+ {
+ case MemoryStreamType.MemoryStream:
+ return new MemoryStream(kTestBlockSize);
+ case MemoryStreamType.ArraySegmentStream:
+ return new ArraySegmentStream(BufferManager);
+ case MemoryStreamType.RecyclableMemoryStream:
+ return new RecyclableMemoryStream(RecyclableMemoryManager);
+ default:
+ throw new ArgumentOutOfRangeException(nameof(memoryStreamType), memoryStreamType, "Invalid MemoryStreamType specified.");
+ }
+ }
+
///
/// Encoder factory for all encoding types.
///
@@ -418,13 +463,13 @@ protected IEncoder CreateEncoder(
{
case EncodingType.Binary:
Assume.That(useReversibleEncoding, "Binary encoding only supports reversible option.");
- return new BinaryEncoder(stream, context, false);
+ return new BinaryEncoder(stream, context, true);
case EncodingType.Xml:
Assume.That(useReversibleEncoding, "Xml encoding only supports reversible option.");
var xmlWriter = XmlWriter.Create(stream);
return new XmlEncoder(systemType, xmlWriter, context);
case EncodingType.Json:
- return new JsonEncoder(context, useReversibleEncoding, topLevelIsArray, stream) {
+ return new JsonEncoder(context, useReversibleEncoding, topLevelIsArray, stream, true) {
IncludeDefaultValues = includeDefaultValues,
IncludeDefaultNumberValues = includeDefaultNumbers
};
diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs
index 840b4fced..94fd0518b 100644
--- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs
+++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs
@@ -60,7 +60,7 @@ BuiltInType builtInType
)
{
object defaultValue = TypeInfo.GetDefaultValue(builtInType);
- EncodeDecodeDataValue(encoderType, builtInType, defaultValue);
+ EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.MemoryStream, defaultValue);
}
///
@@ -76,7 +76,7 @@ BuiltInType builtInType
{
Assume.That(builtInType != BuiltInType.DiagnosticInfo);
object randomData = DataGenerator.GetRandom(builtInType);
- EncodeDecodeDataValue(encoderType, builtInType, randomData);
+ EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.ArraySegmentStream, randomData);
}
///
@@ -120,7 +120,7 @@ BuiltInType builtInType
break;
}
};
- EncodeDecode(encoderType, builtInType, randomData);
+ EncodeDecode(encoderType, builtInType, MemoryStreamType.ArraySegmentStream, randomData);
}
///
@@ -140,7 +140,7 @@ BuiltInType builtInType
// or encoding of extension objects fails.
randomData = ExtensionObject.Null;
}
- EncodeDecode(encoderType, builtInType, randomData);
+ EncodeDecode(encoderType, builtInType, MemoryStreamType.RecyclableMemoryStream, randomData);
}
///
@@ -156,7 +156,7 @@ BuiltInType builtInType
Array boundaryValues = DataGenerator.GetRandomArray(builtInType, true, 10, true);
foreach (var boundaryValue in boundaryValues)
{
- EncodeDecode(encoderType, builtInType, boundaryValue);
+ EncodeDecode(encoderType, builtInType, MemoryStreamType.MemoryStream, boundaryValue);
}
}
@@ -176,7 +176,7 @@ int arrayLength
// ensure different sized arrays contain different data set
SetRandomSeed(arrayLength);
object randomData = DataGenerator.GetRandomArray(builtInType, useBoundaryValues, arrayLength, true);
- EncodeDecodeDataValue(encoderType, builtInType, randomData);
+ EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.ArraySegmentStream, randomData);
}
///
@@ -191,7 +191,7 @@ BuiltInType builtInType
)
{
object randomData = DataGenerator.GetRandomArray(builtInType, false, 0, true);
- EncodeDecodeDataValue(encoderType, builtInType, randomData);
+ EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.RecyclableMemoryStream, randomData);
}
///
@@ -206,7 +206,7 @@ EncodingType encoderType
{
SetRepeatedRandomSeed();
object randomData = DataGenerator.GetRandom(BuiltInType.Variant);
- EncodeDecodeDataValue(encoderType, BuiltInType.Variant, randomData);
+ EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.MemoryStream, randomData);
}
///
@@ -224,11 +224,11 @@ BuiltInType builtInType
{
Assert.Throws(
typeof(ServiceResultException),
- () => EncodeDataValue(EncodingType.Json, builtInType, randomData, false)
+ () => EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.ArraySegmentStream, randomData, false)
);
return;
}
- string json = EncodeDataValue(EncodingType.Json, builtInType, randomData, false);
+ string json = EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.MemoryStream, randomData, false);
PrettifyAndValidateJson(json);
}
@@ -246,7 +246,7 @@ int arrayLength
{
SetRandomSeed(arrayLength);
object randomData = DataGenerator.GetRandomArray(builtInType, useBoundaryValues, arrayLength, true);
- string json = EncodeDataValue(EncodingType.Json, builtInType, randomData, false);
+ string json = EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.RecyclableMemoryStream, randomData, false);
PrettifyAndValidateJson(json);
}
@@ -261,7 +261,7 @@ BuiltInType builtInType
)
{
object randomData = DataGenerator.GetRandomArray(builtInType, false, 0, true);
- string json = EncodeDataValue(EncodingType.Json, builtInType, randomData, false);
+ string json = EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.MemoryStream, randomData, false);
PrettifyAndValidateJson(json);
}
@@ -284,7 +284,7 @@ EncodingType encoderType
//new Variant(new TestEnumType[] { TestEnumType.One, TestEnumType.Two, TestEnumType.Hundred }),
new Variant(new Int32[] { 2, 3, 10 }, new TypeInfo(BuiltInType.Enumeration, 1))
};
- EncodeDecodeDataValue(encoderType, BuiltInType.Variant, variant);
+ EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.ArraySegmentStream, variant);
}
///
@@ -302,7 +302,7 @@ BuiltInType builtInType
int arrayDimension = RandomSource.NextInt32(99) + 1;
Array randomData = DataGenerator.GetRandomArray(builtInType, false, arrayDimension, true);
var variant = new Variant(randomData, new TypeInfo(builtInType, 1));
- EncodeDecodeDataValue(encoderType, BuiltInType.Variant, variant);
+ EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.RecyclableMemoryStream, variant);
}
///
@@ -327,7 +327,7 @@ BuiltInType builtInType
TestContext.Out.WriteLine(randomData);
byte[] buffer;
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.MemoryStream))
{
using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type, true, false))
{
@@ -383,7 +383,7 @@ BuiltInType builtInType
int elements = ElementsFromDimension(dimensions);
Array randomData = DataGenerator.GetRandomArray(builtInType, false, elements, true);
var variant = new Variant(new Matrix(randomData, builtInType, dimensions));
- EncodeDecodeDataValue(encoderType, BuiltInType.Variant, variant);
+ EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.RecyclableMemoryStream, variant);
}
///
@@ -403,7 +403,7 @@ BuiltInType builtInType
int elements = ElementsFromDimension(dimensions);
Array randomData = DataGenerator.GetRandomArray(builtInType, false, elements, true);
var variant = new Variant(new Matrix(randomData, builtInType, dimensions));
- string json = EncodeDataValue(EncodingType.Json, BuiltInType.Variant, variant, false);
+ string json = EncodeDataValue(EncodingType.Json, BuiltInType.Variant, MemoryStreamType.ArraySegmentStream, variant, false);
_ = PrettifyAndValidateJson(json);
}
@@ -432,7 +432,7 @@ public void EncodeMatrixInArray(
TestContext.Out.WriteLine(matrix);
byte[] buffer;
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.MemoryStream))
{
using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type))
{
@@ -504,7 +504,7 @@ BuiltInType builtInType
TestContext.Out.WriteLine(expected);
byte[] buffer;
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.MemoryStream))
{
using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue)))
{
@@ -595,7 +595,7 @@ BuiltInType builtInType
TestContext.Out.WriteLine(expected);
byte[] buffer;
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.ArraySegmentStream))
{
using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue)))
{
@@ -656,8 +656,8 @@ BuiltInType builtInType
[Theory]
[Category("Matrix")]
public void EncodeMatrixInArrayOverflow(
- EncodingType encoderType,
- BuiltInType builtInType
+ EncodingType encoderType,
+ BuiltInType builtInType
)
{
Assume.That(builtInType != BuiltInType.Null);
@@ -687,7 +687,7 @@ BuiltInType builtInType
TestContext.Out.WriteLine(matrix);
byte[] buffer;
- using (var encoderStream = new MemoryStream())
+ using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.RecyclableMemoryStream))
{
using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type))
{
diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs
index c55495a07..2ac94acae 100644
--- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs
+++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderBenchmarks.cs
@@ -27,13 +27,8 @@
* 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 System.Threading;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
using Microsoft.IO;
@@ -47,57 +42,55 @@ namespace Opc.Ua.Core.Tests.Types.Encoders
[NonParallelizable]
[MemoryDiagnoser]
[DisassemblyDiagnoser]
- public class JsonEncoderBenchmarks
+ public class JsonEncoderBenchmarks : EncoderBenchmarks
{
- const int kBufferSize = 4096;
-
- [Params(1, 2, 8, 64)]
- public int PayLoadSize { get; set; } = 64;
-
[Params(128, 512, 1024, 4096, 8192, 65536)]
public int StreamSize { get; set; } = 1024;
///
/// Benchmark overhead to create StreamWriter, MemoryStream is kept open.
///
- [Benchmark]
[Test]
public void StreamWriter()
{
- using (var test = new StreamWriter(m_memoryStream, Encoding.UTF8, StreamSize, true))
+ using (var memoryStream = new MemoryStream())
+ using (var test = new StreamWriter(memoryStream, Encoding.UTF8, StreamSize, true))
+ {
test.Flush();
+ }
}
///
/// Benchmark overhead to create StreamWriter and MemoryStream.
///
- [Benchmark]
[Test]
public void StreamWriterRecyclableMemoryStream()
{
using (var memoryStream = new RecyclableMemoryStream(m_memoryManager))
using (var test = new StreamWriter(memoryStream, Encoding.UTF8, StreamSize))
+ {
test.Flush();
+ }
}
///
/// Benchmark overhead to create StreamWriter and MemoryStream.
///
- [Benchmark]
[Test]
public void StreamWriterMemoryStream()
{
using (var memoryStream = new MemoryStream())
using (var test = new StreamWriter(memoryStream, Encoding.UTF8, StreamSize))
+ {
test.Flush();
+ }
}
///
/// Benchmark encoding with internal memory stream.
///
- [Benchmark]
[Test]
- public void JsonEncoderConstructor2()
+ public void JsonEncoderConstructor()
{
using (var jsonEncoder = new JsonEncoder(m_context, false))
{
@@ -107,1097 +100,186 @@ public void JsonEncoderConstructor2()
}
///
- /// Benchmark encoding with memory stream kept open.
- ///
- [Benchmark]
- [Test]
- public void JsonEncoderStreamLeaveOpenMemoryStream()
- {
- JsonEncoder_StreamLeaveOpen(m_memoryStream);
- }
-
- ///
- /// Benchmark encoding with recyclable memory stream kept open.
- ///
- [Benchmark]
- [Test]
- public void JsonEncoderStreamLeaveOpenRecyclableMemoryStream()
- {
- JsonEncoder_StreamLeaveOpen(m_recyclableMemoryStream);
- }
-
- ///
- /// Benchmark encoding with recyclable memory stream kept open.
- ///
- [Benchmark]
- [Test]
- public void JsonEncoderStreamLeaveOpenArraySegmentStream()
- {
- JsonEncoder_StreamLeaveOpen(m_arraySegmentStream);
- }
-
- ///
- /// Benchmark encoding with memory stream kept open,
- /// use internal reflection to get string from memory stream.
- ///
- [Benchmark]
- [Test]
- public void JsonEncoderConstructorStreamwriterReflection2()
- {
- using (var jsonEncoder = new JsonEncoder(m_context, false, false, m_memoryStream, true, StreamSize))
- {
- TestEncoding(jsonEncoder);
- var result = jsonEncoder.CloseAndReturnText();
- }
- }
-
- #region Private Methods
- private void TestEncoding(IEncoder encoder)
- {
- int payLoadSize = PayLoadSize;
- encoder.WriteByte("Byte", 0);
- while (--payLoadSize > 0)
- {
- encoder.WriteBoolean("Boolean", true);
- encoder.WriteUInt64("UInt64", 1234566890);
- encoder.WriteString("String", "The quick brown fox...");
- encoder.WriteNodeId("NodeId", s_nodeId);
- encoder.WriteInt32Array("Array", s_list);
- }
- }
-
- private void JsonEncoder_StreamLeaveOpen(MemoryStream stream)
- {
- int length1;
- int length2;
- stream.Position = 0;
- using (var jsonEncoder = new JsonEncoder(m_context, false, false, stream, true, StreamSize))
- {
- TestEncoding(jsonEncoder);
- length1 = jsonEncoder.Close();
- }
- using (var jsonEncoder = new JsonEncoder(m_context, false, false, stream, true, StreamSize))
- {
- TestEncoding(jsonEncoder);
- length2 = jsonEncoder.Close();
- }
- var result = Encoding.UTF8.GetString(stream.ToArray());
- Assert.NotNull(result);
- Assert.AreEqual(length1 * 2, length2);
- Assert.AreEqual(length2, result.Length);
- }
- #endregion
-
- #region Test Setup
- [OneTimeSetUp]
- public void OneTimeSetUp()
- {
- // for validating benchmark tests
- m_context = new ServiceMessageContext();
- m_memoryStream = new MemoryStream();
- m_memoryManager = new RecyclableMemoryStreamManager();
- m_recyclableMemoryStream = new RecyclableMemoryStream(m_memoryManager);
- m_bufferManager = new BufferManager(nameof(BinaryEncoder), kBufferSize);
- m_arraySegmentStream = new ArraySegmentStream(m_bufferManager, kBufferSize, 0, kBufferSize);
- }
-
- [OneTimeTearDown]
- public void OneTimeTearDown()
- {
- m_context = null;
- m_memoryStream.Dispose();
- m_memoryStream = null;
- m_recyclableMemoryStream.Dispose();
- m_recyclableMemoryStream = null;
- m_memoryManager = null;
- m_bufferManager = null;
- m_arraySegmentStream.Dispose();
- m_arraySegmentStream = null;
- }
- #endregion
-
- #region Benchmark Setup
- ///
- /// Set up some variables for benchmarks.
+ /// Test encoding with ArrayPool memory stream.
///
- [GlobalSetup]
- public void GlobalSetup()
- {
- // for validating benchmark tests
- m_context = new ServiceMessageContext();
- m_memoryStream = new MemoryStream();
- m_memoryManager = new RecyclableMemoryStreamManager();
- m_recyclableMemoryStream = new RecyclableMemoryStream(m_memoryManager);
- m_bufferManager = new BufferManager(nameof(BinaryEncoder), kBufferSize);
- m_arraySegmentStream = new ArraySegmentStream(m_bufferManager, kBufferSize, 0, kBufferSize);
- }
-
- ///
- /// Tear down benchmark variables.
- ///
- [GlobalCleanup]
- public void GlobalCleanup()
- {
- m_context = null;
- m_memoryStream.Dispose();
- m_memoryStream = null;
- m_recyclableMemoryStream.Dispose();
- m_recyclableMemoryStream = null;
- m_memoryManager = null;
- m_bufferManager = null;
- m_arraySegmentStream.Dispose();
- m_arraySegmentStream = null;
- }
- #endregion
-
- #region Private Fields
- private static NodeId s_nodeId = new NodeId(1234);
- private static IList s_list = new List() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
- private IServiceMessageContext m_context;
- private MemoryStream m_memoryStream;
- private RecyclableMemoryStreamManager m_memoryManager;
- private RecyclableMemoryStream m_recyclableMemoryStream;
- private BufferManager m_bufferManager;
- private ArraySegmentStream m_arraySegmentStream;
- #endregion
- }
-
- [TestFixture, Category("JsonEncoder")]
- [SetCulture("en-us"), SetUICulture("en-us")]
- [NonParallelizable]
- [MemoryDiagnoser]
- [DisassemblyDiagnoser]
- public class JsonEncoderDateTimeBenchmark
- {
- [Params(0, 4, 7)]
- public int DateTimeOmittedZeros { get; set; } = 0;
-
- [Benchmark]
- [Test]
- public void DateTimeEncodeToString()
- {
- _ = m_dateTime.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK", CultureInfo.InvariantCulture);
- }
-
- [Benchmark]
- [Test]
- public void ConvertToUniversalTime()
- {
- _ = JsonEncoder.ConvertUniversalTimeToString(m_dateTime);
- }
-
- #region Test Setup
- [OneTimeSetUp]
- public void OneTimeSetUp()
- {
- // for validating benchmark tests
- m_dateTime = DateTime.UtcNow;
- }
-
- [OneTimeTearDown]
- public void OneTimeTearDown()
- {
- }
- #endregion
-
- #region Benchmark Setup
- ///
- /// Set up some variables for benchmarks.
- ///
- [GlobalSetup]
- public void GlobalSetup()
- {
- // for validating benchmark tests
- switch (DateTimeOmittedZeros)
- {
- case 4: m_dateTime = new DateTime(2011, 11, 11, 11, 11, 11, 999, DateTimeKind.Utc); break;
- case 7: m_dateTime = new DateTime(2011, 11, 11, 11, 11, 11, DateTimeKind.Utc); break;
- default:
- do
- {
- m_dateTime = DateTime.UtcNow;
- } while (m_dateTime.Ticks % 10 == 0);
- break;
- }
- }
-
- ///
- /// Tear down benchmark variables.
- ///
- [GlobalCleanup]
- public void GlobalCleanup()
- {
- }
- #endregion
-
- #region Private Fields
- private DateTime m_dateTime;
- #endregion
- }
-
- [TestFixture, Category("JsonEncoder")]
- [SetCulture("en-us"), SetUICulture("en-us")]
- [NonParallelizable]
- [MemoryDiagnoser]
- [DisassemblyDiagnoser(printSource: true)]
- public class JsonEncoderEscapeStringBenchmarks
- {
- public const int InnerLoops = 100;
- [DatapointSource]
- [Params(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)]
- public int StringVariantIndex { get; set; } = 1;
-
- [DatapointSource]
- // for benchmarking with different escaped strings
- public static readonly string[] EscapeTestStrings =
- {
- // The use case without escape characters, plain text
- "The quick brown fox jumps over the lazy dog.",
- // The use case with many control characters escaped, 1 char spaces
- "\" \n \r \t \b \f \\ ",
- // The use case with many control characters escaped, 2 char spaces
- " \" \n \r \t \b \f \\ ",
- // The use case with many control characters escaped, 3 char spaces
- " \" \n \r \t \b \f \\ ",
- // The use case with many control characters escaped, 5 char spaces
- " \" \n \r \t \b \f \\ ",
- // The use case with many binary characters escaped, 1 char spaces
- "\0 \x01 \x02 \x03 \x04 ",
- // The use case with many binary characters escaped, 2 char spaces
- " \0 \x01 \x02 \x03 \x04 ",
- // The use case with many binary characters escaped, 3 char spaces
- " \0 \x01 \x02 \x03 \x04 ",
- // The use case with many binary characters escaped, 5 char spaces
- " \0 \x01 \x02 \x03 \x04 ",
- // The use case with all escape characters and a long string
- "Ascii characters, special characters \n \b & control characters \0 \x04 ␀ ␁ ␂ ␃ ␄. This is a test.",
- };
-
- ///
- /// Benchmark encoding of the previous implementation.
- ///
- [Benchmark(Baseline = true)]
- public void EscapeStringLegacy()
- {
- m_memoryStream.Position = 0;
- int repeats = InnerLoops;
- while (repeats-- > 0)
- {
- EscapedStringLegacy(m_testString);
- }
- m_streamWriter.Flush();
- }
-
- ///
- /// Benchmark encoding of the previous implementation with snall improvement for binary encoding.
- ///
- [Benchmark]
- public void EscapeStringLegacyPlus()
- {
- m_memoryStream.Position = 0;
- int repeats = InnerLoops;
- while (repeats-- > 0)
- {
- EscapedStringLegacyPlus(m_testString);
- }
- m_streamWriter.Flush();
- }
-
- ///
- /// A new implementation using StringBuilder.
- ///
- [Benchmark]
- public void EscapeStringStringBuilder()
- {
- m_memoryStream.Position = 0;
- int repeats = InnerLoops;
- while (repeats-- > 0)
- {
- EscapeString(m_testString);
- }
- m_streamWriter.Flush();
- }
-
- ///
- /// A new implementation using ThreadLocal StringBuilder.
- ///
- [Benchmark]
- public void EscapeStringThreadLocal()
- {
- m_memoryStream.Position = 0;
- int repeats = InnerLoops;
- while (repeats-- > 0)
- {
- EscapeStringThreadLocal(m_testString);
- }
- m_streamWriter.Flush();
- }
-
- ///
- /// A new implementation using ReadOnlySpan.
- ///
- [Benchmark]
- public void EscapeStringSpan()
+ [Theory]
+ public void JsonEncoderArraySegmentStreamTest(bool toText)
{
- m_memoryStream.Position = 0;
- int repeats = InnerLoops;
- while (repeats-- > 0)
+ using (var memoryStream = new ArraySegmentStream(m_bufferManager, StreamBufferSize, 0, StreamBufferSize))
{
- EscapeStringSpan(m_testString);
+ TestStreamEncode(memoryStream, toText);
}
- m_streamWriter.Flush();
}
///
- /// A new implementation using ReadOnlySpan and char write.
+ /// Test encoding with memory stream kept open.
///
- [Benchmark]
- public void EscapeStringSpanChars()
+ [Theory]
+ public void JsonEncoderMemoryStreamTest(bool toText)
{
- m_memoryStream.Position = 0;
- int repeats = InnerLoops;
- while (repeats-- > 0)
+ using (var memoryStream = new MemoryStream(StreamBufferSize))
{
- EscapeStringSpanChars(m_testString);
+ TestStreamEncode(memoryStream, toText);
}
- m_streamWriter.Flush();
}
///
- /// A new implementation using ReadOnlySpan and char write.
+ /// Test encoding with recyclable memory stream kept open.
///
- [Benchmark]
- public void EscapeStringSpanCharsInline()
+ [Theory]
+ public void JsonEncoderRecyclableMemoryStream(bool toText)
{
- m_memoryStream.Position = 0;
- int repeats = InnerLoops;
- while (repeats-- > 0)
+ using (var memoryStream = new RecyclableMemoryStream(m_memoryManager))
{
- EscapeStringSpanCharsInline(m_testString);
+ TestStreamEncode(memoryStream, toText);
}
- m_streamWriter.Flush();
}
///
- /// A new implementation using ReadOnlySpan and char write with const arrays.
+ /// Benchmark encoding with memory stream kept open.
///
[Benchmark]
- public void EscapeStringSpanCharsInlineConst()
+ [Test]
+ public void JsonEncoderMemoryStream()
{
- m_memoryStream.Position = 0;
- int repeats = InnerLoops;
- while (repeats-- > 0)
+ using (var memoryStream = new MemoryStream(StreamBufferSize))
{
- EscapeStringSpanCharsInlineConst(m_testString);
+ JsonEncoder_StreamLeaveOpen(memoryStream);
+ // get buffer for write
+ _ = memoryStream.ToArray();
}
- m_streamWriter.Flush();
}
///
- /// A new implementation using ReadOnlySpan and IndexOf.
+ /// Benchmark encoding with recyclable memory stream kept open.
///
[Benchmark]
- public void EscapeStringSpanIndex()
+ [Test]
+ public void JsonEncoderRecyclableMemoryStream()
{
- m_memoryStream.Position = 0;
- int repeats = InnerLoops;
- while (repeats-- > 0)
+ using (var recyclableMemoryStream = new RecyclableMemoryStream(m_memoryManager))
{
- EscapeStringSpanIndex(m_testString);
+ JsonEncoder_StreamLeaveOpen(recyclableMemoryStream);
+ // get buffers for write
+ _ = recyclableMemoryStream.GetReadOnlySequence();
}
- m_streamWriter.Flush();
}
///
- /// A new implementation using ReadOnlySpan and Dictionary.
+ /// Benchmark encoding with recyclable memory stream kept open.
///
[Benchmark]
- public void EscapeStringSpanDict()
- {
- m_memoryStream.Position = 0;
- int repeats = InnerLoops;
- while (repeats-- > 0)
- {
- EscapeStringSpanDict(m_testString);
- }
- m_streamWriter.Flush();
- }
-
- [Theory]
- [TestCase("No Escape chars", 0)]
- [TestCase("control chars escaped, 1 char space", 1)]
- [TestCase("control chars escaped, 2 char spaces", 2)]
- [TestCase("control chars escaped, 3 char spaces", 3)]
- [TestCase("control chars escaped, 5 char spaces", 4)]
- [TestCase("binary chars escaped, 1 char space", 5)]
- [TestCase("binary chars escaped, 2 char spaces", 6)]
- [TestCase("binary chars escaped, 3 char spaces", 7)]
- [TestCase("binary chars escaped, 5 char spaces", 8)]
- [TestCase("all escape chars and long string", 9)]
- public void EscapeStringValidation(string name, int index)
- {
- m_testString = EscapeTestStrings[index];
- TestContext.Out.WriteLine(m_testString);
- var testArray = m_testString.ToCharArray();
-
- m_memoryStream.Position = 0;
- EscapeStringLegacy();
- m_streamWriter.Flush();
- byte[] resultLegacy = m_memoryStream.ToArray();
- TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacy));
-
- m_memoryStream.Position = 0;
- EscapeStringLegacyPlus();
- m_streamWriter.Flush();
- byte[] resultLegacyPlus = m_memoryStream.ToArray();
- TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacyPlus));
-
- m_memoryStream.Position = 0;
- EscapeStringStringBuilder();
- m_streamWriter.Flush();
- byte[] result = m_memoryStream.ToArray();
- TestContext.Out.WriteLine(Encoding.UTF8.GetString(result));
-
- m_memoryStream.Position = 0;
- EscapeStringThreadLocal();
- m_streamWriter.Flush();
- byte[] resultThreadLocal = m_memoryStream.ToArray();
- TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultThreadLocal));
-
- m_memoryStream.Position = 0;
- EscapeStringSpan(m_testString);
- m_streamWriter.Flush();
- byte[] resultSpan = m_memoryStream.ToArray();
- TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpan));
-
- m_memoryStream.Position = 0;
- EscapeStringSpanChars(m_testString);
- m_streamWriter.Flush();
- byte[] resultSpanChars = m_memoryStream.ToArray();
- TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanChars));
-
- m_memoryStream.Position = 0;
- EscapeStringSpanCharsInline(m_testString);
- m_streamWriter.Flush();
- byte[] resultSpanCharsInline = m_memoryStream.ToArray();
- TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanCharsInline));
-
- m_memoryStream.Position = 0;
- EscapeStringSpanCharsInlineConst(m_testString);
- m_streamWriter.Flush();
- byte[] resultSpanCharsInlineConst = m_memoryStream.ToArray();
- TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanCharsInlineConst));
-
- m_memoryStream.Position = 0;
- EscapeStringSpanIndex(m_testString);
- m_streamWriter.Flush();
- byte[] resultSpanIndex = m_memoryStream.ToArray();
- TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanIndex));
-
- m_memoryStream.Position = 0;
- EscapeStringSpanDict(m_testString);
- m_streamWriter.Flush();
- byte[] resultSpanDict = m_memoryStream.ToArray();
- TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanDict));
-
- Assert.IsTrue(Utils.IsEqual(resultLegacy, result));
- Assert.IsTrue(Utils.IsEqual(resultLegacy, resultLegacyPlus));
- Assert.IsTrue(Utils.IsEqual(resultLegacy, resultThreadLocal));
- Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpan));
- Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanChars));
- Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanCharsInline));
- Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanCharsInlineConst));
- Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanIndex));
- Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanDict));
- }
-
- #region Test Setup
- [OneTimeSetUp]
- public void OneTimeSetUp()
- {
- m_memoryManager = new RecyclableMemoryStreamManager();
- m_memoryStream = new RecyclableMemoryStream(m_memoryManager);
- m_streamWriter = new StreamWriter(m_memoryStream, new UTF8Encoding(false), m_streamSize, false);
- }
-
- [OneTimeTearDown]
- public void OneTimeTearDown()
- {
- m_streamWriter?.Dispose();
- m_streamWriter = null;
- m_memoryStream?.Dispose();
- m_memoryStream = null;
- m_memoryManager = null;
- }
- #endregion
-
- #region Benchmark Setup
- /// 4
- /// Set up some variables for benchmarks.
- ///
- [GlobalSetup]
- public void GlobalSetup()
- {
- m_memoryManager = new RecyclableMemoryStreamManager();
- m_memoryStream = new RecyclableMemoryStream(m_memoryManager);
- m_streamWriter = new StreamWriter(m_memoryStream, new UTF8Encoding(false), m_streamSize, false);
- m_testString = EscapeTestStrings[StringVariantIndex - 1];
- }
-
- [GlobalCleanup]
- public void GlobalCleanup()
- {
- m_streamWriter?.Dispose();
- m_streamWriter = null;
- m_memoryStream?.Dispose();
- m_memoryStream = null;
- m_memoryManager = null;
- }
- #endregion
-
- #region Private Methods
- ///
- /// Version used previously in JsonEncoder.
- ///
- private void EscapedStringLegacy(string value)
- {
- foreach (char ch in value)
- {
- bool found = false;
-
- for (int ii = 0; ii < m_specialChars.Length; ii++)
- {
- if (m_specialChars[ii] == ch)
- {
- m_streamWriter.Write('\\');
- m_streamWriter.Write(m_substitution[ii]);
- found = true;
- break;
- }
- }
-
- if (!found)
- {
- if (ch < 32)
- {
- m_streamWriter.Write("\\u");
- m_streamWriter.Write("{0:X4}", (int)ch);
- continue;
- }
-
- m_streamWriter.Write(ch);
- }
- }
- }
-
- ///
- /// Version used previously in JsonEncoder plus improvement of binary encoding.
- ///
- ///
- /// For the underlying stream writer it is faster to write two chars than a 2 char string.
- ///
- private void EscapedStringLegacyPlus(string value)
+ [Test]
+ public void JsonEncoderArraySegmentStream()
{
- foreach (char ch in value)
+ using (var arraySegmentStream = new ArraySegmentStream(m_bufferManager))
{
- bool found = false;
-
- for (int ii = 0; ii < m_specialChars.Length; ii++)
+ JsonEncoder_StreamLeaveOpen(arraySegmentStream);
+ // get buffers and return them to buffer manager
+ var buffers = arraySegmentStream.GetBuffers("writer");
+ foreach (var buffer in buffers)
{
- if (m_specialChars[ii] == ch)
- {
- m_streamWriter.Write('\\');
- m_streamWriter.Write(m_substitution[ii]);
- found = true;
- break;
- }
- }
-
- if (!found)
- {
- if (ch < 32)
- {
- m_streamWriter.Write('\\');
- m_streamWriter.Write('u');
- m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
- continue;
- }
-
- m_streamWriter.Write(ch);
+ m_bufferManager.ReturnBuffer(buffer.Array, "testreturn");
}
}
}
///
- /// Using a span to escape the string, write strings to stream writer if possible.
- ///
- private void EscapeStringSpan(string value)
- {
- ReadOnlySpan charSpan = value.AsSpan();
- int lastOffset = 0;
-
- for (int i = 0; i < charSpan.Length; i++)
- {
- bool found = false;
- char ch = charSpan[i];
-
- for (int ii = 0; ii < m_specialChars.Length; ii++)
- {
- if (m_specialChars[ii] == ch)
- {
- if (lastOffset < i)
- {
-#if NETCOREAPP2_1_OR_GREATER
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
-#else
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
-#endif
- }
- lastOffset = i + 1;
- m_streamWriter.Write(m_substitutionStrings[ii]);
- found = true;
- break;
- }
- }
-
- if (!found && ch < 32)
- {
- if (lastOffset < i)
- {
-#if NETCOREAPP2_1_OR_GREATER
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
-#else
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
-#endif
- }
- lastOffset = i + 1;
- m_streamWriter.Write("\\u");
- m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
- }
- }
- if (lastOffset == 0)
- {
- m_streamWriter.Write(value);
- }
- else if (lastOffset < charSpan.Length)
- {
-#if NETCOREAPP2_1_OR_GREATER
- m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset));
-#else
- m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString());
-#endif
- }
- }
-
- ///
- /// Use span to escape the string, write only chars to stream writer.
+ /// Benchmark encoding with recyclable memory stream kept open.
///
- ///
- private void EscapeStringSpanChars(string value)
- {
- ReadOnlySpan charSpan = value.AsSpan();
-
- int lastOffset = 0;
- for (int i = 0; i < charSpan.Length; i++)
- {
- bool found = false;
- char ch = charSpan[i];
-
- for (int ii = 0; ii < m_specialChars.Length; ii++)
- {
- if (m_specialChars[ii] == ch)
- {
- if (lastOffset < i)
- {
-#if NETCOREAPP2_1_OR_GREATER
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
-#else
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
-#endif
- }
- lastOffset = i + 1;
- m_streamWriter.Write('\\');
- m_streamWriter.Write(m_substitution[ii]);
- found = true;
- break;
- }
- }
-
- if (!found && ch < 32)
- {
- if (lastOffset < i - 1)
- {
-#if NETCOREAPP2_1_OR_GREATER
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
-#else
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
-#endif
- }
- else
- {
- while (lastOffset < i)
- {
- m_streamWriter.Write(charSpan[lastOffset++]);
- }
- }
- lastOffset = i + 1;
- m_streamWriter.Write('\\');
- m_streamWriter.Write('u');
- m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
- }
- }
-
- if (lastOffset == 0)
- {
-#if NETCOREAPP2_1_OR_GREATER
- m_streamWriter.Write(charSpan);
-#else
- m_streamWriter.Write(value);
-#endif
- }
- else if (lastOffset < charSpan.Length)
- {
-#if NETCOREAPP2_1_OR_GREATER
- m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset));
-#else
- m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString());
-#endif
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void WriteSpan(ref int lastOffset, ReadOnlySpan valueSpan, int index)
+ [Benchmark]
+ [Test]
+ public void JsonEncoderArraySegmentStreamNoSpan()
{
- if (lastOffset < index - 2)
- {
-#if NETCOREAPP2_1_OR_GREATER
- m_streamWriter.Write(valueSpan.Slice(lastOffset, index - lastOffset));
+ // ECC_SUPPORT is used to distinguish also from platforms which do not support Span
+#if NET6_0_OR_GREATER && ECC_SUPPORT
+ using (var arraySegmentStream = new ArraySegmentStreamNoSpan(m_bufferManager))
#else
- m_streamWriter.Write(valueSpan.Slice(lastOffset, index - lastOffset).ToString());
+ using (var arraySegmentStream = new ArraySegmentStream(m_bufferManager))
#endif
- }
- else
{
- while (lastOffset < index)
- {
- m_streamWriter.Write(valueSpan[lastOffset++]);
- }
- }
- lastOffset = index + 1;
- }
-
- ///
- /// Write only chars to stream writer, inline the write sequence for readability.
- ///
- ///
- private void EscapeStringSpanCharsInline(string value)
- {
- ReadOnlySpan charSpan = value.AsSpan();
- int lastOffset = 0;
-
- for (int i = 0; i < charSpan.Length; i++)
- {
- bool found = false;
- char ch = charSpan[i];
-
- for (int ii = 0; ii < m_specialChars.Length; ii++)
+ JsonEncoder_StreamLeaveOpen(arraySegmentStream);
+ // get buffers and return them to buffer manager
+ var buffers = arraySegmentStream.GetBuffers("writer");
+ foreach (var buffer in buffers)
{
- if (m_specialChars[ii] == ch)
- {
- WriteSpan(ref lastOffset, charSpan, i);
- m_streamWriter.Write('\\');
- m_streamWriter.Write(m_substitution[ii]);
- found = true;
- break;
- }
+ m_bufferManager.ReturnBuffer(buffer.Array, "testreturn");
}
-
- if (!found && ch < 32)
- {
- WriteSpan(ref lastOffset, charSpan, i);
- m_streamWriter.Write('\\');
- m_streamWriter.Write('u');
- m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
- }
- }
-
- if (lastOffset == 0)
- {
-#if NETCOREAPP2_1_OR_GREATER
- m_streamWriter.Write(charSpan);
-#else
- m_streamWriter.Write(value);
-#endif
- }
- else
- {
- WriteSpan(ref lastOffset, charSpan, charSpan.Length);
}
}
- // create version of EscapeStringSpanCharsInline that references cosnt arrays
- private void EscapeStringSpanCharsInlineConst(string value)
+ #region Private Methods
+ private void TestStreamEncode(MemoryStream memoryStream, bool toArray)
{
- ReadOnlySpan charSpan = value.AsSpan();
- int lastOffset = 0;
-
- for (int i = 0; i < charSpan.Length; i++)
- {
- bool found = false;
- char ch = charSpan[i];
-
- for (int ii = 0; ii < m_specialCharsConst.Length; ii++)
- {
- if (m_specialCharsConst[ii] == ch)
- {
- WriteSpan(ref lastOffset, charSpan, i);
- m_streamWriter.Write('\\');
- m_streamWriter.Write(m_substitutionConst[ii]);
- found = true;
- break;
- }
- }
-
- if (!found && ch < 32)
- {
- WriteSpan(ref lastOffset, charSpan, i);
- m_streamWriter.Write('\\');
- m_streamWriter.Write('u');
- m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
- }
- }
-
- if (lastOffset == 0)
+ using (var jsonEncoder = new JsonEncoder(m_context, false, false, memoryStream, true, StreamSize))
{
- m_streamWriter.Write(value);
+ TestEncoding(jsonEncoder);
+ _ = jsonEncoder.Close();
}
- else
+ using (var jsonEncoder = new JsonEncoder(m_context, false, false, memoryStream, true, StreamSize))
{
- WriteSpan(ref lastOffset, charSpan, charSpan.Length);
- }
- }
-
-
- private void EscapeStringSpanIndex(string value)
- {
- ReadOnlySpan charSpan = value.AsSpan();
-
- int lastOffset = 0;
- for (int i = 0; i < charSpan.Length; i++)
- {
- char ch = charSpan[i];
-
- int index = m_specialString.IndexOf(ch);
- if (index >= 0)
+ TestEncoding(jsonEncoder);
+ if (toArray)
{
- if (lastOffset < i)
- {
-#if NETCOREAPP2_1_OR_GREATER
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
-#else
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
-#endif
- }
- lastOffset = i + 1;
- m_streamWriter.Write(m_substitutionStrings[index]);
- continue;
+ int length = jsonEncoder.Close();
+ Assert.AreEqual(length, memoryStream.Position);
+ var result = memoryStream.ToArray();
+ Assert.NotNull(result);
+ Assert.AreEqual(length, result.Length);
}
-
- if (ch < 32)
+ else
{
- if (lastOffset < i)
- {
-#if NETCOREAPP2_1_OR_GREATER
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
-#else
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
-#endif
- }
- lastOffset = i + 1;
- m_streamWriter.Write('\\');
- m_streamWriter.Write('u');
- m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
+ var result = jsonEncoder.CloseAndReturnText();
+ Assert.NotNull(result);
}
}
- if (lastOffset == 0)
- {
- m_streamWriter.Write(value);
- }
- else if (lastOffset < charSpan.Length)
- {
-#if NETCOREAPP2_1_OR_GREATER
- m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset));
-#else
- m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString());
-#endif
- }
}
- private void EscapeStringSpanDict(string value)
+ private void JsonEncoder_StreamLeaveOpen(MemoryStream stream, bool testResult = false)
{
- ReadOnlySpan charSpan = value.AsSpan();
-
- int lastOffset = 0;
- for (int i = 0; i < charSpan.Length; i++)
- {
- char ch = charSpan[i];
-
- if (m_replace.TryGetValue(ch, out string escapeSequence))
- {
- if (lastOffset < i)
- {
-#if NETCOREAPP2_1_OR_GREATER
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
-#else
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
-#endif
- }
- lastOffset = i + 1;
- m_streamWriter.Write(escapeSequence);
- continue;
- }
-
- if (ch < 32)
- {
- if (lastOffset < i)
- {
-#if NETCOREAPP2_1_OR_GREATER
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
-#else
- m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
-#endif
- }
- lastOffset = i + 1;
- m_streamWriter.Write('\\');
- m_streamWriter.Write('u');
- m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
- }
- }
- if (lastOffset == 0)
- {
- m_streamWriter.Write(value);
- }
- else if (lastOffset < charSpan.Length)
+ int length1;
+ int length2;
+ stream.Position = 0;
+ using (var jsonEncoder = new JsonEncoder(m_context, false, false, stream, true, StreamSize))
{
-#if NETCOREAPP2_1_OR_GREATER
- m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset));
-#else
- m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString());
-#endif
+ TestEncoding(jsonEncoder);
+ length1 = jsonEncoder.Close();
}
- }
-
- private void EscapeString(string value)
- {
- StringBuilder stringBuilder = new StringBuilder(value.Length * 2);
-
- foreach (char ch in value)
+ using (var jsonEncoder = new JsonEncoder(m_context, false, false, stream, true, StreamSize))
{
- if (m_replace.TryGetValue(ch, out string escapeSequence))
- {
- stringBuilder.Append(escapeSequence);
- }
- else if (ch < 32)
- {
- stringBuilder.Append("\\u");
- stringBuilder.Append(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
- }
- else
- {
- stringBuilder.Append(ch);
- }
+ TestEncoding(jsonEncoder);
+ length2 = jsonEncoder.Close();
}
- m_streamWriter.Write(stringBuilder);
- }
-
- private void EscapeStringThreadLocal(string value)
- {
- StringBuilder stringBuilder = m_stringBuilderPool.Value;
- stringBuilder.Clear();
- stringBuilder.EnsureCapacity(value.Length * 2);
-
- foreach (char ch in value)
+ if (testResult)
{
- if (m_replace.TryGetValue(ch, out string escapeSequence))
- {
- stringBuilder.Append(escapeSequence);
- }
- else if (ch < 32)
- {
- stringBuilder.Append("\\u");
- stringBuilder.Append(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
- }
- else
- {
- stringBuilder.Append(ch);
- }
+ var result = Encoding.UTF8.GetString(stream.ToArray());
+ Assert.NotNull(result);
+ Assert.AreEqual(length1 * 2, length2);
+ Assert.AreEqual(length2, result.Length);
}
- m_streamWriter.Write(stringBuilder);
}
#endregion
- #region Private Fields
- private ThreadLocal m_stringBuilderPool = new ThreadLocal(() => new StringBuilder());
- private static string m_testString;
- private RecyclableMemoryStreamManager m_memoryManager;
- private RecyclableMemoryStream m_memoryStream;
- private StreamWriter m_streamWriter;
- private int m_streamSize = 1024;
- private static readonly string m_specialString = "\"\\\n\r\t\b\f";
-
- // Declare static readonly characters for the special characters
- private static readonly char sro_quotation = '\"';
- private static readonly char sro_backslash = '\\';
- private static readonly char sro_newline = '\n';
- private static readonly char sro_return = '\r';
- private static readonly char sro_tab = '\t';
- private static readonly char sro_backspace = '\b';
- private static readonly char sro_formfeed = '\f';
- private static readonly char[] m_specialChars = new char[] { sro_quotation, sro_backslash, sro_newline, sro_return, sro_tab, sro_backspace, sro_formfeed };
-
- // Declare static readonly characters for the substitution characters
- private static readonly char sro_quotationSub = '\"';
- private static readonly char sro_backslashSub = '\\';
- private static readonly char sro_newlineSub = 'n';
- private static readonly char sro_returnSub = 'r';
- private static readonly char sro_tabSub = 't';
- private static readonly char sro_backspaceSub = 'b';
- private static readonly char sro_formfeedSub = 'f';
- private static readonly char[] m_substitution = new char[] { sro_quotationSub, sro_backslashSub, sro_newlineSub, sro_returnSub, sro_tabSub, sro_backspaceSub, sro_formfeedSub };
-
- // Special characters as const
- private const char s_quotation = '\"';
- private const char s_backslash = '\\';
- private const char s_newline = '\n';
- private const char s_return = '\r';
- private const char s_tab = '\t';
- private const char s_backspace = '\b';
- private const char s_formfeed = '\f';
-
- private static readonly char[] m_specialCharsConst = new char[] { s_quotation, s_backslash, s_newline, s_return, s_tab, s_backspace, s_formfeed };
+ #region Test Setup
+ [OneTimeSetUp]
+ public new void OneTimeSetUp() => base.OneTimeSetUp();
- // Substitution as const
- private const char s_quotationSub = '\"';
- private const char s_backslashSub = '\\';
- private const char s_newlineSub = 'n';
- private const char s_returnSub = 'r';
- private const char s_tabSub = 't';
- private const char s_backspaceSub = 'b';
- private const char s_formfeedSub = 'f';
+ [OneTimeTearDown]
+ public new void OneTimeTearDown() => base.OneTimeTearDown();
+ #endregion
- private static readonly char[] m_substitutionConst = new char[] { s_quotationSub, s_backslashSub, s_newlineSub, s_returnSub, s_tabSub, s_backspaceSub, s_formfeedSub };
+ #region Benchmark Setup
+ ///
+ /// Set up some variables for benchmarks.
+ ///
+ [GlobalSetup]
+ public new void GlobalSetup() => base.GlobalSetup();
- private static readonly string[] m_substitutionStrings = new string[] { "\\\"", "\\\\", "\\n", "\\r", "\\t", "\\b", "\\f" };
- private static readonly Dictionary m_replace = new Dictionary
- {
- { '\"', "\\\"" },
- { '\\', "\\\\" },
- { '\n', "\\n" },
- { '\r', "\\r" },
- { '\t', "\\t" },
- { '\b', "\\b" },
- { '\f', "\\f" }
- };
+ ///
+ /// Tear down benchmark variables.
+ ///
+ [GlobalCleanup]
+ public new void GlobalCleanup() => base.GlobalCleanup();
#endregion
}
}
diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderDateTimeBenchmark.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderDateTimeBenchmark.cs
new file mode 100644
index 000000000..08a97d438
--- /dev/null
+++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderDateTimeBenchmark.cs
@@ -0,0 +1,110 @@
+/* ========================================================================
+ * Copyright (c) 2005-2024 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.Globalization;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Diagnosers;
+using NUnit.Framework;
+
+namespace Opc.Ua.Core.Tests.Types.Encoders
+{
+ [TestFixture, Category("JsonEncoder")]
+ [SetCulture("en-us"), SetUICulture("en-us")]
+ [NonParallelizable]
+ [MemoryDiagnoser]
+ [DisassemblyDiagnoser]
+ public class JsonEncoderDateTimeBenchmark
+ {
+ [Params(0, 4, 7)]
+ public int DateTimeOmittedZeros { get; set; } = 0;
+
+ [Benchmark]
+ [Test]
+ public void DateTimeEncodeToString()
+ {
+ _ = m_dateTime.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK", CultureInfo.InvariantCulture);
+ }
+
+ [Benchmark]
+ [Test]
+ public void ConvertToUniversalTime()
+ {
+ _ = JsonEncoder.ConvertUniversalTimeToString(m_dateTime);
+ }
+
+ #region Test Setup
+ [OneTimeSetUp]
+ public void OneTimeSetUp()
+ {
+ // for validating benchmark tests
+ m_dateTime = DateTime.UtcNow;
+ }
+
+ [OneTimeTearDown]
+ public void OneTimeTearDown()
+ {
+ }
+ #endregion
+
+ #region Benchmark Setup
+ ///
+ /// Set up some variables for benchmarks.
+ ///
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ // for validating benchmark tests
+ switch (DateTimeOmittedZeros)
+ {
+ case 4: m_dateTime = new DateTime(2011, 11, 11, 11, 11, 11, 999, DateTimeKind.Utc); break;
+ case 7: m_dateTime = new DateTime(2011, 11, 11, 11, 11, 11, DateTimeKind.Utc); break;
+ default:
+ do
+ {
+ m_dateTime = DateTime.UtcNow;
+ } while (m_dateTime.Ticks % 10 == 0);
+ break;
+ }
+ }
+
+ ///
+ /// Tear down benchmark variables.
+ ///
+ [GlobalCleanup]
+ public void GlobalCleanup()
+ {
+ }
+ #endregion
+
+ #region Private Fields
+ private DateTime m_dateTime;
+ #endregion
+ }
+}
diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs
new file mode 100644
index 000000000..c1746fd4d
--- /dev/null
+++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderEscapeStringBenchmarks.cs
@@ -0,0 +1,860 @@
+/* ========================================================================
+ * Copyright (c) 2005-2024 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 BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Diagnosers;
+using Microsoft.IO;
+using NUnit.Framework;
+
+namespace Opc.Ua.Core.Tests.Types.Encoders
+{
+ [TestFixture, Category("JsonEncoder")]
+ [SetCulture("en-us"), SetUICulture("en-us")]
+ [NonParallelizable]
+ [MemoryDiagnoser]
+ [DisassemblyDiagnoser(printSource: true)]
+ public class JsonEncoderEscapeStringBenchmarks
+ {
+ public const int InnerLoops = 100;
+ [DatapointSource]
+ [Params(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)]
+ public int StringVariantIndex { get; set; } = 1;
+
+ [DatapointSource]
+ // for benchmarking with different escaped strings
+ public static readonly string[] EscapeTestStrings =
+ {
+ // The use case without escape characters, plain text
+ "The quick brown fox jumps over the lazy dog.",
+ // The use case with many control characters escaped, 1 char spaces
+ "\" \n \r \t \b \f \\ ",
+ // The use case with many control characters escaped, 2 char spaces
+ " \" \n \r \t \b \f \\ ",
+ // The use case with many control characters escaped, 3 char spaces
+ " \" \n \r \t \b \f \\ ",
+ // The use case with many control characters escaped, 5 char spaces
+ " \" \n \r \t \b \f \\ ",
+ // The use case with many binary characters escaped, 1 char spaces
+ "\0 \x01 \x02 \x03 \x04 ",
+ // The use case with many binary characters escaped, 2 char spaces
+ " \0 \x01 \x02 \x03 \x04 ",
+ // The use case with many binary characters escaped, 3 char spaces
+ " \0 \x01 \x02 \x03 \x04 ",
+ // The use case with many binary characters escaped, 5 char spaces
+ " \0 \x01 \x02 \x03 \x04 ",
+ // The use case with all escape characters and a long string
+ "Ascii characters, special characters \n \b & control characters \0 \x04 ␀ ␁ ␂ ␃ ␄. This is a test.",
+ };
+
+ ///
+ /// Benchmark encoding of the previous implementation.
+ ///
+ [Benchmark(Baseline = true)]
+ public void EscapeStringLegacy()
+ {
+ m_memoryStream.Position = 0;
+ int repeats = InnerLoops;
+ while (repeats-- > 0)
+ {
+ EscapedStringLegacy(m_testString);
+ }
+ m_streamWriter.Flush();
+ }
+
+ ///
+ /// Benchmark encoding of the previous implementation with snall improvement for binary encoding.
+ ///
+ [Benchmark]
+ public void EscapeStringLegacyPlus()
+ {
+ m_memoryStream.Position = 0;
+ int repeats = InnerLoops;
+ while (repeats-- > 0)
+ {
+ EscapedStringLegacyPlus(m_testString);
+ }
+ m_streamWriter.Flush();
+ }
+
+ ///
+ /// A new implementation using StringBuilder.
+ ///
+ [Benchmark]
+ public void EscapeStringStringBuilder()
+ {
+ m_memoryStream.Position = 0;
+ int repeats = InnerLoops;
+ while (repeats-- > 0)
+ {
+ EscapeString(m_testString);
+ }
+ m_streamWriter.Flush();
+ }
+
+ ///
+ /// A new implementation using ReadOnlySpan.
+ ///
+ [Benchmark]
+ public void EscapeStringSpan()
+ {
+ m_memoryStream.Position = 0;
+ int repeats = InnerLoops;
+ while (repeats-- > 0)
+ {
+ EscapeStringSpan(m_testString);
+ }
+ m_streamWriter.Flush();
+ }
+
+ ///
+ /// A new implementation using ReadOnlySpan and char write.
+ ///
+ [Benchmark]
+ public void EscapeStringSpanChars()
+ {
+ m_memoryStream.Position = 0;
+ int repeats = InnerLoops;
+ while (repeats-- > 0)
+ {
+ EscapeStringSpanChars(m_testString);
+ }
+ m_streamWriter.Flush();
+ }
+
+ ///
+ /// A new implementation using ReadOnlySpan and char write.
+ ///
+ [Benchmark]
+ public void EscapeStringSpanCharsInline()
+ {
+ m_memoryStream.Position = 0;
+ int repeats = InnerLoops;
+ while (repeats-- > 0)
+ {
+ EscapeStringSpanCharsInline(m_testString);
+ }
+ m_streamWriter.Flush();
+ }
+
+ ///
+ /// A new implementation using ReadOnlySpan and char write with const arrays.
+ ///
+ [Benchmark]
+ public void EscapeStringSpanCharsInlineConst()
+ {
+ m_memoryStream.Position = 0;
+ int repeats = InnerLoops;
+ while (repeats-- > 0)
+ {
+ EscapeStringSpanCharsInlineConst(m_testString);
+ }
+ m_streamWriter.Flush();
+ }
+
+ ///
+ /// A new implementation using ReadOnlySpan and IndexOf.
+ ///
+ [Benchmark]
+ public void EscapeStringSpanIndex()
+ {
+ m_memoryStream.Position = 0;
+ int repeats = InnerLoops;
+ while (repeats-- > 0)
+ {
+ EscapeStringSpanIndex(m_testString);
+ }
+ m_streamWriter.Flush();
+ }
+
+ ///
+ /// A new implementation using ReadOnlySpan and Dictionary.
+ ///
+ [Benchmark]
+ public void EscapeStringSpanDict()
+ {
+ m_memoryStream.Position = 0;
+ int repeats = InnerLoops;
+ while (repeats-- > 0)
+ {
+ EscapeStringSpanDict(m_testString);
+ }
+ m_streamWriter.Flush();
+ }
+
+ [Theory]
+ [TestCase("No Escape chars", 0)]
+ [TestCase("control chars escaped, 1 char space", 1)]
+ [TestCase("control chars escaped, 2 char spaces", 2)]
+ [TestCase("control chars escaped, 3 char spaces", 3)]
+ [TestCase("control chars escaped, 5 char spaces", 4)]
+ [TestCase("binary chars escaped, 1 char space", 5)]
+ [TestCase("binary chars escaped, 2 char spaces", 6)]
+ [TestCase("binary chars escaped, 3 char spaces", 7)]
+ [TestCase("binary chars escaped, 5 char spaces", 8)]
+ [TestCase("all escape chars and long string", 9)]
+ public void EscapeStringValidation(string name, int index)
+ {
+ m_testString = EscapeTestStrings[index];
+ TestContext.Out.WriteLine(m_testString);
+ var testArray = m_testString.ToCharArray();
+
+ m_memoryStream.Position = 0;
+ EscapeStringLegacy();
+ m_streamWriter.Flush();
+ byte[] resultLegacy = m_memoryStream.ToArray();
+ TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacy));
+
+ m_memoryStream.Position = 0;
+ EscapeStringLegacyPlus();
+ m_streamWriter.Flush();
+ byte[] resultLegacyPlus = m_memoryStream.ToArray();
+ TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultLegacyPlus));
+
+ m_memoryStream.Position = 0;
+ EscapeStringStringBuilder();
+ m_streamWriter.Flush();
+ byte[] result = m_memoryStream.ToArray();
+ TestContext.Out.WriteLine(Encoding.UTF8.GetString(result));
+
+ m_memoryStream.Position = 0;
+ EscapeStringSpan(m_testString);
+ m_streamWriter.Flush();
+ byte[] resultSpan = m_memoryStream.ToArray();
+ TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpan));
+
+ m_memoryStream.Position = 0;
+ EscapeStringSpanChars(m_testString);
+ m_streamWriter.Flush();
+ byte[] resultSpanChars = m_memoryStream.ToArray();
+ TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanChars));
+
+ m_memoryStream.Position = 0;
+ EscapeStringSpanCharsInline(m_testString);
+ m_streamWriter.Flush();
+ byte[] resultSpanCharsInline = m_memoryStream.ToArray();
+ TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanCharsInline));
+
+ m_memoryStream.Position = 0;
+ EscapeStringSpanCharsInlineConst(m_testString);
+ m_streamWriter.Flush();
+ byte[] resultSpanCharsInlineConst = m_memoryStream.ToArray();
+ TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanCharsInlineConst));
+
+ m_memoryStream.Position = 0;
+ EscapeStringSpanIndex(m_testString);
+ m_streamWriter.Flush();
+ byte[] resultSpanIndex = m_memoryStream.ToArray();
+ TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanIndex));
+
+ m_memoryStream.Position = 0;
+ EscapeStringSpanDict(m_testString);
+ m_streamWriter.Flush();
+ byte[] resultSpanDict = m_memoryStream.ToArray();
+ TestContext.Out.WriteLine(Encoding.UTF8.GetString(resultSpanDict));
+
+ Assert.IsTrue(Utils.IsEqual(resultLegacy, result));
+ Assert.IsTrue(Utils.IsEqual(resultLegacy, resultLegacyPlus));
+ Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpan));
+ Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanChars));
+ Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanCharsInline));
+ Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanCharsInlineConst));
+ Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanIndex));
+ Assert.IsTrue(Utils.IsEqual(resultLegacy, resultSpanDict));
+ }
+
+ #region Test Setup
+ [OneTimeSetUp]
+ public void OneTimeSetUp()
+ {
+ m_memoryManager = new RecyclableMemoryStreamManager();
+ m_memoryStream = new RecyclableMemoryStream(m_memoryManager);
+ m_streamWriter = new StreamWriter(m_memoryStream, new UTF8Encoding(false), m_streamSize, false);
+ }
+
+ [OneTimeTearDown]
+ public void OneTimeTearDown()
+ {
+ m_streamWriter?.Dispose();
+ m_streamWriter = null;
+ m_memoryStream?.Dispose();
+ m_memoryStream = null;
+ m_memoryManager = null;
+ }
+ #endregion
+
+ #region Benchmark Setup
+ /// 4
+ /// Set up some variables for benchmarks.
+ ///
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ m_memoryManager = new RecyclableMemoryStreamManager();
+ m_memoryStream = new RecyclableMemoryStream(m_memoryManager);
+ m_streamWriter = new StreamWriter(m_memoryStream, Encoding.UTF8, m_streamSize, false);
+ m_testString = EscapeTestStrings[StringVariantIndex - 1];
+ }
+
+ [GlobalCleanup]
+ public void GlobalCleanup()
+ {
+ m_streamWriter?.Dispose();
+ m_streamWriter = null;
+ m_memoryStream?.Dispose();
+ m_memoryStream = null;
+ m_memoryManager = null;
+ }
+ #endregion
+
+ #region Private Methods
+ ///
+ /// Version used previously in JsonEncoder.
+ ///
+ private void EscapedStringLegacy(string value)
+ {
+ foreach (char ch in value)
+ {
+ bool found = false;
+
+ for (int ii = 0; ii < m_specialChars.Length; ii++)
+ {
+ if (m_specialChars[ii] == ch)
+ {
+ m_streamWriter.Write('\\');
+ m_streamWriter.Write(m_substitution[ii]);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ if (ch < 32)
+ {
+ m_streamWriter.Write("\\u");
+ m_streamWriter.Write("{0:X4}", (int)ch);
+ continue;
+ }
+
+ m_streamWriter.Write(ch);
+ }
+ }
+ }
+
+ ///
+ /// Version used previously in JsonEncoder plus improvement of binary encoding.
+ ///
+ ///
+ /// For the underlying stream writer it is faster to write two chars than a 2 char string.
+ ///
+ private void EscapedStringLegacyPlus(string value)
+ {
+ foreach (char ch in value)
+ {
+ bool found = false;
+
+ for (int ii = 0; ii < m_specialChars.Length; ii++)
+ {
+ if (m_specialChars[ii] == ch)
+ {
+ m_streamWriter.Write('\\');
+ m_streamWriter.Write(m_substitution[ii]);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ if (ch < 32)
+ {
+ m_streamWriter.Write('\\');
+ m_streamWriter.Write('u');
+ m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
+ continue;
+ }
+
+ m_streamWriter.Write(ch);
+ }
+ }
+ }
+
+ ///
+ /// Using a span to escape the string, write strings to stream writer if possible.
+ ///
+ private void EscapeStringSpan(string value)
+ {
+ ReadOnlySpan charSpan = value.AsSpan();
+ int lastOffset = 0;
+
+ for (int i = 0; i < charSpan.Length; i++)
+ {
+ bool found = false;
+ char ch = charSpan[i];
+
+ for (int ii = 0; ii < m_specialChars.Length; ii++)
+ {
+ if (m_specialChars[ii] == ch)
+ {
+ if (lastOffset < i)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
+#else
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
+#endif
+ }
+ lastOffset = i + 1;
+ m_streamWriter.Write(m_substitutionStrings[ii]);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found && ch < 32)
+ {
+ if (lastOffset < i)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
+#else
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
+#endif
+ }
+ lastOffset = i + 1;
+ m_streamWriter.Write("\\u");
+ m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
+ }
+ }
+ if (lastOffset == 0)
+ {
+ m_streamWriter.Write(value);
+ }
+ else if (lastOffset < charSpan.Length)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset));
+#else
+ m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString());
+#endif
+ }
+ }
+
+ ///
+ /// Use span to escape the string, write only chars to stream writer.
+ ///
+ ///
+ private void EscapeStringSpanChars(string value)
+ {
+ ReadOnlySpan charSpan = value.AsSpan();
+
+ int lastOffset = 0;
+ for (int i = 0; i < charSpan.Length; i++)
+ {
+ bool found = false;
+ char ch = charSpan[i];
+
+ for (int ii = 0; ii < m_specialChars.Length; ii++)
+ {
+ if (m_specialChars[ii] == ch)
+ {
+ if (lastOffset < i)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
+#else
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
+#endif
+ }
+ lastOffset = i + 1;
+ m_streamWriter.Write('\\');
+ m_streamWriter.Write(m_substitution[ii]);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found && ch < 32)
+ {
+ if (lastOffset < i - 1)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
+#else
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
+#endif
+ }
+ else
+ {
+ while (lastOffset < i)
+ {
+ m_streamWriter.Write(charSpan[lastOffset++]);
+ }
+ }
+ lastOffset = i + 1;
+ m_streamWriter.Write('\\');
+ m_streamWriter.Write('u');
+ m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
+ }
+ }
+
+ if (lastOffset == 0)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ m_streamWriter.Write(charSpan);
+#else
+ m_streamWriter.Write(value);
+#endif
+ }
+ else if (lastOffset < charSpan.Length)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset));
+#else
+ m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString());
+#endif
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WriteSpan(ref int lastOffset, ReadOnlySpan valueSpan, int index)
+ {
+ if (lastOffset < index - 2)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ m_streamWriter.Write(valueSpan.Slice(lastOffset, index - lastOffset));
+#else
+ m_streamWriter.Write(valueSpan.Slice(lastOffset, index - lastOffset).ToString());
+#endif
+ }
+ else
+ {
+ while (lastOffset < index)
+ {
+ m_streamWriter.Write(valueSpan[lastOffset++]);
+ }
+ }
+ lastOffset = index + 1;
+ }
+
+ ///
+ /// Write only chars to stream writer, inline the write sequence for readability.
+ ///
+ ///
+ private void EscapeStringSpanCharsInline(string value)
+ {
+ ReadOnlySpan charSpan = value.AsSpan();
+ int lastOffset = 0;
+
+ for (int i = 0; i < charSpan.Length; i++)
+ {
+ bool found = false;
+ char ch = charSpan[i];
+
+ for (int ii = 0; ii < m_specialChars.Length; ii++)
+ {
+ if (m_specialChars[ii] == ch)
+ {
+ WriteSpan(ref lastOffset, charSpan, i);
+ m_streamWriter.Write('\\');
+ m_streamWriter.Write(m_substitution[ii]);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found && ch < 32)
+ {
+ WriteSpan(ref lastOffset, charSpan, i);
+ m_streamWriter.Write('\\');
+ m_streamWriter.Write('u');
+ m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
+ }
+ }
+
+ if (lastOffset == 0)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ m_streamWriter.Write(charSpan);
+#else
+ m_streamWriter.Write(value);
+#endif
+ }
+ else
+ {
+ WriteSpan(ref lastOffset, charSpan, charSpan.Length);
+ }
+ }
+
+ // create version of EscapeStringSpanCharsInline that references cosnt arrays
+ private void EscapeStringSpanCharsInlineConst(string value)
+ {
+ ReadOnlySpan charSpan = value.AsSpan();
+ int lastOffset = 0;
+
+ for (int i = 0; i < charSpan.Length; i++)
+ {
+ bool found = false;
+ char ch = charSpan[i];
+
+ for (int ii = 0; ii < m_specialCharsConst.Length; ii++)
+ {
+ if (m_specialCharsConst[ii] == ch)
+ {
+ WriteSpan(ref lastOffset, charSpan, i);
+ m_streamWriter.Write('\\');
+ m_streamWriter.Write(m_substitutionConst[ii]);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found && ch < 32)
+ {
+ WriteSpan(ref lastOffset, charSpan, i);
+ m_streamWriter.Write('\\');
+ m_streamWriter.Write('u');
+ m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
+ }
+ }
+
+ if (lastOffset == 0)
+ {
+ m_streamWriter.Write(value);
+ }
+ else
+ {
+ WriteSpan(ref lastOffset, charSpan, charSpan.Length);
+ }
+ }
+
+
+ private void EscapeStringSpanIndex(string value)
+ {
+ ReadOnlySpan charSpan = value.AsSpan();
+
+ int lastOffset = 0;
+ for (int i = 0; i < charSpan.Length; i++)
+ {
+ char ch = charSpan[i];
+
+ int index = m_specialString.IndexOf(ch);
+ if (index >= 0)
+ {
+ if (lastOffset < i)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
+#else
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
+#endif
+ }
+ lastOffset = i + 1;
+ m_streamWriter.Write(m_substitutionStrings[index]);
+ continue;
+ }
+
+ if (ch < 32)
+ {
+ if (lastOffset < i)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
+#else
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
+#endif
+ }
+ lastOffset = i + 1;
+ m_streamWriter.Write('\\');
+ m_streamWriter.Write('u');
+ m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
+ }
+ }
+ if (lastOffset == 0)
+ {
+ m_streamWriter.Write(value);
+ }
+ else if (lastOffset < charSpan.Length)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset));
+#else
+ m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString());
+#endif
+ }
+ }
+
+ private void EscapeStringSpanDict(string value)
+ {
+ ReadOnlySpan charSpan = value.AsSpan();
+
+ int lastOffset = 0;
+ for (int i = 0; i < charSpan.Length; i++)
+ {
+ char ch = charSpan[i];
+
+ if (m_replace.TryGetValue(ch, out string escapeSequence))
+ {
+ if (lastOffset < i)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
+#else
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
+#endif
+ }
+ lastOffset = i + 1;
+ m_streamWriter.Write(escapeSequence);
+ continue;
+ }
+
+ if (ch < 32)
+ {
+ if (lastOffset < i)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset));
+#else
+ m_streamWriter.Write(charSpan.Slice(lastOffset, i - lastOffset).ToString());
+#endif
+ }
+ lastOffset = i + 1;
+ m_streamWriter.Write('\\');
+ m_streamWriter.Write('u');
+ m_streamWriter.Write(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
+ }
+ }
+ if (lastOffset == 0)
+ {
+ m_streamWriter.Write(value);
+ }
+ else if (lastOffset < charSpan.Length)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset));
+#else
+ m_streamWriter.Write(charSpan.Slice(lastOffset, charSpan.Length - lastOffset).ToString());
+#endif
+ }
+ }
+
+ private void EscapeString(string value)
+ {
+ StringBuilder stringBuilder = new StringBuilder(value.Length * 2);
+
+ foreach (char ch in value)
+ {
+ if (m_replace.TryGetValue(ch, out string escapeSequence))
+ {
+ stringBuilder.Append(escapeSequence);
+ }
+ else if (ch < 32)
+ {
+ stringBuilder.Append("\\u");
+ stringBuilder.Append(((int)ch).ToString("X4", CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ stringBuilder.Append(ch);
+ }
+ }
+ m_streamWriter.Write(stringBuilder);
+ }
+ #endregion
+
+ #region Private Fields
+ private static string m_testString;
+ private RecyclableMemoryStreamManager m_memoryManager;
+ private RecyclableMemoryStream m_memoryStream;
+ private StreamWriter m_streamWriter;
+ private int m_streamSize = 1024;
+ private static readonly string m_specialString = "\"\\\n\r\t\b\f";
+
+ // Declare static readonly characters for the special characters
+ private static readonly char sro_quotation = '\"';
+ private static readonly char sro_backslash = '\\';
+ private static readonly char sro_newline = '\n';
+ private static readonly char sro_return = '\r';
+ private static readonly char sro_tab = '\t';
+ private static readonly char sro_backspace = '\b';
+ private static readonly char sro_formfeed = '\f';
+ private static readonly char[] m_specialChars = new char[] { sro_quotation, sro_backslash, sro_newline, sro_return, sro_tab, sro_backspace, sro_formfeed };
+
+ // Declare static readonly characters for the substitution characters
+ private static readonly char sro_quotationSub = '\"';
+ private static readonly char sro_backslashSub = '\\';
+ private static readonly char sro_newlineSub = 'n';
+ private static readonly char sro_returnSub = 'r';
+ private static readonly char sro_tabSub = 't';
+ private static readonly char sro_backspaceSub = 'b';
+ private static readonly char sro_formfeedSub = 'f';
+ private static readonly char[] m_substitution = new char[] { sro_quotationSub, sro_backslashSub, sro_newlineSub, sro_returnSub, sro_tabSub, sro_backspaceSub, sro_formfeedSub };
+
+ // Special characters as const
+ private const char s_quotation = '\"';
+ private const char s_backslash = '\\';
+ private const char s_newline = '\n';
+ private const char s_return = '\r';
+ private const char s_tab = '\t';
+ private const char s_backspace = '\b';
+ private const char s_formfeed = '\f';
+
+ private static readonly char[] m_specialCharsConst = new char[] { s_quotation, s_backslash, s_newline, s_return, s_tab, s_backspace, s_formfeed };
+
+ // Substitution as const
+ private const char s_quotationSub = '\"';
+ private const char s_backslashSub = '\\';
+ private const char s_newlineSub = 'n';
+ private const char s_returnSub = 'r';
+ private const char s_tabSub = 't';
+ private const char s_backspaceSub = 'b';
+ private const char s_formfeedSub = 'f';
+
+ private static readonly char[] m_substitutionConst = new char[] { s_quotationSub, s_backslashSub, s_newlineSub, s_returnSub, s_tabSub, s_backspaceSub, s_formfeedSub };
+
+ private static readonly string[] m_substitutionStrings = new string[] { "\\\"", "\\\\", "\\n", "\\r", "\\t", "\\b", "\\f" };
+ private static readonly Dictionary m_replace = new Dictionary
+ {
+ { '\"', "\\\"" },
+ { '\\', "\\\\" },
+ { '\n', "\\n" },
+ { '\r', "\\r" },
+ { '\t', "\\t" },
+ { '\b', "\\b" },
+ { '\f', "\\f" }
+ };
+ #endregion
+ }
+}
diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs
index 4cb3ae150..e35b1acfe 100644
--- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs
+++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs
@@ -35,7 +35,9 @@
using System.Text;
using System.Xml;
using BenchmarkDotNet.Attributes;
+using Microsoft.IO;
using NUnit.Framework;
+using Opc.Ua.Bindings;
namespace Opc.Ua.Core.Tests.Types.Encoders
{
@@ -365,14 +367,54 @@ public void ConstructorDefault(bool useReversible, bool topLevelIsArray)
}
///
- /// Use a constructor with external Stream,
+ /// Use a MemoryStream constructor with external Stream,
+ /// keep the stream open for more encodings.
+ ///
+ [Test]
+ public void ConstructorMemoryStream()
+ {
+ using (var memoryStream = new MemoryStream())
+ {
+ ConstructorStream(memoryStream);
+ }
+ }
+
+ ///
+ /// Use a ArraySegmentStream constructor with external Stream,
/// keep the stream open for more encodings.
///
[Test]
- public void ConstructorStream()
+ public void ConstructorArraySegmentStream()
+ {
+ using (var memoryStream = new ArraySegmentStream(BufferManager))
+ {
+ ConstructorStream(memoryStream);
+ }
+ }
+
+ ///
+ /// Use a RecylableMemoryStream constructor with external Stream,
+ /// keep the stream open for more encodings.
+ ///
+ [Test]
+ public void ConstructorRecyclableMemoryStream()
+ {
+ var recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(new RecyclableMemoryStreamManager.Options {
+ BlockSize = BufferManager.MaxSuggestedBufferSize,
+ });
+ using (var memoryStream = new RecyclableMemoryStream(recyclableMemoryStreamManager))
+ {
+ ConstructorStream(memoryStream);
+ }
+ }
+
+ ///
+ /// Use a constructor with external Stream,
+ /// keep the stream open for more encodings.
+ ///
+ public void ConstructorStream(MemoryStream memoryStream)
{
var context = new ServiceMessageContext();
- var memoryStream = new MemoryStream();
using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true))
{
TestEncoding(jsonEncoder);
@@ -384,7 +426,7 @@ public void ConstructorStream()
// recycle the StreamWriter, ensure the result is equal
memoryStream.Position = 0;
- using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true))
+ using (IJsonEncoder jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true))
{
TestEncoding(jsonEncoder);
}
@@ -411,14 +453,141 @@ public void ConstructorStream()
Assert.Throws(() => _ = new StreamWriter(memoryStream));
}
+ ///
+ /// Use a constructor with external ArraySegmentStream,
+ /// keep the stream open for more encodings.
+ /// Alternate use of sequence.
+ ///
+ [Test]
+ public void ConstructorArraySegmentStreamSequence()
+ {
+ var context = new ServiceMessageContext();
+ using (var memoryStream = new ArraySegmentStream(BufferManager))
+ {
+ using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true))
+ {
+ TestEncoding(jsonEncoder);
+ }
+
+ // get the buffers and save the result
+#if NET5_0_OR_GREATER
+ string result1;
+ using (var sequence = memoryStream.GetSequence(nameof(ConstructorStream)))
+ {
+ result1 = Encoding.UTF8.GetString(sequence.Sequence);
+ Assert.IsNotEmpty(result1);
+ TestContext.Out.WriteLine("Result1:");
+ _ = PrettifyAndValidateJson(result1);
+ }
+#else
+ var result1 = Encoding.UTF8.GetString(memoryStream.ToArray());
+ Assert.IsNotEmpty(result1);
+ TestContext.Out.WriteLine("Result1:");
+ _ = PrettifyAndValidateJson(result1);
+#endif
+
+ // recycle the memory stream, ensure the result is equal
+ memoryStream.Position = 0;
+ using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true))
+ {
+ TestEncoding(jsonEncoder);
+ }
+ var result2 = Encoding.UTF8.GetString(memoryStream.ToArray());
+ Assert.IsNotEmpty(result2);
+ TestContext.Out.WriteLine("Result2:");
+ _ = PrettifyAndValidateJson(result2);
+ Assert.AreEqual(result1, result2);
+
+ // recycle the StreamWriter, ensure the result is equal,
+ // use reflection to return result in external stream
+ memoryStream.Position = 0;
+ using (IJsonEncoder jsonEncoder = new JsonEncoder(context, true, false, memoryStream, false))
+ {
+ TestEncoding(jsonEncoder);
+ var result3 = jsonEncoder.CloseAndReturnText();
+ Assert.IsNotEmpty(result3);
+ TestContext.Out.WriteLine("Result3:");
+ _ = PrettifyAndValidateJson(result3);
+ Assert.AreEqual(result1, result3);
+ }
+
+ // ensure the memory stream was closed
+ Assert.Throws(() => _ = new StreamWriter(memoryStream));
+ }
+ }
+
+ ///
+ /// Use a constructor with external RecyclableMemoryStream,
+ /// keep the stream open for more encodings.
+ /// Alternate use of sequence.
+ ///
+ [Test]
+ public void ConstructorRecyclableMemoryStreamSequence()
+ {
+ var context = new ServiceMessageContext();
+ using (var memoryStream = new RecyclableMemoryStream(RecyclableMemoryManager))
+ {
+ using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true))
+ {
+ TestEncoding(jsonEncoder);
+ }
+
+ // get the buffers and save the result
+#if NET5_0_OR_GREATER
+ string result1;
+ {
+ var sequence = memoryStream.GetReadOnlySequence();
+ result1 = Encoding.UTF8.GetString(sequence);
+ Assert.IsNotEmpty(result1);
+ TestContext.Out.WriteLine("Result1:");
+ _ = PrettifyAndValidateJson(result1);
+ }
+#else
+ var result1 = Encoding.UTF8.GetString(memoryStream.ToArray());
+ Assert.IsNotEmpty(result1);
+ TestContext.Out.WriteLine("Result1:");
+ _ = PrettifyAndValidateJson(result1);
+#endif
+
+ // recycle the memory stream, ensure the result is equal
+ memoryStream.Position = 0;
+ using (var jsonEncoder = new JsonEncoder(context, true, false, memoryStream, true))
+ {
+ TestEncoding(jsonEncoder);
+ }
+ var result2 = Encoding.UTF8.GetString(memoryStream.ToArray());
+ Assert.IsNotEmpty(result2);
+ TestContext.Out.WriteLine("Result2:");
+ _ = PrettifyAndValidateJson(result2);
+ Assert.AreEqual(result1, result2);
+
+ // recycle the StreamWriter, ensure the result is equal,
+ // use reflection to return result in external stream
+ memoryStream.Position = 0;
+ using (IJsonEncoder jsonEncoder = new JsonEncoder(context, true, false, memoryStream, false))
+ {
+ TestEncoding(jsonEncoder);
+ var result3 = jsonEncoder.CloseAndReturnText();
+ Assert.IsNotEmpty(result3);
+ TestContext.Out.WriteLine("Result3:");
+ _ = PrettifyAndValidateJson(result3);
+ Assert.AreEqual(result1, result3);
+ }
+
+ // ensure the memory stream was closed
+ Assert.Throws(() => _ = new StreamWriter(memoryStream));
+ }
+ }
+
///
/// Verify reversible Json encoding.
///
[Theory]
- public void JsonEncodeRev(JsonValidationData jsonValidationData)
+ public void JsonEncodeRev(JsonValidationData jsonValidationData, MemoryStreamType memoryStreamType)
{
EncodeJsonVerifyResult(
jsonValidationData.BuiltInType,
+ memoryStreamType,
jsonValidationData.Instance,
true,
jsonValidationData.ExpectedReversible,
@@ -430,10 +599,11 @@ public void JsonEncodeRev(JsonValidationData jsonValidationData)
/// Verify non reversible Json encoding.
///
[Theory]
- public void JsonEncodeNonRev(JsonValidationData jsonValidationData)
+ public void JsonEncodeNonRev(JsonValidationData jsonValidationData, MemoryStreamType memoryStreamType)
{
EncodeJsonVerifyResult(
jsonValidationData.BuiltInType,
+ memoryStreamType,
jsonValidationData.Instance,
false,
jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible,
diff --git a/Tests/Opc.Ua.Core.Tests/Types/Utils/RedactionTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Utils/RedactionTests.cs
new file mode 100644
index 000000000..cd628481f
--- /dev/null
+++ b/Tests/Opc.Ua.Core.Tests/Types/Utils/RedactionTests.cs
@@ -0,0 +1,112 @@
+/* ========================================================================
+ * Copyright (c) 2005-2024 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 NUnit.Framework;
+using Opc.Ua.Redaction;
+
+namespace Opc.Ua.Core.Tests.Types.UtilsTests
+{
+ [TestFixture, Category("Utils")]
+ public class RedactionTests
+ {
+ [Test]
+ public void FallbackStrategyIsInvokedWhenNoStrategyWasAdded()
+ {
+ RedactionStrategies.ResetStrategy();
+
+ string original = "Original test string";
+
+ string result = Redact.Create(original).ToString();
+
+ Assert.That(result, Is.EqualTo(original));
+ }
+
+ [Test]
+ public void FallbackStrategyIsRedactingNullCorrectly()
+ {
+ RedactionStrategies.ResetStrategy();
+
+ string original = null;
+
+ string result = Redact.Create(original).ToString();
+
+ Assert.That(result, Is.EqualTo("null"));
+ }
+
+ [Test]
+ public void StrategyIsInvokedWhenItExists()
+ {
+ RedactionStrategies.ResetStrategy();
+
+ RedactionStrategies.SetStrategy(new TestRedactionStrategy("int_"));
+
+ int original = 123;
+
+ string result = Redact.Create(original).ToString();
+
+ Assert.That(result, Is.EqualTo("int_123"));
+ }
+
+ [Test]
+ public void TheLastStrategyIsInvokedWhenMultipleWereSet()
+ {
+ RedactionStrategies.ResetStrategy();
+
+ RedactionStrategies.SetStrategy(new TestRedactionStrategy("first_"));
+ RedactionStrategies.SetStrategy(new TestRedactionStrategy("second_"));
+
+ int originalNumber = 456;
+
+ string resultNumber = Redact.Create(originalNumber).ToString();
+
+ Assert.That(resultNumber, Is.EqualTo("second_456"));
+
+ string originalString = "test string 890";
+
+ string resultString = Redact.Create(originalString).ToString();
+
+ Assert.That(resultString, Is.EqualTo("second_test string 890"));
+ }
+
+ private class TestRedactionStrategy : IRedactionStrategy
+ {
+ private readonly string m_prefix;
+
+ public TestRedactionStrategy(string prefix)
+ {
+ m_prefix = prefix;
+ }
+
+ public string Redact(object value)
+ {
+ return $"{m_prefix}{value}";
+ }
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.Core.Tests/Types/Utils/SimpleRedactionStrategyTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Utils/SimpleRedactionStrategyTests.cs
new file mode 100644
index 000000000..735d88ece
--- /dev/null
+++ b/Tests/Opc.Ua.Core.Tests/Types/Utils/SimpleRedactionStrategyTests.cs
@@ -0,0 +1,159 @@
+/* ========================================================================
+ * Copyright (c) 2005-2024 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 NUnit.Framework;
+using Opc.Ua.Redaction;
+using Opc.Ua.Types.Redaction;
+
+namespace Opc.Ua.Core.Tests.Types.UtilsTests
+{
+ [TestFixture, Category("Utils")]
+ internal class SimpleRedactionStrategyTests
+ {
+ [SetUp]
+ public void Setup()
+ {
+ RedactionStrategies.SetStrategy(new SimpleRedactionStrategy());
+ }
+
+ [TearDown]
+ public void Teardown()
+ {
+ RedactionStrategies.ResetStrategy();
+ }
+
+ [Test]
+ public void ZeroMinimumLengthIsAllowed()
+ {
+ _ = new SimpleRedactionStrategy(0, 12);
+ }
+
+ [Test]
+ public void ZeroMaximumLengthIsAllowed()
+ {
+ _ = new SimpleRedactionStrategy(12, 0);
+ }
+
+ [Test]
+ public void NegativeMinimumLengthThrows()
+ {
+ Assert.That(
+ Assert.Throws(
+ () => _ = new SimpleRedactionStrategy(-1, 0))
+ .ParamName, Is.EqualTo("minLength"));
+ }
+
+ [Test]
+ public void MaximumLengthLowerThanNegativeOneThrows()
+ {
+ Assert.That(
+ Assert.Throws(
+ () => _ = new SimpleRedactionStrategy(12, -2))
+ .ParamName, Is.EqualTo("maxLength"));
+ }
+
+ [Test]
+ public void NegativeOneMaximumLengthMeansNoLimit()
+ {
+ var strategy = new SimpleRedactionStrategy(0, -1);
+
+ string original = new string('a', 200);
+ string expected = new string('*', 200);
+
+ string result = strategy.Redact(original);
+
+ Assert.That(result, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void RedactString()
+ {
+ var redacted = Redact.Create("my long test string");
+
+ Assert.That(redacted.ToString(), Is.EqualTo("****************"));
+ }
+
+ [Test]
+ public void RedactNullString()
+ {
+ string original = null;
+
+ string result = Redact.Create(original).ToString();
+
+ Assert.That(result, Is.EqualTo("null"));
+ }
+
+ [Test]
+ public void RedactUri()
+ {
+ var redacted = Redact.Create(new Uri("http://example.com:8080"));
+
+ Assert.That(redacted.ToString(), Is.EqualTo("http://***********:8080"));
+ }
+
+ [Test]
+ public void RedactUriBuilder()
+ {
+ var redacted = Redact.Create(new UriBuilder("test.com/index.html"));
+
+ Assert.That(redacted.ToString(), Is.EqualTo("http://********/index.html"));
+ }
+
+ [Test]
+ public void RedactNullUri()
+ {
+ Uri uri = null;
+
+ string result = Redact.Create(uri).ToString();
+
+ Assert.That(result, Is.EqualTo("null"));
+ }
+
+ [Test]
+ public void RedactException()
+ {
+ var original = new Exception("Test message with $ecret");
+
+ string result = Redact.Create(original).ToString();
+
+ Assert.That(result, Does.Not.Contain('$'));
+ }
+
+ [Test]
+ public void RedactNullException()
+ {
+ Exception exception = null;
+
+ string result = Redact.Create(exception).ToString();
+
+ Assert.That(result, Is.EqualTo("null"));
+ }
+ }
+}
diff --git a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj
index 2cc61c4f5..a9fb79ac3 100644
--- a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj
+++ b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj
@@ -19,7 +19,7 @@
all
runtime; build; native; contentfiles; analyzers
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj
index b228e9a8b..576804c4f 100644
--- a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj
+++ b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj
@@ -17,7 +17,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj
index 7bbbbff21..a37c580d7 100644
--- a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj
+++ b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj
@@ -30,7 +30,7 @@
all
runtime; build; native; contentfiles; analyzers
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj
index 7a55a50ac..3d32fcc30 100644
--- a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj
+++ b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj
@@ -16,7 +16,7 @@
all
runtime; build; native; contentfiles; analyzers
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Tests/customtest.bat b/Tests/customtest.bat
index 366a3e37e..4ffc64b19 100644
--- a/Tests/customtest.bat
+++ b/Tests/customtest.bat
@@ -2,19 +2,19 @@
setlocal enabledelayedexpansion
echo This script is used to run custom platform tests for the UA Core Library
-echo Supported parameters: net462, netstandard2.1, net48, net6.0, net8.0
+echo Supported parameters: net462, netstandard2.0, netstandard2.1, net48, net6.0, net8.0
REM Check if the target framework parameter is provided
if "%1"=="" (
echo Usage: %0 [TargetFramework]
- echo Allowed values for TargetFramework: net462, netstandard2.1, net48, net6.0, net8.0, default
+ echo Allowed values for TargetFramework: net462, netstandard2.0, netstandard2.1, net48, net6.0, net8.0, default
goto :eof
)
REM Check if the provided TargetFramework is valid
-set "validFrameworks= default net462 netstandard2.1 net48 net6.0 net8.0"
+set "validFrameworks= default net462 net472 netstandard2.0 netstandard2.1 net48 net6.0 net8.0 "
if "!validFrameworks: %1 =!"=="%validFrameworks%" (
- echo Invalid TargetFramework specified. Allowed values are: default, net462, netstandard2.1, net48, net6.0, net8.0
+ echo Invalid TargetFramework specified. Allowed values are: default, net462, net472 netstandard2.0, netstandard2.1, net48, net6.0, net8.0
goto :eof
)
diff --git a/UA Core Library.sln b/UA Core Library.sln
index 4f68a3306..85c776fc5 100644
--- a/UA Core Library.sln
+++ b/UA Core Library.sln
@@ -47,7 +47,6 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "azurepipelines", "azurepipelines", "{1C25BE72-C337-42AE-9F7C-D6B45F7B7079}"
ProjectSection(SolutionItems) = preProject
.azurepipelines\ci.yml = .azurepipelines\ci.yml
- .azurepipelines\customtest.yml = .azurepipelines\customtest.yml
.azurepipelines\get-matrix.ps1 = .azurepipelines\get-matrix.ps1
.azurepipelines\get-root.ps1 = .azurepipelines\get-root.ps1
.azurepipelines\get-version.ps1 = .azurepipelines\get-version.ps1
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 9b228f911..a97686ef4 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -114,6 +114,30 @@ stages:
jobnamesuffix: net462
agents: '@{ windows = "windows-2022" }'
customtestarget: net462
+- stage: testnet472
+ dependsOn: [build]
+ displayName: 'Test .NET 4.7.2'
+ condition: and(succeeded(), ne(variables.ScheduledBuild, 'False'))
+ jobs:
+ - template: .azurepipelines/test.yml
+ parameters:
+ framework: net472
+ configuration: Release
+ jobnamesuffix: net472
+ agents: '@{ windows = "windows-2022" }'
+ customtestarget: net472
+- stage: testnetstandard20
+ dependsOn: [build]
+ displayName: 'Test .NETStandard 2.0'
+ condition: and(succeeded(), ne(variables.ScheduledBuild, 'False'))
+ jobs:
+ - template: .azurepipelines/test.yml
+ parameters:
+ framework: net6.0
+ configuration: Release
+ jobnamesuffix: netstandard20
+ agents: '@{ windows = "windows-2019"; linux="ubuntu-20.04"}'
+ customtestarget: netstandard2.0
- stage: testnetstandard21
dependsOn: [build]
displayName: 'Test .NETStandard 2.1'
diff --git a/common.props b/common.props
index bbfba2af1..7041993c9 100644
--- a/common.props
+++ b/common.props
@@ -21,9 +21,14 @@
- true
+ true
+ false
false
+
+
+ true
+
images/logo.jpg
@@ -32,6 +37,7 @@
licenses/LICENSE.txt
$(RepositoryUrl)/releases
+ NugetREADME.md
OPCFoundation OPC UA netstandard ios linux dotnet net netcore uwp
@@ -47,6 +53,10 @@
+
+
+
+
diff --git a/targets.props b/targets.props
index 7d41ba1c2..f13e72980 100644
--- a/targets.props
+++ b/targets.props
@@ -30,6 +30,7 @@
net462
net462
net462
+ net462
net462
net462
@@ -41,6 +42,7 @@
net472
net472
net472
+ net472
net472
net472
@@ -53,6 +55,7 @@
net6.0
net6.0
netstandard2.0
+ netstandard2.0
netstandard2.1
netstandard2.0
@@ -64,6 +67,7 @@
net6.0
net6.0
netstandard2.1
+ netstandard2.1
netstandard2.1
netstandard2.1
@@ -75,6 +79,7 @@
net48
net48
net48
+ net48
net48
net48
@@ -86,6 +91,7 @@
net6.0
net6.0
net6.0
+ net6.0
net6.0
net6.0
@@ -97,13 +103,14 @@
net8.0
net8.0
net8.0
+ net8.0
net8.0
net8.0
-
+
@@ -112,6 +119,7 @@
net6.0
net48;net6.0
net48;netstandard2.1;net6.0;net8.0
+ net48;netstandard2.0;netstandard2.1;net6.0;net8.0
net48;netstandard2.1;net6.0;net8.0
net48;netcoreapp3.1;net6.0;net8.0
@@ -124,6 +132,7 @@
netcoreapp3.1
net48;netcoreapp3.1
net48;netstandard2.1
+ net48;netstandard2.0;netstandard2.1
net48;netstandard2.1
net48;netcoreapp3.1
@@ -135,6 +144,7 @@
net48
net48
net48
+ net48
net48
net48