From a7a52b95153f1c28f1470099dc58b1b824e5444e Mon Sep 17 00:00:00 2001 From: NOlbert <45466413+N-Olbert@users.noreply.github.com> Date: Wed, 6 Aug 2025 22:56:58 +0200 Subject: [PATCH 1/2] Test: ensure string inputs with control characters are handled correctly --- .../Execution.Tests/RequestExecutorTests.cs | 32 +++++++++++++++++++ ...hControlCharacters_IsHandledCorrectly.snap | 6 ++++ 2 files changed, 38 insertions(+) create mode 100644 src/HotChocolate/Core/test/Execution.Tests/__snapshots__/RequestExecutorTests.StringInput_WithControlCharacters_IsHandledCorrectly.snap diff --git a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorTests.cs b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorTests.cs index 8d9c67b7733..f0ca0819d73 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorTests.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using HotChocolate.Tests; using HotChocolate.Types; using Microsoft.Extensions.DependencyInjection; @@ -94,6 +95,37 @@ public async Task CancellationToken_Is_Passed_Correctly() Assert.True(tokenWasCorrectlyPassedToResolver); } + [Fact] + public async Task StringInput_WithControlCharacters_IsHandledCorrectly() + { + var schema = SchemaBuilder.New() + .AddQueryType(t => t + .Name("Query") + .Field("echo") + .Argument("arg", arg => arg.Type()) + .Resolve(x => x.ArgumentValue("arg"))) + .Create(); + + var escapedControlChars = JsonSerializer.Serialize(new string( + Enumerable.Range(0, char.MaxValue) + .Select(i => (char)i) + .Where(char.IsControl) + .ToArray())); + + // act + var executor = schema.MakeExecutable(); + var result = await executor.ExecuteAsync( + $$""" + { + delChar: echo(arg: "Hello\u007FWorld") + allControlChars: echo(arg:{{escapedControlChars}}) + } + """); + + // assert + result.MatchSnapshot(); + } + [Fact] public async Task Ensure_Errors_Do_Not_Result_In_Timeouts() { diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/RequestExecutorTests.StringInput_WithControlCharacters_IsHandledCorrectly.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/RequestExecutorTests.StringInput_WithControlCharacters_IsHandledCorrectly.snap new file mode 100644 index 00000000000..603b4bf0b07 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/RequestExecutorTests.StringInput_WithControlCharacters_IsHandledCorrectly.snap @@ -0,0 +1,6 @@ +{ + "data": { + "delChar": "Hello\u007FWorld", + "allControlChars": "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\t\n\u000B\f\r\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\u007F\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008D\u008E\u008F\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009D\u009E\u009F" + } +} From 583a394466c8c8bba550d4bc8ffe5eb8d1f35c90 Mon Sep 17 00:00:00 2001 From: NOlbert <45466413+N-Olbert@users.noreply.github.com> Date: Fri, 29 Aug 2025 20:42:49 +0200 Subject: [PATCH 2/2] Reworked handling of delete control character in string inputs. --- .../Execution.Tests/RequestExecutorTests.cs | 26 +++++++++---------- ...ests.DeleteChar_StringInput_IsAllowed.snap | 7 +++++ ...hControlCharacters_IsHandledCorrectly.snap | 6 ----- .../src/Language.Utf8/GraphQLConstants.cs | 1 - .../src/Language.Utf8/Utf8GraphQLReader.cs | 3 --- 5 files changed, 19 insertions(+), 24 deletions(-) create mode 100644 src/HotChocolate/Core/test/Execution.Tests/__snapshots__/RequestExecutorTests.DeleteChar_StringInput_IsAllowed.snap delete mode 100644 src/HotChocolate/Core/test/Execution.Tests/__snapshots__/RequestExecutorTests.StringInput_WithControlCharacters_IsHandledCorrectly.snap diff --git a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorTests.cs b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorTests.cs index f0ca0819d73..1e04eb13ce6 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/RequestExecutorTests.cs @@ -96,7 +96,7 @@ public async Task CancellationToken_Is_Passed_Correctly() } [Fact] - public async Task StringInput_WithControlCharacters_IsHandledCorrectly() + public async Task DeleteChar_StringInput_IsAllowed() { var schema = SchemaBuilder.New() .AddQueryType(t => t @@ -106,21 +106,19 @@ public async Task StringInput_WithControlCharacters_IsHandledCorrectly() .Resolve(x => x.ArgumentValue("arg"))) .Create(); - var escapedControlChars = JsonSerializer.Serialize(new string( - Enumerable.Range(0, char.MaxValue) - .Select(i => (char)i) - .Where(char.IsControl) - .ToArray())); - // act var executor = schema.MakeExecutable(); - var result = await executor.ExecuteAsync( - $$""" - { - delChar: echo(arg: "Hello\u007FWorld") - allControlChars: echo(arg:{{escapedControlChars}}) - } - """); + var result = await executor.ExecuteAsync($$"""" + { + delChar: echo(arg: "\u007F") + delCharNonEscaped: echo(arg: "{{"\u007f"}}") + delCharNonEscapedBlockString: echo(arg: + """ + {{"\u007f"}} + """) + #Comment with del char -> {{"\u007f"}} <- + } + """"); // assert result.MatchSnapshot(); diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/RequestExecutorTests.DeleteChar_StringInput_IsAllowed.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/RequestExecutorTests.DeleteChar_StringInput_IsAllowed.snap new file mode 100644 index 00000000000..9c647232108 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/RequestExecutorTests.DeleteChar_StringInput_IsAllowed.snap @@ -0,0 +1,7 @@ +{ + "data": { + "delChar": "\u007F", + "delCharNonEscaped": "\u007F", + "delCharNonEscapedBlockString": "\u007F\n" + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/RequestExecutorTests.StringInput_WithControlCharacters_IsHandledCorrectly.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/RequestExecutorTests.StringInput_WithControlCharacters_IsHandledCorrectly.snap deleted file mode 100644 index 603b4bf0b07..00000000000 --- a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/RequestExecutorTests.StringInput_WithControlCharacters_IsHandledCorrectly.snap +++ /dev/null @@ -1,6 +0,0 @@ -{ - "data": { - "delChar": "Hello\u007FWorld", - "allControlChars": "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\t\n\u000B\f\r\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\u007F\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008D\u008E\u008F\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009D\u009E\u009F" - } -} diff --git a/src/HotChocolate/Language/src/Language.Utf8/GraphQLConstants.cs b/src/HotChocolate/Language/src/Language.Utf8/GraphQLConstants.cs index f1dd7b10255..2f03ce3976a 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/GraphQLConstants.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/GraphQLConstants.cs @@ -43,7 +43,6 @@ internal static class GraphQLConstants public const byte GroupSeparator = 29; public const byte RecordSeparator = 30; public const byte UnitSeparator = 31; - public const byte Delete = 127; public const byte A = (byte)'a'; public const byte B = (byte)'b'; diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLReader.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLReader.cs index 87966bc2d7c..3b82ffddb1f 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLReader.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLReader.cs @@ -486,7 +486,6 @@ private void ReadCommentToken() case GraphQLConstants.GroupSeparator: case GraphQLConstants.RecordSeparator: case GraphQLConstants.UnitSeparator: - case GraphQLConstants.Delete: run = false; break; @@ -581,7 +580,6 @@ private void ReadStringValueToken() case GraphQLConstants.GroupSeparator: case GraphQLConstants.RecordSeparator: case GraphQLConstants.UnitSeparator: - case GraphQLConstants.Delete: throw new SyntaxException(this, InvalidCharacterWithinString, code); } } @@ -674,7 +672,6 @@ private void ReadBlockStringToken() case GraphQLConstants.GroupSeparator: case GraphQLConstants.RecordSeparator: case GraphQLConstants.UnitSeparator: - case GraphQLConstants.Delete: throw new SyntaxException( this, string.Format(InvalidCharacterWithinString, code));