From 392c16d8307d3cfe6380a39117fb325d686157cb Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Jul 2026 13:43:41 +0000
Subject: [PATCH 1/8] Add Xml.Escape helper method with tests
---
PowerKit.Tests/XmlTests.cs | 121 +++++++++++++++++++++++++++++++++++++
PowerKit/Xml.cs | 81 +++++++++++++++++++++++++
2 files changed, 202 insertions(+)
create mode 100644 PowerKit.Tests/XmlTests.cs
create mode 100644 PowerKit/Xml.cs
diff --git a/PowerKit.Tests/XmlTests.cs b/PowerKit.Tests/XmlTests.cs
new file mode 100644
index 0000000..875960f
--- /dev/null
+++ b/PowerKit.Tests/XmlTests.cs
@@ -0,0 +1,121 @@
+using FluentAssertions;
+using Xunit;
+
+namespace PowerKit.Tests;
+
+public class XmlTests
+{
+ [Fact]
+ public void Escape_NoSpecialChars_Test()
+ {
+ // Act
+ var result = Xml.Escape("hello world");
+
+ // Assert
+ result.Should().Be("hello world");
+ }
+
+ [Fact]
+ public void Escape_Ampersand_Test()
+ {
+ // Act
+ var result = Xml.Escape("foo & bar");
+
+ // Assert
+ result.Should().Be("foo & bar");
+ }
+
+ [Fact]
+ public void Escape_LessThan_Test()
+ {
+ // Act
+ var result = Xml.Escape("1 < 2");
+
+ // Assert
+ result.Should().Be("1 < 2");
+ }
+
+ [Fact]
+ public void Escape_GreaterThan_Test()
+ {
+ // Act
+ var result = Xml.Escape("2 > 1");
+
+ // Assert
+ result.Should().Be("2 > 1");
+ }
+
+ [Fact]
+ public void Escape_DoubleQuote_Test()
+ {
+ // Act
+ var result = Xml.Escape("say \"hello\"");
+
+ // Assert
+ result.Should().Be("say "hello"");
+ }
+
+ [Fact]
+ public void Escape_SingleQuote_Test()
+ {
+ // Act
+ var result = Xml.Escape("it's");
+
+ // Assert
+ result.Should().Be("it's");
+ }
+
+ [Fact]
+ public void Escape_AllSpecialChars_Test()
+ {
+ // Act
+ var result = Xml.Escape("& < > \" '");
+
+ // Assert
+ result.Should().Be("& < > " '");
+ }
+
+ [Fact]
+ public void Escape_InvalidControlChar_Test()
+ {
+ // Arrange
+ // U+0001 is invalid in XML 1.0 and should be removed
+ var result = Xml.Escape("foo\u0001bar");
+
+ // Assert
+ result.Should().Be("foobar");
+ }
+
+ [Fact]
+ public void Escape_ValidWhitespace_Test()
+ {
+ // Tab, newline, and carriage return are valid XML characters
+ var result = Xml.Escape("foo\t\n\rbar");
+
+ // Assert
+ result.Should().Be("foo\t\n\rbar");
+ }
+
+ [Fact]
+ public void Escape_SurrogatePair_Test()
+ {
+ // Supplementary character U+1F600 (😀), encoded as a surrogate pair in .NET
+ var input = "\U0001F600";
+
+ // Act
+ var result = Xml.Escape(input);
+
+ // Assert
+ result.Should().Be(input);
+ }
+
+ [Fact]
+ public void Escape_EmptyString_Test()
+ {
+ // Act
+ var result = Xml.Escape("");
+
+ // Assert
+ result.Should().BeEmpty();
+ }
+}
diff --git a/PowerKit/Xml.cs b/PowerKit/Xml.cs
new file mode 100644
index 0000000..f8aa5fe
--- /dev/null
+++ b/PowerKit/Xml.cs
@@ -0,0 +1,81 @@
+using System.Text;
+
+namespace PowerKit;
+
+///
+/// Helper methods for working with XML.
+///
+public static class Xml
+{
+ private static bool IsValidXmlChar(char ch) =>
+ ch == '\t'
+ || ch == '\n'
+ || ch == '\r'
+ || (ch >= '\x20' && ch <= '\xD7FF')
+ || (ch >= '\xE000' && ch <= '\xFFFD');
+
+ ///
+ /// Escapes invalid XML characters in the specified string, returning a valid XML string.
+ /// Characters that cannot be represented in XML (such as most control characters) are removed.
+ /// Special XML characters (&, <, >, ", ')
+ /// are replaced with their corresponding XML entities.
+ ///
+ public static string Escape(string str)
+ {
+ StringBuilder? builder = null;
+
+ var i = 0;
+ while (i < str.Length)
+ {
+ var ch = str[i];
+
+ string? replacement;
+ if (ch == '&')
+ replacement = "&";
+ else if (ch == '<')
+ replacement = "<";
+ else if (ch == '>')
+ replacement = ">";
+ else if (ch == '"')
+ replacement = """;
+ else if (ch == '\'')
+ replacement = "'";
+ else if (
+ char.IsHighSurrogate(ch)
+ && i + 1 < str.Length
+ && char.IsLowSurrogate(str[i + 1])
+ )
+ {
+ // Valid surrogate pair — represents a supplementary character (U+10000..U+10FFFF),
+ // which is valid in XML. Append both chars and advance past the pair.
+ builder?.Append(ch);
+ builder?.Append(str[i + 1]);
+ i += 2;
+ continue;
+ }
+ else if (IsValidXmlChar(ch))
+ replacement = null;
+ else
+ {
+ // Truly invalid XML character — skip it.
+ builder ??= new StringBuilder(str, 0, i, str.Length);
+ i++;
+ continue;
+ }
+
+ if (replacement is not null)
+ {
+ builder ??= new StringBuilder(str, 0, i, str.Length);
+ builder.Append(replacement);
+ }
+ else
+ {
+ builder?.Append(ch);
+ }
+
+ i++;
+ }
+
+ return builder?.ToString() ?? str;
+ }
+}
From f3b0f4d5f6249bfafc83b71b30a6b0654e985034 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Jul 2026 13:46:57 +0000
Subject: [PATCH 2/8] Add null check and fix test structure in Xml.Escape
---
PowerKit.Tests/XmlTests.cs | 16 +++++++++++++++-
PowerKit/Xml.cs | 2 ++
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/PowerKit.Tests/XmlTests.cs b/PowerKit.Tests/XmlTests.cs
index 875960f..00f5044 100644
--- a/PowerKit.Tests/XmlTests.cs
+++ b/PowerKit.Tests/XmlTests.cs
@@ -1,3 +1,4 @@
+using System;
using FluentAssertions;
using Xunit;
@@ -80,7 +81,10 @@ public void Escape_InvalidControlChar_Test()
{
// Arrange
// U+0001 is invalid in XML 1.0 and should be removed
- var result = Xml.Escape("foo\u0001bar");
+ var input = "foo\u0001bar";
+
+ // Act
+ var result = Xml.Escape(input);
// Assert
result.Should().Be("foobar");
@@ -118,4 +122,14 @@ public void Escape_EmptyString_Test()
// Assert
result.Should().BeEmpty();
}
+
+ [Fact]
+ public void Escape_Null_Test()
+ {
+ // Act
+ var act = () => Xml.Escape(null!);
+
+ // Assert
+ act.Should().ThrowExactly();
+ }
}
diff --git a/PowerKit/Xml.cs b/PowerKit/Xml.cs
index f8aa5fe..9224b77 100644
--- a/PowerKit/Xml.cs
+++ b/PowerKit/Xml.cs
@@ -1,3 +1,4 @@
+using System;
using System.Text;
namespace PowerKit;
@@ -22,6 +23,7 @@ private static bool IsValidXmlChar(char ch) =>
///
public static string Escape(string str)
{
+ ArgumentNullException.ThrowIfNull(str);
StringBuilder? builder = null;
var i = 0;
From ccaa030db6540f560cc4c8ab3e422f20d6a96637 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Jul 2026 13:48:57 +0000
Subject: [PATCH 3/8] Add isolated surrogate tests for Xml.Escape
---
PowerKit.Tests/XmlTests.cs | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/PowerKit.Tests/XmlTests.cs b/PowerKit.Tests/XmlTests.cs
index 00f5044..aca7879 100644
--- a/PowerKit.Tests/XmlTests.cs
+++ b/PowerKit.Tests/XmlTests.cs
@@ -113,6 +113,34 @@ public void Escape_SurrogatePair_Test()
result.Should().Be(input);
}
+ [Fact]
+ public void Escape_IsolatedHighSurrogate_Test()
+ {
+ // Arrange
+ // An isolated high surrogate (no paired low surrogate) is invalid in XML and should be removed
+ var input = "foo\uD800bar";
+
+ // Act
+ var result = Xml.Escape(input);
+
+ // Assert
+ result.Should().Be("foobar");
+ }
+
+ [Fact]
+ public void Escape_IsolatedLowSurrogate_Test()
+ {
+ // Arrange
+ // An isolated low surrogate is invalid in XML and should be removed
+ var input = "foo\uDC00bar";
+
+ // Act
+ var result = Xml.Escape(input);
+
+ // Assert
+ result.Should().Be("foobar");
+ }
+
[Fact]
public void Escape_EmptyString_Test()
{
From 723e2810ed17b8af88a301ea72e51622583892d4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Jul 2026 13:55:58 +0000
Subject: [PATCH 4/8] Refactor Xml tests into unified act/assert style
---
PowerKit.Tests/XmlTests.cs | 169 ++++---------------------------------
1 file changed, 17 insertions(+), 152 deletions(-)
diff --git a/PowerKit.Tests/XmlTests.cs b/PowerKit.Tests/XmlTests.cs
index aca7879..2ba731a 100644
--- a/PowerKit.Tests/XmlTests.cs
+++ b/PowerKit.Tests/XmlTests.cs
@@ -7,157 +7,22 @@ namespace PowerKit.Tests;
public class XmlTests
{
[Fact]
- public void Escape_NoSpecialChars_Test()
- {
- // Act
- var result = Xml.Escape("hello world");
-
- // Assert
- result.Should().Be("hello world");
- }
-
- [Fact]
- public void Escape_Ampersand_Test()
- {
- // Act
- var result = Xml.Escape("foo & bar");
-
- // Assert
- result.Should().Be("foo & bar");
- }
-
- [Fact]
- public void Escape_LessThan_Test()
- {
- // Act
- var result = Xml.Escape("1 < 2");
-
- // Assert
- result.Should().Be("1 < 2");
- }
-
- [Fact]
- public void Escape_GreaterThan_Test()
- {
- // Act
- var result = Xml.Escape("2 > 1");
-
- // Assert
- result.Should().Be("2 > 1");
- }
-
- [Fact]
- public void Escape_DoubleQuote_Test()
- {
- // Act
- var result = Xml.Escape("say \"hello\"");
-
- // Assert
- result.Should().Be("say "hello"");
- }
-
- [Fact]
- public void Escape_SingleQuote_Test()
- {
- // Act
- var result = Xml.Escape("it's");
-
- // Assert
- result.Should().Be("it's");
- }
-
- [Fact]
- public void Escape_AllSpecialChars_Test()
- {
- // Act
- var result = Xml.Escape("& < > \" '");
-
- // Assert
- result.Should().Be("& < > " '");
- }
-
- [Fact]
- public void Escape_InvalidControlChar_Test()
- {
- // Arrange
- // U+0001 is invalid in XML 1.0 and should be removed
- var input = "foo\u0001bar";
-
- // Act
- var result = Xml.Escape(input);
-
- // Assert
- result.Should().Be("foobar");
- }
-
- [Fact]
- public void Escape_ValidWhitespace_Test()
- {
- // Tab, newline, and carriage return are valid XML characters
- var result = Xml.Escape("foo\t\n\rbar");
-
- // Assert
- result.Should().Be("foo\t\n\rbar");
- }
-
- [Fact]
- public void Escape_SurrogatePair_Test()
- {
- // Supplementary character U+1F600 (😀), encoded as a surrogate pair in .NET
- var input = "\U0001F600";
-
- // Act
- var result = Xml.Escape(input);
-
- // Assert
- result.Should().Be(input);
- }
-
- [Fact]
- public void Escape_IsolatedHighSurrogate_Test()
- {
- // Arrange
- // An isolated high surrogate (no paired low surrogate) is invalid in XML and should be removed
- var input = "foo\uD800bar";
-
- // Act
- var result = Xml.Escape(input);
-
- // Assert
- result.Should().Be("foobar");
- }
-
- [Fact]
- public void Escape_IsolatedLowSurrogate_Test()
- {
- // Arrange
- // An isolated low surrogate is invalid in XML and should be removed
- var input = "foo\uDC00bar";
-
- // Act
- var result = Xml.Escape(input);
-
- // Assert
- result.Should().Be("foobar");
- }
-
- [Fact]
- public void Escape_EmptyString_Test()
- {
- // Act
- var result = Xml.Escape("");
-
- // Assert
- result.Should().BeEmpty();
- }
-
- [Fact]
- public void Escape_Null_Test()
- {
- // Act
- var act = () => Xml.Escape(null!);
-
- // Assert
- act.Should().ThrowExactly();
+ public void Escape_Test()
+ {
+ // Act & assert
+ Xml.Escape("hello world").Should().Be("hello world");
+ Xml.Escape("foo & bar").Should().Be("foo & bar");
+ Xml.Escape("1 < 2").Should().Be("1 < 2");
+ Xml.Escape("2 > 1").Should().Be("2 > 1");
+ Xml.Escape("say \"hello\"").Should().Be("say "hello"");
+ Xml.Escape("it's").Should().Be("it's");
+ Xml.Escape("& < > \" '").Should().Be("& < > " '");
+ Xml.Escape("foo\u0001bar").Should().Be("foobar");
+ Xml.Escape("foo\t\n\rbar").Should().Be("foo\t\n\rbar");
+ Xml.Escape("\U0001F600").Should().Be("\U0001F600");
+ Xml.Escape("foo\uD800bar").Should().Be("foobar");
+ Xml.Escape("foo\uDC00bar").Should().Be("foobar");
+ Xml.Escape("").Should().BeEmpty();
+ Assert.Throws(() => Xml.Escape(null!));
}
}
From 44378b73b0897b28cbeb3e7998237cece7d54817 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Jul 2026 13:56:44 +0000
Subject: [PATCH 5/8] Use FluentAssertions throw assertion in Xml test
---
PowerKit.Tests/XmlTests.cs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/PowerKit.Tests/XmlTests.cs b/PowerKit.Tests/XmlTests.cs
index 2ba731a..9911478 100644
--- a/PowerKit.Tests/XmlTests.cs
+++ b/PowerKit.Tests/XmlTests.cs
@@ -23,6 +23,9 @@ public void Escape_Test()
Xml.Escape("foo\uD800bar").Should().Be("foobar");
Xml.Escape("foo\uDC00bar").Should().Be("foobar");
Xml.Escape("").Should().BeEmpty();
- Assert.Throws(() => Xml.Escape(null!));
+ FluentActions
+ .Invoking(() => Xml.Escape(null!))
+ .Should()
+ .ThrowExactly();
}
}
From 9e24b0464652d5cfb6cbe145184a382a2cca90a1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Jul 2026 18:34:20 +0000
Subject: [PATCH 6/8] Use Assert.Throws in XmlTests null assertion
---
PowerKit.Tests/XmlTests.cs | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/PowerKit.Tests/XmlTests.cs b/PowerKit.Tests/XmlTests.cs
index 9911478..2ba731a 100644
--- a/PowerKit.Tests/XmlTests.cs
+++ b/PowerKit.Tests/XmlTests.cs
@@ -23,9 +23,6 @@ public void Escape_Test()
Xml.Escape("foo\uD800bar").Should().Be("foobar");
Xml.Escape("foo\uDC00bar").Should().Be("foobar");
Xml.Escape("").Should().BeEmpty();
- FluentActions
- .Invoking(() => Xml.Escape(null!))
- .Should()
- .ThrowExactly();
+ Assert.Throws(() => Xml.Escape(null!));
}
}
From edb982c901e2ccf8cc0c1c4434865ec8cf1d4db3 Mon Sep 17 00:00:00 2001
From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com>
Date: Sat, 11 Jul 2026 21:51:32 +0300
Subject: [PATCH 7/8] Update XmlTests.cs
---
PowerKit.Tests/XmlTests.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/PowerKit.Tests/XmlTests.cs b/PowerKit.Tests/XmlTests.cs
index 2ba731a..14ca8da 100644
--- a/PowerKit.Tests/XmlTests.cs
+++ b/PowerKit.Tests/XmlTests.cs
@@ -23,6 +23,5 @@ public void Escape_Test()
Xml.Escape("foo\uD800bar").Should().Be("foobar");
Xml.Escape("foo\uDC00bar").Should().Be("foobar");
Xml.Escape("").Should().BeEmpty();
- Assert.Throws(() => Xml.Escape(null!));
}
}
From 2d8f6a711ff301fc101fcf810c9bd7d41265c902 Mon Sep 17 00:00:00 2001
From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com>
Date: Sat, 11 Jul 2026 21:52:31 +0300
Subject: [PATCH 8/8] Update Xml.cs
---
PowerKit/Xml.cs | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/PowerKit/Xml.cs b/PowerKit/Xml.cs
index 9224b77..bedafe9 100644
--- a/PowerKit/Xml.cs
+++ b/PowerKit/Xml.cs
@@ -23,8 +23,7 @@ private static bool IsValidXmlChar(char ch) =>
///
public static string Escape(string str)
{
- ArgumentNullException.ThrowIfNull(str);
- StringBuilder? builder = null;
+ var builder = default(StringBuilder);
var i = 0;
while (i < str.Length)
@@ -33,15 +32,25 @@ public static string Escape(string str)
string? replacement;
if (ch == '&')
+ {
replacement = "&";
+ }
else if (ch == '<')
+ {
replacement = "<";
+ }
else if (ch == '>')
+ {
replacement = ">";
+ }
else if (ch == '"')
+ {
replacement = """;
+ }
else if (ch == '\'')
+ {
replacement = "'";
+ }
else if (
char.IsHighSurrogate(ch)
&& i + 1 < str.Length
@@ -56,7 +65,9 @@ public static string Escape(string str)
continue;
}
else if (IsValidXmlChar(ch))
+ {
replacement = null;
+ }
else
{
// Truly invalid XML character — skip it.