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 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);
|
||||
|
|
|
|||
|
|
@ -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 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];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue