Skip to content

Commit 930ba09

Browse files
authored
Fix a case where NodeId.Null is modified (#2994)
-Fix for decoding an ExtensionObject NodeId.Null is modified. -Add test case and checks to catch modifcations of NodeId.Null -Add IMMUTABLENULLNODEID define in NodeId.cs to throw InvalidOperationException if NodeId.Null is modified
1 parent 61056d0 commit 930ba09

File tree

5 files changed

+88
-12
lines changed

5 files changed

+88
-12
lines changed

Stack/Opc.Ua.Core/Types/BuiltIn/ExtensionObject.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,8 @@ private NodeId XmlEncodedTypeId
717717
// check for null Id.
718718
if (m_typeId.IsNull)
719719
{
720-
return NodeId.Null;
720+
// note: this NodeId is modified when the ExtensionObject is deserialized.
721+
return new NodeId();
721722
}
722723

723724
return ExpandedNodeId.ToNodeId(m_typeId, m_context.NamespaceUris);

Stack/Opc.Ua.Core/Types/BuiltIn/Matrix.cs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1+
/* Copyright (c) 1996-2022 The OPC Foundation. All rights reserved.
2+
The source code in this file is covered under a dual-license scenario:
3+
- RCL: for OPC Foundation Corporate Members in good-standing
4+
- GPL V2: everybody else
5+
RCL license terms accompanied with this source code. See http://opcfoundation.org/License/RCL/1.00/
6+
GNU General Public License as published by the Free Software Foundation;
7+
version 2 of the License are accompanied with this source code. See http://opcfoundation.org/License/GPLv2
8+
This source code is distributed in the hope that it will be useful,
9+
but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11+
*/
12+
113
using System;
214
using System.Diagnostics;
3-
using System.Globalization;
415
using System.Runtime.Serialization;
516
using System.Text;
617

@@ -30,7 +41,6 @@ public Matrix(Array value, BuiltInType builtInType)
3041

3142
m_elements = Utils.FlattenArray(value);
3243
m_typeInfo = new TypeInfo(builtInType, m_dimensions.Length);
33-
3444
}
3545

3646
/// <summary>
@@ -61,7 +71,6 @@ public Matrix(Array elements, BuiltInType builtInType, params int[] dimensions)
6171

6272
SanityCheckArrayElements(m_elements, builtInType);
6373
}
64-
6574
#endregion
6675

6776
#region Public Members
@@ -242,16 +251,14 @@ public virtual object Clone()
242251
/// <param name="builtInType">The builtInType used for the elements.</param>
243252
[Conditional("DEBUG")]
244253
private static void SanityCheckArrayElements(Array elements, BuiltInType builtInType)
245-
246-
247254
{
248255
#if DEBUG
249256
TypeInfo sanityCheck = TypeInfo.Construct(elements);
250257
Debug.Assert(sanityCheck.BuiltInType == builtInType || builtInType == BuiltInType.Enumeration ||
251-
(sanityCheck.BuiltInType == BuiltInType.ExtensionObject && builtInType == BuiltInType.Null) ||
252-
(sanityCheck.BuiltInType == BuiltInType.Int32 && builtInType == BuiltInType.Enumeration) ||
253-
(sanityCheck.BuiltInType == BuiltInType.ByteString && builtInType == BuiltInType.Byte) ||
254-
(builtInType == BuiltInType.Variant));
258+
(sanityCheck.BuiltInType == BuiltInType.ExtensionObject && builtInType == BuiltInType.Null) ||
259+
(sanityCheck.BuiltInType == BuiltInType.Int32 && builtInType == BuiltInType.Enumeration) ||
260+
(sanityCheck.BuiltInType == BuiltInType.ByteString && builtInType == BuiltInType.Byte) ||
261+
(builtInType == BuiltInType.Variant));
255262
#endif
256263
}
257264
#endregion

Stack/Opc.Ua.Core/Types/BuiltIn/NodeId.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1111
*/
1212

13+
// define to enable checks for a null NodeId modification
14+
// some tests are failing with this enabled, only turn on to catch issues
15+
// #define IMMUTABLENULLNODEID
16+
1317
using System;
1418
using System.Collections;
1519
using System.Collections.Generic;
@@ -866,8 +870,11 @@ internal static NodeId InternalParse(string text, bool namespaceSet)
866870
/// Returns an instance of a null NodeId.
867871
/// </summary>
868872
public static NodeId Null => s_Null;
869-
873+
#if IMMUTABLENULLNODEID
874+
private static readonly NodeId s_Null = new ImmutableNodeId();
875+
#else
870876
private static readonly NodeId s_Null = new NodeId();
877+
#endif
871878
#endregion
872879

873880
#region Public Methods (and some Internals)
@@ -1001,6 +1008,7 @@ public static ExpandedNodeId ToExpandedNodeId(NodeId nodeId, NamespaceTable name
10011008
/// </summary>
10021009
internal void SetNamespaceIndex(ushort value)
10031010
{
1011+
ValidateImmutableNodeIdIsNotModified();
10041012
m_namespaceIndex = value;
10051013
}
10061014

@@ -1009,6 +1017,7 @@ internal void SetNamespaceIndex(ushort value)
10091017
/// </summary>
10101018
internal void SetIdentifier(IdType idType, object value)
10111019
{
1020+
ValidateImmutableNodeIdIsNotModified();
10121021
m_identifierType = idType;
10131022

10141023
switch (idType)
@@ -1038,6 +1047,8 @@ internal void SetIdentifier(IdType idType, object value)
10381047
/// </summary>
10391048
internal void SetIdentifier(string value, IdType idType)
10401049
{
1050+
ValidateImmutableNodeIdIsNotModified();
1051+
10411052
m_identifierType = idType;
10421053
SetIdentifier(IdType.String, value);
10431054
}
@@ -1495,6 +1506,8 @@ internal string IdentifierText
14951506
}
14961507
set
14971508
{
1509+
ValidateImmutableNodeIdIsNotModified();
1510+
14981511
NodeId nodeId = NodeId.Parse(value);
14991512

15001513
m_namespaceIndex = nodeId.NamespaceIndex;
@@ -1859,6 +1872,21 @@ private static void FormatIdentifier(IFormatProvider formatProvider, StringBuild
18591872
}
18601873
}
18611874
}
1875+
1876+
/// <summary>
1877+
/// Validate that an immutable NodeId is not overwritten.
1878+
/// </summary>
1879+
/// <exception cref="InvalidOperationException"></exception>
1880+
[Conditional("IMMUTABLENULLNODEID")]
1881+
private void ValidateImmutableNodeIdIsNotModified()
1882+
{
1883+
#if IMMUTABLENULLNODEID
1884+
if (this is ImmutableNodeId)
1885+
{
1886+
throw new InvalidOperationException("Cannot modify the immutable NodeId.Null.");
1887+
}
1888+
#endif
1889+
}
18621890
#endregion
18631891

18641892
#region Private Fields
@@ -1868,6 +1896,20 @@ private static void FormatIdentifier(IFormatProvider formatProvider, StringBuild
18681896
#endregion
18691897
}
18701898

1899+
#if IMMUTABLENULLNODEID
1900+
#region ImmutableNodeId
1901+
/// <summary>
1902+
/// A NodeId class as helper to catch if the immutable NodeId.Null is being modified.
1903+
/// </summary>
1904+
internal class ImmutableNodeId : NodeId
1905+
{
1906+
internal ImmutableNodeId()
1907+
{
1908+
}
1909+
}
1910+
#endregion
1911+
#endif
1912+
18711913
#region NodeIdCollection Class
18721914
/// <summary>
18731915
/// A collection of NodeIds.
@@ -1993,6 +2035,7 @@ public class NodeIdParsingOptions
19932035
/// </summary>
19942036
public ushort[] ServerMappings { get; set; }
19952037
}
2038+
19962039
#region NodeIdComparer Class
19972040
/// <summary>
19982041
/// Helper which implements a NodeId IEqualityComparer for Linq queries.

Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ public class EncoderCommon
7979
protected BufferManager BufferManager { get; private set; }
8080
protected RecyclableMemoryStreamManager RecyclableMemoryManager { get; private set; }
8181

82-
8382
#region Test Setup
8483
[OneTimeSetUp]
8584
protected void OneTimeSetUp()
@@ -112,6 +111,8 @@ protected void SetUp()
112111
[TearDown]
113112
protected void TearDown()
114113
{
114+
// ensure after every test that the Null NodeId was not modified
115+
Assert.True(NodeId.Null.IsNullNodeId);
115116
}
116117

117118
/// <summary>

Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,30 @@ public void EncodeMatrixInArrayOverflow(
973973
Assert.AreEqual((StatusCode)StatusCodes.BadEncodingLimitsExceeded, (StatusCode)sre.StatusCode, sre.Message);
974974
}
975975
}
976+
977+
/// <summary>
978+
/// Test if deserializing an extensionObject alters the Null NodeId.
979+
/// </summary>
980+
/// <remarks>
981+
/// Issue was raised in github #2974.
982+
/// </remarks>
983+
[Test]
984+
public void EnsureNodeIdNullIsNotModified()
985+
{
986+
var text1 = "[{\"Body\":{\"KeyValuePair\":{\"@xmlns\":\"http://opcfoundation.org/UA/2008/02/Types.xsd\"," +
987+
"\"Key\":{\"Name\":\"o\",\"NamespaceIndex\":\"0\"},\"Value\":{\"Value\":" +
988+
"{\"ListOfExtensionObject\":{\"ExtensionObject\":[" +
989+
"{\"Body\":{\"KeyValuePair\":{\"Key\":{\"Name\":\"stringProp\",\"NamespaceIndex\":\"0\"},\"Value\":{\"Value\":" +
990+
"{\"String\":\"EinString\"}}}},\"TypeId\":{\"Identifier\":\"i=14801\"}},{\"Body\":{\"KeyValuePair\":{\"Key\":" +
991+
"{\"Name\":\"intProp\",\"NamespaceIndex\":\"0\"},\"Value\":{\"Value\":{\"Int32\":\"1\"}}}},\"TypeId\":" +
992+
"{\"Identifier\":\"i=14802\"}}]}}}}},\"TypeId\":" +
993+
"{\"Identifier\":\"i=14803\"}}]";
994+
995+
JsonConvert.DeserializeObject<ExtensionObject[]>(text1);
996+
997+
Assert.NotNull(NodeId.Null);
998+
Assert.True(NodeId.Null.IsNullNodeId);
999+
}
9761000
#endregion
9771001

9781002
#region Private Methods

0 commit comments

Comments
 (0)