Skip to content

Commit e1de039

Browse files
author
Cory Nelson
committed
Add Huffman encoding for HTTP2 headers. Resolves #31308
1 parent a35da48 commit e1de039

File tree

11 files changed

+501
-543
lines changed

11 files changed

+501
-543
lines changed

src/Common/tests/System/Net/Http/GenericLoopbackServer.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,16 @@ public struct HttpHeaderData
8585
{
8686
public string Name { get; }
8787
public string Value { get; }
88-
public bool HuffmanEncoded { get; }
88+
public bool HuffmanEncodedName { get; }
89+
public bool HuffmanEncodedValue { get; }
8990
public byte[] Raw { get; }
9091

91-
public HttpHeaderData(string name, string value, bool huffmanEncoded = false, byte[] raw = null)
92+
public HttpHeaderData(string name, string value, bool huffmanEncodedName = false, bool huffmanEncodedValue = false, byte[] raw = null)
9293
{
9394
Name = name;
9495
Value = value;
95-
HuffmanEncoded = huffmanEncoded;
96+
HuffmanEncodedName = huffmanEncodedName;
97+
HuffmanEncodedValue = huffmanEncodedValue;
9698
Raw = raw;
9799
}
98100

src/Common/tests/System/Net/Http/Http2LoopbackConnection.cs

+34-13
Original file line numberDiff line numberDiff line change
@@ -336,21 +336,26 @@ private static (int bytesConsumed, int value) DecodeInteger(ReadOnlySpan<byte> h
336336
return (2, prefixMask + b);
337337
}
338338

339-
private static (int bytesConsumed, string value) DecodeString(ReadOnlySpan<byte> headerBlock)
339+
private static (int bytesConsumed, string value, bool huffmanEncoded) DecodeString(ReadOnlySpan<byte> headerBlock)
340340
{
341341
(int bytesConsumed, int stringLength) = DecodeInteger(headerBlock, 0b01111111);
342-
if ((headerBlock[0] & 0b10000000) != 0)
342+
bool isHuffmanCoded = (headerBlock[0] & 0b10000000) != 0;
343+
344+
headerBlock = headerBlock.Slice(bytesConsumed, stringLength);
345+
346+
if (isHuffmanCoded)
343347
{
344348
// Huffman encoded
345349
byte[] buffer = new byte[stringLength * 2];
346-
int bytesDecoded = HuffmanDecoder.Decode(headerBlock.Slice(bytesConsumed, stringLength), buffer);
350+
int bytesDecoded = HuffmanDecoder.Decode(headerBlock, buffer);
347351
string value = Encoding.ASCII.GetString(buffer, 0, bytesDecoded);
348-
return (bytesConsumed + stringLength, value);
352+
353+
return (bytesConsumed + stringLength, value, true);
349354
}
350355
else
351356
{
352-
string value = Encoding.ASCII.GetString(headerBlock.Slice(bytesConsumed, stringLength));
353-
return (bytesConsumed + stringLength, value);
357+
string value = Encoding.ASCII.GetString(headerBlock);
358+
return (bytesConsumed + stringLength, value, false);
354359
}
355360
}
356361

@@ -432,9 +437,11 @@ private static (int bytesConsumed, HttpHeaderData headerData) DecodeLiteralHeade
432437
i += bytesConsumed;
433438

434439
string name;
440+
bool nameCompressed = false;
441+
435442
if (index == 0)
436443
{
437-
(bytesConsumed, name) = DecodeString(headerBlock.Slice(i));
444+
(bytesConsumed, name, nameCompressed) = DecodeString(headerBlock.Slice(i));
438445
i += bytesConsumed;
439446
}
440447
else
@@ -443,18 +450,19 @@ private static (int bytesConsumed, HttpHeaderData headerData) DecodeLiteralHeade
443450
}
444451

445452
string value;
446-
(bytesConsumed, value) = DecodeString(headerBlock.Slice(i));
453+
bool valueCompressed;
454+
(bytesConsumed, value, valueCompressed) = DecodeString(headerBlock.Slice(i));
447455
i += bytesConsumed;
448456

449-
return (i, new HttpHeaderData(name, value));
457+
return (i, new HttpHeaderData(name, value, huffmanEncodedName: nameCompressed, huffmanEncodedValue: valueCompressed));
450458
}
451459

452460
private static (int bytesConsumed, HttpHeaderData headerData) DecodeHeader(ReadOnlySpan<byte> headerBlock)
453461
{
454462
int i = 0;
455463

456464
byte b = headerBlock[0];
457-
if ((b & 0b10000000) != 0)
465+
if ((b & 0b10000000) == 0b10000000)
458466
{
459467
// Indexed header
460468
(int bytesConsumed, int index) = DecodeInteger(headerBlock, 0b01111111);
@@ -474,7 +482,7 @@ private static (int bytesConsumed, HttpHeaderData headerData) DecodeHeader(ReadO
474482
}
475483
else
476484
{
477-
// Literal, never indexed
485+
// Literal, never indexed OR literal, without indexing.
478486
return DecodeLiteralHeader(headerBlock, 0b00001111);
479487
}
480488
}
@@ -537,13 +545,14 @@ public async Task<byte[]> ReadBodyAsync()
537545
requestData.RequestId = streamId;
538546

539547
Memory<byte> data = headersFrame.Data;
548+
540549
int i = 0;
541550
while (i < data.Length)
542551
{
543552
(int bytesConsumed, HttpHeaderData headerData) = DecodeHeader(data.Span.Slice(i));
544553

545554
byte[] headerRaw = data.Span.Slice(i, bytesConsumed).ToArray();
546-
headerData = new HttpHeaderData(headerData.Name, headerData.Value, headerData.HuffmanEncoded, headerRaw);
555+
headerData = new HttpHeaderData(headerData.Name, headerData.Value, headerData.HuffmanEncodedName, headerData.HuffmanEncodedValue, headerRaw);
547556

548557
requestData.Headers.Add(headerData);
549558
i += bytesConsumed;
@@ -614,7 +623,19 @@ public async Task SendResponseHeadersAsync(int streamId, bool endStream = true,
614623
{
615624
foreach (HttpHeaderData headerData in headers)
616625
{
617-
bytesGenerated += HPackEncoder.EncodeHeader(headerData.Name, headerData.Value, headerData.HuffmanEncoded ? HPackFlags.HuffmanEncode : HPackFlags.None, headerBlock.AsSpan(bytesGenerated));
626+
HPackFlags hpackFlags = HPackFlags.None;
627+
628+
if (headerData.HuffmanEncodedName)
629+
{
630+
hpackFlags |= HPackFlags.HuffmanEncodeName;
631+
}
632+
633+
if (headerData.HuffmanEncodedValue)
634+
{
635+
hpackFlags |= HPackFlags.HuffmanEncodeValue;
636+
}
637+
638+
bytesGenerated += HPackEncoder.EncodeHeader(headerData.Name, headerData.Value, hpackFlags, headerBlock.AsSpan(bytesGenerated));
618639
}
619640
}
620641

src/Common/tests/System/Net/Http/HuffmanEncoder.cs

+36-17
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public static class HuffmanEncoder
88
{
99
// Stolen from product code
1010
// See https://github.com/dotnet/corefx/blob/ae7b3970bb2c8d76004ea397083ce7ceb1238133/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HPack/Huffman.cs#L12
11-
private static readonly (uint code, int bitLength)[] s_encodingTable = new (uint code, int bitLength)[]
11+
public static readonly (uint code, int bitLength)[] s_encodingTable = new (uint code, int bitLength)[]
1212
{
1313
(0b11111111_11000000_00000000_00000000, 13),
1414
(0b11111111_11111111_10110000_00000000, 23),
@@ -265,8 +265,7 @@ public static class HuffmanEncoder
265265
(0b11111111_11111111_11111101_11000000, 27),
266266
(0b11111111_11111111_11111101_11100000, 27),
267267
(0b11111111_11111111_11111110_00000000, 27),
268-
(0b11111111_11111111_11111011_10000000, 26),
269-
(0b11111111_11111111_11111111_11111100, 30)
268+
(0b11111111_11111111_11111011_10000000, 26)
270269
};
271270

272271
public static int GetEncodedLength(ReadOnlySpan<byte> src)
@@ -281,32 +280,52 @@ public static int GetEncodedLength(ReadOnlySpan<byte> src)
281280
return (bits + 7) / 8;
282281
}
283282

284-
public static int Encode(ReadOnlySpan<byte> src, Span<byte> dst)
283+
// Encoded values are 30 bits at most, so are stored in the table in a uint.
284+
// Convert to ulong here and put the encoded value in the most significant bits.
285+
// This makes the encoding logic below simpler.
286+
private static (ulong code, int bitLength) GetEncodedValue(byte b)
285287
{
286-
ulong buffer = 0;
287-
int bufferLength = 0;
288-
int dstIdx = 0;
288+
(uint code, int bitLength) = s_encodingTable[b];
289+
return (((ulong)code) << 32, bitLength);
290+
}
289291

290-
foreach (byte x in src)
292+
public static int Encode(ReadOnlySpan<byte> source, Span<byte> destination, bool injectEOS = false)
293+
{
294+
ulong currentBits = 0; // We can have 7 bits of rollover plus 30 bits for the next encoded value, so use a ulong
295+
int currentBitCount = 0;
296+
int dstOffset = 0;
297+
298+
for (int i = 0; i < source.Length; i++)
291299
{
292-
(uint code, int codeLength) = s_encodingTable[x];
300+
(ulong code, int bitLength) = GetEncodedValue(source[i]);
301+
302+
// inject EOS if instructed to
303+
if (injectEOS)
304+
{
305+
code |= (ulong)0b11111111_11111111_11111111_11111100 << (32 - bitLength);
306+
bitLength += 30;
307+
injectEOS = false;
308+
}
293309

294-
buffer = buffer << codeLength | code >> 32 - codeLength;
295-
bufferLength += codeLength;
310+
currentBits |= code >> currentBitCount;
311+
currentBitCount += bitLength;
296312

297-
while (bufferLength >= 8)
313+
while (currentBitCount >= 8)
298314
{
299-
bufferLength -= 8;
300-
dst[dstIdx++] = (byte)(buffer >> bufferLength);
315+
destination[dstOffset++] = (byte)(currentBits >> 56);
316+
currentBits <<= 8;
317+
currentBitCount -= 8;
301318
}
302319
}
303320

304-
if (bufferLength != 0)
321+
// Fill any trailing bits with ones, per RFC
322+
if (currentBitCount > 0)
305323
{
306-
dst[dstIdx++] = (byte)(buffer << (8 - bufferLength) | 0xFFu >> bufferLength);
324+
currentBits |= 0xFFFFFFFFFFFFFFFF >> currentBitCount;
325+
destination[dstOffset++] = (byte)(currentBits >> 56);
307326
}
308327

309-
return dstIdx;
328+
return dstOffset;
310329
}
311330
}
312331
}

0 commit comments

Comments
 (0)