File: runtime\Http2\HPackDecoderTest.cs
Web Access
Project: src\src\Shared\test\Shared.Tests\Microsoft.AspNetCore.Shared.Tests.csproj (Microsoft.AspNetCore.Shared.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.HPack;
using System.Text;
using Xunit;
#if KESTREL
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
#endif
 
namespace System.Net.Http.Unit.Tests.HPack
{
    public class HPackDecoderTests
    {
        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 const string _headerNameString = "new-header";
 
        // On purpose longer than 4096 (DefaultStringOctetsSize from HPackDecoder) to trigger https://github.com/dotnet/runtime/issues/78516
        private static readonly string _literalHeaderNameString = string.Concat(Enumerable.Range(0, 4100).Select(c => (char)('a' + (c % 26))));
 
        private static readonly byte[] _headerNameBytes = Encoding.ASCII.GetBytes(_headerNameString);
 
        private static readonly byte[] _literalHeaderNameBytes = Encoding.ASCII.GetBytes(_literalHeaderNameString);
 
        // 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();
 
        // size = 4096 ==> 0x7f, 0x81, 0x1f (7+) prefixed integer
        // size = 4100 ==> 0x7f, 0x85, 0x1f (7+) prefixed integer
        private static readonly byte[] _literalHeaderName = new byte[] { 0x7f, 0x85, 0x1f } // 4100
            .Concat(_literalHeaderNameBytes)
            .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();
 
        private static readonly byte[] _literalEmptyString = new byte[] { 0x00 };
 
        private static readonly byte[] _literalEmptyStringHuffman = new byte[] { 0x80 };
 
        // &        *
        // 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 TestHttpHeadersHandler _handler = new TestHttpHeadersHandler();
 
        public HPackDecoderTests()
        {
            (_dynamicTable, _decoder) = CreateDecoderAndTable();
        }
 
        private static (DynamicTable, HPackDecoder) CreateDecoderAndTable()
        {
            var dynamicTable = new DynamicTable(DynamicTableInitialMaxSize);
            var decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxHeaderFieldSize, dynamicTable);
 
            return (dynamicTable, decoder);
        }
 
        [Fact]
        public void DecodesIndexedHeaderField_StaticTableWithValue()
        {
            _decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: _handler);
            Assert.Equal("GET", _handler.DecodedHeaders[":method"]);
 
            Assert.Equal(":method", _handler.DecodedStaticHeaders[H2StaticTable.MethodGet].Key);
            Assert.Equal("GET", _handler.DecodedStaticHeaders[H2StaticTable.MethodGet].Value);
        }
 
        [Fact]
        public void DecodesIndexedHeaderField_StaticTableWithoutValue()
        {
            byte[] encoded = _literalHeaderFieldWithIndexingIndexedName
                .Concat(_headerValue)
                .ToArray();
 
            _decoder.Decode(encoded, endHeaders: true, handler: _handler);
            Assert.Equal(_headerValueString, _handler.DecodedHeaders[_userAgentString]);
 
            Assert.Equal(_userAgentString, _handler.DecodedStaticHeaders[H2StaticTable.UserAgent].Key);
            Assert.Equal(_headerValueString, _handler.DecodedStaticHeaders[H2StaticTable.UserAgent].Value);
        }
 
        [Fact]
        public void DecodesIndexedHeaderField_DynamicTable()
        {
            // Add the header to the dynamic table
            _dynamicTable.Insert(_headerNameBytes, _headerValueBytes);
 
            // Index it
            _decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: _handler);
            Assert.Equal(_headerValueString, _handler.DecodedHeaders[_headerNameString]);
        }
 
        [Fact]
        public void DecodesIndexedHeaderField_DynamicTable_ReferencedEntryRemovedOnInsertion()
        {
            // Pre-populate the dynamic table so we'll have something to reference.
            // This entry will have index 62 (0x3E).
            _dynamicTable.Insert(_headerNameBytes, _headerValueBytes);
            Assert.Equal(1, _dynamicTable.Count);
 
            Assert.InRange(_dynamicTable.MaxSize, 1, _literalHeaderNameBytes.Length); // Assert that our string will be too big
 
            byte[] encoded = (new byte[] { 0x40 | 0x3E }) // Indexing enabled (0x40) | dynamic table (62 = 0x3E) as a 6-integer, 
                .Concat(_literalHeaderName) // A header value that's too large to fit in the dynamic table
                .ToArray();
 
            _decoder.Decode(encoded, endHeaders: true, handler: _handler);
            Assert.Equal(0, _dynamicTable.Count); // The large entry caused the table to be wiped
            Assert.Equal(_literalHeaderNameString, _handler.DecodedHeaders[_headerNameString]); // but we got the header anyway
        }
 
        [Fact]
        public void DecodesIndexedHeaderField_OutOfRange_Error()
        {
            HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() =>
                _decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: _handler));
            Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message);
            Assert.Empty(_handler.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<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(_handler.DecodedHeaders);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldWithoutIndexing_NewName()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_headerName)
                .Concat(_headerValue)
                .ToArray();
 
            TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_EmptyName()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_literalEmptyString)
                .Concat(_headerValue)
                .ToArray();
 
            HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: _handler));
            Assert.Equal(SR.Format(SR.net_http_invalid_header_name, string.Empty), exception.Message);
            Assert.Empty(_handler.DecodedHeaders);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_EmptyValue()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_headerName)
                .Concat(_literalEmptyString)
                .ToArray();
 
            TestDecodeWithoutIndexing(encoded, _headerNameString, string.Empty);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_EmptyNameAndValue()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_literalEmptyString)
                .Concat(_literalEmptyString)
                .ToArray();
 
            HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: _handler));
            Assert.Equal(SR.Format(SR.net_http_invalid_header_name, string.Empty), exception.Message);
            Assert.Empty(_handler.DecodedHeaders);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedName()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_headerNameHuffman)
                .Concat(_headerValue)
                .ToArray();
 
            TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedName_Empty()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_literalEmptyStringHuffman)
                .Concat(_headerValue)
                .ToArray();
 
            HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: _handler));
            Assert.Equal(SR.Format(SR.net_http_invalid_header_name, string.Empty), exception.Message);
            Assert.Empty(_handler.DecodedHeaders);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedValue()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_headerName)
                .Concat(_headerValueHuffman)
                .ToArray();
 
            TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedValue_Empty()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_headerName)
                .Concat(_literalEmptyStringHuffman)
                .ToArray();
 
            TestDecodeWithoutIndexing(encoded, _headerNameString, string.Empty);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedNameAndValue()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_headerNameHuffman)
                .Concat(_headerValueHuffman)
                .ToArray();
 
            TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedNameAndValue_Empty()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_literalEmptyStringHuffman)
                .Concat(_literalEmptyStringHuffman)
                .ToArray();
 
            HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: _handler));
            Assert.Equal(SR.Format(SR.net_http_invalid_header_name, string.Empty), exception.Message);
            Assert.Empty(_handler.DecodedHeaders);
        }
 
        [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<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(_handler.DecodedHeaders);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldNeverIndexed_NewName()
        {
            byte[] encoded = _literalHeaderFieldNeverIndexedNewName
                .Concat(_headerName)
                .Concat(_headerValue)
                .ToArray();
 
            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()
        {
            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<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(_handler.DecodedHeaders);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldNeverIndexed_NewName_SingleBuffer()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_literalHeaderName)
                .Concat(_headerValue)
                .ToArray();
 
            _decoder.Decode(encoded, endHeaders: true, handler: _handler);
 
            Assert.Single(_handler.DecodedHeaders);
            Assert.True(_handler.DecodedHeaders.ContainsKey(_literalHeaderNameString));
            Assert.Equal(_headerValueString, _handler.DecodedHeaders[_literalHeaderNameString]);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldNeverIndexed_NewName_NameLengthBrokenIntoSeparateBuffers()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_literalHeaderName)
                .Concat(_headerValue)
                .ToArray();
 
            _decoder.Decode(encoded[..1], endHeaders: false, handler: _handler);
            _decoder.Decode(encoded[1..], endHeaders: true, handler: _handler);
 
            Assert.Single(_handler.DecodedHeaders);
            Assert.True(_handler.DecodedHeaders.ContainsKey(_literalHeaderNameString));
            Assert.Equal(_headerValueString, _handler.DecodedHeaders[_literalHeaderNameString]);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldNeverIndexed_NewName_NameBrokenIntoSeparateBuffers()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_literalHeaderName)
                .Concat(_headerValue)
                .ToArray();
 
            _decoder.Decode(encoded[..(_literalHeaderNameString.Length / 2)], endHeaders: false, handler: _handler);
            _decoder.Decode(encoded[(_literalHeaderNameString.Length / 2)..], endHeaders: true, handler: _handler);
 
            Assert.Single(_handler.DecodedHeaders);
            Assert.True(_handler.DecodedHeaders.ContainsKey(_literalHeaderNameString));
            Assert.Equal(_headerValueString, _handler.DecodedHeaders[_literalHeaderNameString]);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldNeverIndexed_NewName_NameAndValueBrokenIntoSeparateBuffers()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_literalHeaderName)
                .Concat(_headerValue)
                .ToArray();
 
            _decoder.Decode(encoded[..^_headerValue.Length], endHeaders: false, handler: _handler);
            _decoder.Decode(encoded[^_headerValue.Length..], endHeaders: true, handler: _handler);
 
            Assert.Single(_handler.DecodedHeaders);
            Assert.True(_handler.DecodedHeaders.ContainsKey(_literalHeaderNameString));
            Assert.Equal(_headerValueString, _handler.DecodedHeaders[_literalHeaderNameString]);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldNeverIndexed_NewName_ValueLengthBrokenIntoSeparateBuffers()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_literalHeaderName)
                .Concat(_headerValue)
                .ToArray();
 
            _decoder.Decode(encoded[..^(_headerValue.Length - 1)], endHeaders: false, handler: _handler);
            _decoder.Decode(encoded[^(_headerValue.Length - 1)..], endHeaders: true, handler: _handler);
 
            Assert.Single(_handler.DecodedHeaders);
            Assert.True(_handler.DecodedHeaders.ContainsKey(_literalHeaderNameString));
            Assert.Equal(_headerValueString, _handler.DecodedHeaders[_literalHeaderNameString]);
        }
 
        [Fact]
        public void DecodesLiteralHeaderFieldNeverIndexed_NewName_ValueBrokenIntoSeparateBuffers()
        {
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(_literalHeaderName)
                .Concat(_headerValue)
                .ToArray();
 
            _decoder.Decode(encoded[..^(_headerValueString.Length / 2)], endHeaders: false, handler: _handler);
            _decoder.Decode(encoded[^(_headerValueString.Length / 2)..], endHeaders: true, handler: _handler);
 
            Assert.Single(_handler.DecodedHeaders);
            Assert.True(_handler.DecodedHeaders.ContainsKey(_literalHeaderNameString));
            Assert.Equal(_headerValueString, _handler.DecodedHeaders[_literalHeaderNameString]);
        }
 
        [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: _handler);
 
            Assert.Equal(30, _dynamicTable.MaxSize);
            Assert.Empty(_handler.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<HPackDecodingException>(() => _decoder.Decode(data, endHeaders: true, handler: _handler));
            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: _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: _handler));
            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: _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: _handler);
 
            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<HPackDecodingException>(() =>
                _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(_handler.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<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: _handler));
            Assert.Equal(SR.Format(SR.net_http_headers_exceeded_length, MaxHeaderFieldSize), exception.Message);
            Assert.Empty(_handler.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: _handler);
 
            Assert.Equal(string8193, _handler.DecodedHeaders[string8193]);
        }
 
        [Fact]
        public void DecodesStringLength_ExceedsLimit_Throws()
        {
            HPackDecoder decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxHeaderFieldSize + 1);
            string string8191 = new string('a', MaxHeaderFieldSize - 1);
            string string8193 = new string('a', MaxHeaderFieldSize + 1);
            string string8194 = new string('a', MaxHeaderFieldSize + 2);
 
            var bytes = new byte[3];
            var success = IntegerEncoder.Encode(8194, 7, bytes, out var written);
 
            byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
                .Concat(new byte[] { 0x7f, 0x80, 0x3f }) // 8191 encoded with 7-bit prefix, no Huffman encoding
                .Concat(Encoding.ASCII.GetBytes(string8191))
                .Concat(new byte[] { 0x7f, 0x80, 0x3f }) // 8191 encoded with 7-bit prefix, no Huffman encoding
                .Concat(Encoding.ASCII.GetBytes(string8191))
                .Concat(_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))
                .Concat(_literalHeaderFieldWithoutIndexingNewName)
                .Concat(new byte[] { 0x7f, 0x83, 0x3f }) // 8194 encoded with 7-bit prefix, no Huffman encoding
                .Concat(Encoding.ASCII.GetBytes(string8194))
                .Concat(new byte[] { 0x7f, 0x83, 0x3f }) // 8194 encoded with 7-bit prefix, no Huffman encoding
                .Concat(Encoding.ASCII.GetBytes(string8194))
                .ToArray();
 
            var ex = Assert.Throws<HPackDecodingException>(() => decoder.Decode(encoded, endHeaders: true, handler: _handler));
            Assert.Equal(SR.Format(SR.net_http_headers_exceeded_length, MaxHeaderFieldSize + 1), ex.Message);
            Assert.Equal(string8191, _handler.DecodedHeaders[string8191]);
            Assert.Equal(string8193, _handler.DecodedHeaders[string8193]);
            Assert.False(_handler.DecodedHeaders.ContainsKey(string8194));
        }
 
        [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[]>
        {
            // 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<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: _handler));
            Assert.Equal(SR.net_http_hpack_incomplete_header_block, exception.Message);
            Assert.Empty(_handler.DecodedHeaders);
        }
 
        public static readonly TheoryData<byte[]> _huffmanDecodingErrorData = new TheoryData<byte[]>
        {
            // 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<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(_handler.DecodedHeaders);
        }
 
        private static void TestDecodeWithIndexing(byte[] encoded, string expectedHeaderName, string expectedHeaderValue)
        {
            TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: true, byteAtATime: false);
            TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: true, byteAtATime: true);
        }
 
        private static void TestDecodeWithoutIndexing(byte[] encoded, string expectedHeaderName, string expectedHeaderValue)
        {
            TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: false, byteAtATime: false);
            TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: false, byteAtATime: true);
        }
 
        private static void TestDecode(byte[] encoded, string expectedHeaderName, string expectedHeaderValue, bool expectDynamicTableEntry, bool byteAtATime)
        {
            var (dynamicTable, decoder) = CreateDecoderAndTable();
            var handler = new TestHttpHeadersHandler();
 
            Assert.Equal(0, dynamicTable.Count);
            Assert.Equal(0, dynamicTable.Size);
 
            if (!byteAtATime)
            {
                decoder.Decode(encoded, endHeaders: true, handler: handler);
            }
            else
            {
                // 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 : IHttpStreamHeadersHandler
    {
        public Dictionary<string, string> DecodedHeaders { get; } = new Dictionary<string, string>();
        public Dictionary<int, KeyValuePair<string, string>> DecodedStaticHeaders { get; } = new Dictionary<int, KeyValuePair<string, string>>();
 
        void IHttpStreamHeadersHandler.OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
        {
            string headerName = Encoding.ASCII.GetString(name);
            string headerValue = Encoding.ASCII.GetString(value);
 
            DecodedHeaders[headerName] = headerValue;
        }
 
        void IHttpStreamHeadersHandler.OnStaticIndexedHeader(int index)
        {
            ref readonly HeaderField entry = ref H2StaticTable.Get(index - 1);
            ((IHttpStreamHeadersHandler)this).OnHeader(entry.Name, entry.Value);
            DecodedStaticHeaders[index] = new KeyValuePair<string, string>(Encoding.ASCII.GetString(entry.Name), Encoding.ASCII.GetString(entry.Value));
        }
 
        void IHttpStreamHeadersHandler.OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
        {
            byte[] name = H2StaticTable.Get(index - 1).Name;
            ((IHttpStreamHeadersHandler)this).OnHeader(name, value);
            DecodedStaticHeaders[index] = new KeyValuePair<string, string>(Encoding.ASCII.GetString(name), Encoding.ASCII.GetString(value));
        }
 
        void IHttpStreamHeadersHandler.OnHeadersComplete(bool endStream) { }
 
        void IHttpStreamHeadersHandler.OnDynamicIndexedHeader(int? index, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
        {
            string headerName = Encoding.ASCII.GetString(name);
            string headerValue = Encoding.ASCII.GetString(value);
 
            DecodedHeaders[headerName] = headerValue;
        }
    }
}