From 3aac75d4fe90f7f2764723616ee28a8e7a13dcf0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 07:04:25 +0000 Subject: [PATCH 1/6] Initial plan From 64b4d190dd67bdd662c49e5966d8e39bea1fb4ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 07:23:55 +0000 Subject: [PATCH 2/6] Fix null array handling in JSON and Binary decoders Co-authored-by: romanett <7413710+romanett@users.noreply.github.com> --- .../Opc.Ua.Core/Types/Encoders/JsonDecoder.cs | 6 + Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs | 7 + .../Types/Encoders/NullArrayEncodingTests.cs | 152 ++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 Tests/Opc.Ua.Core.Tests/Types/Encoders/NullArrayEncodingTests.cs diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs index 07d8e449e..9f0117a4c 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs @@ -2335,6 +2335,12 @@ public Array ReadArray( case BuiltInType.DiagnosticInfo: return ReadDiagnosticInfoArray(fieldName).ToArray(); case BuiltInType.Null: + // For null arrays, read the array structure and return object array with null elements + if (!ReadArrayField(fieldName, out List nullArrayToken)) + { + return null; + } + return new object[nullArrayToken.Count]; case BuiltInType.Number: case BuiltInType.Integer: case BuiltInType.UInteger: diff --git a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs index 9e2e4a25f..8951dc48e 100644 --- a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs @@ -1391,6 +1391,13 @@ public Array ReadArray( case BuiltInType.DiagnosticInfo: return ReadDiagnosticInfoArray(fieldName)?.ToArray(); case BuiltInType.Null: + // For null arrays, read the array length and return object array with null elements + int nullArrayLength = ReadArrayLength(); + if (nullArrayLength < 0) + { + return null; + } + return new object[nullArrayLength]; case BuiltInType.Number: case BuiltInType.Integer: case BuiltInType.UInteger: diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/NullArrayEncodingTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/NullArrayEncodingTests.cs new file mode 100644 index 000000000..a924abe24 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/NullArrayEncodingTests.cs @@ -0,0 +1,152 @@ +/* ======================================================================== + * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.IO; +using NUnit.Framework; +using Opc.Ua.Tests; + +namespace Opc.Ua.Core.Tests.Types.Encoders +{ + [TestFixture, Category("Encoder")] + [Parallelizable] + public class NullArrayEncodingTests + { + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + [TestCase(5)] + public void JsonEncoder_ReadArrayWithBuiltInTypeNull_ReturnsObjectArrayWithNullElements(int arrayLength) + { + // Arrange + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + var context = new ServiceMessageContext(telemetry); + var nullElements = arrayLength > 0 ? string.Join(",", System.Linq.Enumerable.Repeat("null", arrayLength)) : ""; + var json = $"{{\"NullArray\":[{nullElements}]}}"; + + // Act + var decoder = new JsonDecoder(json, context); + var result = decoder.ReadArray("NullArray", ValueRanks.OneDimension, BuiltInType.Null); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + var objectArray = (object[])result; + Assert.That(objectArray.Length, Is.EqualTo(arrayLength)); + + for (int i = 0; i < arrayLength; i++) + { + Assert.That(objectArray[i], Is.Null, $"Element at index {i} should be null"); + } + } + + [Test] + public void JsonEncoder_ReadEmptyArrayWithBuiltInTypeNull_ReturnsEmptyObjectArray() + { + // Arrange + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + var context = new ServiceMessageContext(telemetry); + var json = "{\"NullArray\":[]}"; + + // Act + var decoder = new JsonDecoder(json, context); + var result = decoder.ReadArray("NullArray", ValueRanks.OneDimension, BuiltInType.Null); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + Assert.That(((object[])result).Length, Is.EqualTo(0)); + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + [TestCase(5)] + public void BinaryEncoder_ReadArrayWithBuiltInTypeNull_ReturnsObjectArrayWithNullElements(int arrayLength) + { + // Arrange + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + var context = new ServiceMessageContext(telemetry); + + using (var stream = new MemoryStream()) + { + // Write binary array length + var encoder = new BinaryEncoder(stream, context, false); + encoder.WriteInt32(null, arrayLength); + encoder.Close(); + + var bytes = stream.ToArray(); + + // Act + var decoder = new BinaryDecoder(bytes, context); + var result = decoder.ReadArray(null, ValueRanks.OneDimension, BuiltInType.Null); + decoder.Close(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + var objectArray = (object[])result; + Assert.That(objectArray.Length, Is.EqualTo(arrayLength)); + + for (int i = 0; i < arrayLength; i++) + { + Assert.That(objectArray[i], Is.Null, $"Element at index {i} should be null"); + } + } + } + + [Test] + public void BinaryEncoder_ReadNullArrayWithBuiltInTypeNull_ReturnsNull() + { + // Arrange + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + var context = new ServiceMessageContext(telemetry); + + using (var stream = new MemoryStream()) + { + // Write -1 to indicate null array + var encoder = new BinaryEncoder(stream, context, false); + encoder.WriteInt32(null, -1); + encoder.Close(); + + var bytes = stream.ToArray(); + + // Act + var decoder = new BinaryDecoder(bytes, context); + var result = decoder.ReadArray(null, ValueRanks.OneDimension, BuiltInType.Null); + decoder.Close(); + + // Assert + Assert.That(result, Is.Null); + } + } + } +} From 10bc703086c318ac69d793ea4205a0a4dbb06b42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 07:27:15 +0000 Subject: [PATCH 3/6] Add clarifying comments to null array handling code Co-authored-by: romanett <7413710+romanett@users.noreply.github.com> --- Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs | 3 ++- Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs index 9f0117a4c..385c90c4b 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs @@ -2335,7 +2335,8 @@ public Array ReadArray( case BuiltInType.DiagnosticInfo: return ReadDiagnosticInfoArray(fieldName).ToArray(); case BuiltInType.Null: - // For null arrays, read the array structure and return object array with null elements + // For null arrays, read the array structure and return object array with null elements. + // We only need the array count since all elements are expected to be null. if (!ReadArrayField(fieldName, out List nullArrayToken)) { return null; diff --git a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs index 8951dc48e..8abd756ec 100644 --- a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs @@ -1391,7 +1391,8 @@ public Array ReadArray( case BuiltInType.DiagnosticInfo: return ReadDiagnosticInfoArray(fieldName)?.ToArray(); case BuiltInType.Null: - // For null arrays, read the array length and return object array with null elements + // For null arrays, read the array length and return object array with null elements. + // Array length of -1 indicates a null array reference (not an array with null elements). int nullArrayLength = ReadArrayLength(); if (nullArrayLength < 0) { From 1e95065c956b76986d16a865262acd52d1eeeb4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:16:49 +0000 Subject: [PATCH 4/6] Fix null array handling for multi-dimensional arrays Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com> --- .../Opc.Ua.Core/Types/Encoders/JsonDecoder.cs | 21 ++++++ Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs | 3 + .../Types/Encoders/NullArrayEncodingTests.cs | 71 +++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs index 385c90c4b..887090653 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs @@ -2631,6 +2631,27 @@ public Array ReadArray( [.. dimensions]); break; case BuiltInType.Null: + if (DetermineIEncodeableSystemType(ref systemType, encodeableTypeId)) + { + var newElements = Array.CreateInstance(systemType, elements.Count); + for (int i = 0; i < elements.Count; i++) + { + newElements.SetValue( + Convert.ChangeType( + elements[i], + systemType, + CultureInfo.InvariantCulture), + i); + } + matrix = new Matrix(newElements, builtInType, [.. dimensions]); + break; + } + // For null arrays, create object array with null elements + matrix = new Matrix( + elements.Cast().ToArray(), + builtInType, + [.. dimensions]); + break; case BuiltInType.Number: case BuiltInType.Integer: case BuiltInType.UInteger: diff --git a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs index 8abd756ec..077503884 100644 --- a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs @@ -1905,6 +1905,9 @@ private Array ReadArrayElements(int length, BuiltInType builtInType) break; } case BuiltInType.Null: + // For null arrays, return object array with null elements + array = new object[length]; + break; case BuiltInType.Number: case BuiltInType.Integer: case BuiltInType.UInteger: diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/NullArrayEncodingTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/NullArrayEncodingTests.cs index a924abe24..823c200fd 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/NullArrayEncodingTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/NullArrayEncodingTests.cs @@ -148,5 +148,76 @@ public void BinaryEncoder_ReadNullArrayWithBuiltInTypeNull_ReturnsNull() Assert.That(result, Is.Null); } } + + [Test] + public void JsonEncoder_ReadMultiDimensionalArrayWithBuiltInTypeNull_ReturnsObjectArray() + { + // Arrange - 2x3 matrix of nulls: [[null, null, null], [null, null, null]] + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + var context = new ServiceMessageContext(telemetry); + var json = "{\"NullMatrix\":[[null,null,null],[null,null,null]]}"; + + // Act + var decoder = new JsonDecoder(json, context); + var result = decoder.ReadArray("NullMatrix", ValueRanks.TwoDimensions, BuiltInType.Null); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + var matrix = (object[,])result; + Assert.That(matrix.GetLength(0), Is.EqualTo(2), "First dimension should be 2"); + Assert.That(matrix.GetLength(1), Is.EqualTo(3), "Second dimension should be 3"); + + // Verify all elements are null + for (int i = 0; i < 2; i++) + { + for (int j = 0; j < 3; j++) + { + Assert.That(matrix[i, j], Is.Null, $"Element at [{i},{j}] should be null"); + } + } + } + + [Test] + public void BinaryEncoder_ReadMultiDimensionalArrayWithBuiltInTypeNull_ReturnsObjectArray() + { + // Arrange - 2x3 matrix of nulls + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + var context = new ServiceMessageContext(telemetry); + + using (var stream = new MemoryStream()) + { + // Write binary multi-dimensional array + // First write dimensions array + var encoder = new BinaryEncoder(stream, context, false); + encoder.WriteInt32(null, 2); // number of dimensions + encoder.WriteInt32(null, 2); // dimension 0 length + encoder.WriteInt32(null, 3); // dimension 1 length + encoder.Close(); + + var bytes = stream.ToArray(); + + // Act + var decoder = new BinaryDecoder(bytes, context); + var result = decoder.ReadArray(null, ValueRanks.TwoDimensions, BuiltInType.Null); + decoder.Close(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + var matrix = (object[,])result; + Assert.That(matrix.GetLength(0), Is.EqualTo(2), "First dimension should be 2"); + Assert.That(matrix.GetLength(1), Is.EqualTo(3), "Second dimension should be 3"); + + // Verify all elements are null + for (int i = 0; i < 2; i++) + { + for (int j = 0; j < 3; j++) + { + Assert.That(matrix[i, j], Is.Null, $"Element at [{i},{j}] should be null"); + } + } + } + } } } From 177b550207a08279e6ea5cdad6a545ea2a5d3341 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 06:38:31 +0000 Subject: [PATCH 5/6] Changes before error encountered Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com> --- Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs | 7 ++++++- Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs index 887090653..5bddbbc04 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs @@ -2335,12 +2335,17 @@ public Array ReadArray( case BuiltInType.DiagnosticInfo: return ReadDiagnosticInfoArray(fieldName).ToArray(); case BuiltInType.Null: - // For null arrays, read the array structure and return object array with null elements. + // For null arrays, read the array structure and return array with null elements. // We only need the array count since all elements are expected to be null. + // Use systemType if provided, otherwise use object[]. if (!ReadArrayField(fieldName, out List nullArrayToken)) { return null; } + if (systemType != null) + { + return Array.CreateInstance(systemType, nullArrayToken.Count); + } return new object[nullArrayToken.Count]; case BuiltInType.Number: case BuiltInType.Integer: diff --git a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs index 077503884..0daf7e43f 100644 --- a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs @@ -1391,13 +1391,18 @@ public Array ReadArray( case BuiltInType.DiagnosticInfo: return ReadDiagnosticInfoArray(fieldName)?.ToArray(); case BuiltInType.Null: - // For null arrays, read the array length and return object array with null elements. + // For null arrays, read the array length and return array with null elements. // Array length of -1 indicates a null array reference (not an array with null elements). + // Use systemType if provided, otherwise use object[]. int nullArrayLength = ReadArrayLength(); if (nullArrayLength < 0) { return null; } + if (systemType != null) + { + return Array.CreateInstance(systemType, nullArrayLength); + } return new object[nullArrayLength]; case BuiltInType.Number: case BuiltInType.Integer: From 43fab0af991a993fc3c3fbd85dd589e3e9972695 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 08:36:56 +0000 Subject: [PATCH 6/6] Fix BuiltInType.Null handling to properly decode IEncodeable arrays Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com> --- .../Opc.Ua.Core/Types/Encoders/JsonDecoder.cs | 18 ++++++++---------- Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs | 7 ++++++- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs index 5bddbbc04..527d586f0 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs @@ -2335,7 +2335,12 @@ public Array ReadArray( case BuiltInType.DiagnosticInfo: return ReadDiagnosticInfoArray(fieldName).ToArray(); case BuiltInType.Null: - // For null arrays, read the array structure and return array with null elements. + // For encodeable types with BuiltInType.Null, use the encodeable array reader + if (DetermineIEncodeableSystemType(ref systemType, encodeableTypeId)) + { + return ReadEncodeableArray(fieldName, systemType, encodeableTypeId); + } + // For null arrays without encodeable type, read the array structure and return array with null elements. // We only need the array count since all elements are expected to be null. // Use systemType if provided, otherwise use object[]. if (!ReadArrayField(fieldName, out List nullArrayToken)) @@ -2638,16 +2643,9 @@ public Array ReadArray( case BuiltInType.Null: if (DetermineIEncodeableSystemType(ref systemType, encodeableTypeId)) { + // For null elements of a specific encodeable type, create array with nulls var newElements = Array.CreateInstance(systemType, elements.Count); - for (int i = 0; i < elements.Count; i++) - { - newElements.SetValue( - Convert.ChangeType( - elements[i], - systemType, - CultureInfo.InvariantCulture), - i); - } + // Elements are already null, no need to set them explicitly matrix = new Matrix(newElements, builtInType, [.. dimensions]); break; } diff --git a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs index 0daf7e43f..9ebb6f74a 100644 --- a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs @@ -1391,7 +1391,12 @@ public Array ReadArray( case BuiltInType.DiagnosticInfo: return ReadDiagnosticInfoArray(fieldName)?.ToArray(); case BuiltInType.Null: - // For null arrays, read the array length and return array with null elements. + // For encodeable types with BuiltInType.Null, use the encodeable array reader + if (DetermineIEncodeableSystemType(ref systemType, encodeableTypeId)) + { + return ReadEncodeableArray(fieldName, systemType, encodeableTypeId); + } + // For null arrays without encodeable type, read the array length and return array with null elements. // Array length of -1 indicates a null array reference (not an array with null elements). // Use systemType if provided, otherwise use object[]. int nullArrayLength = ReadArrayLength();