diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs index f2ff438bb..ca25ebd36 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs @@ -1345,17 +1345,18 @@ public void WriteStatusCode(string fieldName, StatusCode value) return; } + // Verbose and NonReversible + PushStructure(fieldName); if (value != StatusCodes.Good) { - PushStructure(fieldName); WriteSimpleField("Code", value.Code.ToString(CultureInfo.InvariantCulture), EscapeOptions.NoFieldNameEscape); string symbolicId = StatusCode.LookupSymbolicId(value.CodeBits); if (!string.IsNullOrEmpty(symbolicId)) { WriteSimpleField("Symbol", symbolicId, EscapeOptions.Quotes | EscapeOptions.NoFieldNameEscape); } - PopStructure(); } + PopStructure(); } /// diff --git a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs index dca47d47e..e6236c51e 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs @@ -1265,10 +1265,37 @@ public static string ReplaceDCLocalhost(string subjectName, string hostname = nu /// public static string EscapeUri(string uri) { - if (!String.IsNullOrWhiteSpace(uri)) + if (!string.IsNullOrWhiteSpace(uri)) { - var builder = new UriBuilder(uri.Replace(";", "%3b")); - return builder.Uri.AbsoluteUri; + // back compat: for not well formed Uri, fall back to legacy formatting behavior - see #2793 + if (!Uri.IsWellFormedUriString(uri, UriKind.Absolute) || + !Uri.TryCreate(uri.Replace(";", "%3b"), UriKind.Absolute, out Uri validUri)) + { + var buffer = new StringBuilder(); + foreach (char ch in uri) + { + switch (ch) + { + case ';': + case '%': + { + buffer.AppendFormat(CultureInfo.InvariantCulture, "%{0:X2}", Convert.ToInt16(ch)); + break; + } + + default: + { + buffer.Append(ch); + break; + } + } + } + return buffer.ToString(); + } + else + { + return validUri.AbsoluteUri; + } } return String.Empty; diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs index bbe1853b0..a15bcda1e 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs @@ -374,13 +374,19 @@ private string BuildExpectedResponse( if (jsonEncoding == JsonEncodingType.Compact || jsonEncoding == JsonEncodingType.Reversible) { oText = "0"; + // default statuscode is not encoded + continue; + } + else if (jsonEncoding == JsonEncodingType.Verbose) + { + oText = "{}"; } else { oText = "{\"Code\": 0,\"Symbol\":\"Good\"}"; // default statuscode is not encoded + continue; } - continue; } else if (property.Name == "Guid") { diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs index b70368980..614be99ac 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs @@ -286,8 +286,8 @@ public class JsonEncoderTests : EncoderCommon $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":88}}", null, $"\"ns=88;b={s_byteString64}\"", null}, - { BuiltInType.StatusCode, new StatusCode(StatusCodes.Good), null, null, null, null}, - { BuiltInType.StatusCode, new StatusCode(StatusCodes.Good), $"{StatusCodes.Good}", "", null, null, true}, + { BuiltInType.StatusCode, new StatusCode(StatusCodes.Good), null, null, null, "{}"}, + { BuiltInType.StatusCode, new StatusCode(StatusCodes.Good), $"{StatusCodes.Good}", "{}", null, "{}", true}, { BuiltInType.StatusCode, new StatusCode(StatusCodes.BadBoundNotFound), $"{StatusCodes.BadBoundNotFound}", $"{{\"Code\":{StatusCodes.BadBoundNotFound}, \"Symbol\":\"{nameof(StatusCodes.BadBoundNotFound)}\"}}"}, { BuiltInType.StatusCode, new StatusCode(StatusCodes.BadCertificateInvalid), @@ -352,6 +352,9 @@ public class JsonEncoderTests : EncoderCommon { BuiltInType.DataValue, new DataValue(), "{}", null}, { BuiltInType.DataValue, new DataValue(StatusCodes.Good), "{}", null}, + { BuiltInType.DataValue, new DataValue(StatusCodes.BadNotWritable), + $"{{\"StatusCode\":{StatusCodes.BadNotWritable}}}", + $"{{\"StatusCode\":{{\"Code\":{StatusCodes.BadNotWritable}, \"Symbol\":\"{nameof(StatusCodes.BadNotWritable)}\"}}}}"}, { BuiltInType.Enumeration, (TestEnumType) 0, "0", "\"0\""}, { BuiltInType.Enumeration, TestEnumType.Three, TestEnumType.Three.ToString("d"), $"\"{TestEnumType.Three}_{TestEnumType.Three.ToString("d")}\""}, @@ -372,6 +375,9 @@ public class JsonEncoderTests : EncoderCommon "{\"Body\":{\"Foo\":\"bar_999\"}}", "{\"Foo\":\"bar_999\"}", "{\"Body\":{\"Foo\":\"bar_999\"}}", null} }.ToArray(); + + [DatapointSource] + public static StatusCode[] GoodAndBadStatusCodes = { StatusCodes.Good, StatusCodes.BadAlreadyExists }; #endregion #region Test Setup @@ -438,6 +444,7 @@ public void ForcePropertiesShouldThrow(JsonEncodingType jsonEncodingType) Assert.Throws(() => encoder.ForceNamespaceUriForIndex1 = true); Assert.Throws(() => encoder.IncludeDefaultNumberValues = true); Assert.Throws(() => encoder.IncludeDefaultValues = true); + Assert.Throws(() => encoder.EncodeNodeIdAsString = false); } } @@ -1162,6 +1169,45 @@ public void DateTimeEncodeStringTestCase(string dateTimeString) DateTimeEncodeStringTest(dateTime); } + /// + /// Validate that a ExpandedNodeId returns the expected + /// result for a not well formed Uri. + /// + [Test] + public void NotWellFormedUriInExpandedNodeId2String() + { + string namespaceUri = "KEPServerEX"; + string nodeName = "Data Type Examples.16 Bit Device.K Registers.Double3"; + String expectedNodeIdString = $"nsu={namespaceUri};s={nodeName}"; + ExpandedNodeId expandedNodeId = new ExpandedNodeId(expectedNodeIdString); + + string stringifiedExpandedNodId = expandedNodeId.ToString(); + TestContext.Out.WriteLine(stringifiedExpandedNodId); + Assert.AreEqual(expectedNodeIdString, stringifiedExpandedNodId); + } + + /// + /// Validate that a statuscode in a DataValue produces valid JSON. + /// + [Theory] + public void DataValueWithStatusCodes( + JsonEncodingType jsonEncodingType, + [ValueSource(nameof(GoodAndBadStatusCodes))] StatusCode statusCodeVariant, + [ValueSource(nameof(GoodAndBadStatusCodes))] StatusCode statusCode) + { + var dataValue = new DataValue() { + Value = new Variant(statusCodeVariant), + ServerTimestamp = DateTime.UtcNow, + StatusCode = statusCode + }; + using (var jsonEncoder = new JsonEncoder(m_context, jsonEncodingType)) + { + jsonEncoder.WriteDataValue("Data", dataValue); + var result = jsonEncoder.CloseAndReturnText(); + PrettifyAndValidateJson(result, true); + } + } + /// /// Validate that the DateTime format strings return an equal result. ///