-
-
Notifications
You must be signed in to change notification settings - Fork 108
Fix JUnit reporter crash on invalid XML characters in test names and exceptions #4262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,39 @@ namespace TUnit.Engine.Xml; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal static class JUnitXmlWriter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Sanitizes a string to be XML-safe by removing invalid XML characters. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// According to XML 1.0 spec, valid characters are: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static string SanitizeForXml(string? value) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (string.IsNullOrEmpty(value)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return string.Empty; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // At this point, value is guaranteed to not be null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var builder = new StringBuilder(value!.Length); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach (var ch in value!) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if character is valid according to XML 1.0 spec | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (ch == 0x9 || ch == 0xA || ch == 0xD || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (ch >= 0x20 && ch <= 0xD7FF) || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (ch >= 0xE000 && ch <= 0xFFFD)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| builder.Append(ch); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Replace invalid character with its hex representation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| builder.Append($"[0x{((int)ch):X}]"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+40
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach (var ch in value!) | |
| { | |
| // Check if character is valid according to XML 1.0 spec | |
| if (ch == 0x9 || ch == 0xA || ch == 0xD || | |
| (ch >= 0x20 && ch <= 0xD7FF) || | |
| (ch >= 0xE000 && ch <= 0xFFFD)) | |
| { | |
| builder.Append(ch); | |
| } | |
| else | |
| { | |
| // Replace invalid character with its hex representation | |
| builder.Append($"[0x{((int)ch):X}]"); | |
| } | |
| var length = value!.Length; | |
| for (var i = 0; i < length; i++) | |
| { | |
| var ch = value[i]; | |
| int codePoint; | |
| var charsConsumed = 1; | |
| // Handle surrogate pairs to correctly process characters in the range U+10000 to U+10FFFF | |
| if (char.IsHighSurrogate(ch) && i + 1 < length && char.IsLowSurrogate(value[i + 1])) | |
| { | |
| codePoint = char.ConvertToUtf32(ch, value[i + 1]); | |
| charsConsumed = 2; | |
| } | |
| else | |
| { | |
| codePoint = ch; | |
| } | |
| // Check if character is valid according to XML 1.0 spec | |
| var isValid = | |
| codePoint == 0x9 || | |
| codePoint == 0xA || | |
| codePoint == 0xD || | |
| (codePoint >= 0x20 && codePoint <= 0xD7FF) || | |
| (codePoint >= 0xE000 && codePoint <= 0xFFFD) || | |
| (codePoint >= 0x10000 && codePoint <= 0x10FFFF); | |
| if (isValid) | |
| { | |
| if (charsConsumed == 1) | |
| { | |
| builder.Append((char)codePoint); | |
| } | |
| else | |
| { | |
| // Append the original surrogate pair | |
| builder.Append(ch); | |
| builder.Append(value[i + 1]); | |
| } | |
| } | |
| else | |
| { | |
| // Replace invalid character (or unpaired surrogate) with its hex representation | |
| builder.Append($"[0x{codePoint:X}]"); | |
| } | |
| if (charsConsumed == 2) | |
| { | |
| i++; | |
| } |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SanitizeForXml method lacks dedicated unit tests. While integration tests exist in TUnit.TestProject, there should be unit tests in TUnit.Engine.Tests that directly test the XML sanitization logic with various edge cases (null, empty, valid control characters, invalid characters, surrogate pairs, boundary conditions). This would ensure the sanitization function works correctly in isolation.
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exception type names from exception.GetType().FullName are written to XML without sanitization. While standard .NET type names won't contain invalid XML characters, custom exception types could theoretically have non-ASCII or problematic characters in their namespace or type names. For consistency and defensive programming, these should also be sanitized.
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exception type names from exception.GetType().FullName are written to XML without sanitization. While standard .NET type names won't contain invalid XML characters, custom exception types could theoretically have non-ASCII or problematic characters in their namespace or type names. For consistency and defensive programming, these should also be sanitized.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| using TUnit.Core; | ||
|
|
||
| namespace TUnit.TestProject; | ||
|
|
||
| public class JUnitReporterInvalidXmlCharacterTests | ||
|
||
| { | ||
| [Test] | ||
| [Arguments("Some valid string")] | ||
| public async Task TestWithValidString(string parameter) | ||
| { | ||
| await Assert.That(parameter).IsNotNull(); | ||
| } | ||
|
|
||
| [Test] | ||
| [Arguments("A string with an invalid \x04 character")] | ||
| public async Task TestWithInvalidXmlCharacter(string parameter) | ||
| { | ||
| await Assert.That(parameter).IsNotNull(); | ||
| } | ||
|
|
||
| [Test] | ||
| [Arguments("Multiple\x01\x02\x03\x04invalid characters")] | ||
| public async Task TestWithMultipleInvalidXmlCharacters(string parameter) | ||
| { | ||
| await Assert.That(parameter).IsNotNull(); | ||
| } | ||
|
|
||
| [Test] | ||
| [Arguments("Test\twith\nvalid\rcontrol characters")] | ||
| public async Task TestWithValidControlCharacters(string parameter) | ||
| { | ||
| await Assert.That(parameter).IsNotNull(); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task TestFailingWithInvalidCharacterInException() | ||
| { | ||
| // This test intentionally fails with an exception message containing invalid XML characters | ||
| throw new InvalidOperationException("Error with invalid \x04 character in exception message"); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The redundant null-forgiving operators (!) on lines 26 and 27 are unnecessary since the check on line 20 already confirms the value is not null or empty. The value cannot be null at this point in the execution.