-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Update shared HPackEncoder #33170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update shared HPackEncoder #33170
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,172 +3,11 @@ | |
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using System.Diagnostics; | ||
| #if KESTREL | ||
| using HeadersEnumerator = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2HeadersEnumerator; | ||
| #else | ||
| using HeadersEnumerator = System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<string, string>>; | ||
| #endif | ||
|
|
||
| namespace System.Net.Http.HPack | ||
| { | ||
| internal class HPackEncoder | ||
| internal static class HPackEncoder | ||
| { | ||
| private HeadersEnumerator _enumerator; | ||
|
|
||
| public bool BeginEncode(HeadersEnumerator enumerator, Span<byte> buffer, out int length) | ||
| { | ||
| _enumerator = enumerator; | ||
| _enumerator.MoveNext(); | ||
|
|
||
| return Encode(buffer, out length); | ||
| } | ||
|
|
||
| public bool BeginEncode(int statusCode, HeadersEnumerator enumerator, Span<byte> buffer, out int length) | ||
| { | ||
| _enumerator = enumerator; | ||
| _enumerator.MoveNext(); | ||
|
|
||
| int statusCodeLength = EncodeStatusCode(statusCode, buffer); | ||
| bool done = Encode(buffer.Slice(statusCodeLength), throwIfNoneEncoded: false, out int headersLength); | ||
| length = statusCodeLength + headersLength; | ||
|
|
||
| return done; | ||
| } | ||
|
|
||
| public bool Encode(Span<byte> buffer, out int length) | ||
| { | ||
| return Encode(buffer, throwIfNoneEncoded: true, out length); | ||
| } | ||
|
|
||
| private bool Encode(Span<byte> buffer, bool throwIfNoneEncoded, out int length) | ||
| { | ||
| int currentLength = 0; | ||
| do | ||
| { | ||
| if (!EncodeHeader(_enumerator.Current.Key, _enumerator.Current.Value, buffer.Slice(currentLength), out int headerLength)) | ||
| { | ||
| if (currentLength == 0 && throwIfNoneEncoded) | ||
| { | ||
| throw new HPackEncodingException(SR.net_http_hpack_encode_failure); | ||
| } | ||
|
|
||
| length = currentLength; | ||
| return false; | ||
| } | ||
|
|
||
| currentLength += headerLength; | ||
| } | ||
| while (_enumerator.MoveNext()); | ||
|
|
||
| length = currentLength; | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| private int EncodeStatusCode(int statusCode, Span<byte> buffer) | ||
| { | ||
| switch (statusCode) | ||
| { | ||
| // Status codes which exist in the HTTP/2 StaticTable. | ||
| case 200: | ||
| case 204: | ||
| case 206: | ||
| case 304: | ||
| case 400: | ||
| case 404: | ||
| case 500: | ||
| buffer[0] = (byte)(0x80 | H2StaticTable.StatusIndex[statusCode]); | ||
| return 1; | ||
| default: | ||
| // Send as Literal Header Field Without Indexing - Indexed Name | ||
| buffer[0] = 0x08; | ||
|
|
||
| ReadOnlySpan<byte> statusBytes = StatusCodes.ToStatusBytes(statusCode); | ||
| buffer[1] = (byte)statusBytes.Length; | ||
| statusBytes.CopyTo(buffer.Slice(2)); | ||
|
|
||
| return 2 + statusBytes.Length; | ||
| } | ||
| } | ||
|
|
||
| private bool EncodeHeader(string name, string value, Span<byte> buffer, out int length) | ||
| { | ||
| int i = 0; | ||
| length = 0; | ||
|
|
||
| if (buffer.Length == 0) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| buffer[i++] = 0; | ||
|
|
||
| if (i == buffer.Length) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| if (!EncodeString(name, buffer.Slice(i), out int nameLength, lowercase: true)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| i += nameLength; | ||
|
|
||
| if (i >= buffer.Length) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| if (!EncodeString(value, buffer.Slice(i), out int valueLength, lowercase: false)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| i += valueLength; | ||
|
|
||
| length = i; | ||
| return true; | ||
| } | ||
|
|
||
| private bool EncodeString(string value, Span<byte> destination, out int bytesWritten, bool lowercase) | ||
| { | ||
| // From https://tools.ietf.org/html/rfc7541#section-5.2 | ||
| // ------------------------------------------------------ | ||
| // 0 1 2 3 4 5 6 7 | ||
| // +---+---+---+---+---+---+---+---+ | ||
| // | H | String Length (7+) | | ||
| // +---+---------------------------+ | ||
| // | String Data (Length octets) | | ||
| // +-------------------------------+ | ||
| const int toLowerMask = 0x20; | ||
|
|
||
| if (destination.Length != 0) | ||
| { | ||
| destination[0] = 0; // TODO: Use Huffman encoding | ||
| if (IntegerEncoder.Encode(value.Length, 7, destination, out int integerLength)) | ||
| { | ||
| Debug.Assert(integerLength >= 1); | ||
|
|
||
| destination = destination.Slice(integerLength); | ||
| if (value.Length <= destination.Length) | ||
| { | ||
| for (int i = 0; i < value.Length; i++) | ||
| { | ||
| char c = value[i]; | ||
| destination[i] = (byte)(lowercase && (uint)(c - 'A') <= ('Z' - 'A') ? c | toLowerMask : c); | ||
| } | ||
|
|
||
| bytesWritten = integerLength + value.Length; | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| bytesWritten = 0; | ||
| return false; | ||
| } | ||
|
|
||
| // Things we should add: | ||
| // * Huffman encoding | ||
| // | ||
|
|
@@ -199,6 +38,43 @@ public static bool EncodeIndexedHeaderField(int index, Span<byte> destination, o | |
| return false; | ||
| } | ||
|
|
||
| /// <summary>Encodes the status code of a response to the :status field.</summary> | ||
| public static bool EncodeStatusHeader(int statusCode, Span<byte> destination, out int bytesWritten) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would merge this idea into
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea but I'll leave that for now. It can be a future optimization. |
||
| { | ||
| // Bytes written depend on whether the status code value maps directly to an index | ||
| switch (statusCode) | ||
| { | ||
| case 200: | ||
| case 204: | ||
| case 206: | ||
| case 304: | ||
| case 400: | ||
| case 404: | ||
| case 500: | ||
| // Status codes which exist in the HTTP/2 StaticTable. | ||
| return EncodeIndexedHeaderField(H2StaticTable.StatusIndex[statusCode], destination, out bytesWritten); | ||
| default: | ||
| // If the status code doesn't have a static index then we need to include the full value. | ||
| // Write a status index and then the number bytes as a string literal. | ||
| if (!EncodeLiteralHeaderFieldWithoutIndexing(H2StaticTable.Status200, destination, out var nameLength)) | ||
| { | ||
| bytesWritten = 0; | ||
| return false; | ||
| } | ||
|
|
||
| var statusBytes = StatusCodes.ToStatusBytes(statusCode); | ||
|
|
||
| if (!EncodeStringLiteral(statusBytes, destination.Slice(nameLength), out var valueLength)) | ||
| { | ||
| bytesWritten = 0; | ||
| return false; | ||
| } | ||
|
|
||
| bytesWritten = nameLength + valueLength; | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| /// <summary>Encodes a "Literal Header Field without Indexing".</summary> | ||
| public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, string value, Span<byte> destination, out int bytesWritten) | ||
| { | ||
|
|
@@ -233,7 +109,7 @@ public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, string val | |
|
|
||
| /// <summary> | ||
| /// Encodes a "Literal Header Field without Indexing", but only the index portion; | ||
| /// a subsequent call to <see cref="EncodeStringLiteral"/> must be used to encode the associated value. | ||
| /// a subsequent call to <c>EncodeStringLiteral</c> must be used to encode the associated value. | ||
| /// </summary> | ||
| public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, Span<byte> destination, out int bytesWritten) | ||
| { | ||
|
|
@@ -266,6 +142,39 @@ public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, Span<byte> | |
| return false; | ||
| } | ||
|
|
||
| /// <summary>Encodes a "Literal Header Field without Indexing - New Name".</summary> | ||
| public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, string value, Span<byte> destination, out int bytesWritten) | ||
| { | ||
| // From https://tools.ietf.org/html/rfc7541#section-6.2.2 | ||
| // ------------------------------------------------------ | ||
| // 0 1 2 3 4 5 6 7 | ||
| // +---+---+---+---+---+---+---+---+ | ||
| // | 0 | 0 | 0 | 0 | 0 | | ||
| // +---+---+-----------------------+ | ||
| // | H | Name Length (7+) | | ||
| // +---+---------------------------+ | ||
| // | Name String (Length octets) | | ||
| // +---+---------------------------+ | ||
| // | H | Value Length (7+) | | ||
| // +---+---------------------------+ | ||
| // | Value String (Length octets) | | ||
| // +-------------------------------+ | ||
|
|
||
| if ((uint)destination.Length >= 3) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the purpose of the cast here?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume it is to eliminate bounds checks. I just copied and pasted this from the method overload that takes a span of alreadys instead of one. |
||
| { | ||
| destination[0] = 0; | ||
| if (EncodeLiteralHeaderName(name, destination.Slice(1), out int nameLength) && | ||
| EncodeStringLiteral(value, destination.Slice(1 + nameLength), out int valueLength)) | ||
| { | ||
| bytesWritten = 1 + nameLength + valueLength; | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| bytesWritten = 0; | ||
| return false; | ||
| } | ||
|
|
||
| /// <summary>Encodes a "Literal Header Field without Indexing - New Name".</summary> | ||
| public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, ReadOnlySpan<string> values, string separator, Span<byte> destination, out int bytesWritten) | ||
| { | ||
|
|
@@ -301,7 +210,7 @@ public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, R | |
|
|
||
| /// <summary> | ||
| /// Encodes a "Literal Header Field without Indexing - New Name", but only the name portion; | ||
| /// a subsequent call to <see cref="EncodeStringLiteral"/> must be used to encode the associated value. | ||
| /// a subsequent call to <c>EncodeStringLiteral</c> must be used to encode the associated value. | ||
| /// </summary> | ||
| public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, Span<byte> destination, out int bytesWritten) | ||
| { | ||
|
|
@@ -397,6 +306,40 @@ private static bool EncodeStringLiteralValue(string value, Span<byte> destinatio | |
| return false; | ||
| } | ||
|
|
||
| public static bool EncodeStringLiteral(ReadOnlySpan<byte> value, Span<byte> destination, out int bytesWritten) | ||
| { | ||
| // From https://tools.ietf.org/html/rfc7541#section-5.2 | ||
| // ------------------------------------------------------ | ||
| // 0 1 2 3 4 5 6 7 | ||
| // +---+---+---+---+---+---+---+---+ | ||
| // | H | String Length (7+) | | ||
| // +---+---------------------------+ | ||
| // | String Data (Length octets) | | ||
| // +-------------------------------+ | ||
|
|
||
| if (destination.Length != 0) | ||
| { | ||
| destination[0] = 0; // TODO: Use Huffman encoding | ||
| if (IntegerEncoder.Encode(value.Length, 7, destination, out int integerLength)) | ||
| { | ||
| Debug.Assert(integerLength >= 1); | ||
|
|
||
| destination = destination.Slice(integerLength); | ||
| if (value.Length <= destination.Length) | ||
| { | ||
| // Note: No validation. Bytes should have already been validated. | ||
| value.CopyTo(destination); | ||
|
|
||
| bytesWritten = integerLength + value.Length; | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| bytesWritten = 0; | ||
| return false; | ||
| } | ||
|
|
||
| public static bool EncodeStringLiteral(string value, Span<byte> destination, out int bytesWritten) | ||
| { | ||
| // From https://tools.ietf.org/html/rfc7541#section-5.2 | ||
|
|
@@ -485,7 +428,7 @@ public static bool EncodeStringLiterals(ReadOnlySpan<string> values, string sepa | |
|
|
||
| /// <summary> | ||
| /// Encodes a "Literal Header Field without Indexing" to a new array, but only the index portion; | ||
| /// a subsequent call to <see cref="EncodeStringLiteral"/> must be used to encode the associated value. | ||
| /// a subsequent call to <c>EncodeStringLiteral</c> must be used to encode the associated value. | ||
| /// </summary> | ||
| public static byte[] EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(int index) | ||
| { | ||
|
|
@@ -497,7 +440,7 @@ public static byte[] EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(int | |
|
|
||
| /// <summary> | ||
| /// Encodes a "Literal Header Field without Indexing - New Name" to a new array, but only the name portion; | ||
| /// a subsequent call to <see cref="EncodeStringLiteral"/> must be used to encode the associated value. | ||
| /// a subsequent call to <c>EncodeStringLiteral</c> must be used to encode the associated value. | ||
| /// </summary> | ||
| public static byte[] EncodeLiteralHeaderFieldWithoutIndexingNewNameToAllocatedArray(string name) | ||
| { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No more state!