296 lines
11 KiB
C#
296 lines
11 KiB
C#
// 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.
|
|
|
|
#nullable enable
|
|
using System.Diagnostics;
|
|
|
|
namespace System.Net.Http.HPack
|
|
{
|
|
internal partial class HPackEncoder
|
|
{
|
|
public const int DefaultHeaderTableSize = 4096;
|
|
|
|
// Internal for testing
|
|
internal readonly EncoderHeaderEntry Head;
|
|
|
|
private readonly bool _allowDynamicCompression;
|
|
private readonly EncoderHeaderEntry[] _headerBuckets;
|
|
private readonly byte _hashMask;
|
|
private uint _headerTableSize;
|
|
private uint _maxHeaderTableSize;
|
|
private bool _pendingTableSizeUpdate;
|
|
private EncoderHeaderEntry? _removed;
|
|
|
|
public HPackEncoder(bool allowDynamicCompression = true, uint maxHeaderTableSize = DefaultHeaderTableSize)
|
|
{
|
|
_allowDynamicCompression = allowDynamicCompression;
|
|
_maxHeaderTableSize = maxHeaderTableSize;
|
|
Head = new EncoderHeaderEntry();
|
|
Head.Initialize(-1, string.Empty, string.Empty, int.MaxValue, null);
|
|
// Bucket count balances memory usage and the expected low number of headers (constrained by the header table size).
|
|
// Performance with different bucket counts hasn't been measured in detail.
|
|
_headerBuckets = new EncoderHeaderEntry[16];
|
|
_hashMask = (byte)(_headerBuckets.Length - 1);
|
|
Head.Before = Head.After = Head;
|
|
}
|
|
|
|
public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize)
|
|
{
|
|
if (_maxHeaderTableSize != maxHeaderTableSize)
|
|
{
|
|
_maxHeaderTableSize = maxHeaderTableSize;
|
|
|
|
// Dynamic table size update will be written next HEADERS frame
|
|
_pendingTableSizeUpdate = true;
|
|
|
|
// Check capacity and remove entries that exceed the new capacity
|
|
EnsureCapacity(0);
|
|
}
|
|
}
|
|
|
|
public bool EnsureDynamicTableSizeUpdate(Span<byte> buffer, out int length)
|
|
{
|
|
// Check if there is a table size update that should be encoded
|
|
if (_pendingTableSizeUpdate)
|
|
{
|
|
bool success = EncodeDynamicTableSizeUpdate((int)_maxHeaderTableSize, buffer, out length);
|
|
_pendingTableSizeUpdate = false;
|
|
return success;
|
|
}
|
|
|
|
length = 0;
|
|
return true;
|
|
}
|
|
|
|
public bool EncodeHeader(Span<byte> buffer, int staticTableIndex, HeaderEncodingHint encodingHint, string name, string value, out int bytesWritten)
|
|
{
|
|
Debug.Assert(!_pendingTableSizeUpdate, "Dynamic table size update should be encoded before headers.");
|
|
|
|
// Never index sensitive value.
|
|
if (encodingHint == HeaderEncodingHint.NeverIndex)
|
|
{
|
|
int index = ResolveDynamicTableIndex(staticTableIndex, name);
|
|
|
|
return index == -1
|
|
? EncodeLiteralHeaderFieldNeverIndexingNewName(name, value, buffer, out bytesWritten)
|
|
: EncodeLiteralHeaderFieldNeverIndexing(index, value, buffer, out bytesWritten);
|
|
}
|
|
|
|
// No dynamic table. Only use the static table.
|
|
if (!_allowDynamicCompression || _maxHeaderTableSize == 0 || encodingHint == HeaderEncodingHint.IgnoreIndex)
|
|
{
|
|
return staticTableIndex == -1
|
|
? EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten)
|
|
: EncodeLiteralHeaderFieldWithoutIndexing(staticTableIndex, value, buffer, out bytesWritten);
|
|
}
|
|
|
|
// Header is greater than the maximum table size.
|
|
// Don't attempt to add dynamic header as all existing dynamic headers will be removed.
|
|
if (HeaderField.GetLength(name.Length, value.Length) > _maxHeaderTableSize)
|
|
{
|
|
int index = ResolveDynamicTableIndex(staticTableIndex, name);
|
|
|
|
return index == -1
|
|
? EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten)
|
|
: EncodeLiteralHeaderFieldWithoutIndexing(index, value, buffer, out bytesWritten);
|
|
}
|
|
|
|
return EncodeDynamicHeader(buffer, staticTableIndex, name, value, out bytesWritten);
|
|
}
|
|
|
|
private int ResolveDynamicTableIndex(int staticTableIndex, string name)
|
|
{
|
|
if (staticTableIndex != -1)
|
|
{
|
|
// Prefer static table index.
|
|
return staticTableIndex;
|
|
}
|
|
|
|
return CalculateDynamicTableIndex(name);
|
|
}
|
|
|
|
private bool EncodeDynamicHeader(Span<byte> buffer, int staticTableIndex, string name, string value, out int bytesWritten)
|
|
{
|
|
EncoderHeaderEntry? headerField = GetEntry(name, value);
|
|
if (headerField != null)
|
|
{
|
|
// Already exists in dynamic table. Write index.
|
|
int index = CalculateDynamicTableIndex(headerField.Index);
|
|
return EncodeIndexedHeaderField(index, buffer, out bytesWritten);
|
|
}
|
|
else
|
|
{
|
|
// Doesn't exist in dynamic table. Add new entry to dynamic table.
|
|
uint headerSize = (uint)HeaderField.GetLength(name.Length, value.Length);
|
|
|
|
int index = ResolveDynamicTableIndex(staticTableIndex, name);
|
|
bool success = index == -1
|
|
? EncodeLiteralHeaderFieldIndexingNewName(name, value, buffer, out bytesWritten)
|
|
: EncodeLiteralHeaderFieldIndexing(index, value, buffer, out bytesWritten);
|
|
|
|
if (success)
|
|
{
|
|
EnsureCapacity(headerSize);
|
|
AddHeaderEntry(name, value, headerSize);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensure there is capacity for the new header. If there is not enough capacity then remove
|
|
/// existing headers until space is available.
|
|
/// </summary>
|
|
private void EnsureCapacity(uint headerSize)
|
|
{
|
|
Debug.Assert(headerSize <= _maxHeaderTableSize, "Header is bigger than dynamic table size.");
|
|
|
|
while (_maxHeaderTableSize - _headerTableSize < headerSize)
|
|
{
|
|
EncoderHeaderEntry? removed = RemoveHeaderEntry();
|
|
Debug.Assert(removed != null);
|
|
|
|
// Removed entries are tracked to be reused.
|
|
PushRemovedEntry(removed);
|
|
}
|
|
}
|
|
|
|
private EncoderHeaderEntry? GetEntry(string name, string value)
|
|
{
|
|
if (_headerTableSize == 0)
|
|
{
|
|
return null;
|
|
}
|
|
int hash = name.GetHashCode();
|
|
int bucketIndex = CalculateBucketIndex(hash);
|
|
for (EncoderHeaderEntry? e = _headerBuckets[bucketIndex]; e != null; e = e.Next)
|
|
{
|
|
// We've already looked up entries based on a hash of the name.
|
|
// Compare value before name as it is more likely to be different.
|
|
if (e.Hash == hash &&
|
|
string.Equals(value, e.Value, StringComparison.Ordinal) &&
|
|
string.Equals(name, e.Name, StringComparison.Ordinal))
|
|
{
|
|
return e;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private int CalculateDynamicTableIndex(string name)
|
|
{
|
|
if (_headerTableSize == 0)
|
|
{
|
|
return -1;
|
|
}
|
|
int hash = name.GetHashCode();
|
|
int bucketIndex = CalculateBucketIndex(hash);
|
|
for (EncoderHeaderEntry? e = _headerBuckets[bucketIndex]; e != null; e = e.Next)
|
|
{
|
|
if (e.Hash == hash && string.Equals(name, e.Name, StringComparison.Ordinal))
|
|
{
|
|
return CalculateDynamicTableIndex(e.Index);
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
private int CalculateDynamicTableIndex(int index)
|
|
{
|
|
return index == -1 ? -1 : index - Head.Before.Index + 1 + H2StaticTable.Count;
|
|
}
|
|
|
|
private void AddHeaderEntry(string name, string value, uint headerSize)
|
|
{
|
|
Debug.Assert(headerSize <= _maxHeaderTableSize, "Header is bigger than dynamic table size.");
|
|
Debug.Assert(headerSize <= _maxHeaderTableSize - _headerTableSize, "Not enough room in dynamic table.");
|
|
|
|
int hash = name.GetHashCode();
|
|
int bucketIndex = CalculateBucketIndex(hash);
|
|
EncoderHeaderEntry? oldEntry = _headerBuckets[bucketIndex];
|
|
// Attempt to reuse removed entry
|
|
EncoderHeaderEntry? newEntry = PopRemovedEntry() ?? new EncoderHeaderEntry();
|
|
newEntry.Initialize(hash, name, value, Head.Before.Index - 1, oldEntry);
|
|
_headerBuckets[bucketIndex] = newEntry;
|
|
newEntry.AddBefore(Head);
|
|
_headerTableSize += headerSize;
|
|
}
|
|
|
|
private void PushRemovedEntry(EncoderHeaderEntry removed)
|
|
{
|
|
if (_removed != null)
|
|
{
|
|
removed.Next = _removed;
|
|
}
|
|
_removed = removed;
|
|
}
|
|
|
|
private EncoderHeaderEntry? PopRemovedEntry()
|
|
{
|
|
if (_removed != null)
|
|
{
|
|
EncoderHeaderEntry? removed = _removed;
|
|
_removed = _removed.Next;
|
|
return removed;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove the oldest entry.
|
|
/// </summary>
|
|
private EncoderHeaderEntry? RemoveHeaderEntry()
|
|
{
|
|
if (_headerTableSize == 0)
|
|
{
|
|
return null;
|
|
}
|
|
EncoderHeaderEntry? eldest = Head.After;
|
|
int hash = eldest.Hash;
|
|
int bucketIndex = CalculateBucketIndex(hash);
|
|
EncoderHeaderEntry? prev = _headerBuckets[bucketIndex];
|
|
EncoderHeaderEntry? e = prev;
|
|
while (e != null)
|
|
{
|
|
EncoderHeaderEntry next = e.Next;
|
|
if (e == eldest)
|
|
{
|
|
if (prev == eldest)
|
|
{
|
|
_headerBuckets[bucketIndex] = next;
|
|
}
|
|
else
|
|
{
|
|
prev.Next = next;
|
|
}
|
|
_headerTableSize -= eldest.CalculateSize();
|
|
eldest.Remove();
|
|
return eldest;
|
|
}
|
|
prev = e;
|
|
e = next;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private int CalculateBucketIndex(int hash)
|
|
{
|
|
return hash & _hashMask;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hint for how the header should be encoded as HPack. This value can be overriden.
|
|
/// For example, a header that is larger than the dynamic table won't be indexed.
|
|
/// </summary>
|
|
internal enum HeaderEncodingHint
|
|
{
|
|
Index,
|
|
IgnoreIndex,
|
|
NeverIndex
|
|
}
|
|
}
|