From ce8096545464e946340bb81f8b9818ef1d8a798e Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 23 Jun 2020 09:20:06 +1200 Subject: [PATCH] HPackDecoder performance (#23083) --- .../src/Internal/Http2/HPackHeaderWriter.cs | 2 +- .../HPackDecoderBenchmark.cs | 135 +++++ .../runtime/Http2/Hpack/DynamicTable.cs | 4 +- .../runtime/Http2/Hpack/H2StaticTable.cs | 27 +- .../runtime/Http2/Hpack/HPackDecoder.cs | 545 +++++++++++------- .../runtime/Http2/Hpack/HPackEncoder.cs | 2 +- .../runtime/Http2/HPackDecoderTest.cs | 233 +++++--- 7 files changed, 661 insertions(+), 287 deletions(-) create mode 100644 src/Servers/Kestrel/perf/Kestrel.Performance/HPackDecoderBenchmark.cs diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs index 33c7b920f3..c914595bc9 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs @@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 case 404: case 500: // Status codes which exist in the HTTP/2 StaticTable. - return HPackEncoder.EncodeIndexedHeaderField(H2StaticTable.StatusIndex[statusCode], buffer, out length); + return HPackEncoder.EncodeIndexedHeaderField(H2StaticTable.GetStatusIndex(statusCode), buffer, out length); default: const string name = ":status"; var value = StatusCodes.ToStatusString(statusCode); diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/HPackDecoderBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/HPackDecoderBenchmark.cs new file mode 100644 index 0000000000..31382bbf5e --- /dev/null +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/HPackDecoderBenchmark.cs @@ -0,0 +1,135 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Net.Http.HPack; +using System.Text; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + public class HPackDecoderBenchmark + { + // Indexed Header Field Representation - Dynamic Table - Index 62 (first index in dynamic table) + private static readonly byte[] _indexedHeaderDynamic = new byte[] { 0xbe }; + + private static readonly byte[] _literalHeaderFieldWithoutIndexingNewName = new byte[] { 0x00 }; + + private const string _headerNameString = "new-header"; + + private static readonly byte[] _headerNameBytes = Encoding.ASCII.GetBytes(_headerNameString); + + private static readonly byte[] _headerName = new byte[] { (byte)_headerNameBytes.Length } + .Concat(_headerNameBytes) + .ToArray(); + + private const string _headerValueString = "value"; + + private static readonly byte[] _headerValueBytes = Encoding.ASCII.GetBytes(_headerValueString); + + private static readonly byte[] _headerValue = new byte[] { (byte)_headerValueBytes.Length } + .Concat(_headerValueBytes) + .ToArray(); + + private static readonly byte[] _literalHeaderFieldNeverIndexed_NewName = _literalHeaderFieldWithoutIndexingNewName + .Concat(_headerName) + .Concat(_headerValue) + .ToArray(); + + private static readonly byte[] _literalHeaderFieldNeverIndexed_NewName_Large; + private static readonly byte[] _literalHeaderFieldNeverIndexed_NewName_Multiple; + private static readonly byte[] _indexedHeaderDynamic_Multiple; + + static HPackDecoderBenchmark() + { + string string8193 = new string('a', 8193); + + _literalHeaderFieldNeverIndexed_NewName_Large = _literalHeaderFieldWithoutIndexingNewName + .Concat(new byte[] { 0x7f, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix, no Huffman encoding + .Concat(Encoding.ASCII.GetBytes(string8193)) + .Concat(new byte[] { 0x7f, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix, no Huffman encoding + .Concat(Encoding.ASCII.GetBytes(string8193)) + .ToArray(); + + _literalHeaderFieldNeverIndexed_NewName_Multiple = _literalHeaderFieldNeverIndexed_NewName + .Concat(_literalHeaderFieldNeverIndexed_NewName) + .Concat(_literalHeaderFieldNeverIndexed_NewName) + .Concat(_literalHeaderFieldNeverIndexed_NewName) + .Concat(_literalHeaderFieldNeverIndexed_NewName) + .ToArray(); + + _indexedHeaderDynamic_Multiple = _indexedHeaderDynamic + .Concat(_indexedHeaderDynamic) + .Concat(_indexedHeaderDynamic) + .Concat(_indexedHeaderDynamic) + .Concat(_indexedHeaderDynamic) + .ToArray(); + } + + private HPackDecoder _decoder; + private TestHeadersHandler _testHeadersHandler; + private DynamicTable _dynamicTable; + + [GlobalSetup] + public void GlobalSetup() + { + _dynamicTable = new DynamicTable(maxSize: 4096); + _dynamicTable.Insert(_headerNameBytes, _headerValueBytes); + _decoder = new HPackDecoder(maxDynamicTableSize: 4096, maxHeadersLength: 65536, _dynamicTable); + _testHeadersHandler = new TestHeadersHandler(); + } + + [Benchmark] + public void DecodesLiteralHeaderFieldNeverIndexed_NewName() + { + _decoder.Decode(_literalHeaderFieldNeverIndexed_NewName, endHeaders: true, handler: _testHeadersHandler); + } + + [Benchmark] + public void DecodesLiteralHeaderFieldNeverIndexed_NewName_Large() + { + _decoder.Decode(_literalHeaderFieldNeverIndexed_NewName_Large, endHeaders: true, handler: _testHeadersHandler); + } + + [Benchmark] + public void DecodesLiteralHeaderFieldNeverIndexed_NewName_Multiple() + { + _decoder.Decode(_literalHeaderFieldNeverIndexed_NewName_Multiple, endHeaders: true, handler: _testHeadersHandler); + } + + [Benchmark] + public void DecodesIndexedHeaderField_DynamicTable() + { + _decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: _testHeadersHandler); + } + + [Benchmark] + public void DecodesIndexedHeaderField_DynamicTable_Multiple() + { + _decoder.Decode(_indexedHeaderDynamic_Multiple, endHeaders: true, handler: _testHeadersHandler); + } + + private class TestHeadersHandler : IHttpHeadersHandler + { + public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) + { + } + + public void OnHeadersComplete(bool endStream) + { + } + + public void OnStaticIndexedHeader(int index) + { + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + } + } + } +} diff --git a/src/Shared/runtime/Http2/Hpack/DynamicTable.cs b/src/Shared/runtime/Http2/Hpack/DynamicTable.cs index 5a8fdf170f..9e93dca87c 100644 --- a/src/Shared/runtime/Http2/Hpack/DynamicTable.cs +++ b/src/Shared/runtime/Http2/Hpack/DynamicTable.cs @@ -25,7 +25,7 @@ namespace System.Net.Http.HPack public int MaxSize => _maxSize; - public HeaderField this[int index] + public ref readonly HeaderField this[int index] { get { @@ -42,7 +42,7 @@ namespace System.Net.Http.HPack index += _buffer.Length; } - return _buffer[index]; + return ref _buffer[index]; } } diff --git a/src/Shared/runtime/Http2/Hpack/H2StaticTable.cs b/src/Shared/runtime/Http2/Hpack/H2StaticTable.cs index 7f3b775582..c0f203fef4 100644 --- a/src/Shared/runtime/Http2/Hpack/H2StaticTable.cs +++ b/src/Shared/runtime/Http2/Hpack/H2StaticTable.cs @@ -9,23 +9,22 @@ namespace System.Net.Http.HPack { internal static class H2StaticTable { - // Index of status code into s_staticDecoderTable - private static readonly Dictionary s_statusIndex = new Dictionary - { - [200] = 8, - [204] = 9, - [206] = 10, - [304] = 11, - [400] = 12, - [404] = 13, - [500] = 14, - }; - public static int Count => s_staticDecoderTable.Length; - public static HeaderField Get(int index) => s_staticDecoderTable[index]; + public static ref readonly HeaderField Get(int index) => ref s_staticDecoderTable[index]; - public static IReadOnlyDictionary StatusIndex => s_statusIndex; + public static int GetStatusIndex(int status) => + status switch + { + 200 => 8, + 204 => 9, + 206 => 10, + 304 => 11, + 400 => 12, + 404 => 13, + 500 => 14, + _ => throw new ArgumentOutOfRangeException() + }; private static readonly HeaderField[] s_staticDecoderTable = new HeaderField[] { diff --git a/src/Shared/runtime/Http2/Hpack/HPackDecoder.cs b/src/Shared/runtime/Http2/Hpack/HPackDecoder.cs index 3fe3c86243..b07dc47d9a 100644 --- a/src/Shared/runtime/Http2/Hpack/HPackDecoder.cs +++ b/src/Shared/runtime/Http2/Hpack/HPackDecoder.cs @@ -5,6 +5,7 @@ #nullable enable using System.Buffers; using System.Diagnostics; +using System.Numerics; #if KESTREL using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; #endif @@ -37,7 +38,6 @@ namespace System.Net.Http.HPack // | 1 | Index (7+) | // +---+---------------------------+ private const byte IndexedHeaderFieldMask = 0x80; - private const byte IndexedHeaderFieldRepresentation = 0x80; // http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1 // 0 1 2 3 4 5 6 7 @@ -45,7 +45,6 @@ namespace System.Net.Http.HPack // | 0 | 1 | Index (6+) | // +---+---+-----------------------+ private const byte LiteralHeaderFieldWithIncrementalIndexingMask = 0xc0; - private const byte LiteralHeaderFieldWithIncrementalIndexingRepresentation = 0x40; // http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.2 // 0 1 2 3 4 5 6 7 @@ -53,7 +52,6 @@ namespace System.Net.Http.HPack // | 0 | 0 | 0 | 0 | Index (4+) | // +---+---+-----------------------+ private const byte LiteralHeaderFieldWithoutIndexingMask = 0xf0; - private const byte LiteralHeaderFieldWithoutIndexingRepresentation = 0x00; // http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.3 // 0 1 2 3 4 5 6 7 @@ -61,7 +59,6 @@ namespace System.Net.Http.HPack // | 0 | 0 | 0 | 1 | Index (4+) | // +---+---+-----------------------+ private const byte LiteralHeaderFieldNeverIndexedMask = 0xf0; - private const byte LiteralHeaderFieldNeverIndexedRepresentation = 0x10; // http://httpwg.org/specs/rfc7541.html#rfc.section.6.3 // 0 1 2 3 4 5 6 7 @@ -69,7 +66,6 @@ namespace System.Net.Http.HPack // | 0 | 0 | 1 | Max size (5+) | // +---+---------------------------+ private const byte DynamicTableSizeUpdateMask = 0xe0; - private const byte DynamicTableSizeUpdateRepresentation = 0x20; // http://httpwg.org/specs/rfc7541.html#rfc.section.5.2 // 0 1 2 3 4 5 6 7 @@ -92,6 +88,8 @@ namespace System.Net.Http.HPack private byte[] _stringOctets; private byte[] _headerNameOctets; private byte[] _headerValueOctets; + private (int start, int length)? _headerNameRange; + private (int start, int length)? _headerValueRange; private State _state = State.Ready; private byte[]? _headerName; @@ -124,107 +122,247 @@ namespace System.Net.Http.HPack { foreach (ReadOnlyMemory segment in data) { - DecodeInternal(segment.Span, endHeaders, handler); + DecodeInternal(segment.Span, handler); } CheckIncompleteHeaderBlock(endHeaders); } - public void Decode(ReadOnlySpan data, bool endHeaders, IHttpHeadersHandler? handler) + public void Decode(ReadOnlySpan data, bool endHeaders, IHttpHeadersHandler handler) { - DecodeInternal(data, endHeaders, handler); + DecodeInternal(data, handler); CheckIncompleteHeaderBlock(endHeaders); } - private void DecodeInternal(ReadOnlySpan data, bool endHeaders, IHttpHeadersHandler? handler) + private void DecodeInternal(ReadOnlySpan data, IHttpHeadersHandler handler) { - int intResult; + int currentIndex = 0; - for (int i = 0; i < data.Length; i++) + do { - byte b = data[i]; switch (_state) { case State.Ready: - // TODO: Instead of masking and comparing each prefix value, - // consider doing a 16-way switch on the first four bits (which is the max prefix size). - // Look at this once we have more concrete perf data. - if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldRepresentation) + Parse(data, ref currentIndex, handler); + break; + case State.HeaderFieldIndex: + ParseHeaderFieldIndex(data, ref currentIndex, handler); + break; + case State.HeaderNameIndex: + ParseHeaderNameIndex(data, ref currentIndex, handler); + break; + case State.HeaderNameLength: + ParseHeaderNameLength(data, ref currentIndex, handler); + break; + case State.HeaderNameLengthContinue: + ParseHeaderNameLengthContinue(data, ref currentIndex, handler); + break; + case State.HeaderName: + ParseHeaderName(data, ref currentIndex, handler); + break; + case State.HeaderValueLength: + ParseHeaderValueLength(data, ref currentIndex, handler); + break; + case State.HeaderValueLengthContinue: + ParseHeaderValueLengthContinue(data, ref currentIndex, handler); + break; + case State.HeaderValue: + ParseHeaderValue(data, ref currentIndex, handler); + break; + case State.DynamicTableSizeUpdate: + ParseDynamicTableSizeUpdate(data, ref currentIndex); + break; + default: + // Can't happen + Debug.Fail("HPACK decoder reach an invalid state"); + throw new NotImplementedException(_state.ToString()); + } + } + // Parse methods each check the length. This check is to see whether there is still data available + // and to continue parsing. + while (currentIndex < data.Length); + + // If a header range was set, but the value was not in the data, then copy the range + // to the name buffer. Must copy because because the data will be replaced and the range + // will no longer be valid. + if (_headerNameRange != null) + { + EnsureStringCapacity(ref _headerNameOctets); + _headerName = _headerNameOctets; + + ReadOnlySpan headerBytes = data.Slice(_headerNameRange.GetValueOrDefault().start, _headerNameRange.GetValueOrDefault().length); + headerBytes.CopyTo(_headerName); + _headerNameLength = headerBytes.Length; + _headerNameRange = null; + } + } + + private void ParseDynamicTableSizeUpdate(ReadOnlySpan data, ref int currentIndex) + { + if (TryDecodeInteger(data, ref currentIndex, out int intResult)) + { + SetDynamicHeaderTableSize(intResult); + _state = State.Ready; + } + } + + private void ParseHeaderValueLength(ReadOnlySpan data, ref int currentIndex, IHttpHeadersHandler handler) + { + if (currentIndex < data.Length) + { + byte b = data[currentIndex++]; + + _huffman = IsHuffmanEncoded(b); + + if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out int intResult)) + { + OnStringLength(intResult, nextState: State.HeaderValue); + + if (intResult == 0) + { + OnString(nextState: State.Ready); + ProcessHeaderValue(data, handler); + } + else + { + ParseHeaderValue(data, ref currentIndex, handler); + } + } + else + { + _state = State.HeaderValueLengthContinue; + ParseHeaderValueLengthContinue(data, ref currentIndex, handler); + } + } + } + + private void ParseHeaderNameLengthContinue(ReadOnlySpan data, ref int currentIndex, IHttpHeadersHandler handler) + { + if (TryDecodeInteger(data, ref currentIndex, out int intResult)) + { + // IntegerDecoder disallows overlong encodings, where an integer is encoded with more bytes than is strictly required. + // 0 should always be represented by a single byte, so we shouldn't need to check for it in the continuation case. + Debug.Assert(intResult != 0, "A header name length of 0 should never be encoded with a continuation byte."); + + OnStringLength(intResult, nextState: State.HeaderName); + ParseHeaderName(data, ref currentIndex, handler); + } + } + + private void ParseHeaderValueLengthContinue(ReadOnlySpan data, ref int currentIndex, IHttpHeadersHandler handler) + { + if (TryDecodeInteger(data, ref currentIndex, out int intResult)) + { + // 0 should always be represented by a single byte, so we shouldn't need to check for it in the continuation case. + Debug.Assert(intResult != 0, "A header value length of 0 should never be encoded with a continuation byte."); + + OnStringLength(intResult, nextState: State.HeaderValue); + ParseHeaderValue(data, ref currentIndex, handler); + } + } + + private void ParseHeaderFieldIndex(ReadOnlySpan data, ref int currentIndex, IHttpHeadersHandler handler) + { + if (TryDecodeInteger(data, ref currentIndex, out int intResult)) + { + OnIndexedHeaderField(intResult, handler); + } + } + + private void ParseHeaderNameIndex(ReadOnlySpan data, ref int currentIndex, IHttpHeadersHandler handler) + { + if (TryDecodeInteger(data, ref currentIndex, out int intResult)) + { + OnIndexedHeaderName(intResult); + ParseHeaderValueLength(data, ref currentIndex, handler); + } + } + + private void ParseHeaderNameLength(ReadOnlySpan data, ref int currentIndex, IHttpHeadersHandler handler) + { + if (currentIndex < data.Length) + { + byte b = data[currentIndex++]; + + _huffman = IsHuffmanEncoded(b); + + if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out int intResult)) + { + if (intResult == 0) + { + throw new HPackDecodingException(SR.Format(SR.net_http_invalid_header_name, "")); + } + + OnStringLength(intResult, nextState: State.HeaderName); + ParseHeaderName(data, ref currentIndex, handler); + } + else + { + _state = State.HeaderNameLengthContinue; + ParseHeaderNameLengthContinue(data, ref currentIndex, handler); + } + } + } + + private void Parse(ReadOnlySpan data, ref int currentIndex, IHttpHeadersHandler handler) + { + if (currentIndex < data.Length) + { + Debug.Assert(_state == State.Ready, "Should be ready to parse a new header."); + + byte b = data[currentIndex++]; + + switch (BitOperations.LeadingZeroCount(b) - 24) // byte 'b' is extended to uint, so will have 24 extra 0s. + { + case 0: // Indexed Header Field { _headersObserved = true; int val = b & ~IndexedHeaderFieldMask; - if (_integerDecoder.BeginTryDecode((byte)val, IndexedHeaderFieldPrefix, out intResult)) + if (_integerDecoder.BeginTryDecode((byte)val, IndexedHeaderFieldPrefix, out int intResult)) { OnIndexedHeaderField(intResult, handler); } else { _state = State.HeaderFieldIndex; + ParseHeaderFieldIndex(data, ref currentIndex, handler); } + break; } - else if ((b & LiteralHeaderFieldWithIncrementalIndexingMask) == LiteralHeaderFieldWithIncrementalIndexingRepresentation) - { - _headersObserved = true; - - _index = true; - int val = b & ~LiteralHeaderFieldWithIncrementalIndexingMask; - - if (val == 0) - { - _state = State.HeaderNameLength; - } - else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithIncrementalIndexingPrefix, out intResult)) - { - OnIndexedHeaderName(intResult); - } - else - { - _state = State.HeaderNameIndex; - } - } - else if ((b & LiteralHeaderFieldWithoutIndexingMask) == LiteralHeaderFieldWithoutIndexingRepresentation) - { - _headersObserved = true; - - _index = false; - int val = b & ~LiteralHeaderFieldWithoutIndexingMask; - - if (val == 0) - { - _state = State.HeaderNameLength; - } - else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithoutIndexingPrefix, out intResult)) - { - OnIndexedHeaderName(intResult); - } - else - { - _state = State.HeaderNameIndex; - } - } - else if ((b & LiteralHeaderFieldNeverIndexedMask) == LiteralHeaderFieldNeverIndexedRepresentation) - { - _headersObserved = true; - - _index = false; - int val = b & ~LiteralHeaderFieldNeverIndexedMask; - - if (val == 0) - { - _state = State.HeaderNameLength; - } - else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldNeverIndexedPrefix, out intResult)) - { - OnIndexedHeaderName(intResult); - } - else - { - _state = State.HeaderNameIndex; - } - } - else if ((b & DynamicTableSizeUpdateMask) == DynamicTableSizeUpdateRepresentation) + case 1: // Literal Header Field with Incremental Indexing + ParseLiteralHeaderField( + data, + ref currentIndex, + b, + LiteralHeaderFieldWithIncrementalIndexingMask, + LiteralHeaderFieldWithIncrementalIndexingPrefix, + index: true, + handler); + break; + case 4: + default: // Literal Header Field without Indexing + ParseLiteralHeaderField( + data, + ref currentIndex, + b, + LiteralHeaderFieldWithoutIndexingMask, + LiteralHeaderFieldWithoutIndexingPrefix, + index: false, + handler); + break; + case 3: // Literal Header Field Never Indexed + ParseLiteralHeaderField( + data, + ref currentIndex, + b, + LiteralHeaderFieldNeverIndexedMask, + LiteralHeaderFieldNeverIndexedPrefix, + index: false, + handler); + break; + case 2: // Dynamic Table Size Update { // https://tools.ietf.org/html/rfc7541#section-4.2 // This dynamic table size @@ -235,125 +373,107 @@ namespace System.Net.Http.HPack throw new HPackDecodingException(SR.net_http_hpack_late_dynamic_table_size_update); } - if (_integerDecoder.BeginTryDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix, out intResult)) + if (_integerDecoder.BeginTryDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix, out int intResult)) { SetDynamicHeaderTableSize(intResult); } else { _state = State.DynamicTableSizeUpdate; + ParseDynamicTableSizeUpdate(data, ref currentIndex); } + break; } - else - { - // Can't happen - Debug.Fail("Unreachable code"); - throw new InvalidOperationException("Unreachable code."); - } + } + } + } - break; - case State.HeaderFieldIndex: - if (_integerDecoder.TryDecode(b, out intResult)) - { - OnIndexedHeaderField(intResult, handler); - } + private void ParseLiteralHeaderField(ReadOnlySpan data, ref int currentIndex, byte b, byte mask, byte indexPrefix, bool index, IHttpHeadersHandler handler) + { + _headersObserved = true; - break; - case State.HeaderNameIndex: - if (_integerDecoder.TryDecode(b, out intResult)) - { - OnIndexedHeaderName(intResult); - } + _index = index; + int val = b & ~mask; - break; - case State.HeaderNameLength: - _huffman = (b & HuffmanMask) != 0; + if (val == 0) + { + _state = State.HeaderNameLength; + ParseHeaderNameLength(data, ref currentIndex, handler); + } + else + { + if (_integerDecoder.BeginTryDecode((byte)val, indexPrefix, out int intResult)) + { + OnIndexedHeaderName(intResult); + ParseHeaderValueLength(data, ref currentIndex, handler); + } + else + { + _state = State.HeaderNameIndex; + ParseHeaderNameIndex(data, ref currentIndex, handler); + } + } + } - if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult)) - { - if (intResult == 0) - { - throw new HPackDecodingException(SR.Format(SR.net_http_invalid_header_name, "")); - } + private void ParseHeaderName(ReadOnlySpan data, ref int currentIndex, IHttpHeadersHandler handler) + { + // Read remaining chars, up to the length of the current data + int count = Math.Min(_stringLength - _stringIndex, data.Length - currentIndex); - OnStringLength(intResult, nextState: State.HeaderName); - } - else - { - _state = State.HeaderNameLengthContinue; - } + // Check whether the whole string is available in the data and no decompression required. + // If string is good then mark its range. + // NOTE: it may need to be copied to buffer later the if value is not current data. + if (count == _stringLength && !_huffman) + { + // Fast path. Store the range rather than copying. + _headerNameRange = (start: currentIndex, count); + currentIndex += count; - break; - case State.HeaderNameLengthContinue: - if (_integerDecoder.TryDecode(b, out intResult)) - { - // IntegerDecoder disallows overlong encodings, where an integer is encoded with more bytes than is strictly required. - // 0 should always be represented by a single byte, so we shouldn't need to check for it in the continuation case. - Debug.Assert(intResult != 0, "A header name length of 0 should never be encoded with a continuation byte."); + _state = State.HeaderValueLength; + } + else + { + // Copy string to temporary buffer. + // _stringOctets was already + data.Slice(currentIndex, count).CopyTo(_stringOctets.AsSpan(_stringIndex)); + _stringIndex += count; + currentIndex += count; - OnStringLength(intResult, nextState: State.HeaderName); - } + if (_stringIndex == _stringLength) + { + OnString(nextState: State.HeaderValueLength); + ParseHeaderValueLength(data, ref currentIndex, handler); + } + } + } - break; - case State.HeaderName: - _stringOctets[_stringIndex++] = b; + private void ParseHeaderValue(ReadOnlySpan data, ref int currentIndex, IHttpHeadersHandler handler) + { + // Read remaining chars, up to the length of the current data + int count = Math.Min(_stringLength - _stringIndex, data.Length - currentIndex); - if (_stringIndex == _stringLength) - { - OnString(nextState: State.HeaderValueLength); - } + // Check whether the whole string is available in the data and no decompressed required. + // If string is good then mark its range. + if (count == _stringLength && !_huffman) + { + // Fast path. Store the range rather than copying. + _headerValueRange = (start: currentIndex, count); + currentIndex += count; - break; - case State.HeaderValueLength: - _huffman = (b & HuffmanMask) != 0; + _state = State.Ready; + ProcessHeaderValue(data, handler); + } + else + { + // Copy string to temporary buffer. + data.Slice(currentIndex, count).CopyTo(_stringOctets.AsSpan(_stringIndex)); + _stringIndex += count; + currentIndex += count; - if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult)) - { - OnStringLength(intResult, nextState: State.HeaderValue); - - if (intResult == 0) - { - ProcessHeaderValue(handler); - } - } - else - { - _state = State.HeaderValueLengthContinue; - } - - break; - case State.HeaderValueLengthContinue: - if (_integerDecoder.TryDecode(b, out intResult)) - { - // IntegerDecoder disallows overlong encodings where an integer is encoded with more bytes than is strictly required. - // 0 should always be represented by a single byte, so we shouldn't need to check for it in the continuation case. - Debug.Assert(intResult != 0, "A header value length of 0 should never be encoded with a continuation byte."); - - OnStringLength(intResult, nextState: State.HeaderValue); - } - - break; - case State.HeaderValue: - _stringOctets[_stringIndex++] = b; - - if (_stringIndex == _stringLength) - { - ProcessHeaderValue(handler); - } - - break; - case State.DynamicTableSizeUpdate: - if (_integerDecoder.TryDecode(b, out intResult)) - { - SetDynamicHeaderTableSize(intResult); - _state = State.Ready; - } - - break; - default: - // Can't happen - Debug.Fail("HPACK decoder reach an invalid state"); - throw new NotImplementedException(_state.ToString()); + if (_stringIndex == _stringLength) + { + OnString(nextState: State.Ready); + ProcessHeaderValue(data, handler); } } } @@ -371,14 +491,20 @@ namespace System.Net.Http.HPack } } - private void ProcessHeaderValue(IHttpHeadersHandler? handler) + private void ProcessHeaderValue(ReadOnlySpan data, IHttpHeadersHandler handler) { - OnString(nextState: State.Ready); + ReadOnlySpan headerNameSpan = _headerNameRange == null + ? new Span(_headerName, 0, _headerNameLength) + : data.Slice(_headerNameRange.GetValueOrDefault().start, _headerNameRange.GetValueOrDefault().length); - var headerNameSpan = new Span(_headerName, 0, _headerNameLength); - var headerValueSpan = new Span(_headerValueOctets, 0, _headerValueLength); + ReadOnlySpan headerValueSpan = _headerValueRange == null + ? new Span(_headerValueOctets, 0, _headerValueLength) + : data.Slice(_headerValueRange.GetValueOrDefault().start, _headerValueRange.GetValueOrDefault().length); - handler?.OnHeader(headerNameSpan, headerValueSpan); + handler.OnHeader(headerNameSpan, headerValueSpan); + + _headerNameRange = null; + _headerValueRange = null; if (_index) { @@ -395,18 +521,17 @@ namespace System.Net.Http.HPack } } - private void OnIndexedHeaderField(int index, IHttpHeadersHandler? handler) + private void OnIndexedHeaderField(int index, IHttpHeadersHandler handler) { - HeaderField header = GetHeader(index); - handler?.OnHeader(header.Name, header.Value); + ref readonly HeaderField header = ref GetHeader(index); + handler.OnHeader(header.Name, header.Value); _state = State.Ready; } private void OnIndexedHeaderName(int index) { - HeaderField header = GetHeader(index); - _headerName = header.Name; - _headerNameLength = header.Name.Length; + _headerName = GetHeader(index).Name; + _headerNameLength = _headerName.Length; _state = State.HeaderValueLength; } @@ -437,11 +562,7 @@ namespace System.Net.Http.HPack } else { - if (dst.Length < _stringLength) - { - dst = new byte[Math.Max(_stringLength, dst.Length * 2)]; - } - + EnsureStringCapacity(ref dst); Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength); return _stringLength; } @@ -467,13 +588,41 @@ namespace System.Net.Http.HPack _state = nextState; } - private HeaderField GetHeader(int index) + private void EnsureStringCapacity(ref byte[] dst) + { + if (dst.Length < _stringLength) + { + dst = new byte[Math.Max(_stringLength, dst.Length * 2)]; + } + } + + private bool TryDecodeInteger(ReadOnlySpan data, ref int currentIndex, out int result) + { + for (; currentIndex < data.Length; currentIndex++) + { + if (_integerDecoder.TryDecode(data[currentIndex], out result)) + { + currentIndex++; + return true; + } + } + + result = default; + return false; + } + + private static bool IsHuffmanEncoded(byte b) + { + return (b & HuffmanMask) != 0; + } + + private ref readonly HeaderField GetHeader(int index) { try { - return index <= H2StaticTable.Count - ? H2StaticTable.Get(index - 1) - : _dynamicTable[index - H2StaticTable.Count - 1]; + return ref index <= H2StaticTable.Count + ? ref H2StaticTable.Get(index - 1) + : ref _dynamicTable[index - H2StaticTable.Count - 1]; } catch (IndexOutOfRangeException) { diff --git a/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs b/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs index 97cdea1c50..4a8511e682 100644 --- a/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs +++ b/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs @@ -54,7 +54,7 @@ namespace System.Net.Http.HPack case 404: case 500: // Status codes which exist in the HTTP/2 StaticTable. - return EncodeIndexedHeaderField(H2StaticTable.StatusIndex[statusCode], destination, out bytesWritten); + return EncodeIndexedHeaderField(H2StaticTable.GetStatusIndex(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. diff --git a/src/Shared/test/Shared.Tests/runtime/Http2/HPackDecoderTest.cs b/src/Shared/test/Shared.Tests/runtime/Http2/HPackDecoderTest.cs index 2afdb29901..d5d26ccb37 100644 --- a/src/Shared/test/Shared.Tests/runtime/Http2/HPackDecoderTest.cs +++ b/src/Shared/test/Shared.Tests/runtime/Http2/HPackDecoderTest.cs @@ -14,7 +14,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; namespace System.Net.Http.Unit.Tests.HPack { - public class HPackDecoderTests : IHttpHeadersHandler + public class HPackDecoderTests { private const int DynamicTableInitialMaxSize = 4096; private const int MaxHeaderFieldSize = 8192; @@ -89,42 +89,26 @@ namespace System.Net.Http.Unit.Tests.HPack private readonly DynamicTable _dynamicTable; private readonly HPackDecoder _decoder; - - private readonly Dictionary _decodedHeaders = new Dictionary(); + private readonly TestHttpHeadersHandler _handler = new TestHttpHeadersHandler(); public HPackDecoderTests() { - _dynamicTable = new DynamicTable(DynamicTableInitialMaxSize); - _decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxHeaderFieldSize, _dynamicTable); + (_dynamicTable, _decoder) = CreateDecoderAndTable(); } - void IHttpHeadersHandler.OnHeader(ReadOnlySpan name, ReadOnlySpan value) + private static (DynamicTable, HPackDecoder) CreateDecoderAndTable() { - string headerName = Encoding.ASCII.GetString(name); - string headerValue = Encoding.ASCII.GetString(value); + var dynamicTable = new DynamicTable(DynamicTableInitialMaxSize); + var decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxHeaderFieldSize, dynamicTable); - _decodedHeaders[headerName] = headerValue; + return (dynamicTable, decoder); } - void IHttpHeadersHandler.OnStaticIndexedHeader(int index) - { - // Not yet implemented for HPACK. - throw new NotImplementedException(); - } - - void IHttpHeadersHandler.OnStaticIndexedHeader(int index, ReadOnlySpan value) - { - // Not yet implemented for HPACK. - throw new NotImplementedException(); - } - - void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { } - [Fact] public void DecodesIndexedHeaderField_StaticTable() { - _decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: this); - Assert.Equal("GET", _decodedHeaders[":method"]); + _decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: _handler); + Assert.Equal("GET", _handler.DecodedHeaders[":method"]); } [Fact] @@ -134,17 +118,17 @@ namespace System.Net.Http.Unit.Tests.HPack _dynamicTable.Insert(_headerNameBytes, _headerValueBytes); // Index it - _decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: this); - Assert.Equal(_headerValueString, _decodedHeaders[_headerNameString]); + _decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: _handler); + Assert.Equal(_headerValueString, _handler.DecodedHeaders[_headerNameString]); } [Fact] public void DecodesIndexedHeaderField_OutOfRange_Error() { HPackDecodingException exception = Assert.Throws(() => - _decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: this)); + _decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: _handler)); Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message); - Assert.Empty(_decodedHeaders); + Assert.Empty(_handler.DecodedHeaders); } [Fact] @@ -218,9 +202,9 @@ namespace System.Net.Http.Unit.Tests.HPack // 11 1110 (Indexed Name - Index 62 encoded with 6-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) // Index 62 is the first entry in the dynamic table. If there's nothing there, the decoder should throw. - HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x7e }, endHeaders: true, handler: this)); + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x7e }, endHeaders: true, handler: _handler)); Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message); - Assert.Empty(_decodedHeaders); + Assert.Empty(_handler.DecodedHeaders); } [Fact] @@ -294,9 +278,9 @@ namespace System.Net.Http.Unit.Tests.HPack // 1111 0010 1111 (Indexed Name - Index 62 encoded with 4-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) // Index 62 is the first entry in the dynamic table. If there's nothing there, the decoder should throw. - HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x0f, 0x2f }, endHeaders: true, handler: this)); + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x0f, 0x2f }, endHeaders: true, handler: _handler)); Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message); - Assert.Empty(_decodedHeaders); + Assert.Empty(_handler.DecodedHeaders); } [Fact] @@ -310,6 +294,19 @@ namespace System.Net.Http.Unit.Tests.HPack TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString); } + [Fact] + public void DecodesLiteralHeaderFieldNeverIndexed_NewName_Duplicated() + { + byte[] encoded = _literalHeaderFieldNeverIndexedNewName + .Concat(_headerName) + .Concat(_headerValue) + .ToArray(); + + encoded = encoded.Concat(encoded).ToArray(); + + TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString); + } + [Fact] public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedName() { @@ -376,9 +373,9 @@ namespace System.Net.Http.Unit.Tests.HPack // 1111 0010 1111 (Indexed Name - Index 62 encoded with 4-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) // Index 62 is the first entry in the dynamic table. If there's nothing there, the decoder should throw. - HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x1f, 0x2f }, endHeaders: true, handler: this)); + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x1f, 0x2f }, endHeaders: true, handler: _handler)); Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message); - Assert.Empty(_decodedHeaders); + Assert.Empty(_handler.DecodedHeaders); } [Fact] @@ -389,10 +386,10 @@ namespace System.Net.Http.Unit.Tests.HPack Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); - _decoder.Decode(new byte[] { 0x3e }, endHeaders: true, handler: this); + _decoder.Decode(new byte[] { 0x3e }, endHeaders: true, handler: _handler); Assert.Equal(30, _dynamicTable.MaxSize); - Assert.Empty(_decodedHeaders); + Assert.Empty(_handler.DecodedHeaders); } [Fact] @@ -404,7 +401,7 @@ namespace System.Net.Http.Unit.Tests.HPack Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); byte[] data = _indexedHeaderStatic.Concat(new byte[] { 0x3e }).ToArray(); - HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(data, endHeaders: true, handler: this)); + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(data, endHeaders: true, handler: _handler)); Assert.Equal(SR.net_http_hpack_late_dynamic_table_size_update, exception.Message); } @@ -413,13 +410,13 @@ namespace System.Net.Http.Unit.Tests.HPack { Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); - _decoder.Decode(_indexedHeaderStatic, endHeaders: false, handler: this); - Assert.Equal("GET", _decodedHeaders[":method"]); + _decoder.Decode(_indexedHeaderStatic, endHeaders: false, handler: _handler); + Assert.Equal("GET", _handler.DecodedHeaders[":method"]); // 001 (Dynamic Table Size Update) // 11110 (30 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) byte[] data = new byte[] { 0x3e }; - HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(data, endHeaders: true, handler: this)); + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(data, endHeaders: true, handler: _handler)); Assert.Equal(SR.net_http_hpack_late_dynamic_table_size_update, exception.Message); } @@ -428,12 +425,12 @@ namespace System.Net.Http.Unit.Tests.HPack { Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); - _decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: this); - Assert.Equal("GET", _decodedHeaders[":method"]); + _decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: _handler); + Assert.Equal("GET", _handler.DecodedHeaders[":method"]); // 001 (Dynamic Table Size Update) // 11110 (30 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) - _decoder.Decode(new byte[] { 0x3e }, endHeaders: true, handler: this); + _decoder.Decode(new byte[] { 0x3e }, endHeaders: true, handler: _handler); Assert.Equal(30, _dynamicTable.MaxSize); } @@ -447,9 +444,9 @@ namespace System.Net.Http.Unit.Tests.HPack Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); HPackDecodingException exception = Assert.Throws(() => - _decoder.Decode(new byte[] { 0x3f, 0xe2, 0x1f }, endHeaders: true, handler: this)); + _decoder.Decode(new byte[] { 0x3f, 0xe2, 0x1f }, endHeaders: true, handler: _handler)); Assert.Equal(SR.Format(SR.net_http_hpack_large_table_size_update, 4097, DynamicTableInitialMaxSize), exception.Message); - Assert.Empty(_decodedHeaders); + Assert.Empty(_handler.DecodedHeaders); } [Fact] @@ -459,9 +456,9 @@ namespace System.Net.Http.Unit.Tests.HPack .Concat(new byte[] { 0xff, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix .ToArray(); - HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(encoded, endHeaders: true, handler: this)); + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(encoded, endHeaders: true, handler: _handler)); Assert.Equal(SR.Format(SR.net_http_headers_exceeded_length, MaxHeaderFieldSize), exception.Message); - Assert.Empty(_decodedHeaders); + Assert.Empty(_handler.DecodedHeaders); } [Fact] @@ -477,9 +474,57 @@ namespace System.Net.Http.Unit.Tests.HPack .Concat(Encoding.ASCII.GetBytes(string8193)) .ToArray(); - decoder.Decode(encoded, endHeaders: true, handler: this); + decoder.Decode(encoded, endHeaders: true, handler: _handler); - Assert.Equal(string8193, _decodedHeaders[string8193]); + Assert.Equal(string8193, _handler.DecodedHeaders[string8193]); + } + + [Fact] + public void DecodesStringLength_IndividualBytes() + { + HPackDecoder decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxHeaderFieldSize + 1); + string string8193 = new string('a', MaxHeaderFieldSize + 1); + + byte[] encoded = _literalHeaderFieldWithoutIndexingNewName + .Concat(new byte[] { 0x7f, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix, no Huffman encoding + .Concat(Encoding.ASCII.GetBytes(string8193)) + .Concat(new byte[] { 0x7f, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix, no Huffman encoding + .Concat(Encoding.ASCII.GetBytes(string8193)) + .ToArray(); + + for (int i = 0; i < encoded.Length; i++) + { + bool end = i + 1 == encoded.Length; + + decoder.Decode(new byte[] { encoded[i] }, endHeaders: end, handler: _handler); + } + + Assert.Equal(string8193, _handler.DecodedHeaders[string8193]); + } + + [Fact] + public void DecodesHeaderNameAndValue_SeparateSegments() + { + HPackDecoder decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxHeaderFieldSize + 1); + string string8193 = new string('a', MaxHeaderFieldSize + 1); + + byte[][] segments = new byte[][] + { + _literalHeaderFieldWithoutIndexingNewName, + new byte[] { 0x7f, 0x82, 0x3f }, // 8193 encoded with 7-bit prefix, no Huffman encoding + Encoding.ASCII.GetBytes(string8193), + new byte[] { 0x7f, 0x82, 0x3f }, // 8193 encoded with 7-bit prefix, no Huffman encoding + Encoding.ASCII.GetBytes(string8193) + }; + + for (int i = 0; i < segments.Length; i++) + { + bool end = i + 1 == segments.Length; + + decoder.Decode(segments[i], endHeaders: end, handler: _handler); + } + + Assert.Equal(string8193, _handler.DecodedHeaders[string8193]); } public static readonly TheoryData _incompleteHeaderBlockData = new TheoryData @@ -567,9 +612,9 @@ namespace System.Net.Http.Unit.Tests.HPack [MemberData(nameof(_incompleteHeaderBlockData))] public void DecodesIncompleteHeaderBlock_Error(byte[] encoded) { - HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(encoded, endHeaders: true, handler: this)); + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(encoded, endHeaders: true, handler: _handler)); Assert.Equal(SR.net_http_hpack_incomplete_header_block, exception.Message); - Assert.Empty(_decodedHeaders); + Assert.Empty(_handler.DecodedHeaders); } public static readonly TheoryData _huffmanDecodingErrorData = new TheoryData @@ -601,43 +646,89 @@ namespace System.Net.Http.Unit.Tests.HPack [MemberData(nameof(_huffmanDecodingErrorData))] public void WrapsHuffmanDecodingExceptionInHPackDecodingException(byte[] encoded) { - HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(encoded, endHeaders: true, handler: this)); + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(encoded, endHeaders: true, handler: _handler)); Assert.Equal(SR.net_http_hpack_huffman_decode_failed, exception.Message); Assert.IsType(exception.InnerException); - Assert.Empty(_decodedHeaders); + Assert.Empty(_handler.DecodedHeaders); } - private void TestDecodeWithIndexing(byte[] encoded, string expectedHeaderName, string expectedHeaderValue) + private static void TestDecodeWithIndexing(byte[] encoded, string expectedHeaderName, string expectedHeaderValue) { - TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: true); + TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: true, byteAtATime: false); + TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: true, byteAtATime: true); } - private void TestDecodeWithoutIndexing(byte[] encoded, string expectedHeaderName, string expectedHeaderValue) + private static void TestDecodeWithoutIndexing(byte[] encoded, string expectedHeaderName, string expectedHeaderValue) { - TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: false); + TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: false, byteAtATime: false); + TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: false, byteAtATime: true); } - private void TestDecode(byte[] encoded, string expectedHeaderName, string expectedHeaderValue, bool expectDynamicTableEntry) + private static void TestDecode(byte[] encoded, string expectedHeaderName, string expectedHeaderValue, bool expectDynamicTableEntry, bool byteAtATime) { - Assert.Equal(0, _dynamicTable.Count); - Assert.Equal(0, _dynamicTable.Size); + var (dynamicTable, decoder) = CreateDecoderAndTable(); + var handler = new TestHttpHeadersHandler(); - _decoder.Decode(encoded, endHeaders: true, handler: this); + Assert.Equal(0, dynamicTable.Count); + Assert.Equal(0, dynamicTable.Size); - Assert.Equal(expectedHeaderValue, _decodedHeaders[expectedHeaderName]); - - if (expectDynamicTableEntry) + if (!byteAtATime) { - Assert.Equal(1, _dynamicTable.Count); - Assert.Equal(expectedHeaderName, Encoding.ASCII.GetString(_dynamicTable[0].Name)); - Assert.Equal(expectedHeaderValue, Encoding.ASCII.GetString(_dynamicTable[0].Value)); - Assert.Equal(expectedHeaderName.Length + expectedHeaderValue.Length + 32, _dynamicTable.Size); + decoder.Decode(encoded, endHeaders: true, handler: handler); } else { - Assert.Equal(0, _dynamicTable.Count); - Assert.Equal(0, _dynamicTable.Size); + // Parse data in 1 byte chunks, separated by empty chunks + for (int i = 0; i < encoded.Length; i++) + { + bool end = i + 1 == encoded.Length; + + decoder.Decode(Array.Empty(), endHeaders: false, handler: handler); + decoder.Decode(new byte[] { encoded[i] }, endHeaders: end, handler: handler); + } + } + + Assert.Equal(expectedHeaderValue, handler.DecodedHeaders[expectedHeaderName]); + + if (expectDynamicTableEntry) + { + Assert.Equal(1, dynamicTable.Count); + Assert.Equal(expectedHeaderName, Encoding.ASCII.GetString(dynamicTable[0].Name)); + Assert.Equal(expectedHeaderValue, Encoding.ASCII.GetString(dynamicTable[0].Value)); + Assert.Equal(expectedHeaderName.Length + expectedHeaderValue.Length + 32, dynamicTable.Size); + } + else + { + Assert.Equal(0, dynamicTable.Count); + Assert.Equal(0, dynamicTable.Size); } } } + + public class TestHttpHeadersHandler : IHttpHeadersHandler + { + public Dictionary DecodedHeaders { get; } = new Dictionary(); + + void IHttpHeadersHandler.OnHeader(ReadOnlySpan name, ReadOnlySpan value) + { + string headerName = Encoding.ASCII.GetString(name); + string headerValue = Encoding.ASCII.GetString(value); + + DecodedHeaders[headerName] = headerValue; + } + + void IHttpHeadersHandler.OnStaticIndexedHeader(int index) + { + // Not yet implemented for HPACK. + throw new NotImplementedException(); + } + + void IHttpHeadersHandler.OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + // Not yet implemented for HPACK. + throw new NotImplementedException(); + } + + void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { } + } }