HPackDecoder performance (#23083)
This commit is contained in:
parent
efeb8508dc
commit
ce80965454
|
|
@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
case 404:
|
case 404:
|
||||||
case 500:
|
case 500:
|
||||||
// Status codes which exist in the HTTP/2 StaticTable.
|
// 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:
|
default:
|
||||||
const string name = ":status";
|
const string name = ":status";
|
||||||
var value = StatusCodes.ToStatusString(statusCode);
|
var value = StatusCodes.ToStatusString(statusCode);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ namespace System.Net.Http.HPack
|
||||||
|
|
||||||
public int MaxSize => _maxSize;
|
public int MaxSize => _maxSize;
|
||||||
|
|
||||||
public HeaderField this[int index]
|
public ref readonly HeaderField this[int index]
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
|
@ -42,7 +42,7 @@ namespace System.Net.Http.HPack
|
||||||
index += _buffer.Length;
|
index += _buffer.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _buffer[index];
|
return ref _buffer[index];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,23 +9,22 @@ namespace System.Net.Http.HPack
|
||||||
{
|
{
|
||||||
internal static class H2StaticTable
|
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 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[]
|
private static readonly HeaderField[] s_staticDecoderTable = new HeaderField[]
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Numerics;
|
||||||
#if KESTREL
|
#if KESTREL
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -37,7 +38,6 @@ namespace System.Net.Http.HPack
|
||||||
// | 1 | Index (7+) |
|
// | 1 | Index (7+) |
|
||||||
// +---+---------------------------+
|
// +---+---------------------------+
|
||||||
private const byte IndexedHeaderFieldMask = 0x80;
|
private const byte IndexedHeaderFieldMask = 0x80;
|
||||||
private const byte IndexedHeaderFieldRepresentation = 0x80;
|
|
||||||
|
|
||||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1
|
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1
|
||||||
// 0 1 2 3 4 5 6 7
|
// 0 1 2 3 4 5 6 7
|
||||||
|
|
@ -45,7 +45,6 @@ namespace System.Net.Http.HPack
|
||||||
// | 0 | 1 | Index (6+) |
|
// | 0 | 1 | Index (6+) |
|
||||||
// +---+---+-----------------------+
|
// +---+---+-----------------------+
|
||||||
private const byte LiteralHeaderFieldWithIncrementalIndexingMask = 0xc0;
|
private const byte LiteralHeaderFieldWithIncrementalIndexingMask = 0xc0;
|
||||||
private const byte LiteralHeaderFieldWithIncrementalIndexingRepresentation = 0x40;
|
|
||||||
|
|
||||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.2
|
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.2
|
||||||
// 0 1 2 3 4 5 6 7
|
// 0 1 2 3 4 5 6 7
|
||||||
|
|
@ -53,7 +52,6 @@ namespace System.Net.Http.HPack
|
||||||
// | 0 | 0 | 0 | 0 | Index (4+) |
|
// | 0 | 0 | 0 | 0 | Index (4+) |
|
||||||
// +---+---+-----------------------+
|
// +---+---+-----------------------+
|
||||||
private const byte LiteralHeaderFieldWithoutIndexingMask = 0xf0;
|
private const byte LiteralHeaderFieldWithoutIndexingMask = 0xf0;
|
||||||
private const byte LiteralHeaderFieldWithoutIndexingRepresentation = 0x00;
|
|
||||||
|
|
||||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.3
|
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.3
|
||||||
// 0 1 2 3 4 5 6 7
|
// 0 1 2 3 4 5 6 7
|
||||||
|
|
@ -61,7 +59,6 @@ namespace System.Net.Http.HPack
|
||||||
// | 0 | 0 | 0 | 1 | Index (4+) |
|
// | 0 | 0 | 0 | 1 | Index (4+) |
|
||||||
// +---+---+-----------------------+
|
// +---+---+-----------------------+
|
||||||
private const byte LiteralHeaderFieldNeverIndexedMask = 0xf0;
|
private const byte LiteralHeaderFieldNeverIndexedMask = 0xf0;
|
||||||
private const byte LiteralHeaderFieldNeverIndexedRepresentation = 0x10;
|
|
||||||
|
|
||||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.3
|
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.3
|
||||||
// 0 1 2 3 4 5 6 7
|
// 0 1 2 3 4 5 6 7
|
||||||
|
|
@ -69,7 +66,6 @@ namespace System.Net.Http.HPack
|
||||||
// | 0 | 0 | 1 | Max size (5+) |
|
// | 0 | 0 | 1 | Max size (5+) |
|
||||||
// +---+---------------------------+
|
// +---+---------------------------+
|
||||||
private const byte DynamicTableSizeUpdateMask = 0xe0;
|
private const byte DynamicTableSizeUpdateMask = 0xe0;
|
||||||
private const byte DynamicTableSizeUpdateRepresentation = 0x20;
|
|
||||||
|
|
||||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.5.2
|
// http://httpwg.org/specs/rfc7541.html#rfc.section.5.2
|
||||||
// 0 1 2 3 4 5 6 7
|
// 0 1 2 3 4 5 6 7
|
||||||
|
|
@ -92,6 +88,8 @@ namespace System.Net.Http.HPack
|
||||||
private byte[] _stringOctets;
|
private byte[] _stringOctets;
|
||||||
private byte[] _headerNameOctets;
|
private byte[] _headerNameOctets;
|
||||||
private byte[] _headerValueOctets;
|
private byte[] _headerValueOctets;
|
||||||
|
private (int start, int length)? _headerNameRange;
|
||||||
|
private (int start, int length)? _headerValueRange;
|
||||||
|
|
||||||
private State _state = State.Ready;
|
private State _state = State.Ready;
|
||||||
private byte[]? _headerName;
|
private byte[]? _headerName;
|
||||||
|
|
@ -124,107 +122,247 @@ namespace System.Net.Http.HPack
|
||||||
{
|
{
|
||||||
foreach (ReadOnlyMemory<byte> segment in data)
|
foreach (ReadOnlyMemory<byte> segment in data)
|
||||||
{
|
{
|
||||||
DecodeInternal(segment.Span, endHeaders, handler);
|
DecodeInternal(segment.Span, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckIncompleteHeaderBlock(endHeaders);
|
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);
|
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)
|
switch (_state)
|
||||||
{
|
{
|
||||||
case State.Ready:
|
case State.Ready:
|
||||||
// TODO: Instead of masking and comparing each prefix value,
|
Parse(data, ref currentIndex, handler);
|
||||||
// consider doing a 16-way switch on the first four bits (which is the max prefix size).
|
break;
|
||||||
// Look at this once we have more concrete perf data.
|
case State.HeaderFieldIndex:
|
||||||
if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldRepresentation)
|
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;
|
_headersObserved = true;
|
||||||
|
|
||||||
int val = b & ~IndexedHeaderFieldMask;
|
int val = b & ~IndexedHeaderFieldMask;
|
||||||
|
|
||||||
if (_integerDecoder.BeginTryDecode((byte)val, IndexedHeaderFieldPrefix, out intResult))
|
if (_integerDecoder.BeginTryDecode((byte)val, IndexedHeaderFieldPrefix, out int intResult))
|
||||||
{
|
{
|
||||||
OnIndexedHeaderField(intResult, handler);
|
OnIndexedHeaderField(intResult, handler);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_state = State.HeaderFieldIndex;
|
_state = State.HeaderFieldIndex;
|
||||||
|
ParseHeaderFieldIndex(data, ref currentIndex, handler);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
else if ((b & LiteralHeaderFieldWithIncrementalIndexingMask) == LiteralHeaderFieldWithIncrementalIndexingRepresentation)
|
case 1: // Literal Header Field with Incremental Indexing
|
||||||
{
|
ParseLiteralHeaderField(
|
||||||
_headersObserved = true;
|
data,
|
||||||
|
ref currentIndex,
|
||||||
_index = true;
|
b,
|
||||||
int val = b & ~LiteralHeaderFieldWithIncrementalIndexingMask;
|
LiteralHeaderFieldWithIncrementalIndexingMask,
|
||||||
|
LiteralHeaderFieldWithIncrementalIndexingPrefix,
|
||||||
if (val == 0)
|
index: true,
|
||||||
{
|
handler);
|
||||||
_state = State.HeaderNameLength;
|
break;
|
||||||
}
|
case 4:
|
||||||
else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithIncrementalIndexingPrefix, out intResult))
|
default: // Literal Header Field without Indexing
|
||||||
{
|
ParseLiteralHeaderField(
|
||||||
OnIndexedHeaderName(intResult);
|
data,
|
||||||
}
|
ref currentIndex,
|
||||||
else
|
b,
|
||||||
{
|
LiteralHeaderFieldWithoutIndexingMask,
|
||||||
_state = State.HeaderNameIndex;
|
LiteralHeaderFieldWithoutIndexingPrefix,
|
||||||
}
|
index: false,
|
||||||
}
|
handler);
|
||||||
else if ((b & LiteralHeaderFieldWithoutIndexingMask) == LiteralHeaderFieldWithoutIndexingRepresentation)
|
break;
|
||||||
{
|
case 3: // Literal Header Field Never Indexed
|
||||||
_headersObserved = true;
|
ParseLiteralHeaderField(
|
||||||
|
data,
|
||||||
_index = false;
|
ref currentIndex,
|
||||||
int val = b & ~LiteralHeaderFieldWithoutIndexingMask;
|
b,
|
||||||
|
LiteralHeaderFieldNeverIndexedMask,
|
||||||
if (val == 0)
|
LiteralHeaderFieldNeverIndexedPrefix,
|
||||||
{
|
index: false,
|
||||||
_state = State.HeaderNameLength;
|
handler);
|
||||||
}
|
break;
|
||||||
else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithoutIndexingPrefix, out intResult))
|
case 2: // Dynamic Table Size Update
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
// https://tools.ietf.org/html/rfc7541#section-4.2
|
// https://tools.ietf.org/html/rfc7541#section-4.2
|
||||||
// This dynamic table size
|
// 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);
|
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);
|
SetDynamicHeaderTableSize(intResult);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_state = State.DynamicTableSizeUpdate;
|
_state = State.DynamicTableSizeUpdate;
|
||||||
|
ParseDynamicTableSizeUpdate(data, ref currentIndex);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
{
|
}
|
||||||
// Can't happen
|
}
|
||||||
Debug.Fail("Unreachable code");
|
|
||||||
throw new InvalidOperationException("Unreachable code.");
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
private void ParseLiteralHeaderField(ReadOnlySpan<byte> data, ref int currentIndex, byte b, byte mask, byte indexPrefix, bool index, IHttpHeadersHandler handler)
|
||||||
case State.HeaderFieldIndex:
|
{
|
||||||
if (_integerDecoder.TryDecode(b, out intResult))
|
_headersObserved = true;
|
||||||
{
|
|
||||||
OnIndexedHeaderField(intResult, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
_index = index;
|
||||||
case State.HeaderNameIndex:
|
int val = b & ~mask;
|
||||||
if (_integerDecoder.TryDecode(b, out intResult))
|
|
||||||
{
|
|
||||||
OnIndexedHeaderName(intResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
if (val == 0)
|
||||||
case State.HeaderNameLength:
|
{
|
||||||
_huffman = (b & HuffmanMask) != 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))
|
private void ParseHeaderName(ReadOnlySpan<byte> data, ref int currentIndex, IHttpHeadersHandler handler)
|
||||||
{
|
{
|
||||||
if (intResult == 0)
|
// Read remaining chars, up to the length of the current data
|
||||||
{
|
int count = Math.Min(_stringLength - _stringIndex, data.Length - currentIndex);
|
||||||
throw new HPackDecodingException(SR.Format(SR.net_http_invalid_header_name, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
OnStringLength(intResult, nextState: State.HeaderName);
|
// Check whether the whole string is available in the data and no decompression required.
|
||||||
}
|
// If string is good then mark its range.
|
||||||
else
|
// NOTE: it may need to be copied to buffer later the if value is not current data.
|
||||||
{
|
if (count == _stringLength && !_huffman)
|
||||||
_state = State.HeaderNameLengthContinue;
|
{
|
||||||
}
|
// Fast path. Store the range rather than copying.
|
||||||
|
_headerNameRange = (start: currentIndex, count);
|
||||||
|
currentIndex += count;
|
||||||
|
|
||||||
break;
|
_state = State.HeaderValueLength;
|
||||||
case State.HeaderNameLengthContinue:
|
}
|
||||||
if (_integerDecoder.TryDecode(b, out intResult))
|
else
|
||||||
{
|
{
|
||||||
// IntegerDecoder disallows overlong encodings, where an integer is encoded with more bytes than is strictly required.
|
// Copy string to temporary buffer.
|
||||||
// 0 should always be represented by a single byte, so we shouldn't need to check for it in the continuation case.
|
// _stringOctets was already
|
||||||
Debug.Assert(intResult != 0, "A header name length of 0 should never be encoded with a continuation byte.");
|
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;
|
private void ParseHeaderValue(ReadOnlySpan<byte> data, ref int currentIndex, IHttpHeadersHandler handler)
|
||||||
case State.HeaderName:
|
{
|
||||||
_stringOctets[_stringIndex++] = b;
|
// Read remaining chars, up to the length of the current data
|
||||||
|
int count = Math.Min(_stringLength - _stringIndex, data.Length - currentIndex);
|
||||||
|
|
||||||
if (_stringIndex == _stringLength)
|
// Check whether the whole string is available in the data and no decompressed required.
|
||||||
{
|
// If string is good then mark its range.
|
||||||
OnString(nextState: State.HeaderValueLength);
|
if (count == _stringLength && !_huffman)
|
||||||
}
|
{
|
||||||
|
// Fast path. Store the range rather than copying.
|
||||||
|
_headerValueRange = (start: currentIndex, count);
|
||||||
|
currentIndex += count;
|
||||||
|
|
||||||
break;
|
_state = State.Ready;
|
||||||
case State.HeaderValueLength:
|
ProcessHeaderValue(data, handler);
|
||||||
_huffman = (b & HuffmanMask) != 0;
|
}
|
||||||
|
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))
|
if (_stringIndex == _stringLength)
|
||||||
{
|
{
|
||||||
OnStringLength(intResult, nextState: State.HeaderValue);
|
OnString(nextState: State.Ready);
|
||||||
|
ProcessHeaderValue(data, handler);
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
ReadOnlySpan<byte> headerValueSpan = _headerValueRange == null
|
||||||
var headerValueSpan = new Span<byte>(_headerValueOctets, 0, _headerValueLength);
|
? 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)
|
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);
|
ref readonly HeaderField header = ref GetHeader(index);
|
||||||
handler?.OnHeader(header.Name, header.Value);
|
handler.OnHeader(header.Name, header.Value);
|
||||||
_state = State.Ready;
|
_state = State.Ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnIndexedHeaderName(int index)
|
private void OnIndexedHeaderName(int index)
|
||||||
{
|
{
|
||||||
HeaderField header = GetHeader(index);
|
_headerName = GetHeader(index).Name;
|
||||||
_headerName = header.Name;
|
_headerNameLength = _headerName.Length;
|
||||||
_headerNameLength = header.Name.Length;
|
|
||||||
_state = State.HeaderValueLength;
|
_state = State.HeaderValueLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -437,11 +562,7 @@ namespace System.Net.Http.HPack
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (dst.Length < _stringLength)
|
EnsureStringCapacity(ref dst);
|
||||||
{
|
|
||||||
dst = new byte[Math.Max(_stringLength, dst.Length * 2)];
|
|
||||||
}
|
|
||||||
|
|
||||||
Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength);
|
Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength);
|
||||||
return _stringLength;
|
return _stringLength;
|
||||||
}
|
}
|
||||||
|
|
@ -467,13 +588,41 @@ namespace System.Net.Http.HPack
|
||||||
_state = nextState;
|
_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
|
try
|
||||||
{
|
{
|
||||||
return index <= H2StaticTable.Count
|
return ref index <= H2StaticTable.Count
|
||||||
? H2StaticTable.Get(index - 1)
|
? ref H2StaticTable.Get(index - 1)
|
||||||
: _dynamicTable[index - H2StaticTable.Count - 1];
|
: ref _dynamicTable[index - H2StaticTable.Count - 1];
|
||||||
}
|
}
|
||||||
catch (IndexOutOfRangeException)
|
catch (IndexOutOfRangeException)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ namespace System.Net.Http.HPack
|
||||||
case 404:
|
case 404:
|
||||||
case 500:
|
case 500:
|
||||||
// Status codes which exist in the HTTP/2 StaticTable.
|
// 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:
|
default:
|
||||||
// If the status code doesn't have a static index then we need to include the full value.
|
// 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.
|
// Write a status index and then the number bytes as a string literal.
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||||
|
|
||||||
namespace System.Net.Http.Unit.Tests.HPack
|
namespace System.Net.Http.Unit.Tests.HPack
|
||||||
{
|
{
|
||||||
public class HPackDecoderTests : IHttpHeadersHandler
|
public class HPackDecoderTests
|
||||||
{
|
{
|
||||||
private const int DynamicTableInitialMaxSize = 4096;
|
private const int DynamicTableInitialMaxSize = 4096;
|
||||||
private const int MaxHeaderFieldSize = 8192;
|
private const int MaxHeaderFieldSize = 8192;
|
||||||
|
|
@ -89,42 +89,26 @@ namespace System.Net.Http.Unit.Tests.HPack
|
||||||
|
|
||||||
private readonly DynamicTable _dynamicTable;
|
private readonly DynamicTable _dynamicTable;
|
||||||
private readonly HPackDecoder _decoder;
|
private readonly HPackDecoder _decoder;
|
||||||
|
private readonly TestHttpHeadersHandler _handler = new TestHttpHeadersHandler();
|
||||||
private readonly Dictionary<string, string> _decodedHeaders = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
public HPackDecoderTests()
|
public HPackDecoderTests()
|
||||||
{
|
{
|
||||||
_dynamicTable = new DynamicTable(DynamicTableInitialMaxSize);
|
(_dynamicTable, _decoder) = CreateDecoderAndTable();
|
||||||
_decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxHeaderFieldSize, _dynamicTable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void IHttpHeadersHandler.OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
private static (DynamicTable, HPackDecoder) CreateDecoderAndTable()
|
||||||
{
|
{
|
||||||
string headerName = Encoding.ASCII.GetString(name);
|
var dynamicTable = new DynamicTable(DynamicTableInitialMaxSize);
|
||||||
string headerValue = Encoding.ASCII.GetString(value);
|
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]
|
[Fact]
|
||||||
public void DecodesIndexedHeaderField_StaticTable()
|
public void DecodesIndexedHeaderField_StaticTable()
|
||||||
{
|
{
|
||||||
_decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: this);
|
_decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: _handler);
|
||||||
Assert.Equal("GET", _decodedHeaders[":method"]);
|
Assert.Equal("GET", _handler.DecodedHeaders[":method"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -134,17 +118,17 @@ namespace System.Net.Http.Unit.Tests.HPack
|
||||||
_dynamicTable.Insert(_headerNameBytes, _headerValueBytes);
|
_dynamicTable.Insert(_headerNameBytes, _headerValueBytes);
|
||||||
|
|
||||||
// Index it
|
// Index it
|
||||||
_decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: this);
|
_decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: _handler);
|
||||||
Assert.Equal(_headerValueString, _decodedHeaders[_headerNameString]);
|
Assert.Equal(_headerValueString, _handler.DecodedHeaders[_headerNameString]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DecodesIndexedHeaderField_OutOfRange_Error()
|
public void DecodesIndexedHeaderField_OutOfRange_Error()
|
||||||
{
|
{
|
||||||
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() =>
|
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.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message);
|
||||||
Assert.Empty(_decodedHeaders);
|
Assert.Empty(_handler.DecodedHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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)
|
// 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.
|
// 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.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message);
|
||||||
Assert.Empty(_decodedHeaders);
|
Assert.Empty(_handler.DecodedHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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)
|
// 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.
|
// 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.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message);
|
||||||
Assert.Empty(_decodedHeaders);
|
Assert.Empty(_handler.DecodedHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -310,6 +294,19 @@ namespace System.Net.Http.Unit.Tests.HPack
|
||||||
TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
|
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]
|
[Fact]
|
||||||
public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedName()
|
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)
|
// 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.
|
// 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.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message);
|
||||||
Assert.Empty(_decodedHeaders);
|
Assert.Empty(_handler.DecodedHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -389,10 +386,10 @@ namespace System.Net.Http.Unit.Tests.HPack
|
||||||
|
|
||||||
Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
|
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.Equal(30, _dynamicTable.MaxSize);
|
||||||
Assert.Empty(_decodedHeaders);
|
Assert.Empty(_handler.DecodedHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -404,7 +401,7 @@ namespace System.Net.Http.Unit.Tests.HPack
|
||||||
Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
|
Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
|
||||||
|
|
||||||
byte[] data = _indexedHeaderStatic.Concat(new byte[] { 0x3e }).ToArray();
|
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);
|
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);
|
Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
|
||||||
|
|
||||||
_decoder.Decode(_indexedHeaderStatic, endHeaders: false, handler: this);
|
_decoder.Decode(_indexedHeaderStatic, endHeaders: false, handler: _handler);
|
||||||
Assert.Equal("GET", _decodedHeaders[":method"]);
|
Assert.Equal("GET", _handler.DecodedHeaders[":method"]);
|
||||||
|
|
||||||
// 001 (Dynamic Table Size Update)
|
// 001 (Dynamic Table Size Update)
|
||||||
// 11110 (30 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation)
|
// 11110 (30 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation)
|
||||||
byte[] data = new byte[] { 0x3e };
|
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);
|
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);
|
Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
|
||||||
|
|
||||||
_decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: this);
|
_decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: _handler);
|
||||||
Assert.Equal("GET", _decodedHeaders[":method"]);
|
Assert.Equal("GET", _handler.DecodedHeaders[":method"]);
|
||||||
|
|
||||||
// 001 (Dynamic Table Size Update)
|
// 001 (Dynamic Table Size Update)
|
||||||
// 11110 (30 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation)
|
// 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);
|
Assert.Equal(30, _dynamicTable.MaxSize);
|
||||||
}
|
}
|
||||||
|
|
@ -447,9 +444,9 @@ namespace System.Net.Http.Unit.Tests.HPack
|
||||||
Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
|
Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
|
||||||
|
|
||||||
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() =>
|
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.Equal(SR.Format(SR.net_http_hpack_large_table_size_update, 4097, DynamicTableInitialMaxSize), exception.Message);
|
||||||
Assert.Empty(_decodedHeaders);
|
Assert.Empty(_handler.DecodedHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -459,9 +456,9 @@ namespace System.Net.Http.Unit.Tests.HPack
|
||||||
.Concat(new byte[] { 0xff, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix
|
.Concat(new byte[] { 0xff, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix
|
||||||
.ToArray();
|
.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.Equal(SR.Format(SR.net_http_headers_exceeded_length, MaxHeaderFieldSize), exception.Message);
|
||||||
Assert.Empty(_decodedHeaders);
|
Assert.Empty(_handler.DecodedHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -477,9 +474,57 @@ namespace System.Net.Http.Unit.Tests.HPack
|
||||||
.Concat(Encoding.ASCII.GetBytes(string8193))
|
.Concat(Encoding.ASCII.GetBytes(string8193))
|
||||||
.ToArray();
|
.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[]>
|
public static readonly TheoryData<byte[]> _incompleteHeaderBlockData = new TheoryData<byte[]>
|
||||||
|
|
@ -567,9 +612,9 @@ namespace System.Net.Http.Unit.Tests.HPack
|
||||||
[MemberData(nameof(_incompleteHeaderBlockData))]
|
[MemberData(nameof(_incompleteHeaderBlockData))]
|
||||||
public void DecodesIncompleteHeaderBlock_Error(byte[] encoded)
|
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.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[]>
|
public static readonly TheoryData<byte[]> _huffmanDecodingErrorData = new TheoryData<byte[]>
|
||||||
|
|
@ -601,43 +646,89 @@ namespace System.Net.Http.Unit.Tests.HPack
|
||||||
[MemberData(nameof(_huffmanDecodingErrorData))]
|
[MemberData(nameof(_huffmanDecodingErrorData))]
|
||||||
public void WrapsHuffmanDecodingExceptionInHPackDecodingException(byte[] encoded)
|
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.Equal(SR.net_http_hpack_huffman_decode_failed, exception.Message);
|
||||||
Assert.IsType<HuffmanDecodingException>(exception.InnerException);
|
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);
|
var (dynamicTable, decoder) = CreateDecoderAndTable();
|
||||||
Assert.Equal(0, _dynamicTable.Size);
|
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 (!byteAtATime)
|
||||||
|
|
||||||
if (expectDynamicTableEntry)
|
|
||||||
{
|
{
|
||||||
Assert.Equal(1, _dynamicTable.Count);
|
decoder.Decode(encoded, endHeaders: true, handler: handler);
|
||||||
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
|
else
|
||||||
{
|
{
|
||||||
Assert.Equal(0, _dynamicTable.Count);
|
// Parse data in 1 byte chunks, separated by empty chunks
|
||||||
Assert.Equal(0, _dynamicTable.Size);
|
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) { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue