Skip to content

Commit

Permalink
Merge pull request #170 from bevanweiss/nullids_remastered
Browse files Browse the repository at this point in the history
OPC-UA Spec has a number of 'null' NodeId values.
  • Loading branch information
nauful authored Jun 25, 2024
2 parents 91326b6 + 769988a commit 34253aa
Show file tree
Hide file tree
Showing 8 changed files with 331 additions and 5 deletions.
14 changes: 14 additions & 0 deletions LibUA.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NET", "NET", "{B5D51E19-F3E
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibUA", "NET\LibUA\LibUA.csproj", "{A02727B5-BA44-4B42-A75F-B35E295DBAF0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibUA.Tests", "NET Core\LibUA.Tests\LibUA.Tests.csproj", "{14422ED0-3219-4816-961B-00E5BC4EC757}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibUA.Benchmarks", "NET Core\LibUA.Benchmarks\LibUA.Benchmarks.csproj", "{89681A86-9667-4A67-8D92-A5847E6D19FC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -37,6 +41,14 @@ Global
{A02727B5-BA44-4B42-A75F-B35E295DBAF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A02727B5-BA44-4B42-A75F-B35E295DBAF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A02727B5-BA44-4B42-A75F-B35E295DBAF0}.Release|Any CPU.Build.0 = Release|Any CPU
{14422ED0-3219-4816-961B-00E5BC4EC757}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{14422ED0-3219-4816-961B-00E5BC4EC757}.Debug|Any CPU.Build.0 = Debug|Any CPU
{14422ED0-3219-4816-961B-00E5BC4EC757}.Release|Any CPU.ActiveCfg = Release|Any CPU
{14422ED0-3219-4816-961B-00E5BC4EC757}.Release|Any CPU.Build.0 = Release|Any CPU
{89681A86-9667-4A67-8D92-A5847E6D19FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{89681A86-9667-4A67-8D92-A5847E6D19FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{89681A86-9667-4A67-8D92-A5847E6D19FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{89681A86-9667-4A67-8D92-A5847E6D19FC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -46,6 +58,8 @@ Global
{62DE01CE-6B65-46AE-B338-DC2D21F98EAA} = {6F1B9BE5-B2AB-4794-A642-AA0FA74DFF2E}
{809924A5-E31A-465D-9E1F-BD1AA1F63340} = {6F1B9BE5-B2AB-4794-A642-AA0FA74DFF2E}
{A02727B5-BA44-4B42-A75F-B35E295DBAF0} = {B5D51E19-F3EC-4193-BADC-E7F1862D5FFF}
{14422ED0-3219-4816-961B-00E5BC4EC757} = {6F1B9BE5-B2AB-4794-A642-AA0FA74DFF2E}
{89681A86-9667-4A67-8D92-A5847E6D19FC} = {6F1B9BE5-B2AB-4794-A642-AA0FA74DFF2E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {03381D17-B1F1-48B8-9F30-97F101AEA4AD}
Expand Down
22 changes: 22 additions & 0 deletions NET Core/LibUA.Benchmarks/LibUA.Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
<ProduceReferenceAssembly>True</ProduceReferenceAssembly>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.12" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\LibUA\LibUA.csproj" />
</ItemGroup>

</Project>
108 changes: 108 additions & 0 deletions NET Core/LibUA.Benchmarks/NodeIds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using BenchmarkDotNet.Attributes;
using LibUA.Core;

namespace LibUA.Benchmarks
{
[MarkdownExporter]
[MemoryDiagnoser(false)]
public class NodeId_Benchmarks
{
public readonly static NodeId[] NullNodes =
[
new NodeId(0,0),
new NodeId(0, null, NodeIdNetType.String),
new NodeId(0, string.Empty),
new NodeId(0, null, NodeIdNetType.Guid),
new NodeId(0, Guid.Empty.ToByteArray(), NodeIdNetType.Guid),
new NodeId(0, null, NodeIdNetType.ByteString),
new NodeId(0, [], NodeIdNetType.ByteString)
];

public readonly static NodeId[] NumericNodes =
[
new NodeId(2, 1),
new NodeId(2, 100),
new NodeId(2, 200),
new NodeId(2, 300),
new NodeId(2, 400),
new NodeId(2, 500),
new NodeId(2, 600),
new NodeId(2, 700),
new NodeId(2, 800),
new NodeId(2, 900),
new NodeId(2, 1000),
];

public readonly static NodeId[] StringNodes =
[
new NodeId(2, "Test String 1"),
new NodeId(2, "Test String 2"),
new NodeId(2, "Test String 3"),
new NodeId(2, "Test String 4"),
new NodeId(2, "Test String 5"),
new NodeId(2, "Test String 6"),
new NodeId(2, "Test String 7"),
new NodeId(2, "Test String 8"),
new NodeId(2, "Test String 9"),
new NodeId(2, "Test String 10"),
];

public readonly static NodeId[] GuidNodes =
[
new NodeId(2, new Guid().ToByteArray(), NodeIdNetType.Guid),
new NodeId(2, new Guid().ToByteArray(), NodeIdNetType.Guid),
new NodeId(2, new Guid().ToByteArray(), NodeIdNetType.Guid),
new NodeId(2, new Guid().ToByteArray(), NodeIdNetType.Guid),
new NodeId(2, new Guid().ToByteArray(), NodeIdNetType.Guid),
new NodeId(2, new Guid().ToByteArray(), NodeIdNetType.Guid),
new NodeId(2, new Guid().ToByteArray(), NodeIdNetType.Guid),
new NodeId(2, new Guid().ToByteArray(), NodeIdNetType.Guid),
new NodeId(2, new Guid().ToByteArray(), NodeIdNetType.Guid),
new NodeId(2, new Guid().ToByteArray(), NodeIdNetType.Guid),
];

public readonly static NodeId[] ByteStringNodes =
[
new NodeId(2, new byte[] { 0, 1, 2, 3 }, NodeIdNetType.Guid),
new NodeId(2, new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, NodeIdNetType.Guid),
new NodeId(2, new byte[] { 2, 1, 2, 3 }, NodeIdNetType.Guid),
new NodeId(2, new byte[] { 0, 3, 2, 3 }, NodeIdNetType.Guid),
new NodeId(2, new byte[] { 0, 1, 2, 1 }, NodeIdNetType.Guid),
new NodeId(2, new byte[] { 0, 6, 2, 3, 7, 5, 6, 4, 255 }, NodeIdNetType.Guid),
new NodeId(2, new byte[] { 0, 1, 8, 3 }, NodeIdNetType.Guid),
new NodeId(2, new byte[] { 0, 9, 2, 3 }, NodeIdNetType.Guid),
new NodeId(2, new byte[] { 0, 1, 2, 0, 1, 5, 6, 7 }, NodeIdNetType.Guid),
];

public readonly static NodeId[] Nodes =
[
.. NullNodes,
.. StringNodes,
.. GuidNodes,
.. ByteStringNodes
];


[Benchmark]
public void NodeIdEquivalency()
{
for(int i = 0; i < Nodes.Length; i++)
{
for(int j = 0; j < Nodes.Length; j++)
{
_ = Nodes[i].Equals(Nodes[j]);
}
}
}

[Benchmark]
public void NodeIdAllocations()
{
NodeId[] nodes = new NodeId[1_000_000];
for (int i = 0; i < 1_000_000; i++)
{
nodes[i] = new NodeId(0, 0);
}
}
}
}
10 changes: 10 additions & 0 deletions NET Core/LibUA.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;

internal class Program
{
private static void Main(string[] args)
{
var summary = BenchmarkRunner.Run(typeof(Program).Assembly);
}
}
26 changes: 26 additions & 0 deletions NET Core/LibUA.Tests/LibUA.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\LibUA\LibUA.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
118 changes: 118 additions & 0 deletions NET Core/LibUA.Tests/NodeId_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using LibUA.Core;

namespace LibUA.Tests
{
public class NodeId_Tests
{
// OPC 10000-3: Address Space Model
// 8.2.4 Identifier value
// A canonical null NodeId has an IdType equal to Numeric, a NamespaceIndex equal to 0 and an
// Identifier equal to 0.
//
// In addition to the canonical null NodeId the alternative values defined in Table 23 shall be
// considered a null NodeId.
// IdType NamespaceIndex Null Value
// String 0 A null or Empty String(“”)
// Guid 0 A Guid initialised with zeros(e.g. 00000000-0000-0000-0000-000000)
// Opaque 0 A null or Empty ByteString
[Fact]
public void NodeId_NullEquivalence()
{
NodeId nullNumeric = new NodeId(0, 0);
NodeId nullString = new NodeId(0, null);
NodeId emptyString = new NodeId(0, string.Empty);
NodeId nullGuid = new NodeId(0, null, NodeIdNetType.Guid);
NodeId emptyGuid = new NodeId(0, new Guid().ToByteArray(), NodeIdNetType.Guid);
NodeId nullBytes = new NodeId(0, null, NodeIdNetType.ByteString);
NodeId emptyBytes = new NodeId(0, new byte[0], NodeIdNetType.ByteString);

Assert.Equal(nullNumeric, nullNumeric);
Assert.Equal(nullNumeric, nullString);
Assert.Equal(nullNumeric, emptyString);
Assert.Equal(nullNumeric, nullGuid);
Assert.Equal(nullNumeric, emptyGuid);
Assert.Equal(nullNumeric, nullBytes);
Assert.Equal(nullNumeric, emptyBytes);
}

[Fact]
public void NodeId_NumericEquivalence()
{
Assert.Equal(new NodeId(2, 100), new NodeId(2, 100));
}

[Fact]
public void NodeId_NumericNamespaceNonEquivalence()
{
Assert.NotEqual(new NodeId(2, 100), new NodeId(3, 100));
}

[Fact]
public void NodeId_NumericValueNonEquivalence()
{
Assert.NotEqual(new NodeId(2, 100), new NodeId(2, 101));
}

[Fact]
public void NodeId_StringEquivalence()
{
Assert.Equal(new NodeId(2, "Test String"), new NodeId(2, "Test String"));
}

[Fact]
public void NodeId_StringNamespaceNonEquivalence()
{
Assert.NotEqual(new NodeId(2, "Test String"), new NodeId(3, "Test String"));
}

[Fact]
public void NodeId_StringValueNonEquivalence()
{
Assert.NotEqual(new NodeId(2, "Test String"), new NodeId(2, "Test String2"));
}

[Fact]
public void NodeId_GuidEquivalence()
{
var guid = new Guid();
var guid2 = new Guid(guid.ToByteArray());
Assert.Equal(new NodeId(2, guid.ToByteArray(), NodeIdNetType.Guid), new NodeId(2, guid2.ToByteArray(), NodeIdNetType.Guid));
}

[Fact]
public void NodeId_GuidNamespaceNonEquivalence()
{
var guid = new Guid();
var guid2 = new Guid(guid.ToByteArray());
Assert.NotEqual(new NodeId(2, guid.ToByteArray(), NodeIdNetType.Guid), new NodeId(3, guid2.ToByteArray(), NodeIdNetType.Guid));
}

[Fact]
public void NodeId_GuidValueNonEquivalence()
{
var byteArray1 = Enumerable.Range(0, 16).Select(x => (byte)x).ToArray();
var byteArray2 = Enumerable.Range(1, 16).Select(x => (byte)x).ToArray();
var guid1 = new Guid(byteArray1);
var guid2 = new Guid(byteArray2);
Assert.NotEqual(new NodeId(2, guid1.ToByteArray(), NodeIdNetType.Guid), new NodeId(2, guid2.ToByteArray(), NodeIdNetType.Guid));
}

[Fact]
public void NodeId_OpaqueEquivalence()
{
Assert.Equal(new NodeId(2, [0, 1, 2, 3, 4], NodeIdNetType.ByteString), new NodeId(2, [0, 1, 2, 3, 4], NodeIdNetType.ByteString));
}

[Fact]
public void NodeId_OpaqueNamespaceNonEquivalence()
{
Assert.NotEqual(new NodeId(2, [0, 1, 2, 3, 4], NodeIdNetType.ByteString), new NodeId(3, [0, 1, 2, 3, 4], NodeIdNetType.ByteString));
}

[Fact]
public void NodeId_OpaqueValueNonEquivalence()
{
Assert.NotEqual(new NodeId(2, [0, 1, 2, 3, 4], NodeIdNetType.ByteString), new NodeId(2, [0, 1, 2, 3, 5], NodeIdNetType.ByteString));
}
}
}
34 changes: 31 additions & 3 deletions NET Core/LibUA/AddressSpace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ public UInt32 NumericIdentifier
get; protected set;
}

private object _ObjectIdentifier;
public byte[] ByteStringIdentifier
{
get; protected set;
get => (byte[])_ObjectIdentifier;
protected set => _ObjectIdentifier = (object)value;
}

public string StringIdentifier
{
get; protected set;
get => (string)_ObjectIdentifier;
protected set => _ObjectIdentifier = (object)value;
}

public NodeIdNetType IdType
Expand Down Expand Up @@ -156,6 +159,27 @@ public override int GetHashCode()
return (int)(res & 0x7FFFFFFF);
}

public bool IsNull()
{
// OPC 10000-3: Address Space Model
// 8.2.4 Identifier value
// A canonical null NodeId has an IdType equal to Numeric, a NamespaceIndex equal to 0 and an
// Identifier equal to 0.
//
// In addition to the canonical null NodeId the alternative values defined in Table 23 shall be
// considered a null NodeId.
// IdType NamespaceIndex Null Value
// String 0 A null or Empty String(“”)
// Guid 0 A Guid initialised with zeros(e.g. 00000000-0000-0000-0000-000000)
// Opaque 0 A null or Empty ByteString
return NamespaceIndex == 0 && (
(IdType == NodeIdNetType.Numeric && NumericIdentifier == 0)
|| (IdType == NodeIdNetType.String && string.IsNullOrEmpty(StringIdentifier))
|| (IdType == NodeIdNetType.Guid && (ByteStringIdentifier is null || Guid.Empty == new Guid(ByteStringIdentifier)))
|| (IdType == NodeIdNetType.ByteString && (ByteStringIdentifier is null || ByteStringIdentifier.Length == 0))
);
}

public bool EqualsNumeric(UInt16 ns, UInt32 addr)
{
if (IdType != NodeIdNetType.Numeric) { return false; }
Expand All @@ -173,13 +197,17 @@ public bool Equals(NodeId other)
return false;
}

if (this.IsNull() && other.IsNull())
{
return true;
}
if (IdType != other.IdType)
{
return false;
}

return IdType == NodeIdNetType.Numeric ? NumericIdentifier == other.NumericIdentifier
: IdType == NodeIdNetType.String ? StringIdentifier == other.StringIdentifier
: IdType == NodeIdNetType.String ? String.Equals(StringIdentifier, other.StringIdentifier, StringComparison.Ordinal)
: EqualByteString(ByteStringIdentifier, other.ByteStringIdentifier);
}

Expand Down
Loading

0 comments on commit 34253aa

Please sign in to comment.