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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ appsettings.user.json
.playwright-mcp/

node_modules/
.nitro/
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,38 @@ public void Parse_Should_RunSchemaValidation_When_EarlierSchemaOnSharedLogFailed
});
}

[Fact]
public void Parse_Should_PreserveBlockStringBackslashes_When_DescriptionContainsThem()
{
// arrange
var sourceSchemaText =
new SourceSchemaText(
"A",
""""
type Query {
"""
Matches ^[A-Z]{2}-\d{4}$
Literal sequences: \n and \\
"""
code: String
}
"""");
var log = new CompositionLog();
var parser = new SourceSchemaParser(sourceSchemaText, log);

// act
var result = parser.Parse();

// assert
Assert.True(result.IsSuccess);
Assert.Empty(log);
result.Value.QueryType!.Fields["code"].Description.MatchInlineSnapshot(
"""
Matches ^[A-Z]{2}-\d{4}$
Literal sequences: \n and \\
""");
}

[Fact]
public void Parse_Should_ReportUndefinedUnionMemberType_When_UnionNamesMissingType()
{
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,6 @@
<data name="Utf8Helper_InvalidEscapeChar" xml:space="preserve">
<value>The escape char `{0}` is invalid.</value>
</data>
<data name="Utf8Helper_InvalidQuoteEscapeCount" xml:space="preserve">
<value>The quote escape string has to consist of three quotes `\"""`.</value>
</data>
<data name="NewLineMustBeGreaterOrEqualToOne" xml:space="preserve">
<value>Must be greater or equal to 1.</value>
</data>
Expand Down
80 changes: 41 additions & 39 deletions src/HotChocolate/Language/src/Language.Utf8/Utf8Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ internal static class Utf8Helper
{
public static void Validate(ReadOnlySpan<byte> escapedString, bool isBlockString)
{
if (isBlockString)
{
// Within a block string `\"""` is the only escape sequence and it never fails.
return;
}

var position = 0;
var highSurrogate = -1;

Expand All @@ -31,19 +37,6 @@ public static void Validate(ReadOnlySpan<byte> escapedString, bool isBlockString
}

var code = escapedString[position++];
if (isBlockString && code == GraphQLCharacters.Quote)
{
if (position + 1 >= escapedString.Length
|| escapedString[position] != GraphQLCharacters.Quote
|| escapedString[position + 1] != GraphQLCharacters.Quote)
{
throw new Utf8EncodingException(Utf8Helper_InvalidQuoteEscapeCount);
}

position += 2;
continue;
}

if (code == GraphQLCharacters.U)
{
if (position + 3 >= escapedString.Length)
Expand Down Expand Up @@ -272,6 +265,12 @@ private static void ProcessEscapeSequence(
ref int highSurrogate,
bool isBlockString)
{
if (isBlockString)
{
ProcessBlockStringEscapeSequence(escaped, unescaped, ref readPos, ref writePos);
return;
}

if (readPos + 1 >= escaped.Length)
{
throw new Utf8EncodingException(
Expand All @@ -283,14 +282,39 @@ private static void ProcessEscapeSequence(
var code = escaped[readPos++];

// Hot path: simple escape characters (most common)
if (code != GraphQLCharacters.U && !isBlockString && code.IsValidEscapeCharacter())
if (code != GraphQLCharacters.U && code.IsValidEscapeCharacter())
{
unescaped[writePos++] = code.EscapeCharacter();
return;
}

// Cold path: unicode, block strings, and error handling
ProcessEscapeSequenceCold(escaped, unescaped, ref readPos, ref writePos, ref highSurrogate, isBlockString, code);
// Cold path: unicode and error handling
ProcessEscapeSequenceCold(escaped, unescaped, ref readPos, ref writePos, ref highSurrogate, code);
}

// Within a block string `\"""` is the only escape sequence, every other
// backslash is a literal character.
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ProcessBlockStringEscapeSequence(
in ReadOnlySpan<byte> escaped,
Span<byte> unescaped,
ref int readPos,
ref int writePos)
{
if (readPos + 3 < escaped.Length
&& escaped[readPos + 1] == GraphQLCharacters.Quote
&& escaped[readPos + 2] == GraphQLCharacters.Quote
&& escaped[readPos + 3] == GraphQLCharacters.Quote)
{
readPos += 4;
unescaped[writePos++] = GraphQLCharacters.Quote;
unescaped[writePos++] = GraphQLCharacters.Quote;
unescaped[writePos++] = GraphQLCharacters.Quote;
return;
}

readPos++;
unescaped[writePos++] = GraphQLCharacters.Backslash;
}

[MethodImpl(MethodImplOptions.NoInlining)]
Expand All @@ -300,26 +324,9 @@ private static void ProcessEscapeSequenceCold(
ref int readPos,
ref int writePos,
ref int highSurrogate,
bool isBlockString,
byte code)
{
if (isBlockString && code == GraphQLCharacters.Quote)
{
if (readPos + 1 < escaped.Length
&& escaped[readPos] == GraphQLCharacters.Quote
&& escaped[readPos + 1] == GraphQLCharacters.Quote)
{
readPos += 2;
unescaped[writePos++] = GraphQLCharacters.Quote;
unescaped[writePos++] = GraphQLCharacters.Quote;
unescaped[writePos++] = GraphQLCharacters.Quote;
}
else
{
throw new Utf8EncodingException(Utf8Helper_InvalidQuoteEscapeCount);
}
}
else if (code == GraphQLCharacters.U)
if (code == GraphQLCharacters.U)
{
if (readPos + 3 >= escaped.Length)
{
Expand Down Expand Up @@ -365,11 +372,6 @@ private static void ProcessEscapeSequenceCold(
UnescapeUtf8Hex(unicodeDecimal, ref writePos, unescaped);
}
}
else if (code.IsValidEscapeCharacter())
{
// Block string with non-quote, non-unicode escape
unescaped[writePos++] = code.EscapeCharacter();
}
else
{
throw new Utf8EncodingException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,30 @@ public void ParseOperationDocument_Should_RejectInvalidDescriptionEscapeLikeClas
Assert.IsType<Utf8EncodingException>(packed);
}

[Fact]
public void ParseOperationDocument_Should_KeepBlockStringBackslashesLiteral_When_ArgumentIsBlockString()
{
// arrange
const string source =
""""
{ field(arg: """pattern \d and \n""") }
"""";
var sourceBytes = Encoding.UTF8.GetBytes(source);

// act
var classic = Utf8GraphQLParser.Parse(sourceBytes);
var packed = Record.Exception(() => Utf8GraphQLOperationParser.Parse(sourceBytes));
var operation = (OperationDefinitionNode)classic.Definitions[0];
var field = (FieldNode)operation.SelectionSet.Selections[0];

// assert
Assert.Null(packed);
Assert.IsType<StringValueNode>(field.Arguments[0].Value).Value.MatchInlineSnapshot(
"""
pattern \d and \n
""");
}

[Theory]
[InlineData("query Q($value: String = \"abc\") { field }", "\"abc\"")]
[InlineData("query Q($value: String = \"\"\"abc\"\"\") { field }", "\"\"\"abc\"\"\"")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,18 @@ public void Unescape_StandardEscapeChars_OutputIsUnescaped(
Encoding.UTF8.GetString(output.ToArray()));
}

[InlineData("\\b", "\b")]
[InlineData("\\f", "\f")]
[InlineData("\\n", "\n")]
[InlineData("\\r", "\r")]
[InlineData("\\t", "\t")]
[InlineData("\\\"\"\"", "\"\"\"")]
[InlineData("\\d")]
[InlineData("\\`")]
[InlineData("\\{")]
[InlineData("\\$")]
[InlineData("\\\"")]
[InlineData("\\\"\"")]
[InlineData("\\n")]
[InlineData("\\\\")]
[InlineData("\\u0041")]
[Theory]
public void Unescape_BlockStringEscapeChars_OutputIsUnescaped(
string escaped, string unescaped)
public void Unescape_Should_KeepBackslashLiteral_When_BlockStringHasNoTripleQuoteEscape(
string escaped)
{
// arrange
var inputData = Encoding.UTF8.GetBytes("hello_123_" + escaped);
Expand All @@ -67,34 +70,63 @@ public void Unescape_BlockStringEscapeChars_OutputIsUnescaped(
Utf8Helper.Unescape(in input, ref output, true);

// assert
Assert.Equal("hello_123_" + unescaped,
Assert.Equal("hello_123_" + escaped,
Encoding.UTF8.GetString(output.ToArray()));
}

[InlineData("\\\"\"")]
[InlineData("\\\"")]
[Theory]
public void Unescape_BlockStringInvalidEscapeChars_Exception(
string escaped)
[Fact]
public void Unescape_Should_WriteTripleQuote_When_BlockStringEscapesTripleQuote()
{
// arrange
var inputData = Encoding.UTF8.GetBytes("hello_123_" + escaped);
var inputData = "hello_123_\\\"\"\"_end"u8.ToArray();
var outputBuffer = new byte[inputData.Length];

var input = new ReadOnlySpan<byte>(inputData);
var output = new Span<byte>(outputBuffer);

// act
try
{
Utf8Helper.Unescape(in input, ref output, true);
Utf8Helper.Unescape(in input, ref output, true);

// assert
Assert.Fail("The unescape method should fail.");
}
catch
{
}
// assert
Assert.Equal("hello_123_\"\"\"_end",
Encoding.UTF8.GetString(output.ToArray()));
}

[Fact]
public void Unescape_Should_WriteTripleQuote_When_BlockStringEscapeFollowsLiteralBackslash()
{
// arrange
// the tokenizer treats the second backslash as the start of the `\"""` escape.
var inputData = "hello_123_\\\\\"\"\"_end"u8.ToArray();
var outputBuffer = new byte[inputData.Length];

var input = new ReadOnlySpan<byte>(inputData);
var output = new Span<byte>(outputBuffer);

// act
Utf8Helper.Unescape(in input, ref output, true);

// assert
Assert.Equal("hello_123_\\\"\"\"_end",
Encoding.UTF8.GetString(output.ToArray()));
}

[Fact]
public void Unescape_Should_KeepTrailingBackslash_When_IsBlockString()
{
// arrange
var inputData = "hello_123_\\"u8.ToArray();
var outputBuffer = new byte[inputData.Length];

var input = new ReadOnlySpan<byte>(inputData);
var output = new Span<byte>(outputBuffer);

// act
Utf8Helper.Unescape(in input, ref output, true);

// assert
Assert.Equal("hello_123_\\",
Encoding.UTF8.GetString(output.ToArray()));
}

[InlineData("\\u0024", "$")]
Expand Down
Loading
Loading