File: src\Shared\runtime\Http2\Hpack\HPackEncoder.cs
Web Access
Project: src\src\Servers\Kestrel\Core\src\Microsoft.AspNetCore.Server.Kestrel.Core.csproj (Microsoft.AspNetCore.Server.Kestrel.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable enable
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
 
namespace System.Net.Http.HPack
{
    internal static partial class HPackEncoder
    {
        // Things we should add:
        // * Huffman encoding
        //
        // Things we should consider adding:
        // * Dynamic table encoding:
        //   This would make the encoder stateful, which complicates things significantly.
        //   Additionally, it's not clear exactly what strings we would add to the dynamic table
        //   without some additional guidance from the user about this.
        //   So for now, don't do dynamic encoding.
 
        /// <summary>Encodes an "Indexed Header Field".</summary>
        public static bool EncodeIndexedHeaderField(int index, Span<byte> destination, out int bytesWritten)
        {
            // From https://tools.ietf.org/html/rfc7541#section-6.1
            // ----------------------------------------------------
            //   0   1   2   3   4   5   6   7
            // +---+---+---+---+---+---+---+---+
            // | 1 |        Index (7+)         |
            // +---+---------------------------+
 
            if (destination.Length != 0)
            {
                destination[0] = 0x80;
                return IntegerEncoder.Encode(index, 7, destination, out bytesWritten);
            }
 
            bytesWritten = 0;
            return false;
        }
 
        /// <summary>Encodes the status code of a response to the :status field.</summary>
        public static bool EncodeStatusHeader(int statusCode, Span<byte> destination, out int bytesWritten)
        {
            // Bytes written depend on whether the status code value maps directly to an index
            if (H2StaticTable.TryGetStatusIndex(statusCode, out var index))
            {
                // Status codes which exist in the HTTP/2 StaticTable.
                return EncodeIndexedHeaderField(index, destination, out bytesWritten);
            }
            else
            {
                // If the status code doesn't have a static index then we need to include the full value.
                // Write a status index and then the number bytes as a string literal.
                if (!EncodeLiteralHeaderFieldWithoutIndexing(H2StaticTable.Status200, destination, out var nameLength))
                {
                    bytesWritten = 0;
                    return false;
                }
 
                var statusBytes = StatusCodes.ToStatusBytes(statusCode);
 
                if (!EncodeStringLiteral(statusBytes, destination.Slice(nameLength), out var valueLength))
                {
                    bytesWritten = 0;
                    return false;
                }
 
                bytesWritten = nameLength + valueLength;
                return true;
            }
        }
 
        /// <summary>Encodes a "Literal Header Field without Indexing".</summary>
        public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, string value, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
        {
            // From https://tools.ietf.org/html/rfc7541#section-6.2.2
            // ------------------------------------------------------
            //   0   1   2   3   4   5   6   7
            // +---+---+---+---+---+---+---+---+
            // | 0 | 0 | 0 | 0 |  Index (4+)   |
            // +---+---+-----------------------+
            // | H |     Value Length (7+)     |
            // +---+---------------------------+
            // | Value String (Length octets)  |
            // +-------------------------------+
 
            if ((uint)destination.Length >= 2)
            {
                destination[0] = 0;
                if (IntegerEncoder.Encode(index, 4, destination, out int indexLength))
                {
                    Debug.Assert(indexLength >= 1);
                    if (EncodeStringLiteral(value, valueEncoding, destination.Slice(indexLength), out int nameLength))
                    {
                        bytesWritten = indexLength + nameLength;
                        return true;
                    }
                }
            }
 
            bytesWritten = 0;
            return false;
        }
 
        /// <summary>Encodes a "Literal Header Field never Indexing".</summary>
        public static bool EncodeLiteralHeaderFieldNeverIndexing(int index, string value, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
        {
            // From https://tools.ietf.org/html/rfc7541#section-6.2.3
            // ------------------------------------------------------
            //   0   1   2   3   4   5   6   7
            // +---+---+---+---+---+---+---+---+
            // | 0 | 0 | 0 | 1 |  Index (4+)   |
            // +---+---+-----------------------+
            // | H |     Value Length (7+)     |
            // +---+---------------------------+
            // | Value String (Length octets)  |
            // +-------------------------------+
 
            if ((uint)destination.Length >= 2)
            {
                destination[0] = 0x10;
                if (IntegerEncoder.Encode(index, 4, destination, out int indexLength))
                {
                    Debug.Assert(indexLength >= 1);
                    if (EncodeStringLiteral(value, valueEncoding, destination.Slice(indexLength), out int nameLength))
                    {
                        bytesWritten = indexLength + nameLength;
                        return true;
                    }
                }
            }
 
            bytesWritten = 0;
            return false;
        }
 
        /// <summary>Encodes a "Literal Header Field with Indexing".</summary>
        public static bool EncodeLiteralHeaderFieldIndexing(int index, string value, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
        {
            // From https://tools.ietf.org/html/rfc7541#section-6.2.2
            // ------------------------------------------------------
            //   0   1   2   3   4   5   6   7
            // +---+---+---+---+---+---+---+---+
            // | 0 | 1 |      Index (6+)       |
            // +---+---+-----------------------+
            // | H |     Value Length (7+)     |
            // +---+---------------------------+
            // | Value String (Length octets)  |
            // +-------------------------------+
 
            if ((uint)destination.Length >= 2)
            {
                destination[0] = 0x40;
                if (IntegerEncoder.Encode(index, 6, destination, out int indexLength))
                {
                    Debug.Assert(indexLength >= 1);
                    if (EncodeStringLiteral(value, valueEncoding, destination.Slice(indexLength), out int nameLength))
                    {
                        bytesWritten = indexLength + nameLength;
                        return true;
                    }
                }
            }
 
            bytesWritten = 0;
            return false;
        }
 
        /// <summary>
        /// Encodes a "Literal Header Field without Indexing", but only the index portion;
        /// a subsequent call to <c>EncodeStringLiteral</c> must be used to encode the associated value.
        /// </summary>
        public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, Span<byte> destination, out int bytesWritten)
        {
            // From https://tools.ietf.org/html/rfc7541#section-6.2.2
            // ------------------------------------------------------
            //   0   1   2   3   4   5   6   7
            // +---+---+---+---+---+---+---+---+
            // | 0 | 0 | 0 | 0 |  Index (4+)   |
            // +---+---+-----------------------+
            //
            // ... expected after this:
            //
            // | H |     Value Length (7+)     |
            // +---+---------------------------+
            // | Value String (Length octets)  |
            // +-------------------------------+
 
            if ((uint)destination.Length != 0)
            {
                destination[0] = 0;
                if (IntegerEncoder.Encode(index, 4, destination, out int indexLength))
                {
                    Debug.Assert(indexLength >= 1);
                    bytesWritten = indexLength;
                    return true;
                }
            }
 
            bytesWritten = 0;
            return false;
        }
 
        /// <summary>Encodes a "Literal Header Field with Indexing - New Name".</summary>
        public static bool EncodeLiteralHeaderFieldIndexingNewName(string name, string value, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
        {
            // From https://tools.ietf.org/html/rfc7541#section-6.2.2
            // ------------------------------------------------------
            //   0   1   2   3   4   5   6   7
            // +---+---+---+---+---+---+---+---+
            // | 0 | 1 |           0           |
            // +---+---+-----------------------+
            // | H |     Name Length (7+)      |
            // +---+---------------------------+
            // |  Name String (Length octets)  |
            // +---+---------------------------+
            // | H |     Value Length (7+)     |
            // +---+---------------------------+
            // | Value String (Length octets)  |
            // +-------------------------------+
 
            return EncodeLiteralHeaderNewNameCore(0x40, name, value, valueEncoding, destination, out bytesWritten);
        }
 
        /// <summary>Encodes a "Literal Header Field without Indexing - New Name".</summary>
        public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, string value, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
        {
            // From https://tools.ietf.org/html/rfc7541#section-6.2.2
            // ------------------------------------------------------
            //   0   1   2   3   4   5   6   7
            // +---+---+---+---+---+---+---+---+
            // | 0 | 0 | 0 | 0 |       0       |
            // +---+---+-----------------------+
            // | H |     Name Length (7+)      |
            // +---+---------------------------+
            // |  Name String (Length octets)  |
            // +---+---------------------------+
            // | H |     Value Length (7+)     |
            // +---+---------------------------+
            // | Value String (Length octets)  |
            // +-------------------------------+
 
            return EncodeLiteralHeaderNewNameCore(0, name, value, valueEncoding, destination, out bytesWritten);
        }
 
        /// <summary>Encodes a "Literal Header Field never Indexing - New Name".</summary>
        public static bool EncodeLiteralHeaderFieldNeverIndexingNewName(string name, string value, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
        {
            // From https://tools.ietf.org/html/rfc7541#section-6.2.3
            // ------------------------------------------------------
            //   0   1   2   3   4   5   6   7
            // +---+---+---+---+---+---+---+---+
            // | 0 | 0 | 0 | 1 |       0       |
            // +---+---+-----------------------+
            // | H |     Name Length (7+)      |
            // +---+---------------------------+
            // |  Name String (Length octets)  |
            // +---+---------------------------+
            // | H |     Value Length (7+)     |
            // +---+---------------------------+
            // | Value String (Length octets)  |
            // +-------------------------------+
 
            return EncodeLiteralHeaderNewNameCore(0x10, name, value, valueEncoding, destination, out bytesWritten);
        }
 
        private static bool EncodeLiteralHeaderNewNameCore(byte mask, string name, string value, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
        {
            if ((uint)destination.Length >= 3)
            {
                destination[0] = mask;
                if (EncodeLiteralHeaderName(name, destination.Slice(1), out int nameLength) &&
                    EncodeStringLiteral(value, valueEncoding, destination.Slice(1 + nameLength), out int valueLength))
                {
                    bytesWritten = 1 + nameLength + valueLength;
                    return true;
                }
            }
 
            bytesWritten = 0;
            return false;
        }
 
        /// <summary>Encodes a "Literal Header Field without Indexing - New Name".</summary>
        public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, ReadOnlySpan<string> values, byte[] separator, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
        {
            // From https://tools.ietf.org/html/rfc7541#section-6.2.2
            // ------------------------------------------------------
            //   0   1   2   3   4   5   6   7
            // +---+---+---+---+---+---+---+---+
            // | 0 | 0 | 0 | 0 |       0       |
            // +---+---+-----------------------+
            // | H |     Name Length (7+)      |
            // +---+---------------------------+
            // |  Name String (Length octets)  |
            // +---+---------------------------+
            // | H |     Value Length (7+)     |
            // +---+---------------------------+
            // | Value String (Length octets)  |
            // +-------------------------------+
 
            if ((uint)destination.Length >= 3)
            {
                destination[0] = 0;
                if (EncodeLiteralHeaderName(name, destination.Slice(1), out int nameLength) &&
                    EncodeStringLiterals(values, separator, valueEncoding, destination.Slice(1 + nameLength), out int valueLength))
                {
                    bytesWritten = 1 + nameLength + valueLength;
                    return true;
                }
            }
 
            bytesWritten = 0;
            return false;
        }
 
        /// <summary>
        /// Encodes a "Literal Header Field without Indexing - New Name", but only the name portion;
        /// a subsequent call to <c>EncodeStringLiteral</c> must be used to encode the associated value.
        /// </summary>
        public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, Span<byte> destination, out int bytesWritten)
        {
            // From https://tools.ietf.org/html/rfc7541#section-6.2.2
            // ------------------------------------------------------
            //   0   1   2   3   4   5   6   7
            // +---+---+---+---+---+---+---+---+
            // | 0 | 0 | 0 | 0 |       0       |
            // +---+---+-----------------------+
            // | H |     Name Length (7+)      |
            // +---+---------------------------+
            // |  Name String (Length octets)  |
            // +---+---------------------------+
            //
            // ... expected after this:
            //
            // | H |     Value Length (7+)     |
            // +---+---------------------------+
            // | Value String (Length octets)  |
            // +-------------------------------+
 
            if ((uint)destination.Length >= 2)
            {
                destination[0] = 0;
                if (EncodeLiteralHeaderName(name, destination.Slice(1), out int nameLength))
                {
                    bytesWritten = 1 + nameLength;
                    return true;
                }
            }
 
            bytesWritten = 0;
            return false;
        }
 
        private static bool EncodeLiteralHeaderName(string value, Span<byte> destination, out int bytesWritten)
        {
            // From https://tools.ietf.org/html/rfc7541#section-5.2
            // ------------------------------------------------------
            //   0   1   2   3   4   5   6   7
            // +---+---+---+---+---+---+---+---+
            // | H |    String Length (7+)     |
            // +---+---------------------------+
            // |  String Data (Length octets)  |
            // +-------------------------------+
 
            Debug.Assert(Ascii.IsValid(value));
 
            if (destination.Length != 0)
            {
                destination[0] = 0; // TODO: Use Huffman encoding
                if (IntegerEncoder.Encode(value.Length, 7, destination, out int integerLength))
                {
                    Debug.Assert(integerLength >= 1);
 
                    destination = destination.Slice(integerLength);
                    if (value.Length <= destination.Length)
                    {
                        OperationStatus status = Ascii.ToLower(value, destination, out int valueBytesWritten);
                        Debug.Assert(status == OperationStatus.Done);
                        Debug.Assert(valueBytesWritten == value.Length);
 
                        bytesWritten = integerLength + value.Length;
                        return true;
                    }
                }
            }
 
            bytesWritten = 0;
            return false;
        }
 
        private static void EncodeValueStringPart(string value, Span<byte> destination)
        {
            Debug.Assert(destination.Length >= value.Length);
 
            OperationStatus status = Ascii.FromUtf16(value, destination, out int bytesWritten);
 
            if (status == OperationStatus.InvalidData)
            {
                throw new HttpRequestException(SR.net_http_request_invalid_char_encoding);
            }
 
            Debug.Assert(status == OperationStatus.Done);
            Debug.Assert(bytesWritten == value.Length);
        }
 
        public static bool EncodeStringLiteral(ReadOnlySpan<byte> value, Span<byte> destination, out int bytesWritten)
        {
            // From https://tools.ietf.org/html/rfc7541#section-5.2
            // ------------------------------------------------------
            //   0   1   2   3   4   5   6   7
            // +---+---+---+---+---+---+---+---+
            // | H |    String Length (7+)     |
            // +---+---------------------------+
            // |  String Data (Length octets)  |
            // +-------------------------------+
 
            if (destination.Length != 0)
            {
                destination[0] = 0; // TODO: Use Huffman encoding
                if (IntegerEncoder.Encode(value.Length, 7, destination, out int integerLength))
                {
                    Debug.Assert(integerLength >= 1);
 
                    destination = destination.Slice(integerLength);
                    if (value.Length <= destination.Length)
                    {
                        // Note: No validation. Bytes should have already been validated.
                        value.CopyTo(destination);
 
                        bytesWritten = integerLength + value.Length;
                        return true;
                    }
                }
            }
 
            bytesWritten = 0;
            return false;
        }
 
        public static bool EncodeStringLiteral(string value, Span<byte> destination, out int bytesWritten)
        {
            return EncodeStringLiteral(value, valueEncoding: null, destination, out bytesWritten);
        }
 
        public static bool EncodeStringLiteral(string value, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
        {
            // From https://tools.ietf.org/html/rfc7541#section-5.2
            // ------------------------------------------------------
            //   0   1   2   3   4   5   6   7
            // +---+---+---+---+---+---+---+---+
            // | H |    String Length (7+)     |
            // +---+---------------------------+
            // |  String Data (Length octets)  |
            // +-------------------------------+
 
            if (destination.Length != 0)
            {
                destination[0] = 0; // TODO: Use Huffman encoding
 
                int encodedStringLength = valueEncoding is null || ReferenceEquals(valueEncoding, Encoding.Latin1)
                    ? value.Length
                    : valueEncoding.GetByteCount(value);
 
                if (IntegerEncoder.Encode(encodedStringLength, 7, destination, out int integerLength))
                {
                    Debug.Assert(integerLength >= 1);
                    destination = destination.Slice(integerLength);
                    if (encodedStringLength <= destination.Length)
                    {
                        if (valueEncoding is null)
                        {
                            EncodeValueStringPart(value, destination);
                        }
                        else
                        {
                            int written = valueEncoding.GetBytes(value, destination);
                            Debug.Assert(written == encodedStringLength);
                        }
 
                        bytesWritten = integerLength + encodedStringLength;
                        return true;
                    }
                }
            }
 
            bytesWritten = 0;
            return false;
        }
 
        public static bool EncodeDynamicTableSizeUpdate(int value, Span<byte> destination, out int bytesWritten)
        {
            // From https://tools.ietf.org/html/rfc7541#section-6.3
            // ----------------------------------------------------
            //   0   1   2   3   4   5   6   7
            // +---+---+---+---+---+---+---+---+
            // | 0 | 0 | 1 |   Max size (5+)   |
            // +---+---------------------------+
 
            if (destination.Length != 0)
            {
                destination[0] = 0x20;
                return IntegerEncoder.Encode(value, 5, destination, out bytesWritten);
            }
 
            bytesWritten = 0;
            return false;
        }
 
        public static bool EncodeStringLiterals(ReadOnlySpan<string> values, byte[]? separator, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
        {
            bytesWritten = 0;
 
            if (values.Length == 0)
            {
                return EncodeStringLiteral("", valueEncoding: null, destination, out bytesWritten);
            }
            else if (values.Length == 1)
            {
                return EncodeStringLiteral(values[0], valueEncoding, destination, out bytesWritten);
            }
 
            if (destination.Length != 0)
            {
                Debug.Assert(separator != null);
                Debug.Assert(Ascii.IsValid(separator));
                int valueLength = checked((values.Length - 1) * separator.Length);
 
                // Calculate length of all values.
                if (valueEncoding is null || ReferenceEquals(valueEncoding, Encoding.Latin1))
                {
                    foreach (string part in values)
                    {
                        valueLength = checked(valueLength + part.Length);
                    }
                }
                else
                {
                    foreach (string part in values)
                    {
                        valueLength = checked(valueLength + valueEncoding.GetByteCount(part));
                    }
                }
 
                destination[0] = 0;
                if (IntegerEncoder.Encode(valueLength, 7, destination, out int integerLength))
                {
                    Debug.Assert(integerLength >= 1);
                    destination = destination.Slice(integerLength);
                    if (destination.Length >= valueLength)
                    {
                        if (valueEncoding is null)
                        {
                            string value = values[0];
                            EncodeValueStringPart(value, destination);
                            destination = destination.Slice(value.Length);
 
                            for (int i = 1; i < values.Length; i++)
                            {
                                separator.CopyTo(destination);
                                destination = destination.Slice(separator.Length);
 
                                value = values[i];
                                EncodeValueStringPart(value, destination);
                                destination = destination.Slice(value.Length);
                            }
                        }
                        else
                        {
                            int written = valueEncoding.GetBytes(values[0], destination);
                            destination = destination.Slice(written);
 
                            for (int i = 1; i < values.Length; i++)
                            {
                                separator.CopyTo(destination);
                                destination = destination.Slice(separator.Length);
 
                                written = valueEncoding.GetBytes(values[i], destination);
                                destination = destination.Slice(written);
                            }
                        }
 
                        bytesWritten = integerLength + valueLength;
                        return true;
                    }
                }
            }
 
            return false;
        }
 
        /// <summary>
        /// Encodes a "Literal Header Field without Indexing" to a new array, but only the index portion;
        /// a subsequent call to <c>EncodeStringLiteral</c> must be used to encode the associated value.
        /// </summary>
        public static byte[] EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(int index)
        {
            Span<byte> span = stackalloc byte[256];
            bool success = EncodeLiteralHeaderFieldWithoutIndexing(index, span, out int length);
            Debug.Assert(success, $"Stack-allocated space was too small for index '{index}'.");
            return span.Slice(0, length).ToArray();
        }
 
        /// <summary>
        /// Encodes a "Literal Header Field without Indexing - New Name" to a new array, but only the name portion;
        /// a subsequent call to <c>EncodeStringLiteral</c> must be used to encode the associated value.
        /// </summary>
        public static byte[] EncodeLiteralHeaderFieldWithoutIndexingNewNameToAllocatedArray(string name)
        {
            Span<byte> span = stackalloc byte[256];
            bool success = EncodeLiteralHeaderFieldWithoutIndexingNewName(name, span, out int length);
            Debug.Assert(success, $"Stack-allocated space was too small for \"{name}\".");
            return span.Slice(0, length).ToArray();
        }
 
        /// <summary>Encodes a "Literal Header Field without Indexing" to a new array.</summary>
        public static byte[] EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(int index, string value)
        {
            Span<byte> span =
#if DEBUG
                stackalloc byte[4]; // to validate growth algorithm
#else
                stackalloc byte[512];
#endif
            while (true)
            {
                if (EncodeLiteralHeaderFieldWithoutIndexing(index, value, valueEncoding: null, span, out int length))
                {
                    return span.Slice(0, length).ToArray();
                }
 
                // This is a rare path, only used once per HTTP/2 connection and only
                // for very long host names.  Just allocate rather than complicate
                // the code with ArrayPool usage.  In practice we should never hit this,
                // as hostnames should be <= 255 characters.
                span = new byte[span.Length * 2];
            }
        }
    }
}