Skip to main content

src/nhttp_hpack.erl

-module(nhttp_hpack).

-moduledoc """
HPACK header compression for HTTP/2 (RFC 7541).

This module implements the HPACK header compression format used by
HTTP/2. It provides stateful encoding and decoding of header fields
using static and dynamic tables.

## Usage

```erlang
{ok, EncState0} = nhttp_hpack:new(),
{ok, DecState0} = nhttp_hpack:new(),

Headers = [{<<":method">>, <<"GET">>}, {<<":path">>, <<"/">>}],
{ok, HeaderBlock, EncState1} = nhttp_hpack:encode(Headers, EncState0),

{ok, DecodedHeaders, DecState1} = nhttp_hpack:decode(HeaderBlock, DecState0).
```
""".

%%%-----------------------------------------------------------------------------
%% INLINE DIRECTIVES
%%%-----------------------------------------------------------------------------
-compile({inline, [has_uppercase/1]}).

%%%-----------------------------------------------------------------------------
%% STATE MANAGEMENT
%%%-----------------------------------------------------------------------------
-export([
    is_empty/1,
    new/0,
    new/1,
    set_max_table_size/2,
    table_size/1
]).

%%%-----------------------------------------------------------------------------
%% DECODING
%%%-----------------------------------------------------------------------------
-export([decode/2, decode/3]).

%%%-----------------------------------------------------------------------------
%% ENCODING
%%%-----------------------------------------------------------------------------
-export([encode/2, encode/3]).

%%%-----------------------------------------------------------------------------
%% TYPE EXPORTS
%%%-----------------------------------------------------------------------------
-export_type([decode_error/0, decode_opts/0, encode_opts/0, headers/0, state/0]).

%%%-----------------------------------------------------------------------------
%% TYPES
%%%-----------------------------------------------------------------------------
-type headers() :: [{Name :: binary(), Value :: binary()}].
-type encode_opts() :: #{
    huffman => boolean()
}.
-type decode_opts() :: #{
    max_list_size => pos_integer() | infinity
}.
-type decode_error() ::
    dynamic_table_size_exceeded
    | invalid_table_index
    | integer_overflow
    | invalid_huffman
    | incomplete_header_block
    | header_list_too_large
    | uppercase_header_name.

%%%-----------------------------------------------------------------------------
%% CONSTANTS
%%%-----------------------------------------------------------------------------
-define(ENTRY_OVERHEAD, 32).

%%%-----------------------------------------------------------------------------
%% RECORDS
%%%-----------------------------------------------------------------------------
-record(hpack, {
    size = 0 :: non_neg_integer(),
    max_size = 4096 :: non_neg_integer(),
    configured_max_size = 4096 :: non_neg_integer(),
    next_seq = 0 :: non_neg_integer(),
    oldest_seq = 0 :: non_neg_integer(),
    entries = #{} :: #{non_neg_integer() => {pos_integer(), {binary(), binary()}}},
    full_index = #{} :: #{{binary(), binary()} => non_neg_integer()},
    name_index = #{} :: #{binary() => non_neg_integer()}
}).

-opaque state() :: #hpack{}.

%%%-----------------------------------------------------------------------------
%% STATE MANAGEMENT
%%%-----------------------------------------------------------------------------
-doc "Check if the dynamic table is empty.".
-spec is_empty(State :: state()) -> boolean().
is_empty(#hpack{size = 0}) ->
    true;
is_empty(_) ->
    false.

-doc "Create a new HPACK state with default max size (4096 bytes).".
-spec new() -> {ok, state()}.
new() ->
    {ok, #hpack{}}.

-doc "Create a new HPACK state with specified max size.".
-spec new(MaxSize :: non_neg_integer()) -> {ok, state()}.
new(MaxSize) ->
    {ok, #hpack{max_size = MaxSize, configured_max_size = MaxSize}}.

-doc "Update the maximum table size (from SETTINGS_HEADER_TABLE_SIZE). Immediately evicts entries if the new size is smaller than current table size.".
-spec set_max_table_size(MaxSize :: non_neg_integer(), State :: state()) -> {ok, state()}.
set_max_table_size(MaxSize, State) ->
    {ok, update_table_size(MaxSize, State#hpack{configured_max_size = MaxSize})}.

-doc "Get the current dynamic table size in bytes.".
-spec table_size(State :: state()) -> non_neg_integer().
table_size(#hpack{size = Size}) ->
    Size.

%%%-----------------------------------------------------------------------------
%% DECODING
%%%-----------------------------------------------------------------------------
-doc "Decode a header block.".
-spec decode(Data :: binary(), State :: state()) ->
    {ok, Headers :: headers(), NewState :: state()} | {error, decode_error()}.
decode(Data, State) ->
    decode(Data, State, #{}).

-doc """
Decode a header block, aborting with `{error, header_list_too_large}` once
the cumulative decoded list size exceeds `max_list_size`. The check matches
the RFC 9113 §10.5.1 octet count (name + value + 32 per entry).
""".
-spec decode(Data :: binary(), State :: state(), Opts :: decode_opts()) ->
    {ok, Headers :: headers(), NewState :: state()} | {error, decode_error()}.
decode(Data, State, Opts) ->
    Limit = maps:get(max_list_size, Opts, infinity),
    decode_block(Data, State, [], 0, Limit).

%%%-----------------------------------------------------------------------------
%% ENCODING
%%%-----------------------------------------------------------------------------
-doc "Encode headers without Huffman encoding.".
-spec encode(Headers :: headers(), State :: state()) -> {ok, iodata(), state()}.
encode(Headers, State) ->
    encode(Headers, State, #{huffman => false}).

-doc "Encode headers with options.".
-spec encode(Headers :: headers(), State :: state(), Opts :: encode_opts()) ->
    {ok, iodata(), state()}.
encode(Headers, State0, Opts) ->
    UseHuffman = maps:get(huffman, Opts, false),
    {Prefix, State1} = maybe_emit_table_size_update(State0),
    {Data, State2} = encode_headers(Headers, State1, UseHuffman, []),
    {ok, [Prefix | Data], State2}.

%%%-----------------------------------------------------------------------------
%% INTERNAL FUNCTIONS
%%%-----------------------------------------------------------------------------
-spec clear_table(state()) -> state().
clear_table(State = #hpack{next_seq = NextSeq}) ->
    State#hpack{
        size = 0,
        oldest_seq = NextSeq,
        entries = #{},
        full_index = #{},
        name_index = #{}
    }.

-spec decode_block(
    binary(),
    state(),
    headers(),
    non_neg_integer(),
    pos_integer() | infinity
) ->
    {ok, headers(), state()} | {error, decode_error()}.
decode_block(
    <<2#001:3, Rest/bits>>, State = #hpack{configured_max_size = ConfigMax}, Acc, Total, Limit
) ->
    maybe
        {ok, MaxSize, Rest2} ?= map_int_error(nhttp_int:dec5(Rest)),
        case MaxSize =< ConfigMax of
            true ->
                State2 = update_table_size(MaxSize, State),
                decode_block(Rest2, State2, Acc, Total, Limit);
            false ->
                {error, dynamic_table_size_exceeded}
        end
    end;
decode_block(Data, State, Acc, Total, Limit) ->
    decode_headers(Data, State, Acc, Total, Limit).

-spec decode_headers(
    binary(),
    state(),
    headers(),
    non_neg_integer(),
    pos_integer() | infinity
) ->
    {ok, headers(), state()} | {error, decode_error()}.
decode_headers(<<>>, State, Acc, _Total, _Limit) ->
    {ok, lists:reverse(Acc), State};
decode_headers(<<2#1:1, Rest/bits>>, State, Acc, Total, Limit) ->
    maybe
        {ok, Index, Rest2} ?= map_int_error(nhttp_int:dec7(Rest)),
        {ok, {Name, Value}} ?= lookup(Index, State),
        {ok, NewAcc, NewTotal} ?= push_header({Name, Value}, Acc, Total, Limit),
        decode_headers(Rest2, State, NewAcc, NewTotal, Limit)
    end;
decode_headers(<<2#01:2, 2#000000:6, Rest/bits>>, State, Acc, Total, Limit) ->
    maybe
        {ok, Name, Rest2} ?= map_str_error(nhttp_str:decode(Rest)),
        ok ?= validate_name_no_uppercase(Name),
        {ok, Value, Rest3} ?= map_str_error(nhttp_str:decode(Rest2)),
        State2 = insert({Name, Value}, State),
        {ok, NewAcc, NewTotal} ?= push_header({Name, Value}, Acc, Total, Limit),
        decode_headers(Rest3, State2, NewAcc, NewTotal, Limit)
    end;
decode_headers(<<2#01:2, Rest/bits>>, State, Acc, Total, Limit) ->
    maybe
        {ok, Index, Rest2} ?= map_int_error(nhttp_int:dec6(Rest)),
        {ok, {Name, _}} ?= lookup(Index, State),
        {ok, Value, Rest3} ?= map_str_error(nhttp_str:decode(Rest2)),
        State2 = insert({Name, Value}, State),
        {ok, NewAcc, NewTotal} ?= push_header({Name, Value}, Acc, Total, Limit),
        decode_headers(Rest3, State2, NewAcc, NewTotal, Limit)
    end;
decode_headers(<<2#0000:4, 2#0000:4, Rest/bits>>, State, Acc, Total, Limit) ->
    maybe
        {ok, Name, Rest2} ?= map_str_error(nhttp_str:decode(Rest)),
        ok ?= validate_name_no_uppercase(Name),
        {ok, Value, Rest3} ?= map_str_error(nhttp_str:decode(Rest2)),
        {ok, NewAcc, NewTotal} ?= push_header({Name, Value}, Acc, Total, Limit),
        decode_headers(Rest3, State, NewAcc, NewTotal, Limit)
    end;
decode_headers(<<2#0000:4, Rest/bits>>, State, Acc, Total, Limit) ->
    maybe
        {ok, Index, Rest2} ?= map_int_error(nhttp_int:dec4(Rest)),
        {ok, {Name, _}} ?= lookup(Index, State),
        {ok, Value, Rest3} ?= map_str_error(nhttp_str:decode(Rest2)),
        {ok, NewAcc, NewTotal} ?= push_header({Name, Value}, Acc, Total, Limit),
        decode_headers(Rest3, State, NewAcc, NewTotal, Limit)
    end;
decode_headers(<<2#0001:4, 2#0000:4, Rest/bits>>, State, Acc, Total, Limit) ->
    maybe
        {ok, Name, Rest2} ?= map_str_error(nhttp_str:decode(Rest)),
        ok ?= validate_name_no_uppercase(Name),
        {ok, Value, Rest3} ?= map_str_error(nhttp_str:decode(Rest2)),
        {ok, NewAcc, NewTotal} ?= push_header({Name, Value}, Acc, Total, Limit),
        decode_headers(Rest3, State, NewAcc, NewTotal, Limit)
    end;
decode_headers(<<2#0001:4, Rest/bits>>, State, Acc, Total, Limit) ->
    maybe
        {ok, Index, Rest2} ?= map_int_error(nhttp_int:dec4(Rest)),
        {ok, {Name, _}} ?= lookup(Index, State),
        {ok, Value, Rest3} ?= map_str_error(nhttp_str:decode(Rest2)),
        {ok, NewAcc, NewTotal} ?= push_header({Name, Value}, Acc, Total, Limit),
        decode_headers(Rest3, State, NewAcc, NewTotal, Limit)
    end;
decode_headers(_, _, _, _, _) ->
    {error, incomplete_header_block}.

encode_headers([], State, _, Acc) ->
    {lists:reverse(Acc), State};
encode_headers([{Name, Value} | Tail], State, UseHuffman, Acc) ->
    Header = {Name, Value},
    case find(Header, State) of
        {field, Index} ->
            Encoded = nhttp_int:enc7(Index, 2#1),
            encode_headers(Tail, State, UseHuffman, [Encoded | Acc]);
        {name, Index} ->
            State2 = insert(Header, State),
            Encoded = [nhttp_int:enc6(Index, 2#01) | nhttp_str:encode(Value, UseHuffman)],
            encode_headers(Tail, State2, UseHuffman, [Encoded | Acc]);
        not_found ->
            State2 = insert(Header, State),
            Encoded = [
                <<2#01:2, 0:6>>
                | [nhttp_str:encode(Name, UseHuffman) | nhttp_str:encode(Value, UseHuffman)]
            ],
            encode_headers(Tail, State2, UseHuffman, [Encoded | Acc])
    end.

-spec evict_to_size(non_neg_integer(), state()) -> state().
evict_to_size(TargetSize, State = #hpack{size = Size}) when Size =< TargetSize ->
    State;
evict_to_size(
    TargetSize,
    State = #hpack{
        size = Size,
        oldest_seq = OldestSeq,
        entries = Entries
    }
) ->
    case maps:get(OldestSeq, Entries, undefined) of
        undefined ->
            State;
        {EntrySize, _Header} ->
            NewState = State#hpack{
                size = Size - EntrySize,
                oldest_seq = OldestSeq + 1,
                entries = maps:remove(OldestSeq, Entries)
            },
            evict_to_size(TargetSize, NewState)
    end.

-spec find({binary(), binary()}, state()) ->
    {field, pos_integer()} | {name, pos_integer()} | not_found.
find({<<":authority">>, <<>>}, _) -> {field, 1};
find({<<":authority">>, _}, _) -> {name, 1};
find({<<":method">>, <<"GET">>}, _) -> {field, 2};
find({<<":method">>, <<"POST">>}, _) -> {field, 3};
find({<<":method">>, _}, _) -> {name, 2};
find({<<":path">>, <<"/">>}, _) -> {field, 4};
find({<<":path">>, <<"/index.html">>}, _) -> {field, 5};
find({<<":path">>, _}, _) -> {name, 4};
find({<<":scheme">>, <<"http">>}, _) -> {field, 6};
find({<<":scheme">>, <<"https">>}, _) -> {field, 7};
find({<<":scheme">>, _}, _) -> {name, 6};
find({<<":status">>, <<"200">>}, _) -> {field, 8};
find({<<":status">>, <<"204">>}, _) -> {field, 9};
find({<<":status">>, <<"206">>}, _) -> {field, 10};
find({<<":status">>, <<"304">>}, _) -> {field, 11};
find({<<":status">>, <<"400">>}, _) -> {field, 12};
find({<<":status">>, <<"404">>}, _) -> {field, 13};
find({<<":status">>, <<"500">>}, _) -> {field, 14};
find({<<":status">>, _}, _) -> {name, 8};
find({<<"accept-charset">>, <<>>}, _) -> {field, 15};
find({<<"accept-charset">>, _}, _) -> {name, 15};
find({<<"accept-encoding">>, <<"gzip, deflate">>}, _) -> {field, 16};
find({<<"accept-encoding">>, _}, _) -> {name, 16};
find({<<"accept-language">>, <<>>}, _) -> {field, 17};
find({<<"accept-language">>, _}, _) -> {name, 17};
find({<<"accept-ranges">>, <<>>}, _) -> {field, 18};
find({<<"accept-ranges">>, _}, _) -> {name, 18};
find({<<"accept">>, <<>>}, _) -> {field, 19};
find({<<"accept">>, _}, _) -> {name, 19};
find({<<"access-control-allow-origin">>, <<>>}, _) -> {field, 20};
find({<<"access-control-allow-origin">>, _}, _) -> {name, 20};
find({<<"age">>, <<>>}, _) -> {field, 21};
find({<<"age">>, _}, _) -> {name, 21};
find({<<"allow">>, <<>>}, _) -> {field, 22};
find({<<"allow">>, _}, _) -> {name, 22};
find({<<"authorization">>, <<>>}, _) -> {field, 23};
find({<<"authorization">>, _}, _) -> {name, 23};
find({<<"cache-control">>, <<>>}, _) -> {field, 24};
find({<<"cache-control">>, _}, _) -> {name, 24};
find({<<"content-disposition">>, <<>>}, _) -> {field, 25};
find({<<"content-disposition">>, _}, _) -> {name, 25};
find({<<"content-encoding">>, <<>>}, _) -> {field, 26};
find({<<"content-encoding">>, _}, _) -> {name, 26};
find({<<"content-language">>, <<>>}, _) -> {field, 27};
find({<<"content-language">>, _}, _) -> {name, 27};
find({<<"content-length">>, <<>>}, _) -> {field, 28};
find({<<"content-length">>, _}, _) -> {name, 28};
find({<<"content-location">>, <<>>}, _) -> {field, 29};
find({<<"content-location">>, _}, _) -> {name, 29};
find({<<"content-range">>, <<>>}, _) -> {field, 30};
find({<<"content-range">>, _}, _) -> {name, 30};
find({<<"content-type">>, <<>>}, _) -> {field, 31};
find({<<"content-type">>, _}, _) -> {name, 31};
find({<<"cookie">>, <<>>}, _) -> {field, 32};
find({<<"cookie">>, _}, _) -> {name, 32};
find({<<"date">>, <<>>}, _) -> {field, 33};
find({<<"date">>, _}, _) -> {name, 33};
find({<<"etag">>, <<>>}, _) -> {field, 34};
find({<<"etag">>, _}, _) -> {name, 34};
find({<<"expect">>, <<>>}, _) -> {field, 35};
find({<<"expect">>, _}, _) -> {name, 35};
find({<<"expires">>, <<>>}, _) -> {field, 36};
find({<<"expires">>, _}, _) -> {name, 36};
find({<<"from">>, <<>>}, _) -> {field, 37};
find({<<"from">>, _}, _) -> {name, 37};
find({<<"host">>, <<>>}, _) -> {field, 38};
find({<<"host">>, _}, _) -> {name, 38};
find({<<"if-match">>, <<>>}, _) -> {field, 39};
find({<<"if-match">>, _}, _) -> {name, 39};
find({<<"if-modified-since">>, <<>>}, _) -> {field, 40};
find({<<"if-modified-since">>, _}, _) -> {name, 40};
find({<<"if-none-match">>, <<>>}, _) -> {field, 41};
find({<<"if-none-match">>, _}, _) -> {name, 41};
find({<<"if-range">>, <<>>}, _) -> {field, 42};
find({<<"if-range">>, _}, _) -> {name, 42};
find({<<"if-unmodified-since">>, <<>>}, _) -> {field, 43};
find({<<"if-unmodified-since">>, _}, _) -> {name, 43};
find({<<"last-modified">>, <<>>}, _) -> {field, 44};
find({<<"last-modified">>, _}, _) -> {name, 44};
find({<<"link">>, <<>>}, _) -> {field, 45};
find({<<"link">>, _}, _) -> {name, 45};
find({<<"location">>, <<>>}, _) -> {field, 46};
find({<<"location">>, _}, _) -> {name, 46};
find({<<"max-forwards">>, <<>>}, _) -> {field, 47};
find({<<"max-forwards">>, _}, _) -> {name, 47};
find({<<"proxy-authenticate">>, <<>>}, _) -> {field, 48};
find({<<"proxy-authenticate">>, _}, _) -> {name, 48};
find({<<"proxy-authorization">>, <<>>}, _) -> {field, 49};
find({<<"proxy-authorization">>, _}, _) -> {name, 49};
find({<<"range">>, <<>>}, _) -> {field, 50};
find({<<"range">>, _}, _) -> {name, 50};
find({<<"referer">>, <<>>}, _) -> {field, 51};
find({<<"referer">>, _}, _) -> {name, 51};
find({<<"refresh">>, <<>>}, _) -> {field, 52};
find({<<"refresh">>, _}, _) -> {name, 52};
find({<<"retry-after">>, <<>>}, _) -> {field, 53};
find({<<"retry-after">>, _}, _) -> {name, 53};
find({<<"server">>, <<>>}, _) -> {field, 54};
find({<<"server">>, _}, _) -> {name, 54};
find({<<"set-cookie">>, <<>>}, _) -> {field, 55};
find({<<"set-cookie">>, _}, _) -> {name, 55};
find({<<"strict-transport-security">>, <<>>}, _) -> {field, 56};
find({<<"strict-transport-security">>, _}, _) -> {name, 56};
find({<<"transfer-encoding">>, <<>>}, _) -> {field, 57};
find({<<"transfer-encoding">>, _}, _) -> {name, 57};
find({<<"user-agent">>, <<>>}, _) -> {field, 58};
find({<<"user-agent">>, _}, _) -> {name, 58};
find({<<"vary">>, <<>>}, _) -> {field, 59};
find({<<"vary">>, _}, _) -> {name, 59};
find({<<"via">>, <<>>}, _) -> {field, 60};
find({<<"via">>, _}, _) -> {name, 60};
find({<<"www-authenticate">>, <<>>}, _) -> {field, 61};
find({<<"www-authenticate">>, _}, _) -> {name, 61};
find(Header, State) -> find_dyn(Header, State).

-spec find_dyn({binary(), binary()}, state()) ->
    {field, pos_integer()} | {name, pos_integer()} | not_found.
find_dyn({Name, _Value} = Header, #hpack{
    next_seq = NextSeq,
    oldest_seq = OldestSeq,
    full_index = FullIndex,
    name_index = NameIndex
}) ->
    case maps:get(Header, FullIndex, undefined) of
        Seq when is_integer(Seq), Seq >= OldestSeq ->
            Index = 62 + (NextSeq - 1 - Seq),
            {field, Index};
        _ ->
            case maps:get(Name, NameIndex, undefined) of
                NameSeq when is_integer(NameSeq), NameSeq >= OldestSeq ->
                    Index = 62 + (NextSeq - 1 - NameSeq),
                    {name, Index};
                _ ->
                    not_found
            end
    end.

-spec has_uppercase(binary()) -> boolean().
has_uppercase(<<>>) -> false;
has_uppercase(<<C, _/binary>>) when C >= $A, C =< $Z -> true;
has_uppercase(<<_, Rest/binary>>) -> has_uppercase(Rest).

-spec insert({binary(), binary()}, state()) -> state().
insert({Name, Value}, State = #hpack{max_size = MaxSize, next_seq = NextSeq}) ->
    EntrySize = byte_size(Name) + byte_size(Value) + ?ENTRY_OVERHEAD,
    case EntrySize > MaxSize of
        true ->
            clear_table(State);
        false ->
            TargetSize = MaxSize - EntrySize,
            State1 = evict_to_size(TargetSize, State),
            Header = {Name, Value},
            #hpack{
                size = Size1,
                entries = Entries1,
                full_index = FullIndex1,
                name_index = NameIndex1
            } = State1,
            State1#hpack{
                size = Size1 + EntrySize,
                next_seq = NextSeq + 1,
                entries = maps:put(NextSeq, {EntrySize, Header}, Entries1),
                full_index = maps:put(Header, NextSeq, FullIndex1),
                name_index = maps:put(Name, NextSeq, NameIndex1)
            }
    end.

-spec lookup(pos_integer(), state()) -> {ok, {binary(), binary()}} | {error, decode_error()}.
lookup(1, _) ->
    {ok, {<<":authority">>, <<>>}};
lookup(2, _) ->
    {ok, {<<":method">>, <<"GET">>}};
lookup(3, _) ->
    {ok, {<<":method">>, <<"POST">>}};
lookup(4, _) ->
    {ok, {<<":path">>, <<"/">>}};
lookup(5, _) ->
    {ok, {<<":path">>, <<"/index.html">>}};
lookup(6, _) ->
    {ok, {<<":scheme">>, <<"http">>}};
lookup(7, _) ->
    {ok, {<<":scheme">>, <<"https">>}};
lookup(8, _) ->
    {ok, {<<":status">>, <<"200">>}};
lookup(9, _) ->
    {ok, {<<":status">>, <<"204">>}};
lookup(10, _) ->
    {ok, {<<":status">>, <<"206">>}};
lookup(11, _) ->
    {ok, {<<":status">>, <<"304">>}};
lookup(12, _) ->
    {ok, {<<":status">>, <<"400">>}};
lookup(13, _) ->
    {ok, {<<":status">>, <<"404">>}};
lookup(14, _) ->
    {ok, {<<":status">>, <<"500">>}};
lookup(15, _) ->
    {ok, {<<"accept-charset">>, <<>>}};
lookup(16, _) ->
    {ok, {<<"accept-encoding">>, <<"gzip, deflate">>}};
lookup(17, _) ->
    {ok, {<<"accept-language">>, <<>>}};
lookup(18, _) ->
    {ok, {<<"accept-ranges">>, <<>>}};
lookup(19, _) ->
    {ok, {<<"accept">>, <<>>}};
lookup(20, _) ->
    {ok, {<<"access-control-allow-origin">>, <<>>}};
lookup(21, _) ->
    {ok, {<<"age">>, <<>>}};
lookup(22, _) ->
    {ok, {<<"allow">>, <<>>}};
lookup(23, _) ->
    {ok, {<<"authorization">>, <<>>}};
lookup(24, _) ->
    {ok, {<<"cache-control">>, <<>>}};
lookup(25, _) ->
    {ok, {<<"content-disposition">>, <<>>}};
lookup(26, _) ->
    {ok, {<<"content-encoding">>, <<>>}};
lookup(27, _) ->
    {ok, {<<"content-language">>, <<>>}};
lookup(28, _) ->
    {ok, {<<"content-length">>, <<>>}};
lookup(29, _) ->
    {ok, {<<"content-location">>, <<>>}};
lookup(30, _) ->
    {ok, {<<"content-range">>, <<>>}};
lookup(31, _) ->
    {ok, {<<"content-type">>, <<>>}};
lookup(32, _) ->
    {ok, {<<"cookie">>, <<>>}};
lookup(33, _) ->
    {ok, {<<"date">>, <<>>}};
lookup(34, _) ->
    {ok, {<<"etag">>, <<>>}};
lookup(35, _) ->
    {ok, {<<"expect">>, <<>>}};
lookup(36, _) ->
    {ok, {<<"expires">>, <<>>}};
lookup(37, _) ->
    {ok, {<<"from">>, <<>>}};
lookup(38, _) ->
    {ok, {<<"host">>, <<>>}};
lookup(39, _) ->
    {ok, {<<"if-match">>, <<>>}};
lookup(40, _) ->
    {ok, {<<"if-modified-since">>, <<>>}};
lookup(41, _) ->
    {ok, {<<"if-none-match">>, <<>>}};
lookup(42, _) ->
    {ok, {<<"if-range">>, <<>>}};
lookup(43, _) ->
    {ok, {<<"if-unmodified-since">>, <<>>}};
lookup(44, _) ->
    {ok, {<<"last-modified">>, <<>>}};
lookup(45, _) ->
    {ok, {<<"link">>, <<>>}};
lookup(46, _) ->
    {ok, {<<"location">>, <<>>}};
lookup(47, _) ->
    {ok, {<<"max-forwards">>, <<>>}};
lookup(48, _) ->
    {ok, {<<"proxy-authenticate">>, <<>>}};
lookup(49, _) ->
    {ok, {<<"proxy-authorization">>, <<>>}};
lookup(50, _) ->
    {ok, {<<"range">>, <<>>}};
lookup(51, _) ->
    {ok, {<<"referer">>, <<>>}};
lookup(52, _) ->
    {ok, {<<"refresh">>, <<>>}};
lookup(53, _) ->
    {ok, {<<"retry-after">>, <<>>}};
lookup(54, _) ->
    {ok, {<<"server">>, <<>>}};
lookup(55, _) ->
    {ok, {<<"set-cookie">>, <<>>}};
lookup(56, _) ->
    {ok, {<<"strict-transport-security">>, <<>>}};
lookup(57, _) ->
    {ok, {<<"transfer-encoding">>, <<>>}};
lookup(58, _) ->
    {ok, {<<"user-agent">>, <<>>}};
lookup(59, _) ->
    {ok, {<<"vary">>, <<>>}};
lookup(60, _) ->
    {ok, {<<"via">>, <<>>}};
lookup(61, _) ->
    {ok, {<<"www-authenticate">>, <<>>}};
lookup(Index, #hpack{next_seq = NextSeq, oldest_seq = OldestSeq, entries = Entries}) when
    Index > 61
->
    Seq = NextSeq - 1 - (Index - 62),
    case Seq >= OldestSeq andalso Seq < NextSeq of
        true ->
            case maps:get(Seq, Entries, undefined) of
                {_, Header} -> {ok, Header};
                undefined -> {error, invalid_table_index}
            end;
        false ->
            {error, invalid_table_index}
    end;
lookup(0, _) ->
    {error, invalid_table_index}.

-spec map_int_error({ok, non_neg_integer(), bitstring()} | {error, nhttp_int:decode_error()}) ->
    {ok, non_neg_integer(), bitstring()} | {error, decode_error()}.
map_int_error({ok, _, _} = Ok) -> Ok;
map_int_error({error, incomplete}) -> {error, incomplete_header_block};
map_int_error({error, overflow}) -> {error, integer_overflow}.

-spec map_str_error({ok, binary(), bitstring()} | {error, nhttp_str:decode_error()}) ->
    {ok, binary(), bitstring()} | {error, decode_error()}.
map_str_error({ok, _, _} = Ok) -> Ok;
map_str_error({error, incomplete}) -> {error, incomplete_header_block};
map_str_error({error, invalid_huffman}) -> {error, invalid_huffman}.

-spec maybe_emit_table_size_update(state()) -> {iodata(), state()}.
maybe_emit_table_size_update(State = #hpack{max_size = MaxSize, configured_max_size = MaxSize}) ->
    {[], State};
maybe_emit_table_size_update(State0 = #hpack{configured_max_size = MaxSize}) ->
    State1 = update_table_size(MaxSize, State0#hpack{max_size = MaxSize}),
    {nhttp_int:enc5(MaxSize, 2#001), State1}.

-spec push_header(
    {binary(), binary()},
    headers(),
    non_neg_integer(),
    pos_integer() | infinity
) ->
    {ok, headers(), non_neg_integer()} | {error, header_list_too_large}.
push_header({Name, Value} = Header, Acc, Total, infinity) ->
    {ok, [Header | Acc], Total + byte_size(Name) + byte_size(Value) + 32};
push_header({Name, Value} = Header, Acc, Total, Limit) ->
    NewTotal = Total + byte_size(Name) + byte_size(Value) + 32,
    case NewTotal =< Limit of
        true -> {ok, [Header | Acc], NewTotal};
        false -> {error, header_list_too_large}
    end.

-spec update_table_size(non_neg_integer(), state()) -> state().
update_table_size(0, State) ->
    clear_table(State#hpack{max_size = 0});
update_table_size(MaxSize, State = #hpack{max_size = MaxSize}) ->
    State;
update_table_size(MaxSize, State) ->
    State1 = evict_to_size(MaxSize, State),
    State1#hpack{max_size = MaxSize}.

-spec validate_name_no_uppercase(binary()) -> ok | {error, decode_error()}.
validate_name_no_uppercase(Name) ->
    case has_uppercase(Name) of
        true -> {error, uppercase_header_name};
        false -> ok
    end.