Skip to content

Commit ef19263

Browse files
Re-add body arguments functionality (#862)
1 parent f2c929f commit ef19263

18 files changed

Lines changed: 310 additions & 83 deletions

File tree

API/Protocol/PathSegment.cs

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Buffers;
2+
using System.Text;
23

34
namespace GenHTTP.Api.Protocol;
45

@@ -8,6 +9,9 @@ namespace GenHTTP.Api.Protocol;
89
[MemoryView]
910
public readonly partial struct PathSegment
1011
{
12+
private static readonly Encoding Ascii = Encoding.ASCII;
13+
14+
private static readonly Encoding Utf8 = Encoding.UTF8;
1115

1216
#region Functionality
1317

@@ -18,33 +22,18 @@ public string Decode()
1822

1923
if (span.IndexOf((byte)'%') < 0)
2024
{
21-
return System.Text.Encoding.ASCII.GetString(span);
25+
return Ascii.GetString(span);
2226
}
2327

2428
byte[]? rented = null;
2529

26-
var buffer = span.Length <= 256
27-
? stackalloc byte[span.Length]
28-
: (rented = ArrayPool<byte>.Shared.Rent(span.Length));
30+
var buffer = span.Length <= 256 ? stackalloc byte[span.Length] : (rented = ArrayPool<byte>.Shared.Rent(span.Length));
2931

3032
try
3133
{
32-
int write = 0, i = 0;
34+
var written = PercentEncoding.Decode(span, buffer);
3335

34-
while (i < span.Length)
35-
{
36-
if (span[i] == '%' && i + 2 < span.Length && IsHexDigit(span[i + 1]) && IsHexDigit(span[i + 2]))
37-
{
38-
buffer[write++] = (byte)((HexValue(span[i + 1]) << 4) | HexValue(span[i + 2]));
39-
i += 3;
40-
}
41-
else
42-
{
43-
buffer[write++] = span[i++];
44-
}
45-
}
46-
47-
return System.Text.Encoding.UTF8.GetString(buffer[..write]);
36+
return Utf8.GetString(buffer[..written]);
4837
}
4938
finally
5039
{
@@ -55,10 +44,6 @@ public string Decode()
5544
}
5645
}
5746

58-
private static bool IsHexDigit(byte b) => (uint)(b - '0') <= 9 || (uint)((b | 0x20) - 'a') <= 5;
59-
60-
private static int HexValue(byte b) => b <= '9' ? b - '0' : (b | 0x20) - 'a' + 10;
61-
6247
#endregion
6348

6449
}

API/Protocol/PercentEncoding.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using System.Runtime.CompilerServices;
2+
3+
namespace GenHTTP.Api.Protocol;
4+
5+
/// <summary>
6+
/// Shared low level helpers to decode percent-encoded ("URL encoded") bytes.
7+
/// </summary>
8+
public static class PercentEncoding
9+
{
10+
11+
/// <summary>
12+
/// Determines whether the given byte is a valid hexadecimal digit (0-9, a-f, A-F).
13+
/// </summary>
14+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
15+
public static bool IsHexDigit(byte value) => (uint)(value - '0') <= 9 || (uint)((value | 0x20) - 'a') <= 5;
16+
17+
/// <summary>
18+
/// Converts a hexadecimal digit byte into its numeric value.
19+
/// </summary>
20+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
21+
public static int HexValue(byte value) => value <= '9' ? value - '0' : (value | 0x20) - 'a' + 10;
22+
23+
/// <summary>
24+
/// Attempts to decode the two hex digits following a "%" escape sequence.
25+
/// </summary>
26+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
27+
public static bool TryDecode(byte high, byte low, out byte value)
28+
{
29+
if (!IsHexDigit(high) || !IsHexDigit(low))
30+
{
31+
value = 0;
32+
return false;
33+
}
34+
35+
value = (byte)((HexValue(high) << 4) | HexValue(low));
36+
return true;
37+
}
38+
39+
/// <summary>
40+
/// Decodes a percent-encoded ("URL encoded") sequence of bytes into the given buffer.
41+
/// </summary>
42+
/// <param name="source">The encoded bytes to decode</param>
43+
/// <param name="target">The buffer the decoded bytes are written to (must be at least as large as <paramref name="source"/>)</param>
44+
/// <param name="decodePlus">Whether a "+" character should be decoded into a space (as used by form encoded content)</param>
45+
/// <returns>The number of bytes written to <paramref name="target"/></returns>
46+
public static int Decode(ReadOnlySpan<byte> source, Span<byte> target, bool decodePlus = false)
47+
{
48+
var write = 0;
49+
50+
for (var read = 0; read < source.Length; read++)
51+
{
52+
var current = source[read];
53+
54+
if (decodePlus && current == (byte)'+')
55+
{
56+
target[write++] = (byte)' ';
57+
}
58+
else if (current == (byte)'%' && read + 2 < source.Length && TryDecode(source[read + 1], source[read + 2], out var decoded))
59+
{
60+
target[write++] = decoded;
61+
read += 2;
62+
}
63+
else
64+
{
65+
target[write++] = current;
66+
}
67+
}
68+
69+
return write;
70+
}
71+
72+
}

Modules/Conversion/Serializers/Forms/FormFormat.cs

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Reflection;
2-
using System.Text;
32
using System.Web;
43

54
using GenHTTP.Api.Content;
@@ -100,51 +99,6 @@ public ValueTask<ReadOnlyMemory<byte>> SerializeAsync(object data)
10099
});
101100
}
102101

103-
public static async ValueTask<Dictionary<string, string>?> GetContentAsync(IRequest request) // todo: refactor into an own type
104-
{
105-
var contentType = request.Header.Headers.GetEntry(KnownHeaders.ContentType);
106-
107-
if (contentType is not null)
108-
{
109-
if (new ContentType(contentType.Value.Bytes) == ContentType.ApplicationWwwFormUrlEncoded) // todo: ugly API
110-
{
111-
var content = await GetRequestContentAsync(request); // todo: make this memory based?
112-
113-
var query = HttpUtility.ParseQueryString(content);
114-
115-
var result = new Dictionary<string, string>(query.Count);
116-
117-
foreach (var key in query.AllKeys)
118-
{
119-
var value = query[key];
120-
121-
if (key is not null && value is not null)
122-
{
123-
result.Add(key, value);
124-
}
125-
}
126-
127-
return result;
128-
}
129-
}
130-
131-
return null;
132-
}
133-
134-
private static async ValueTask<string> GetRequestContentAsync(IRequest request)
135-
{
136-
var requestContent = request.GetBody();
137-
138-
if (requestContent is null)
139-
{
140-
throw new InvalidOperationException("Request content has to be set");
141-
}
142-
143-
var buffer = await requestContent.AsMemoryAsync();
144-
145-
return Encoding.UTF8.GetString(buffer.Span);
146-
}
147-
148102
#endregion
149103

150104
}

Modules/DependencyInjection/Infrastructure/DependencyInjector.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ public bool Supports(IServer server, Type type)
2525
return (provider.GetService(type) != null);
2626
}
2727

28-
public object? GetValue(IHandler handler, IRequest request, Type targetType)
28+
public ValueTask<object?> GetValueAsync(IHandler handler, IRequest request, Type targetType)
2929
{
3030
var scope = request.GetServiceScope();
3131

32-
return scope.ServiceProvider.GetService(targetType);
32+
return new ValueTask<object?>(scope.ServiceProvider.GetService(targetType));
3333
}
3434

3535
}

Modules/IO/BodyArguments.cs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using GenHTTP.Api.Protocol;
2+
3+
namespace GenHTTP.Modules.IO;
4+
5+
/// <summary>
6+
/// Provides access to the key/value pairs of a request body that
7+
/// has been encoded as "application/x-www-form-urlencoded".
8+
/// </summary>
9+
public sealed class BodyArguments : IKeyValueList
10+
{
11+
private readonly KeyValuePair<ByteString, ByteString>[] _entries;
12+
13+
#region Initialization
14+
15+
/// <summary>
16+
/// A body that does not contain any arguments.
17+
/// </summary>
18+
public static readonly BodyArguments Empty = new([]);
19+
20+
private BodyArguments(KeyValuePair<ByteString, ByteString>[] entries)
21+
{
22+
_entries = entries;
23+
}
24+
25+
/// <summary>
26+
/// Reads and parses the given body as form encoded content.
27+
/// </summary>
28+
/// <param name="body">The body to be parsed</param>
29+
/// <returns>The arguments found within the body</returns>
30+
public static async ValueTask<BodyArguments> CreateAsync(IRequestBody body)
31+
{
32+
var memory = await body.AsMemoryAsync();
33+
34+
return Parse(memory);
35+
}
36+
37+
#endregion
38+
39+
#region Get-/Setters
40+
41+
public int Count => _entries.Length;
42+
43+
public KeyValuePair<ByteString, ByteString> this[int index] => _entries[index];
44+
45+
#endregion
46+
47+
#region Functionality
48+
49+
private static BodyArguments Parse(ReadOnlyMemory<byte> memory)
50+
{
51+
if (memory.IsEmpty)
52+
{
53+
return Empty;
54+
}
55+
56+
var entries = new List<KeyValuePair<ByteString, ByteString>>();
57+
58+
var remaining = memory;
59+
60+
while (!remaining.IsEmpty)
61+
{
62+
var separator = remaining.Span.IndexOf((byte)'&');
63+
64+
var pair = (separator < 0) ? remaining : remaining[..separator];
65+
66+
if (!pair.IsEmpty)
67+
{
68+
entries.Add(ParsePair(pair));
69+
}
70+
71+
remaining = (separator < 0) ? default : remaining[(separator + 1)..];
72+
}
73+
74+
return new BodyArguments(entries.ToArray());
75+
}
76+
77+
private static KeyValuePair<ByteString, ByteString> ParsePair(ReadOnlyMemory<byte> pair)
78+
{
79+
var separator = pair.Span.IndexOf((byte)'=');
80+
81+
return (separator < 0) ? new(Decode(pair), default)
82+
: new(Decode(pair[..separator]), Decode(pair[(separator + 1)..]));
83+
}
84+
85+
/// <summary>
86+
/// Decodes a percent- and "+"-encoded token, only allocating a new
87+
/// buffer if the token actually requires decoding.
88+
/// </summary>
89+
private static ByteString Decode(ReadOnlyMemory<byte> raw)
90+
{
91+
var source = raw.Span;
92+
93+
if (source.IndexOfAny((byte)'%', (byte)'+') < 0)
94+
{
95+
return new ByteString(raw);
96+
}
97+
98+
var target = new byte[source.Length];
99+
100+
var written = PercentEncoding.Decode(source, target, decodePlus: true);
101+
102+
return new ByteString(target.AsMemory(0, written));
103+
}
104+
105+
#endregion
106+
107+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using GenHTTP.Api.Protocol;
2+
3+
namespace GenHTTP.Modules.IO;
4+
5+
public static class RequestBodyExtensions
6+
{
7+
8+
/// <summary>
9+
/// Reads and parses the body as form encoded ("application/x-www-form-urlencoded") content.
10+
/// </summary>
11+
/// <param name="body">The body to be parsed</param>
12+
/// <returns>The arguments found within the body</returns>
13+
public static ValueTask<BodyArguments> AsBodyArgumentsAsync(this IRequestBody body) => BodyArguments.CreateAsync(body);
14+
15+
}

Modules/Reflection/Generation/CodeProvider.Arguments.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ private static void AppendInjectedArgument(this StringBuilder sb, OperationArgum
126126
sb.AppendLine(" {");
127127
sb.AppendLine($" if (injector.Supports(request.Server, typeof({safeType})))");
128128
sb.AppendLine(" {");
129-
sb.AppendLine($" arg{index} = ({safeType})injector.GetValue(handler, request, typeof({safeType}));");
129+
sb.AppendLine($" arg{index} = ({safeType})await injector.GetValueAsync(handler, request, typeof({safeType}));");
130130
sb.AppendLine(" }");
131131
sb.AppendLine(" }");
132132
}

Modules/Reflection/Generation/CodeProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ private static bool CheckAsync(Operation operation)
9191
if (operation.Method.ReturnType.IsAsync())
9292
return true;
9393

94-
if (operation.Arguments.Any(a => a.Value.Source == OperationArgumentSource.Body || a.Value.Source == OperationArgumentSource.Content))
94+
if (operation.Arguments.Any(a => a.Value.Source == OperationArgumentSource.Body || a.Value.Source == OperationArgumentSource.Content || a.Value.Source == OperationArgumentSource.Injected))
9595
return true;
9696

9797
if (operation.Result.Sink == OperationResultSink.Dynamic)

Modules/Reflection/Injection.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ public static class Injection
88
public static InjectionRegistryBuilder Empty() => new();
99

1010
public static InjectionRegistryBuilder Default() => new InjectionRegistryBuilder().Add(new RequestInjector())
11-
.Add(new HandlerInjector());
11+
.Add(new HandlerInjector())
12+
.Add(new BodyArgumentsInjector());
1213
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using GenHTTP.Api.Content;
2+
using GenHTTP.Api.Infrastructure;
3+
using GenHTTP.Api.Protocol;
4+
5+
using GenHTTP.Modules.IO;
6+
7+
namespace GenHTTP.Modules.Reflection.Injectors;
8+
9+
public class BodyArgumentsInjector : IParameterInjector
10+
{
11+
12+
public bool Supports(IServer server, Type type) => type == typeof(BodyArguments);
13+
14+
public async ValueTask<object?> GetValueAsync(IHandler handler, IRequest request, Type targetType)
15+
{
16+
var body = request.GetBody();
17+
18+
return (body is null) ? BodyArguments.Empty : await body.AsBodyArgumentsAsync();
19+
}
20+
21+
}

0 commit comments

Comments
 (0)