Skip to content
Open
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
@@ -1,7 +1,6 @@
using System.Collections;
using System.Collections.Frozen;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using HotChocolate.Types;

namespace HotChocolate.Fusion.Types.Collections;
Expand Down Expand Up @@ -31,7 +30,7 @@ public FusionEnumValueCollection(FusionEnumValue[] values)
_map = values.ToFrozenDictionary(t => t.Name);
_values = values;
_values.PartitionByAccessibility(out _length);
_utf8AccessibleNames = BuildUtf8AccessibleNames(_values, _length);
_utf8AccessibleNames = Utf8NameIndex.Create(_values, _length, static v => v.Name);
}

/// <summary>
Expand Down Expand Up @@ -276,76 +275,7 @@ public bool ContainsName(string name, bool allowInaccessibleFields)
/// specified name; otherwise, <c>false</c>.
/// </returns>
public bool ContainsName(ReadOnlySpan<byte> utf8Name)
{
var utf8Names = _utf8AccessibleNames;
var count = utf8Names.Length;

if (count <= 8)
{
for (var i = 0; i < count; i++)
{
if (utf8Name.SequenceEqual(utf8Names[i]))
{
return true;
}
}

return false;
}

var low = 0;
var high = count - 1;

while (low <= high)
{
var mid = low + ((high - low) >> 1);
var comparison = CompareLengthThenBytes(utf8Names[mid], utf8Name);

if (comparison == 0)
{
return true;
}

if (comparison < 0)
{
low = mid + 1;
}
else
{
high = mid - 1;
}
}

return false;
}

private static byte[][] BuildUtf8AccessibleNames(FusionEnumValue[] values, int length)
{
var utf8Names = new byte[length][];

for (var i = 0; i < length; i++)
{
utf8Names[i] = Encoding.UTF8.GetBytes(values[i].Name);
}

// Sorted by length ascending then lexicographic byte order so the span
// lookup can binary search with a length-first comparison.
Array.Sort(utf8Names, static (a, b) => CompareLengthThenBytes(a, b));

return utf8Names;
}

private static int CompareLengthThenBytes(ReadOnlySpan<byte> x, ReadOnlySpan<byte> y)
{
var lengthComparison = x.Length - y.Length;

if (lengthComparison != 0)
{
return lengthComparison;
}

return x.SequenceCompareTo(y);
}
=> Utf8NameIndex.IndexOf(_utf8AccessibleNames, utf8Name) != -1;

/// <summary>
/// Returns an enumerator for the enum values in the collection.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,42 @@ public sealed class FusionInputFieldDefinitionCollection
: FusionFieldDefinitionCollection<FusionInputFieldDefinition>
, IReadOnlyFieldDefinitionCollection<IInputValueDefinition>
{
private readonly byte[][] _utf8AccessibleNames;
private readonly FusionInputFieldDefinition[] _utf8AccessibleFields;

public FusionInputFieldDefinitionCollection(FusionInputFieldDefinition[] fields)
: base(fields)
{
_utf8AccessibleNames = Utf8NameIndex.Create(fields, Count, static f => f.Name, out _utf8AccessibleFields);
}

/// <summary>
/// Gets the accessible field whose name matches the specified UTF-8 encoded <paramref name="utf8Name"/>.
/// </summary>
/// <param name="utf8Name">
/// The GraphQL field name as UTF-8 bytes.
/// </param>
/// <param name="field">
/// When this method returns, contains the field with the specified name if found;
/// otherwise, <c>null</c>.
/// </param>
/// <returns>
/// <c>true</c> if the field was found; otherwise, <c>false</c>.
/// </returns>
public bool TryGetField(
ReadOnlySpan<byte> utf8Name,
[NotNullWhen(true)] out FusionInputFieldDefinition? field)
{
var index = Utf8NameIndex.IndexOf(_utf8AccessibleNames, utf8Name);

if (index == -1)
{
field = null;
return false;
}

field = _utf8AccessibleFields[index];
return true;
}

IInputValueDefinition IReadOnlyFieldDefinitionCollection<IInputValueDefinition>.this[string name]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System.Text;

namespace HotChocolate.Fusion.Types.Collections;

/// <summary>
/// Looks up items by their UTF-8 encoded GraphQL name.
/// </summary>
internal static class Utf8NameIndex
{
// Sorted by length ascending then lexicographic byte order so the span
// lookup can binary search with a length-first comparison.
private static readonly Comparer<byte[]> s_comparer =
Comparer<byte[]>.Create(static (a, b) => CompareLengthThenBytes(a, b));

/// <summary>
/// Creates the UTF-8 names of the first <paramref name="length"/> items, sorted for <see cref="IndexOf"/>.
/// </summary>
public static byte[][] Create<T>(T[] items, int length, Func<T, string> getName)
{
var utf8Names = Encode(items, length, getName);
Array.Sort(utf8Names, s_comparer);
return utf8Names;
}

/// <summary>
/// Creates the UTF-8 names of the first <paramref name="length"/> items, sorted for <see cref="IndexOf"/>,
/// and returns the items in the same order through <paramref name="sortedItems"/>.
/// </summary>
public static byte[][] Create<T>(T[] items, int length, Func<T, string> getName, out T[] sortedItems)
{
var utf8Names = Encode(items, length, getName);
sortedItems = items[..length];
Array.Sort(utf8Names, sortedItems, s_comparer);
return utf8Names;
}

/// <summary>
/// Gets the index of <paramref name="utf8Name"/> in names created by this index,
/// or -1 if it is not contained.
/// </summary>
public static int IndexOf(byte[][] utf8Names, ReadOnlySpan<byte> utf8Name)
{
var count = utf8Names.Length;

if (count <= 8)
{
for (var i = 0; i < count; i++)
{
if (utf8Name.SequenceEqual(utf8Names[i]))
{
return i;
}
}

return -1;
}

var low = 0;
var high = count - 1;

while (low <= high)
{
var mid = low + ((high - low) >> 1);
var comparison = CompareLengthThenBytes(utf8Names[mid], utf8Name);

if (comparison == 0)
{
return mid;
}

if (comparison < 0)
{
low = mid + 1;
}
else
{
high = mid - 1;
}
}

return -1;
}

private static byte[][] Encode<T>(T[] items, int length, Func<T, string> getName)
{
var utf8Names = new byte[length][];

for (var i = 0; i < length; i++)
{
utf8Names[i] = Encoding.UTF8.GetBytes(getName(items[i]));
}

return utf8Names;
}

private static int CompareLengthThenBytes(ReadOnlySpan<byte> x, ReadOnlySpan<byte> y)
{
var lengthComparison = x.Length - y.Length;

if (lengthComparison != 0)
{
return lengthComparison;
}

return x.SequenceCompareTo(y);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public FusionInputFieldDefinition(

Index = index;
Name = name;
NameNode = new NameNode(name);
Description = description;
DefaultValue = defaultValue;
DeprecationReason = string.IsNullOrWhiteSpace(deprecationReason) ? null : deprecationReason;
Expand All @@ -43,6 +44,8 @@ public FusionInputFieldDefinition(

public string Name { get; }

internal NameNode NameNode { get; }

public string? Description { get; }

public ITypeSystemMember DeclaringMember
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ private bool TryParseInputObject(
foreach (var property in element.EnumerateObject())
{
if (_ignoreAdditionalInputFields
&& !inputObjectType.Fields.ContainsName(property.Name))
&& !TryGetInputField(inputObjectType, property, out _))
{
continue;
}
Expand Down Expand Up @@ -286,7 +286,7 @@ private bool TryParseInputObject(
{
foreach (var property in element.EnumerateObject())
{
if (!inputObjectType.Fields.TryGetField(property.Name, out var fieldDefinition))
if (!TryGetInputField(inputObjectType, property, out var fieldDefinition))
{
if (_ignoreAdditionalInputFields)
{
Expand Down Expand Up @@ -326,7 +326,7 @@ private bool TryParseInputObject(
ArrayPool<ObjectFieldNode>.Shared.Return(temp);
}

PushPathSegment(property.Name);
PushPathSegment(fieldDefinition.Name);

try
{
Expand All @@ -341,7 +341,7 @@ private bool TryParseInputObject(
return false;
}

buffer[count++] = new ObjectFieldNode(property.Name, fieldValue);
buffer[count++] = new ObjectFieldNode(null, fieldDefinition.NameNode, fieldValue);
processed[fieldDefinition.Index] = true;
processedCount++;
}
Expand Down Expand Up @@ -390,6 +390,28 @@ private bool TryParseInputObject(
}
}

private static bool TryGetInputField(
FusionInputObjectTypeDefinition inputObjectType,
JsonProperty property,
[NotNullWhen(true)] out FusionInputFieldDefinition? field)
{
#if NET9_0_OR_GREATER
var utf8Name = JsonMarshal.GetRawUtf8PropertyName(property);

if (inputObjectType.Fields.TryGetField(utf8Name, out field))
{
return true;
}

if (utf8Name.IndexOf((byte)'\\') == -1)
{
return false;
}
#endif

return inputObjectType.Fields.TryGetField(property.Name, out field);
}

private readonly bool TryParseScalar(
FusionScalarTypeDefinition scalarType,
JsonElement element,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using HotChocolate.Fusion.Types;
using HotChocolate.Fusion.Types.Collections;

namespace HotChocolate.Fusion.Execution.Types;

public class FusionInputFieldDefinitionCollectionTests
{
[Fact]
public void TryGetField_Should_ReturnMatchingField_When_SmallCollectionContainsName()
{
// arrange
var collection = new FusionInputFieldDefinitionCollection(
[Field(0, "id"), Field(1, "name"), Field(2, "price")]);

// act
var found = collection.TryGetField("name"u8, out var field);

// assert
Assert.True(found);
Assert.Equal("name", field!.Name);
}

[Fact]
public void TryGetField_Should_ReturnMatchingField_When_LargeCollectionContainsName()
{
// arrange
var collection = new FusionInputFieldDefinitionCollection(
[.. Enumerable.Range(0, 20).Select(i => Field(i, "f" + new string('x', 19 - i)))]);

// act
var found = collection.TryGetField("fxxxxx"u8, out var field);

// assert
Assert.True(found);
Assert.Equal(14, field!.Index);
}

[Fact]
public void TryGetField_Should_ReturnFalse_When_FieldIsInaccessible()
{
// arrange
var collection = new FusionInputFieldDefinitionCollection(
[Field(0, "visible"), Field(1, "hidden", isInaccessible: true)]);

// act
var found = collection.TryGetField("hidden"u8, out var field);

// assert
Assert.False(found);
Assert.Null(field);
}

[Fact]
public void TryGetField_Should_ReturnFalse_When_NameIsJsonEscaped()
{
// arrange
var collection = new FusionInputFieldDefinitionCollection([Field(0, "id")]);

// act
var found = collection.TryGetField("\\u0069d"u8, out var field);

// assert
Assert.False(found);
Assert.Null(field);
}

private static FusionInputFieldDefinition Field(int index, string name, bool isInaccessible = false)
=> new(index, name, description: null, defaultValue: null, deprecationReason: null, isInaccessible);
}
Loading
Loading