HPackDecoder performance (#23083)

This commit is contained in:
James Newton-King 2020-06-23 09:20:06 +12:00 committed by GitHub
parent efeb8508dc
commit ce80965454
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 661 additions and 287 deletions

View File

@ -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);

View File

@ -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<byte> name, ReadOnlySpan<byte> value)
{
}
public void OnHeadersComplete(bool endStream)
{
}
public void OnStaticIndexedHeader(int index)
{
}
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
{
}
}
}
}

View File

@ -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];
}
}

View File

@ -9,23 +9,22 @@ namespace System.Net.Http.HPack
{
internal static class H2StaticTable
{
// Index of status code into s_staticDecoderTable
private static readonly Dictionary<int, int> s_statusIndex = new Dictionary<int, int>
{
[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<int, int> 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[]
{

View File

@ -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<byte> segment in data)
{
DecodeInternal(segment.Span, endHeaders, handler);
DecodeInternal(segment.Span, handler);
}
CheckIncompleteHeaderBlock(endHeaders);
}
public void Decode(ReadOnlySpan<byte> data, bool endHeaders, IHttpHeadersHandler? handler)
public void Decode(ReadOnlySpan<byte> data, bool endHeaders, IHttpHeadersHandler handler)
{
DecodeInternal(data, endHeaders, handler);
DecodeInternal(data, handler);
CheckIncompleteHeaderBlock(endHeaders);
}
private void DecodeInternal(ReadOnlySpan<byte> data, bool endHeaders, IHttpHeadersHandler? handler)
private void DecodeInternal(ReadOnlySpan<byte> 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<byte> headerBytes = data.Slice(_headerNameRange.GetValueOrDefault().start, _headerNameRange.GetValueOrDefault().length);
headerBytes.CopyTo(_headerName);
_headerNameLength = headerBytes.Length;
_headerNameRange = null;
}
}
private void ParseDynamicTableSizeUpdate(ReadOnlySpan<byte> data, ref int currentIndex)
{
if (TryDecodeInteger(data, ref currentIndex, out int intResult))
{
SetDynamicHeaderTableSize(intResult);
_state = State.Ready;
}
}
private void ParseHeaderValueLength(ReadOnlySpan<byte> 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<byte> 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<byte> 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<byte> data, ref int currentIndex, IHttpHeadersHandler handler)
{
if (TryDecodeInteger(data, ref currentIndex, out int intResult))
{
OnIndexedHeaderField(intResult, handler);
}
}
private void ParseHeaderNameIndex(ReadOnlySpan<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> data, IHttpHeadersHandler handler)
{
OnString(nextState: State.Ready);
ReadOnlySpan<byte> headerNameSpan = _headerNameRange == null
? new Span<byte>(_headerName, 0, _headerNameLength)
: data.Slice(_headerNameRange.GetValueOrDefault().start, _headerNameRange.GetValueOrDefault().length);
var headerNameSpan = new Span<byte>(_headerName, 0, _headerNameLength);
var headerValueSpan = new Span<byte>(_headerValueOctets, 0, _headerValueLength);
ReadOnlySpan<byte> headerValueSpan = _headerValueRange == null
? new Span<byte>(_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<byte> 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)
{

View File

@ -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.

View File

@ -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<string, string> _decodedHeaders = new Dictionary<string, string>();
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<byte> name, ReadOnlySpan<byte> 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<byte> 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<HPackDecodingException>(() =>
_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<HPackDecodingException>(() => _decoder.Decode(new byte[] { 0x7e }, endHeaders: true, handler: this));
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _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<HPackDecodingException>(() => _decoder.Decode(new byte[] { 0x0f, 0x2f }, endHeaders: true, handler: this));
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _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<HPackDecodingException>(() => _decoder.Decode(new byte[] { 0x1f, 0x2f }, endHeaders: true, handler: this));
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _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<HPackDecodingException>(() => _decoder.Decode(data, endHeaders: true, handler: this));
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _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<HPackDecodingException>(() => _decoder.Decode(data, endHeaders: true, handler: this));
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _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<HPackDecodingException>(() =>
_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<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: this));
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _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<byte[]> _incompleteHeaderBlockData = new TheoryData<byte[]>
@ -567,9 +612,9 @@ namespace System.Net.Http.Unit.Tests.HPack
[MemberData(nameof(_incompleteHeaderBlockData))]
public void DecodesIncompleteHeaderBlock_Error(byte[] encoded)
{
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: this));
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _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<byte[]> _huffmanDecodingErrorData = new TheoryData<byte[]>
@ -601,43 +646,89 @@ namespace System.Net.Http.Unit.Tests.HPack
[MemberData(nameof(_huffmanDecodingErrorData))]
public void WrapsHuffmanDecodingExceptionInHPackDecodingException(byte[] encoded)
{
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: this));
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: _handler));
Assert.Equal(SR.net_http_hpack_huffman_decode_failed, exception.Message);
Assert.IsType<HuffmanDecodingException>(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<byte>(), 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<string, string> DecodedHeaders { get; } = new Dictionary<string, string>();
void IHttpHeadersHandler.OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> 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<byte> value)
{
// Not yet implemented for HPACK.
throw new NotImplementedException();
}
void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { }
}
}