diff --git a/PowerKit.Tests/XmlTests.cs b/PowerKit.Tests/XmlTests.cs new file mode 100644 index 0000000..14ca8da --- /dev/null +++ b/PowerKit.Tests/XmlTests.cs @@ -0,0 +1,27 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace PowerKit.Tests; + +public class XmlTests +{ + [Fact] + 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(); + } +} diff --git a/PowerKit/Xml.cs b/PowerKit/Xml.cs new file mode 100644 index 0000000..bedafe9 --- /dev/null +++ b/PowerKit/Xml.cs @@ -0,0 +1,94 @@ +using System; +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) + { + var builder = default(StringBuilder); + + 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; + } +}