Skip to content

Commit

Permalink
Fix ExpandedNodeId.Format output for not well formed uri and JSON Ver…
Browse files Browse the repository at this point in the history
…bose WriteStatusCode (#2794)

-JSON Encoder Verbose and NonReversible encoding with WriteStatusCode may produce invalid JSON
-ExpandedNodeId.Format does not handle not well formed uri correctly, as in the previous implementation
  • Loading branch information
mregen authored Oct 11, 2024
1 parent 0215eb5 commit cb621df
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 8 deletions.
5 changes: 3 additions & 2 deletions Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/// <summary>
Expand Down
33 changes: 30 additions & 3 deletions Stack/Opc.Ua.Core/Types/Utils/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1265,10 +1265,37 @@ public static string ReplaceDCLocalhost(string subjectName, string hostname = nu
/// </summary>
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
{
Expand Down
50 changes: 48 additions & 2 deletions Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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")}\""},
Expand All @@ -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
Expand Down Expand Up @@ -438,6 +444,7 @@ public void ForcePropertiesShouldThrow(JsonEncodingType jsonEncodingType)
Assert.Throws<NotSupportedException>(() => encoder.ForceNamespaceUriForIndex1 = true);
Assert.Throws<NotSupportedException>(() => encoder.IncludeDefaultNumberValues = true);
Assert.Throws<NotSupportedException>(() => encoder.IncludeDefaultValues = true);
Assert.Throws<NotSupportedException>(() => encoder.EncodeNodeIdAsString = false);
}
}

Expand Down Expand Up @@ -1162,6 +1169,45 @@ public void DateTimeEncodeStringTestCase(string dateTimeString)
DateTimeEncodeStringTest(dateTime);
}

/// <summary>
/// Validate that a ExpandedNodeId returns the expected
/// result for a not well formed Uri.
/// </summary>
[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);
}

/// <summary>
/// Validate that a statuscode in a DataValue produces valid JSON.
/// </summary>
[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);
}
}

/// <summary>
/// Validate that the DateTime format strings return an equal result.
/// </summary>
Expand Down

0 comments on commit cb621df

Please sign in to comment.