// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. // See THIRD-PARTY-NOTICES.TXT in the project root for license information. using System.Buffers; using System.Linq; using System.Collections.Generic; using System.Text; using System.Net.Http.HPack; using Xunit; #if KESTREL using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; #endif namespace System.Net.Http.Unit.Tests.HPack { public class HPackDecoderTests : IHttpHeadersHandler { private const int DynamicTableInitialMaxSize = 4096; private const int MaxHeaderFieldSize = 8192; // Indexed Header Field Representation - Static Table - Index 2 (:method: GET) private static readonly byte[] _indexedHeaderStatic = new byte[] { 0x82 }; // Indexed Header Field Representation - Dynamic Table - Index 62 (first index in dynamic table) private static readonly byte[] _indexedHeaderDynamic = new byte[] { 0xbe }; // Literal Header Field with Incremental Indexing Representation - New Name private static readonly byte[] _literalHeaderFieldWithIndexingNewName = new byte[] { 0x40 }; // Literal Header Field with Incremental Indexing Representation - Indexed Name - Index 58 (user-agent) private static readonly byte[] _literalHeaderFieldWithIndexingIndexedName = new byte[] { 0x7a }; // Literal Header Field without Indexing Representation - New Name private static readonly byte[] _literalHeaderFieldWithoutIndexingNewName = new byte[] { 0x00 }; // Literal Header Field without Indexing Representation - Indexed Name - Index 58 (user-agent) private static readonly byte[] _literalHeaderFieldWithoutIndexingIndexedName = new byte[] { 0x0f, 0x2b }; // Literal Header Field Never Indexed Representation - New Name private static readonly byte[] _literalHeaderFieldNeverIndexedNewName = new byte[] { 0x10 }; // Literal Header Field Never Indexed Representation - Indexed Name - Index 58 (user-agent) private static readonly byte[] _literalHeaderFieldNeverIndexedIndexedName = new byte[] { 0x1f, 0x2b }; private const string _userAgentString = "user-agent"; private static readonly byte[] _userAgentBytes = Encoding.ASCII.GetBytes(_userAgentString); private const string _headerNameString = "new-header"; private static readonly byte[] _headerNameBytes = Encoding.ASCII.GetBytes(_headerNameString); // n e w - h e a d e r * // 10101000 10111110 00010110 10011100 10100011 10010000 10110110 01111111 private static readonly byte[] _headerNameHuffmanBytes = new byte[] { 0xa8, 0xbe, 0x16, 0x9c, 0xa3, 0x90, 0xb6, 0x7f }; private const string _headerValueString = "value"; private static readonly byte[] _headerValueBytes = Encoding.ASCII.GetBytes(_headerValueString); // v a l u e * // 11101110 00111010 00101101 00101111 private static readonly byte[] _headerValueHuffmanBytes = new byte[] { 0xee, 0x3a, 0x2d, 0x2f }; private static readonly byte[] _headerName = new byte[] { (byte)_headerNameBytes.Length } .Concat(_headerNameBytes) .ToArray(); private static readonly byte[] _headerNameHuffman = new byte[] { (byte)(0x80 | _headerNameHuffmanBytes.Length) } .Concat(_headerNameHuffmanBytes) .ToArray(); private static readonly byte[] _headerValue = new byte[] { (byte)_headerValueBytes.Length } .Concat(_headerValueBytes) .ToArray(); private static readonly byte[] _headerValueHuffman = new byte[] { (byte)(0x80 | _headerValueHuffmanBytes.Length) } .Concat(_headerValueHuffmanBytes) .ToArray(); // & * // 11111000 11111111 private static readonly byte[] _huffmanLongPadding = new byte[] { 0x82, 0xf8, 0xff }; // EOS * // 11111111 11111111 11111111 11111111 private static readonly byte[] _huffmanEos = new byte[] { 0x84, 0xff, 0xff, 0xff, 0xff }; private readonly DynamicTable _dynamicTable; private readonly HPackDecoder _decoder; private readonly Dictionary _decodedHeaders = new Dictionary(); public HPackDecoderTests() { _dynamicTable = new DynamicTable(DynamicTableInitialMaxSize); _decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxHeaderFieldSize, _dynamicTable); } void IHttpHeadersHandler.OnHeader(ReadOnlySpan name, ReadOnlySpan value) { string headerName = Encoding.ASCII.GetString(name); string headerValue = Encoding.ASCII.GetString(value); _decodedHeaders[headerName] = headerValue; } void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { } [Fact] public void DecodesIndexedHeaderField_StaticTable() { _decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: this); Assert.Equal("GET", _decodedHeaders[":method"]); } [Fact] public void DecodesIndexedHeaderField_DynamicTable() { // Add the header to the dynamic table _dynamicTable.Insert(_headerNameBytes, _headerValueBytes); // Index it _decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: this); Assert.Equal(_headerValueString, _decodedHeaders[_headerNameString]); } [Fact] public void DecodesIndexedHeaderField_OutOfRange_Error() { HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: this)); Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message); Assert.Empty(_decodedHeaders); } [Fact] public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName() { byte[] encoded = _literalHeaderFieldWithIndexingNewName .Concat(_headerName) .Concat(_headerValue) .ToArray(); TestDecodeWithIndexing(encoded, _headerNameString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName_HuffmanEncodedName() { byte[] encoded = _literalHeaderFieldWithIndexingNewName .Concat(_headerNameHuffman) .Concat(_headerValue) .ToArray(); TestDecodeWithIndexing(encoded, _headerNameString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName_HuffmanEncodedValue() { byte[] encoded = _literalHeaderFieldWithIndexingNewName .Concat(_headerName) .Concat(_headerValueHuffman) .ToArray(); TestDecodeWithIndexing(encoded, _headerNameString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName_HuffmanEncodedNameAndValue() { byte[] encoded = _literalHeaderFieldWithIndexingNewName .Concat(_headerNameHuffman) .Concat(_headerValueHuffman) .ToArray(); TestDecodeWithIndexing(encoded, _headerNameString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldWithIncrementalIndexing_IndexedName() { byte[] encoded = _literalHeaderFieldWithIndexingIndexedName .Concat(_headerValue) .ToArray(); TestDecodeWithIndexing(encoded, _userAgentString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldWithIncrementalIndexing_IndexedName_HuffmanEncodedValue() { byte[] encoded = _literalHeaderFieldWithIndexingIndexedName .Concat(_headerValueHuffman) .ToArray(); TestDecodeWithIndexing(encoded, _userAgentString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldWithIncrementalIndexing_IndexedName_OutOfRange_Error() { // 01 (Literal Header Field without Indexing 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. HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x7e }, endHeaders: true, handler: this)); Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message); Assert.Empty(_decodedHeaders); } [Fact] public void DecodesLiteralHeaderFieldWithoutIndexing_NewName() { byte[] encoded = _literalHeaderFieldWithoutIndexingNewName .Concat(_headerName) .Concat(_headerValue) .ToArray(); TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedName() { byte[] encoded = _literalHeaderFieldWithoutIndexingNewName .Concat(_headerNameHuffman) .Concat(_headerValue) .ToArray(); TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedValue() { byte[] encoded = _literalHeaderFieldWithoutIndexingNewName .Concat(_headerName) .Concat(_headerValueHuffman) .ToArray(); TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedNameAndValue() { byte[] encoded = _literalHeaderFieldWithoutIndexingNewName .Concat(_headerNameHuffman) .Concat(_headerValueHuffman) .ToArray(); TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldWithoutIndexing_IndexedName() { byte[] encoded = _literalHeaderFieldWithoutIndexingIndexedName .Concat(_headerValue) .ToArray(); TestDecodeWithoutIndexing(encoded, _userAgentString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldWithoutIndexing_IndexedName_HuffmanEncodedValue() { byte[] encoded = _literalHeaderFieldWithoutIndexingIndexedName .Concat(_headerValueHuffman) .ToArray(); TestDecodeWithoutIndexing(encoded, _userAgentString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldWithoutIndexing_IndexedName_OutOfRange_Error() { // 0000 (Literal Header Field without Indexing 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. HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x0f, 0x2f }, endHeaders: true, handler: this)); Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message); Assert.Empty(_decodedHeaders); } [Fact] public void DecodesLiteralHeaderFieldNeverIndexed_NewName() { byte[] encoded = _literalHeaderFieldNeverIndexedNewName .Concat(_headerName) .Concat(_headerValue) .ToArray(); TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedName() { byte[] encoded = _literalHeaderFieldNeverIndexedNewName .Concat(_headerNameHuffman) .Concat(_headerValue) .ToArray(); TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedValue() { byte[] encoded = _literalHeaderFieldNeverIndexedNewName .Concat(_headerName) .Concat(_headerValueHuffman) .ToArray(); TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedNameAndValue() { byte[] encoded = _literalHeaderFieldNeverIndexedNewName .Concat(_headerNameHuffman) .Concat(_headerValueHuffman) .ToArray(); TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldNeverIndexed_IndexedName() { // 0001 (Literal Header Field Never Indexed Representation) // 1111 0010 1011 (Indexed Name - Index 58 encoded with 4-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) // Concatenated with value bytes byte[] encoded = _literalHeaderFieldNeverIndexedIndexedName .Concat(_headerValue) .ToArray(); TestDecodeWithoutIndexing(encoded, _userAgentString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldNeverIndexed_IndexedName_HuffmanEncodedValue() { // 0001 (Literal Header Field Never Indexed Representation) // 1111 0010 1011 (Indexed Name - Index 58 encoded with 4-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) // Concatenated with Huffman encoded value bytes byte[] encoded = _literalHeaderFieldNeverIndexedIndexedName .Concat(_headerValueHuffman) .ToArray(); TestDecodeWithoutIndexing(encoded, _userAgentString, _headerValueString); } [Fact] public void DecodesLiteralHeaderFieldNeverIndexed_IndexedName_OutOfRange_Error() { // 0001 (Literal Header Field Never Indexed 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. HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x1f, 0x2f }, endHeaders: true, handler: this)); Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message); Assert.Empty(_decodedHeaders); } [Fact] public void DecodesDynamicTableSizeUpdate() { // 001 (Dynamic Table Size Update) // 11110 (30 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); _decoder.Decode(new byte[] { 0x3e }, endHeaders: true, handler: this); Assert.Equal(30, _dynamicTable.MaxSize); Assert.Empty(_decodedHeaders); } [Fact] public void DecodesDynamicTableSizeUpdate_AfterIndexedHeaderStatic_Error() { // 001 (Dynamic Table Size Update) // 11110 (30 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); byte[] data = _indexedHeaderStatic.Concat(new byte[] { 0x3e }).ToArray(); HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(data, endHeaders: true, handler: this)); Assert.Equal(SR.net_http_hpack_late_dynamic_table_size_update, exception.Message); } [Fact] public void DecodesDynamicTableSizeUpdate_AfterIndexedHeaderStatic_SubsequentDecodeCall_Error() { Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); _decoder.Decode(_indexedHeaderStatic, endHeaders: false, handler: this); Assert.Equal("GET", _decodedHeaders[":method"]); // 001 (Dynamic Table Size Update) // 11110 (30 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) byte[] data = new byte[] { 0x3e }; HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(data, endHeaders: true, handler: this)); Assert.Equal(SR.net_http_hpack_late_dynamic_table_size_update, exception.Message); } [Fact] public void DecodesDynamicTableSizeUpdate_AfterIndexedHeaderStatic_ResetAfterEndHeaders_Succeeds() { Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); _decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: this); Assert.Equal("GET", _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); Assert.Equal(30, _dynamicTable.MaxSize); } [Fact] public void DecodesDynamicTableSizeUpdate_GreaterThanLimit_Error() { // 001 (Dynamic Table Size Update) // 11111 11100010 00011111 (4097 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x3f, 0xe2, 0x1f }, endHeaders: true, handler: this)); Assert.Equal(SR.Format(SR.net_http_hpack_large_table_size_update, 4097, DynamicTableInitialMaxSize), exception.Message); Assert.Empty(_decodedHeaders); } [Fact] public void DecodesStringLength_GreaterThanLimit_Error() { byte[] encoded = _literalHeaderFieldWithoutIndexingNewName .Concat(new byte[] { 0xff, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix .ToArray(); HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(encoded, endHeaders: true, handler: this)); Assert.Equal(SR.Format(SR.net_http_headers_exceeded_length, MaxHeaderFieldSize), exception.Message); Assert.Empty(_decodedHeaders); } [Fact] public void DecodesStringLength_LimitConfigurable() { 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(); decoder.Decode(encoded, endHeaders: true, handler: this); Assert.Equal(string8193, _decodedHeaders[string8193]); } public static readonly TheoryData _incompleteHeaderBlockData = new TheoryData { // Indexed Header Field Representation - incomplete index encoding new byte[] { 0xff }, // Literal Header Field with Incremental Indexing Representation - New Name - incomplete header name length encoding new byte[] { 0x40, 0x7f }, // Literal Header Field with Incremental Indexing Representation - New Name - incomplete header name new byte[] { 0x40, 0x01 }, new byte[] { 0x40, 0x02, 0x61 }, // Literal Header Field with Incremental Indexing Representation - New Name - incomplete header value length encoding new byte[] { 0x40, 0x01, 0x61, 0x7f }, // Literal Header Field with Incremental Indexing Representation - New Name - incomplete header value new byte[] { 0x40, 0x01, 0x61, 0x01 }, new byte[] { 0x40, 0x01, 0x61, 0x02, 0x61 }, // Literal Header Field with Incremental Indexing Representation - Indexed Name - incomplete index encoding new byte[] { 0x7f }, // Literal Header Field with Incremental Indexing Representation - Indexed Name - incomplete header value length encoding new byte[] { 0x7a, 0xff }, // Literal Header Field with Incremental Indexing Representation - Indexed Name - incomplete header value new byte[] { 0x7a, 0x01 }, new byte[] { 0x7a, 0x02, 0x61 }, // Literal Header Field without Indexing - New Name - incomplete header name length encoding new byte[] { 0x00, 0xff }, // Literal Header Field without Indexing - New Name - incomplete header name new byte[] { 0x00, 0x01 }, new byte[] { 0x00, 0x02, 0x61 }, // Literal Header Field without Indexing - New Name - incomplete header value length encoding new byte[] { 0x00, 0x01, 0x61, 0xff }, // Literal Header Field without Indexing - New Name - incomplete header value new byte[] { 0x00, 0x01, 0x61, 0x01 }, new byte[] { 0x00, 0x01, 0x61, 0x02, 0x61 }, // Literal Header Field without Indexing Representation - Indexed Name - incomplete index encoding new byte[] { 0x0f }, // Literal Header Field without Indexing Representation - Indexed Name - incomplete header value length encoding new byte[] { 0x02, 0xff }, // Literal Header Field without Indexing Representation - Indexed Name - incomplete header value new byte[] { 0x02, 0x01 }, new byte[] { 0x02, 0x02, 0x61 }, // Literal Header Field Never Indexed - New Name - incomplete header name length encoding new byte[] { 0x10, 0xff }, // Literal Header Field Never Indexed - New Name - incomplete header name new byte[] { 0x10, 0x01 }, new byte[] { 0x10, 0x02, 0x61 }, // Literal Header Field Never Indexed - New Name - incomplete header value length encoding new byte[] { 0x10, 0x01, 0x61, 0xff }, // Literal Header Field Never Indexed - New Name - incomplete header value new byte[] { 0x10, 0x01, 0x61, 0x01 }, new byte[] { 0x10, 0x01, 0x61, 0x02, 0x61 }, // Literal Header Field Never Indexed Representation - Indexed Name - incomplete index encoding new byte[] { 0x1f }, // Literal Header Field Never Indexed Representation - Indexed Name - incomplete header value length encoding new byte[] { 0x12, 0xff }, // Literal Header Field Never Indexed Representation - Indexed Name - incomplete header value new byte[] { 0x12, 0x01 }, new byte[] { 0x12, 0x02, 0x61 }, // Dynamic Table Size Update - incomplete max size encoding new byte[] { 0x3f } }; [Theory] [MemberData(nameof(_incompleteHeaderBlockData))] public void DecodesIncompleteHeaderBlock_Error(byte[] encoded) { HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(encoded, endHeaders: true, handler: this)); Assert.Equal(SR.net_http_hpack_incomplete_header_block, exception.Message); Assert.Empty(_decodedHeaders); } public static readonly TheoryData _huffmanDecodingErrorData = new TheoryData { // Invalid Huffman encoding in header name _literalHeaderFieldWithIndexingNewName.Concat(_huffmanLongPadding).ToArray(), _literalHeaderFieldWithIndexingNewName.Concat(_huffmanEos).ToArray(), _literalHeaderFieldWithoutIndexingNewName.Concat(_huffmanLongPadding).ToArray(), _literalHeaderFieldWithoutIndexingNewName.Concat(_huffmanEos).ToArray(), _literalHeaderFieldNeverIndexedNewName.Concat(_huffmanLongPadding).ToArray(), _literalHeaderFieldNeverIndexedNewName.Concat(_huffmanEos).ToArray(), // Invalid Huffman encoding in header value _literalHeaderFieldWithIndexingIndexedName.Concat(_huffmanLongPadding).ToArray(), _literalHeaderFieldWithIndexingIndexedName.Concat(_huffmanEos).ToArray(), _literalHeaderFieldWithoutIndexingIndexedName.Concat(_huffmanLongPadding).ToArray(), _literalHeaderFieldWithoutIndexingIndexedName.Concat(_huffmanEos).ToArray(), _literalHeaderFieldNeverIndexedIndexedName.Concat(_huffmanLongPadding).ToArray(), _literalHeaderFieldNeverIndexedIndexedName.Concat(_huffmanEos).ToArray() }; [Theory] [MemberData(nameof(_huffmanDecodingErrorData))] public void WrapsHuffmanDecodingExceptionInHPackDecodingException(byte[] encoded) { HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(encoded, endHeaders: true, handler: this)); Assert.Equal(SR.net_http_hpack_huffman_decode_failed, exception.Message); Assert.IsType(exception.InnerException); Assert.Empty(_decodedHeaders); } private void TestDecodeWithIndexing(byte[] encoded, string expectedHeaderName, string expectedHeaderValue) { TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: true); } private void TestDecodeWithoutIndexing(byte[] encoded, string expectedHeaderName, string expectedHeaderValue) { TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: false); } private void TestDecode(byte[] encoded, string expectedHeaderName, string expectedHeaderValue, bool expectDynamicTableEntry) { Assert.Equal(0, _dynamicTable.Count); Assert.Equal(0, _dynamicTable.Size); _decoder.Decode(encoded, endHeaders: true, handler: this); Assert.Equal(expectedHeaderValue, _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); } } } }