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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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);
Expand Down Expand Up @@ -584,6 +599,33 @@ private readonly ReadOnlyMemorySegment WriteValue(ReadOnlySpan<byte> 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<byte> 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));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1792,23 +1792,23 @@ private void WriteValueNode(IValueNode value)
break;

case StringValueNode sv:
_jsonWriter.WriteStringValue(sv.Value);
_jsonWriter.WriteStringValue(sv.AsSpan());
Comment thread
tobias-tengler marked this conversation as resolved.
break;

case IntValueNode iv:
WriteRawAscii(iv.Value);
_jsonWriter.WriteRawValue(iv.AsSpan());
Comment thread
tobias-tengler marked this conversation as resolved.
break;

case FloatValueNode fv:
WriteRawAscii(fv.Value);
_jsonWriter.WriteRawValue(fv.AsSpan());
break;

case BooleanValueNode bv:
_jsonWriter.WriteBooleanValue(bv.Value);
break;

case EnumValueNode ev:
_jsonWriter.WriteStringValue(ev.Value);
_jsonWriter.WriteStringValue(ev.AsSpan());
break;

case ObjectValueNode ov:
Expand Down Expand Up @@ -1836,13 +1836,6 @@ private void WriteValueNode(IValueNode value)
}
}

private void WriteRawAscii(string value)
{
Span<byte> buffer = stackalloc byte[value.Length];
Encoding.UTF8.GetBytes(value.AsSpan(), buffer);
_jsonWriter.WriteRawValue(buffer);
}

private void WriteCompositeResultValue(CompositeResultElement value)
=> value.WriteTo(_jsonWriter);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<VariableDefinitionNode>
{
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,
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<VariableDefinitionNode>
{
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<StringValueNode>(coercedVariableValues!["escaped"].Value).Value);
Assert.Equal("plain", Assert.IsType<StringValueNode>(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<VariableDefinitionNode>
{
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<EnumValueNode>(coercedVariableValues!["plain"].Value).Value);
Assert.Equal("GREEN", Assert.IsType<EnumValueNode>(coercedVariableValues["escaped"].Value).Value);
}

private static IError CoerceError(
FusionSchemaDefinition schema,
VariableDefinitionNode variableDefinition,
Expand Down
Loading