Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CodeBlocker.Test/TemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace CodeBlocker.Tests;

using System.Text;
using ktsu.CodeBlocker;
using ktsu.CodeBlocker.Templates;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand Down Expand Up @@ -48,6 +49,31 @@ public void ParameterEscapesAQuotedDefaultValue(string value, string expectedLit
$"string name = {expectedLiteral}",
Render(new ParameterTemplate { Type = "string", Name = "name", DefaultValue = value, DefaultValueIsQuoted = true }));

[TestMethod]
public void ParameterEscapesALoneSurrogateInAQuotedDefaultValue()
{
// These are built in code rather than passed through DataRow: an attribute argument is
// stored as UTF-8, which cannot carry a lone surrogate either.
(string Value, string ExpectedLiteral)[] cases =
[
("a\uD800b", @"""a\ud800b"""),
("a\uDC00b", @"""a\udc00b"""),
("end\uD83D", @"""end\ud83d"""),
("\uDE00start", @"""\ude00start"""),
("\uDE00\uD83D", @"""\ude00\ud83d"""),
("pair \uD83D\uDE00", "\"pair \uD83D\uDE00\""),
];

UTF8Encoding strictUtf8 = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
foreach ((string value, string expectedLiteral) in cases)
{
string rendered = Render(new ParameterTemplate { Type = "string", Name = "name", DefaultValue = value, DefaultValueIsQuoted = true });

Assert.AreEqual($"string name = {expectedLiteral}", rendered);
_ = strictUtf8.GetBytes(rendered);
}
}

[TestMethod]
public void ParameterKeepsItsAttributesOnTheDeclarationLine()
{
Expand Down
34 changes: 30 additions & 4 deletions CodeBlocker/Templates/TemplateRendering.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,18 @@ internal static void SpliceFragment(CodeBlocker parent, string fragment)
/// <returns>The quoted literal.</returns>
/// <remarks>
/// Backslashes, quotes and the common control characters take their short escapes; any other
/// control character, and the line and paragraph separators the compiler also treats as line
/// breaks, take a <c>\uXXXX</c> escape.
/// control character, the line and paragraph separators the compiler also treats as line
/// breaks, and any surrogate that is not half of a valid pair take a <c>\uXXXX</c> escape. A
/// lone surrogate has no UTF-8 encoding, so written raw it would make the generated file
/// impossible to save faithfully: a strict encoder throws and a replacing one changes the value.
/// </remarks>
internal static string QuoteStringLiteral(string value)
{
StringBuilder literal = new(value.Length + 2);
literal.Append('"');
foreach (char c in value)
for (int i = 0; i < value.Length; i++)
{
char c = value[i];
string? escape = c switch
{
'\\' => @"\\",
Expand All @@ -146,7 +149,7 @@ internal static string QuoteStringLiteral(string value)
'\r' => @"\r",
'\t' => @"\t",
'\v' => @"\v",
_ when char.IsControl(c) || c is '\u2028' or '\u2029' =>
_ when char.IsControl(c) || c is '\u2028' or '\u2029' || IsLoneSurrogate(value, i) =>
@"\u" + ((int)c).ToString("x4", CultureInfo.InvariantCulture),
_ => null,
};
Expand All @@ -165,6 +168,29 @@ _ when char.IsControl(c) || c is '\u2028' or '\u2029' =>
return literal.ToString();
}

/// <summary>
/// Reports whether the character at <paramref name="index"/> is a surrogate that is not half of
/// a high-low pair.
/// </summary>
/// <param name="value">The text.</param>
/// <param name="index">The position of the character to test.</param>
/// <returns><see langword="true"/> for an unpaired surrogate; otherwise <see langword="false"/>.</returns>
private static bool IsLoneSurrogate(string value, int index)
{
char c = value[index];
if (char.IsHighSurrogate(c))
{
return index + 1 >= value.Length || !char.IsLowSurrogate(value[index + 1]);
}

if (char.IsLowSurrogate(c))
{
return index == 0 || !char.IsHighSurrogate(value[index - 1]);
}

return false;
}

/// <summary>
/// Writes a parenthesised, comma-separated parameter list.
/// </summary>
Expand Down
Loading