HPACK fixes and tests (dotnet/corefx#38324)
authorCory Nelson <phrosty@gmail.com>
Wed, 12 Jun 2019 16:46:28 +0000 (09:46 -0700)
committerGitHub <noreply@github.com>
Wed, 12 Jun 2019 16:46:28 +0000 (09:46 -0700)
* HPACK correctness tests/updates. Resolves dotnet/corefx#31316.

Fixes:
- Fix check allowing out-of-bounds write in IntegerEncoder when an integer requires more than one byte and there is not enough buffer space.
- Encode Content-Type with a "Literal Header Without Indexing -- Indexed Name" rather than with a literal name.

Updates: (ported from ASP.NET HPACK code)
- Dynamic table size update must be the first instruction in the header block. Throw an exception when not the case.
- Throw an exception when we've reached the end of header data and we are still mid-parse.

New:
- Asserts, documentation, and tests for IntegerEncoder and IntegerDecoder.
- Tests to verify HttpClient is correctly encoding the different variants of headers.
- HPackDecoder tests (ported from ASP.NET)

* Address review feedback and fix CI.

* Address review feedback.

* Fix licensing to use ASP.NET's licensing. Add a TPN.

Commit migrated from https://github.com/dotnet/corefx/commit/ed526597f3f0d4653588ba0129fcaf18e2e5a4ef

20 files changed:
src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs
src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs
src/libraries/System.Net.Http/src/Resources/Strings.resx
src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeaders.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HPack/DynamicTable.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HPack/HPackDecoder.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HPack/HPackDecodingException.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HPack/HPackEncoder.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HPack/HeaderField.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HPack/Huffman.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HPack/HuffmanDecodingException.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HPack/IntegerDecoder.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HPack/IntegerEncoder.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HPack/StaticTable.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HPackTest.cs [new file with mode: 0644]
src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj
src/libraries/System.Net.Http/tests/UnitTests/HPack/HPackDecoderTest.cs [new file with mode: 0644]
src/libraries/System.Net.Http/tests/UnitTests/HPack/HPackIntegerTest.cs [new file with mode: 0644]
src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj

index a7200ad..9f6c08e 100644 (file)
@@ -44,12 +44,16 @@ namespace System.Net.Test.Common
     {
         public string Name { get; }
         public string Value { get; }
+        public byte[] Raw { get; }
 
-        public HttpHeaderData(string name, string value)
+        public HttpHeaderData(string name, string value, byte[] raw = null)
         {
             Name = name;
             Value = value;
+            Raw = raw;
         }
+
+        public override string ToString() => Name == null ? "<empty>" : (Name + ": " + (Value ?? string.Empty));
     }
 
     public class HttpRequestData
index 869c55e..9c6f792 100644 (file)
@@ -579,6 +579,9 @@ namespace System.Net.Test.Common
             {
                 (int bytesConsumed, HttpHeaderData headerData) = DecodeHeader(data.Span.Slice(i));
 
+                byte[] headerRaw = data.Span.Slice(i, bytesConsumed).ToArray();
+                headerData = new HttpHeaderData(headerData.Name, headerData.Value, headerRaw);
+
                 requestData.Headers.Add(headerData);
                 i += bytesConsumed;
             }
index 683647e..04104a3 100644 (file)
@@ -1,4 +1,5 @@
-<root>
+<?xml version="1.0" encoding="utf-8"?>
+<root>
   <!-- 
     Microsoft ResX Schema 
     
   <data name="net_nego_not_supported_empty_target_with_defaultcreds" xml:space="preserve">
     <value>Target name should be non empty if default credentials are passed.</value>
   </data>
+  <data name="net_http_hpack_huffman_decode_failed" xml:space="preserve">
+    <value>Huffman-coded literal string failed to decode.</value>
+  </data>
+  <data name="net_http_hpack_incomplete_header_block" xml:space="preserve">
+    <value>Incomplete header block received.</value>
+  </data>
+  <data name="net_http_hpack_late_dynamic_table_size_update" xml:space="preserve">
+    <value>Dynamic table size update received after beginning of header block.</value>
+  </data>
 </root>
index 8ea05a7..64e4931 100644 (file)
@@ -37,7 +37,7 @@ namespace System.Net.Http.Headers
         public static readonly KnownHeader ContentMD5 = new KnownHeader("Content-MD5", HttpHeaderType.Content, ByteArrayHeaderParser.Parser);
         public static readonly KnownHeader ContentRange = new KnownHeader("Content-Range", HttpHeaderType.Content, GenericHeaderParser.ContentRangeParser, null, StaticTable.ContentRange);
         public static readonly KnownHeader ContentSecurityPolicy = new KnownHeader("Content-Security-Policy");
-        public static readonly KnownHeader ContentType = new KnownHeader("Content-Type", HttpHeaderType.Content, MediaTypeHeaderParser.SingleValueParser);
+        public static readonly KnownHeader ContentType = new KnownHeader("Content-Type", HttpHeaderType.Content, MediaTypeHeaderParser.SingleValueParser, null, StaticTable.ContentType);
         public static readonly KnownHeader Cookie = new KnownHeader("Cookie", StaticTable.Cookie);
         public static readonly KnownHeader Cookie2 = new KnownHeader("Cookie2");
         public static readonly KnownHeader Date = new KnownHeader("Date", HttpHeaderType.General, DateHeaderParser.Parser, null, StaticTable.Date);
index fcd8c53..cfa3faa 100644 (file)
@@ -1,6 +1,6 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
+// 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.
 
 namespace System.Net.Http.HPack
 {
index 14bbb20..3d57c3d 100644 (file)
@@ -1,6 +1,6 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
+// 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.Diagnostics;
 
@@ -97,6 +97,7 @@ namespace System.Net.Http.HPack
         private int _headerValueLength;
         private bool _index;
         private bool _huffman;
+        private bool _headersObserved;
 
         public HPackDecoder(int maxDynamicTableSize = DefaultHeaderTableSize)
             : this(maxDynamicTableSize, new DynamicTable(maxDynamicTableSize))
@@ -111,7 +112,7 @@ namespace System.Net.Http.HPack
             _dynamicTable = dynamicTable;
         }
 
-        public void Decode(ReadOnlySpan<byte> data, HeaderCallback onHeader, object onHeaderState)
+        public void Decode(ReadOnlySpan<byte> data, bool endHeaders, HeaderCallback onHeader, object onHeaderState)
         {
             for (int i = 0; i < data.Length; i++)
             {
@@ -125,6 +126,8 @@ namespace System.Net.Http.HPack
                         // Look at this once we have more concrete perf data.
                         if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldRepresentation)
                         {
+                            _headersObserved = true;
+
                             int val = b & ~IndexedHeaderFieldMask;
 
                             if (_integerDecoder.StartDecode((byte)val, IndexedHeaderFieldPrefix))
@@ -138,6 +141,8 @@ namespace System.Net.Http.HPack
                         }
                         else if ((b & LiteralHeaderFieldWithIncrementalIndexingMask) == LiteralHeaderFieldWithIncrementalIndexingRepresentation)
                         {
+                            _headersObserved = true;
+
                             _index = true;
                             int val = b & ~LiteralHeaderFieldWithIncrementalIndexingMask;
 
@@ -156,6 +161,8 @@ namespace System.Net.Http.HPack
                         }
                         else if ((b & LiteralHeaderFieldWithoutIndexingMask) == LiteralHeaderFieldWithoutIndexingRepresentation)
                         {
+                            _headersObserved = true;
+
                             _index = false;
                             int val = b & ~LiteralHeaderFieldWithoutIndexingMask;
 
@@ -174,6 +181,8 @@ namespace System.Net.Http.HPack
                         }
                         else if ((b & LiteralHeaderFieldNeverIndexedMask) == LiteralHeaderFieldNeverIndexedRepresentation)
                         {
+                            _headersObserved = true;
+
                             _index = false;
                             int val = b & ~LiteralHeaderFieldNeverIndexedMask;
 
@@ -192,6 +201,15 @@ namespace System.Net.Http.HPack
                         }
                         else if ((b & DynamicTableSizeUpdateMask) == DynamicTableSizeUpdateRepresentation)
                         {
+                            // https://tools.ietf.org/html/rfc7541#section-4.2
+                            // This dynamic table size
+                            // update MUST occur at the beginning of the first header block
+                            // following the change to the dynamic table size.
+                            if (_headersObserved)
+                            {
+                                throw new HPackDecodingException(SR.net_http_hpack_late_dynamic_table_size_update);
+                            }
+
                             if (_integerDecoder.StartDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix))
                             {
                                 // TODO: validate that it's less than what's defined via SETTINGS
@@ -315,6 +333,16 @@ namespace System.Net.Http.HPack
                         throw new InternalException(_state);
                 }
             }
+
+            if (endHeaders)
+            {
+                if (_state != State.Ready)
+                {
+                    throw new HPackDecodingException(SR.net_http_hpack_incomplete_header_block);
+                }
+
+                _headersObserved = false;
+            }
         }
 
         public void CompleteDecode()
@@ -385,10 +413,10 @@ namespace System.Net.Http.HPack
                     _headerValueLength = Decode(ref _headerValueOctets);
                 }
             }
-            catch (HuffmanDecodingException)
+            catch (HuffmanDecodingException ex)
             {
                 // Error in huffman encoding.
-                throw new HPackDecodingException();
+                throw new HPackDecodingException(SR.net_http_hpack_huffman_decode_failed, ex);
             }
 
             _state = nextState;
index 2752e77..d6fc585 100644 (file)
@@ -1,6 +1,6 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
+// 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.
 
 namespace System.Net.Http.HPack
 {
@@ -10,5 +10,13 @@ namespace System.Net.Http.HPack
         public HPackDecodingException()
         {
         }
+
+        public HPackDecodingException(string message) : base(message)
+        {
+        }
+
+        public HPackDecodingException(string message, Exception innerException) : base(message, innerException)
+        {
+        }
     }
 }
index 2a53eeb..a5dbf1c 100644 (file)
@@ -1,6 +1,6 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
+// 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.Diagnostics;
 
index a289f7e..94eadc8 100644 (file)
@@ -1,6 +1,6 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
+// 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.
 
 namespace System.Net.Http.HPack
 {
index 2cc5d88..7b70e0b 100644 (file)
@@ -1,6 +1,6 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
+// 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.Diagnostics;
 
index 02a6444..0c48996 100644 (file)
@@ -1,6 +1,6 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
+// 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.
 
 namespace System.Net.Http.HPack
 {
index a619579..2d67012 100644 (file)
@@ -1,6 +1,8 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
+// 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.Diagnostics;
 
 namespace System.Net.Http.HPack
 {
@@ -11,8 +13,15 @@ namespace System.Net.Http.HPack
 
         public int Value { get; private set; }
 
+        /// <summary>
+        /// Decodes the first byte of the integer.
+        /// </summary>
+        /// <param name="prefixLength">The length of the prefix, in bits, that the integer was encoded with. Must be between 1 and 8.</param>
+        /// <returns>If the integer has been fully decoded, true. Otherwise, false -- <see cref="Decode(byte)"/> must be called on subsequent bytes.</returns>
         public bool StartDecode(byte b, int prefixLength)
         {
+            Debug.Assert(prefixLength >= 1 && prefixLength <= 8);
+
             if (b < ((1 << prefixLength) - 1))
             {
                 Value = b;
@@ -26,6 +35,10 @@ namespace System.Net.Http.HPack
             }
         }
 
+        /// <summary>
+        /// Decodes subsequent bytes of an integer.
+        /// </summary>
+        /// <returns>If the integer has been fully decoded, true. Otherwise, false -- <see cref="Decode(byte)"/> must be called on subsequent bytes.</returns>
         public bool Decode(byte b)
         {
             _i = _i + (b & 127) * (1 << _m);
index 1dd6624..b98bfd3 100644 (file)
@@ -1,13 +1,26 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
+// 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.Diagnostics;
 
 namespace System.Net.Http.HPack
 {
     internal static class IntegerEncoder
     {
+        /// <summary>
+        /// Encodes an integer into one or more bytes.
+        /// </summary>
+        /// <param name="value">The value to encode. Must not be negative.</param>
+        /// <param name="numBits">The length of the prefix, in bits, to encode <paramref name="value"/> within. Must be between 1 and 8.</param>
+        /// <param name="destination">The destination span to encode <paramref name="value"/> to.</param>
+        /// <param name="bytesWritten">The number of bytes used to encode <paramref name="value"/>.</param>
+        /// <returns>If <paramref name="destination"/> had enough storage to encode <paramref name="value"/>, true. Otherwise, false.</returns>
         public static bool Encode(int value, int numBits, Span<byte> destination, out int bytesWritten)
         {
+            Debug.Assert(value >= 0);
+            Debug.Assert(numBits >= 1 && numBits <= 8);
+
             if (destination.Length == 0)
             {
                 bytesWritten = 0;
@@ -40,7 +53,7 @@ namespace System.Net.Http.HPack
                 {
                     destination[i++] = (byte)(value % 128 + 128);
 
-                    if (i > destination.Length)
+                    if (i >= destination.Length)
                     {
                         bytesWritten = 0;
                         return false;
index ff0c45e..87655a0 100644 (file)
@@ -1,6 +1,6 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
+// 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.Text;
 
index 769a0bf..31cdee3 100644 (file)
@@ -316,6 +316,7 @@ namespace System.Net.Http
 
             _hpackDecoder.Decode(
                 GetFrameData(_incomingBuffer.ActiveSpan.Slice(0, frameHeader.Length), frameHeader.PaddedFlag, frameHeader.PriorityFlag),
+                frameHeader.EndHeadersFlag,
                 s_http2StreamOnResponseHeader,
                 http2Stream);
             _incomingBuffer.Discard(frameHeader.Length);
@@ -331,6 +332,7 @@ namespace System.Net.Http
 
                 _hpackDecoder.Decode(
                     _incomingBuffer.ActiveSpan.Slice(0, frameHeader.Length),
+                    frameHeader.EndHeadersFlag,
                     s_http2StreamOnResponseHeader,
                     http2Stream);
                 _incomingBuffer.Discard(frameHeader.Length);
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HPackTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HPackTest.cs
new file mode 100644 (file)
index 0000000..521d807
--- /dev/null
@@ -0,0 +1,78 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Diagnostics;
+using System.IO;
+using System.Net.Test.Common;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+using System.Data;
+using System.Runtime.InteropServices.ComTypes;
+
+namespace System.Net.Http.Functional.Tests
+{
+    public class HPackTest : HttpClientHandlerTestBase
+    {
+        protected override bool UseSocketsHttpHandler => true;
+        protected override bool UseHttp2 => true;
+
+        public HPackTest(ITestOutputHelper output) : base(output)
+        {
+        }
+
+        private const string LiteralHeaderName = "x-literal-header";
+        private const string LiteralHeaderValue = "testing 456";
+
+        [Theory]
+        [MemberData(nameof(HeaderEncodingTestData))]
+        public async Task HPack_HeaderEncoding(string headerName, string expectedValue, byte[] expectedEncoding)
+        {
+            await Http2LoopbackServer.CreateClientAndServerAsync(
+                async uri =>
+                {
+                    using HttpClient client = CreateHttpClient();
+
+                    using HttpRequestMessage request = new HttpRequestMessage();
+                    request.Method = HttpMethod.Post;
+                    request.RequestUri = uri;
+                    request.Version = HttpVersion.Version20;
+                    request.Content = new StringContent("testing 123");
+                    request.Headers.Add(LiteralHeaderName, LiteralHeaderValue);
+
+                    (await client.SendAsync(request)).Dispose();
+                },
+                async server =>
+                {
+                    await server.EstablishConnectionAsync();
+                    (int streamId, HttpRequestData requestData) = await server.ReadAndParseRequestHeaderAsync();
+
+                    HttpHeaderData header = requestData.Headers.Single(x => x.Name == headerName);
+                    Assert.Equal(expectedValue, header.Value);
+                    Assert.True(expectedEncoding.AsSpan().SequenceEqual(header.Raw));
+
+                    await server.SendDefaultResponseAsync(streamId);
+                });
+        }
+
+        public static IEnumerable<object[]> HeaderEncodingTestData()
+        {
+            // Indexed name, indexed value.
+            yield return new object[] { ":method", "POST", new byte[] { 0x83 } };
+            yield return new object[] { ":path", "/", new byte[] { 0x84 } };
+
+            // Indexed name, literal value.
+            yield return new object[] { "content-type", "text/plain; charset=utf-8", new byte[] { 0x0F, 0x10, 0x19, 0x74, 0x65, 0x78, 0x74, 0x2F, 0x70, 0x6C, 0x61, 0x69, 0x6E, 0x3B, 0x20, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3D, 0x75, 0x74, 0x66, 0x2D, 0x38 } };
+
+            // Literal name, literal value.
+            yield return new object[] { LiteralHeaderName, LiteralHeaderValue, new byte[] { 0x00, 0x10, 0x78, 0x2D, 0x6C, 0x69, 0x74, 0x65, 0x72, 0x61, 0x6C, 0x2D, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x0B, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6E, 0x67, 0x20, 0x34, 0x35, 0x36 } };
+        }
+    }
+}
index 51f84f4..7ed7af6 100644 (file)
     <Compile Include="HttpClientHandlerTest.Http1.cs" />
     <Compile Include="HttpClientHandlerTest.Http2.cs" />
     <Compile Include="CustomContent.netcore.cs" />
+    <Compile Include="HPackTest.cs" />
     <Compile Include="$(CommonTestPath)\System\Net\Http\Http2Frames.cs">
       <Link>Common\System\Net\Http\Http2Frames.cs</Link>
     </Compile>
diff --git a/src/libraries/System.Net.Http/tests/UnitTests/HPack/HPackDecoderTest.cs b/src/libraries/System.Net.Http/tests/UnitTests/HPack/HPackDecoderTest.cs
new file mode 100644 (file)
index 0000000..468dea2
--- /dev/null
@@ -0,0 +1,592 @@
+// 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.
+
+// Note: this is largely a copy of tests from ASP.NET, with minimal changes
+// needed to support API differences. As such, it has some stylistic
+// non-conformities that are kept around to allow easier diffing in future.
+// See https://github.com/aspnet/AspNetCore/blob/master/src/Servers/Kestrel/Core/test/HPackDecoderTests.cs
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Text;
+using System.Net.Http.HPack;
+using Xunit;
+using System.Buffers;
+
+namespace System.Net.Http.Unit.Tests.HPack
+{
+    public class HPackDecoderTest
+    {
+        private const int DynamicTableInitialMaxSize = 4096;
+
+        // 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<string, string> _decodedHeaders = new Dictionary<string, string>();
+
+        public HPackDecoderTest()
+        {
+            _dynamicTable = new DynamicTable(DynamicTableInitialMaxSize);
+            _decoder = new HPackDecoder(DynamicTableInitialMaxSize, _dynamicTable);
+        }
+
+        void OnHeader(object state, ReadOnlySpan<byte> headerName, ReadOnlySpan<byte> headerValue)
+        {
+            string name = Encoding.ASCII.GetString(headerName);
+            string value = Encoding.ASCII.GetString(headerValue);
+
+            _decodedHeaders[name] = value;
+        }
+
+        [Fact]
+        public void DecodesIndexedHeaderField_StaticTable()
+        {
+            _decoder.Decode(_indexedHeaderStatic, endHeaders: true, onHeader: OnHeader, onHeaderState: null);
+            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, onHeader: OnHeader, onHeaderState: null);
+            Assert.Equal(_headerValueString, _decodedHeaders[_headerNameString]);
+        }
+
+        [Fact]
+        public void DecodesIndexedHeaderField_OutOfRange_Error()
+        {
+            var exception = Assert.Throws<HPackDecodingException>(() =>
+                _decoder.Decode(_indexedHeaderDynamic, endHeaders: true, onHeader: OnHeader, onHeaderState: null));
+            Assert.Empty(_decodedHeaders);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName()
+        {
+            var encoded = _literalHeaderFieldWithIndexingNewName
+                .Concat(_headerName)
+                .Concat(_headerValue)
+                .ToArray();
+
+            TestDecodeWithIndexing(encoded, _headerNameString, _headerValueString);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName_HuffmanEncodedName()
+        {
+            var encoded = _literalHeaderFieldWithIndexingNewName
+                .Concat(_headerNameHuffman)
+                .Concat(_headerValue)
+                .ToArray();
+
+            TestDecodeWithIndexing(encoded, _headerNameString, _headerValueString);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName_HuffmanEncodedValue()
+        {
+            var encoded = _literalHeaderFieldWithIndexingNewName
+                .Concat(_headerName)
+                .Concat(_headerValueHuffman)
+                .ToArray();
+
+            TestDecodeWithIndexing(encoded, _headerNameString, _headerValueString);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName_HuffmanEncodedNameAndValue()
+        {
+            var encoded = _literalHeaderFieldWithIndexingNewName
+                .Concat(_headerNameHuffman)
+                .Concat(_headerValueHuffman)
+                .ToArray();
+
+            TestDecodeWithIndexing(encoded, _headerNameString, _headerValueString);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldWithIncrementalIndexing_IndexedName()
+        {
+            var encoded = _literalHeaderFieldWithIndexingIndexedName
+                .Concat(_headerValue)
+                .ToArray();
+
+            TestDecodeWithIndexing(encoded, _userAgentString, _headerValueString);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldWithIncrementalIndexing_IndexedName_HuffmanEncodedValue()
+        {
+            var 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.
+
+            var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(new byte[] { 0x7e }, endHeaders: true, onHeader: OnHeader, onHeaderState: null));
+            Assert.Empty(_decodedHeaders);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldWithoutIndexing_NewName()
+        {
+            var encoded = _literalHeaderFieldWithoutIndexingNewName
+                .Concat(_headerName)
+                .Concat(_headerValue)
+                .ToArray();
+
+            TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedName()
+        {
+            var encoded = _literalHeaderFieldWithoutIndexingNewName
+                .Concat(_headerNameHuffman)
+                .Concat(_headerValue)
+                .ToArray();
+
+            TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedValue()
+        {
+            var encoded = _literalHeaderFieldWithoutIndexingNewName
+                .Concat(_headerName)
+                .Concat(_headerValueHuffman)
+                .ToArray();
+
+            TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedNameAndValue()
+        {
+            var encoded = _literalHeaderFieldWithoutIndexingNewName
+                .Concat(_headerNameHuffman)
+                .Concat(_headerValueHuffman)
+                .ToArray();
+
+            TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldWithoutIndexing_IndexedName()
+        {
+            var encoded = _literalHeaderFieldWithoutIndexingIndexedName
+                .Concat(_headerValue)
+                .ToArray();
+
+            TestDecodeWithoutIndexing(encoded, _userAgentString, _headerValueString);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldWithoutIndexing_IndexedName_HuffmanEncodedValue()
+        {
+            var 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.
+
+            var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(new byte[] { 0x0f, 0x2f }, endHeaders: true, onHeader: OnHeader, onHeaderState: null));
+            Assert.Empty(_decodedHeaders);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldNeverIndexed_NewName()
+        {
+            var encoded = _literalHeaderFieldNeverIndexedNewName
+                .Concat(_headerName)
+                .Concat(_headerValue)
+                .ToArray();
+
+            TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedName()
+        {
+            var encoded = _literalHeaderFieldNeverIndexedNewName
+                .Concat(_headerNameHuffman)
+                .Concat(_headerValue)
+                .ToArray();
+
+            TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedValue()
+        {
+            var encoded = _literalHeaderFieldNeverIndexedNewName
+                .Concat(_headerName)
+                .Concat(_headerValueHuffman)
+                .ToArray();
+
+            TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
+        }
+
+        [Fact]
+        public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedNameAndValue()
+        {
+            var 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
+            var 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
+            var 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.
+
+            var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(new byte[] { 0x1f, 0x2f }, endHeaders: true, onHeader: OnHeader, onHeaderState: null));
+            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, onHeader: OnHeader, onHeaderState: null);
+
+            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);
+
+            var data = _indexedHeaderStatic.Concat(new byte[] { 0x3e }).ToArray();
+            var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(data, endHeaders: true, onHeader: OnHeader, onHeaderState: null));
+        }
+
+        [Fact]
+        public void DecodesDynamicTableSizeUpdate_AfterIndexedHeaderStatic_SubsequentDecodeCall_Error()
+        {
+            Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
+
+            _decoder.Decode(_indexedHeaderStatic, endHeaders: false, onHeader: OnHeader, onHeaderState: null);
+            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)
+            var data = new byte[] { 0x3e };
+            var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(data, endHeaders: true, onHeader: OnHeader, onHeaderState: null));
+        }
+
+        [Fact]
+        public void DecodesDynamicTableSizeUpdate_AfterIndexedHeaderStatic_ResetAfterEndHeaders_Succeeds()
+        {
+            Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
+
+            _decoder.Decode(_indexedHeaderStatic, endHeaders: true, onHeader: OnHeader, onHeaderState: null);
+            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, onHeader: OnHeader, onHeaderState: null);
+
+            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);
+
+            var exception = Assert.Throws<HPackDecodingException>(() =>
+                _decoder.Decode(new byte[] { 0x3f, 0xe2, 0x1f }, endHeaders: true, onHeader: OnHeader, onHeaderState: null));
+            Assert.Empty(_decodedHeaders);
+        }
+
+        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)
+        {
+            var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, onHeader: OnHeader, onHeaderState: null));
+            Assert.Empty(_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)
+        {
+            var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, onHeader: OnHeader, onHeaderState: null));
+            Assert.IsType<HuffmanDecodingException>(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, onHeader: OnHeader, onHeaderState: null);
+
+            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);
+            }
+        }
+    }
+}
diff --git a/src/libraries/System.Net.Http/tests/UnitTests/HPack/HPackIntegerTest.cs b/src/libraries/System.Net.Http/tests/UnitTests/HPack/HPackIntegerTest.cs
new file mode 100644 (file)
index 0000000..2bd3fa9
--- /dev/null
@@ -0,0 +1,77 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xunit;
+using System.Net.Http.HPack;
+
+namespace System.Net.Http.Unit.Tests.HPack
+{
+    public class HPackIntegerTest
+    {
+        [Theory]
+        [MemberData(nameof(IntegerCodecExactSamples))]
+        public void HPack_IntegerEncode(int value, int bits, byte[] expectedResult)
+        {
+            Span<byte> actualResult = new byte[64];
+            bool success = IntegerEncoder.Encode(value, bits, actualResult, out int bytesWritten);
+
+            Assert.True(success);
+            Assert.Equal(expectedResult.Length, bytesWritten);
+            Assert.True(actualResult.Slice(0, bytesWritten).SequenceEqual(expectedResult));
+        }
+
+        [Theory]
+        [MemberData(nameof(IntegerCodecExactSamples))]
+        public void HPack_IntegerEncode_ShortBuffer(int value, int bits, byte[] expectedResult)
+        {
+            Span<byte> actualResult = new byte[expectedResult.Length - 1];
+            bool success = IntegerEncoder.Encode(value, bits, actualResult, out int bytesWritten);
+
+            Assert.False(success);
+        }
+
+        [Theory]
+        [MemberData(nameof(IntegerCodecRoundTripSamples))]
+        public void HPack_IntegerRoundTrip(int value, int bits)
+        {
+            var decoder = new IntegerDecoder();
+
+            Span<byte> encoded = stackalloc byte[5];
+            Assert.True(IntegerEncoder.Encode(value, bits, encoded, out int bytesWritten));
+
+            bool finished = decoder.StartDecode(encoded[0], bits);
+
+            int i = 1;
+            for (; !finished && i < encoded.Length; ++i)
+            {
+                finished = decoder.Decode(encoded[i]);
+            }
+
+            Assert.True(finished);
+            Assert.Equal(bytesWritten, i);
+            Assert.Equal(value, decoder.Value);
+        }
+
+        public static IEnumerable<object[]> IntegerCodecExactSamples()
+        {
+            yield return new object[] { 10, 5, new byte[] { 0x0A } };
+            yield return new object[] { 1337, 5, new byte[] { 0x1F, 0x9A, 0x0A } };
+            yield return new object[] { 42, 8, new byte[] { 0x2A } };
+        }
+
+        public static IEnumerable<object[]> IntegerCodecRoundTripSamples()
+        {
+            for (int i = 0; i < 2048; ++i)
+            {
+                for (int prefixLength = 1; prefixLength <= 8; ++prefixLength)
+                {
+                    yield return new object[] { i, prefixLength };
+                }
+            }
+        }
+    }
+}
index c1f69dd..17a23b1 100644 (file)
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <ProjectGuid>{5F9C3C9F-652E-461E-B2D6-85D264F5A733}</ProjectGuid>
     <StringResourcesPath>../../src/Resources/Strings.resx</StringResourcesPath>
@@ -27,6 +27,9 @@
     <Compile Include="$(CommonPath)\CoreLib\System\IO\StreamHelpers.CopyValidation.cs">
       <Link>ProductionCode\Common\CoreLib\System\IO\StreamHelpers.CopyValidation.cs</Link>
     </Compile>
+    <Compile Include="$(CommonPath)\System\Net\InternalException.cs">
+      <Link>Common\System\Net\InternalException.cs</Link>
+    </Compile>
     <Compile Include="$(CommonPath)\System\Net\HttpDateParser.cs">
       <Link>Common\System\Net\HttpDateParser.cs</Link>
     </Compile>
     <Compile Include="..\..\src\System\Net\Http\SocketsHttpHandler\HPack\HPackEncoder.cs">
       <Link>ProductionCode\System\Net\Http\SocketsHttpHandler\HPack\HPackEncoder.cs</Link>
     </Compile>
+    <Compile Include="..\..\src\System\Net\Http\SocketsHttpHandler\HPack\HPackDecoder.cs">
+      <Link>ProductionCode\System\Net\Http\SocketsHttpHandler\HPack\HPackDecoder.cs</Link>
+    </Compile>
+    <Compile Include="..\..\src\System\Net\Http\SocketsHttpHandler\HPack\HPackDecodingException.cs">
+      <Link>ProductionCode\System\Net\Http\SocketsHttpHandler\HPack\HPackDecodingException.cs</Link>
+    </Compile>
+    <Compile Include="..\..\src\System\Net\Http\SocketsHttpHandler\HPack\Huffman.cs">
+      <Link>ProductionCode\System\Net\Http\SocketsHttpHandler\HPack\Huffman.cs</Link>
+    </Compile>
+    <Compile Include="..\..\src\System\Net\Http\SocketsHttpHandler\HPack\HuffmanDecodingException.cs">
+      <Link>ProductionCode\System\Net\Http\SocketsHttpHandler\HPack\HuffmanDecodingException.cs</Link>
+    </Compile>
     <Compile Include="..\..\src\System\Net\Http\SocketsHttpHandler\HPack\IntegerEncoder.cs">
       <Link>ProductionCode\System\Net\Http\SocketsHttpHandler\HPack\IntegerEncoder.cs</Link>
     </Compile>
+    <Compile Include="..\..\src\System\Net\Http\SocketsHttpHandler\HPack\IntegerDecoder.cs">
+      <Link>ProductionCode\System\Net\Http\SocketsHttpHandler\HPack\IntegerDecoder.cs</Link>
+    </Compile>
     <Compile Include="..\..\src\System\Net\Http\SocketsHttpHandler\HPack\StaticTable.cs">
       <Link>ProductionCode\System\Net\Http\SocketsHttpHandler\HPack\StaticTable.cs</Link>
     </Compile>
+    <Compile Include="..\..\src\System\Net\Http\SocketsHttpHandler\HPack\DynamicTable.cs">
+      <Link>ProductionCode\System\Net\Http\SocketsHttpHandler\HPack\DynamicTable.cs</Link>
+    </Compile>
     <Compile Include="..\..\src\System\Net\Http\SocketsHttpHandler\SystemProxyInfo.cs">
       <Link>ProductionCode\System\Net\Http\SocketsHttpHandler\SystemProxyInfo.cs</Link>
     </Compile>
     <Compile Include="Headers\UriHeaderParserTest.cs" />
     <Compile Include="Headers\ViaHeaderValueTest.cs" />
     <Compile Include="Headers\WarningHeaderValueTest.cs" />
+    <Compile Include="HPack\HPackDecoderTest.cs" />
+    <Compile Include="HPack\HPackIntegerTest.cs" />
     <Compile Include="HttpContentTest.cs" />
     <Compile Include="HttpRuleParserTest.cs" />
     <Compile Include="MockContent.cs" />
       <Link>WinHttpHandler\UnitTests\TestServer.cs</Link>
     </Compile>
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>