diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/JsonVariableCoercion.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/JsonVariableCoercion.cs index e39fd005d6e..819591f6556 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/JsonVariableCoercion.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/JsonVariableCoercion.cs @@ -454,21 +454,37 @@ private readonly bool TryParseEnum( return false; } - var enumValue = element.GetString()!; + var utf8Name = GetRawStringContent(element); - if (!enumType.Values.ContainsName(enumValue)) + if (enumType.Values.ContainsName(utf8Name)) { - value = null; - error = ErrorBuilder.New() - .SetMessage("The value `{0}` is not a valid value for the enum type `{1}`.", enumValue, enumType.Name) - .SetExtension("variable", $"{BuildPath()}") - .Build(); - return false; + value = new EnumValueNode(WriteValue(utf8Name)); + error = null; + return true; } - value = new EnumValueNode(enumValue); - error = null; - return true; + if (utf8Name.IndexOf((byte)'\\') != -1) + { + // the JSON string escapes characters of the name, so we look it up unescaped. + var name = element.GetString()!; + + if (enumType.Values.ContainsName(name)) + { + value = new EnumValueNode(name); + error = null; + return true; + } + } + + value = null; + error = ErrorBuilder.New() + .SetMessage( + "The value `{0}` is not a valid value for the enum type `{1}`.", + element.GetString(), + enumType.Name) + .SetExtension("variable", $"{BuildPath()}") + .Build(); + return false; } private readonly IValueNode ParseLiteral(JsonElement element, int depth) @@ -490,8 +506,7 @@ private readonly IValueNode ParseLiteral(JsonElement element, int depth) return BooleanValueNode.False; case JsonValueKind.String: - var stringValue = element.GetString()!; - return new StringValueNode(null, stringValue, false); + return new StringValueNode(null, WriteStringValue(element), false); case JsonValueKind.Number: var span = JsonMarshal.GetRawUtf8Value(element); @@ -584,6 +599,33 @@ private readonly ReadOnlyMemorySegment WriteValue(ReadOnlySpan value) return _memory.Write(value); } + private readonly ReadOnlyMemorySegment WriteStringValue(JsonElement element) + { + var content = GetRawStringContent(element); + + if (content.IndexOf((byte)'\\') == -1) + { + return WriteValue(content); + } + + var reader = new Utf8JsonReader(JsonMarshal.GetRawUtf8Value(element)); + reader.Read(); + + // the unescaped value is never longer than the escaped value. + _memory ??= new Utf8MemoryBuilder(); + var start = _memory.NextIndex; + var written = reader.CopyString(_memory.GetSpan(content.Length)); + _memory.Advance(written); + return _memory.GetMemorySegment(start, written); + } + + private static ReadOnlySpan GetRawStringContent(JsonElement element) + { + // the raw value of a JSON string includes the surrounding quotes. + var raw = JsonMarshal.GetRawUtf8Value(element); + return raw[1..^1]; + } + private void PushPathSegment(string name) => PushPathSegment(new DeferredPathSegment(name)); diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index df034a64763..94e5e732bfa 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -1792,15 +1792,15 @@ private void WriteValueNode(IValueNode value) break; case StringValueNode sv: - _jsonWriter.WriteStringValue(sv.Value); + _jsonWriter.WriteStringValue(sv.AsSpan()); break; case IntValueNode iv: - WriteRawAscii(iv.Value); + _jsonWriter.WriteRawValue(iv.AsSpan()); break; case FloatValueNode fv: - WriteRawAscii(fv.Value); + _jsonWriter.WriteRawValue(fv.AsSpan()); break; case BooleanValueNode bv: @@ -1808,7 +1808,7 @@ private void WriteValueNode(IValueNode value) break; case EnumValueNode ev: - _jsonWriter.WriteStringValue(ev.Value); + _jsonWriter.WriteStringValue(ev.AsSpan()); break; case ObjectValueNode ov: @@ -1836,13 +1836,6 @@ private void WriteValueNode(IValueNode value) } } - private void WriteRawAscii(string value) - { - Span buffer = stackalloc byte[value.Length]; - Encoding.UTF8.GetBytes(value.AsSpan(), buffer); - _jsonWriter.WriteRawValue(buffer); - } - private void WriteCompositeResultValue(CompositeResultElement value) => value.WriteTo(_jsonWriter); diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreTests.cs b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreTests.cs index 384faa5f60f..ebe54130586 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Results/FetchResultStoreTests.cs @@ -4,6 +4,7 @@ using HotChocolate.Buffers; using HotChocolate.Execution; using HotChocolate.Execution.Errors; +using HotChocolate.Features; using HotChocolate.Fusion.Execution.Clients; using HotChocolate.Fusion.Execution.Nodes; using HotChocolate.Fusion.Language; @@ -2116,6 +2117,70 @@ type Qux implements Foo { } """); + [Fact] + public void CreateVariableValueSets_Should_WriteForwardedVariablesAsUtf8_When_ValuesAreEscapedOrNonAscii() + { + // arrange + var schema = ComposeSchema( + """ + type Query { + field(text: String, plain: String, color: Color, plainColor: Color, count: Int, ratio: Float): String + } + + enum Color { + RED + GREEN + } + """); + var variableDefinitions = new List + { + VariableDefinition("text", "String"), + VariableDefinition("plain", "String"), + VariableDefinition("color", "Color"), + VariableDefinition("plainColor", "Color"), + VariableDefinition("count", "Int"), + VariableDefinition("ratio", "Float") + }; + using var variables = JsonDocument.Parse( + """ + { + "text": "café \"quoted\" \\ \n 😀 über", + "plain": "plain", + "color": "GREEN", + "plainColor": "RED", + "count": 42, + "ratio": 1.5e3 + } + """); + Assert.True( + VariableCoercionHelper.TryCoerceVariableValues( + new FeatureProvider(), + schema, + variableDefinitions, + variables.RootElement, + ignoreAdditionalInputFields: false, + out var coercedVariableValues, + out var error), + error?.Message); + using var store = new FetchResultStore(); + + // act + var result = store.CreateVariableValueSets( + CompactPath.Root, + variableDefinitions + .Select(t => Field(t.Variable.Name.Value, coercedVariableValues[t.Variable.Name.Value].Value)) + .ToArray()); + + // assert + var json = Encoding.UTF8.GetString(result.Values.AsSequence()); + json.MatchInlineSnapshot( + """ + {"text":"caf\u00E9 \u0022quoted\u0022 \\ \n \uD83D\uDE00 \u00FCber","plain":"plain","color":"GREEN","plainColor":"RED","count":42,"ratio":1.5e3} + """); + using var forwarded = JsonDocument.Parse(json); + Assert.True(JsonElement.DeepEquals(variables.RootElement, forwarded.RootElement)); + } + private static VariableValues CreateVariableValues( FetchResultStore store, CompactPath path, @@ -2130,6 +2195,14 @@ private static VariableValues WithAdditionalPaths( AdditionalPaths = new CompactPathSegment(additionalPaths, 0, additionalPaths.Length) }; + private static VariableDefinitionNode VariableDefinition(string name, string typeName) + => new(null, new VariableNode(name), description: null, new NamedTypeNode(typeName), null, []); + + private sealed class FeatureProvider : IFeatureProvider + { + public IFeatureCollection Features { get; } = new FeatureCollection(); + } + private static ObjectFieldNode Field(string name, IValueNode value) => new(name, value); diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/VariableCoercionHelperTests.cs b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/VariableCoercionHelperTests.cs index fcecb6a1da6..36cdfb878f2 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/VariableCoercionHelperTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/VariableCoercionHelperTests.cs @@ -679,6 +679,91 @@ public void TryCoerceVariableValues_Should_StillRequireOneOfFieldToBeSet_When_Ad """); } + [Fact] + public void TryCoerceVariableValues_Should_UnescapeString_When_StringContainsEscapeSequences() + { + // arrange + var schema = ComposeSchema( + """ + type Query { + field(a: String, b: String): String + } + """); + var variableDefinitions = new List + { + CreateVariableDefinition("escaped", new NamedTypeNode("String")), + CreateVariableDefinition("plain", new NamedTypeNode("String")) + }; + using var variableValues = JsonDocument.Parse( + """ + { + "escaped": "caf\u00e9\n\ud83d\ude00 \\ \"end\"", + "plain": "plain" + } + """); + + // act + var success = VariableCoercionHelper.TryCoerceVariableValues( + new MockFeatureProvider(), + schema, + variableDefinitions, + variableValues.RootElement, + ignoreAdditionalInputFields: false, + out var coercedVariableValues, + out var error); + + // assert + Assert.True(success, error?.Message); + Assert.Equal( + "caf\u00e9\n\ud83d\ude00 \\ \"end\"", + Assert.IsType(coercedVariableValues!["escaped"].Value).Value); + Assert.Equal("plain", Assert.IsType(coercedVariableValues["plain"].Value).Value); + } + + [Fact] + public void TryCoerceVariableValues_Should_CoerceEnum_When_NameIsPlainOrEscaped() + { + // arrange + var schema = ComposeSchema( + """ + type Query { + field(a: Color, b: Color): String + } + + enum Color { + RED + GREEN + } + """); + var variableDefinitions = new List + { + CreateVariableDefinition("plain", new NamedTypeNode("Color")), + CreateVariableDefinition("escaped", new NamedTypeNode("Color")) + }; + using var variableValues = JsonDocument.Parse( + """ + { + "plain": "GREEN", + "escaped": "GR\u0045EN" + } + """); + + // act + var success = VariableCoercionHelper.TryCoerceVariableValues( + new MockFeatureProvider(), + schema, + variableDefinitions, + variableValues.RootElement, + ignoreAdditionalInputFields: false, + out var coercedVariableValues, + out var error); + + // assert + Assert.True(success, error?.Message); + Assert.Equal("GREEN", Assert.IsType(coercedVariableValues!["plain"].Value).Value); + Assert.Equal("GREEN", Assert.IsType(coercedVariableValues["escaped"].Value).Value); + } + private static IError CoerceError( FusionSchemaDefinition schema, VariableDefinitionNode variableDefinition,