%%% -*- erlang -*- %%% %%% This file is part of hackney released under the Apache 2 license. %%% See the NOTICE for more information. %%% %%% Copyright (c) 2024-2026 Benoit Chesneau %%% %%% @doc Optimized QPACK header compression for HTTP/3 (RFC 9204). %%% %%% This implementation provides O(1) static table lookups using maps %%% and supports an optional dynamic table for improved compression. %%% %%% Key optimizations: %%% - Static table as maps for O(1) field/name lookups %%% - Static table as tuple for O(1) index access %%% - Optional dynamic table with O(1) map-based lookups %%% @end -module(hackney_qpack). -export([ encode/1, encode/2, decode/1, decode/2, init/0, init/1 ]). %% Entry overhead per RFC 9204 Section 3.2.1 -define(ENTRY_OVERHEAD, 32). %% State record for stateful encoding/decoding -record(qpack_state, { %% Dynamic table (optional, disabled by default) use_dynamic = false :: boolean(), dyn_field_index = #{} :: #{header() => pos_integer()}, dyn_name_index = #{} :: #{binary() => pos_integer()}, dyn_entries = [] :: [{pos_integer(), header()}], dyn_size = 0 :: non_neg_integer(), dyn_max_size = 0 :: non_neg_integer(), insert_count = 0 :: non_neg_integer() }). -opaque state() :: #qpack_state{}. -export_type([state/0]). -type header() :: {binary(), binary()}. %%==================================================================== %% Static Table (RFC 9204 Appendix A) - O(1) Access %%==================================================================== %% Static table as tuple for O(1) index access -define(STATIC_TABLE, { %% 0-14 {<<":authority">>, undefined}, {<<":path">>, <<"/">>}, {<<":age">>, <<"0">>}, {<<"content-disposition">>, undefined}, {<<"content-length">>, <<"0">>}, {<<"cookie">>, undefined}, {<<"date">>, undefined}, {<<"etag">>, undefined}, {<<"if-modified-since">>, undefined}, {<<"if-none-match">>, undefined}, {<<"last-modified">>, undefined}, {<<"link">>, undefined}, {<<"location">>, undefined}, {<<"referer">>, undefined}, {<<"set-cookie">>, undefined}, %% 15-29 {<<":method">>, <<"CONNECT">>}, {<<":method">>, <<"DELETE">>}, {<<":method">>, <<"GET">>}, {<<":method">>, <<"HEAD">>}, {<<":method">>, <<"OPTIONS">>}, {<<":method">>, <<"POST">>}, {<<":method">>, <<"PUT">>}, {<<":scheme">>, <<"http">>}, {<<":scheme">>, <<"https">>}, {<<":status">>, <<"103">>}, {<<":status">>, <<"200">>}, {<<":status">>, <<"304">>}, {<<":status">>, <<"404">>}, {<<":status">>, <<"503">>}, {<<"accept">>, <<"*/*">>}, %% 30-44 {<<"accept">>, <<"application/dns-message">>}, {<<"accept-encoding">>, <<"gzip, deflate, br">>}, {<<"accept-ranges">>, <<"bytes">>}, {<<"access-control-allow-headers">>, <<"cache-control">>}, {<<"access-control-allow-headers">>, <<"content-type">>}, {<<"access-control-allow-origin">>, <<"*">>}, {<<"cache-control">>, <<"max-age=0">>}, {<<"cache-control">>, <<"max-age=2592000">>}, {<<"cache-control">>, <<"max-age=604800">>}, {<<"cache-control">>, <<"no-cache">>}, {<<"cache-control">>, <<"no-store">>}, {<<"cache-control">>, <<"public, max-age=31536000">>}, {<<"content-encoding">>, <<"br">>}, {<<"content-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"application/dns-message">>}, %% 45-59 {<<"content-type">>, <<"application/javascript">>}, {<<"content-type">>, <<"application/json">>}, {<<"content-type">>, <<"application/x-www-form-urlencoded">>}, {<<"content-type">>, <<"image/gif">>}, {<<"content-type">>, <<"image/jpeg">>}, {<<"content-type">>, <<"image/png">>}, {<<"content-type">>, <<"text/css">>}, {<<"content-type">>, <<"text/html; charset=utf-8">>}, {<<"content-type">>, <<"text/plain">>}, {<<"content-type">>, <<"text/plain;charset=utf-8">>}, {<<"range">>, <<"bytes=0-">>}, {<<"strict-transport-security">>, <<"max-age=31536000">>}, {<<"strict-transport-security">>, <<"max-age=31536000; includesubdomains">>}, {<<"strict-transport-security">>, <<"max-age=31536000; includesubdomains; preload">>}, {<<"vary">>, <<"accept-encoding">>}, %% 60-74 {<<"vary">>, <<"origin">>}, {<<"x-content-type-options">>, <<"nosniff">>}, {<<"x-xss-protection">>, <<"1; mode=block">>}, {<<":status">>, <<"100">>}, {<<":status">>, <<"204">>}, {<<":status">>, <<"206">>}, {<<":status">>, <<"302">>}, {<<":status">>, <<"400">>}, {<<":status">>, <<"403">>}, {<<":status">>, <<"421">>}, {<<":status">>, <<"425">>}, {<<":status">>, <<"500">>}, {<<"accept-language">>, undefined}, {<<"access-control-allow-credentials">>, <<"FALSE">>}, {<<"access-control-allow-credentials">>, <<"TRUE">>}, %% 75-89 {<<"access-control-allow-headers">>, <<"*">>}, {<<"access-control-allow-methods">>, <<"get">>}, {<<"access-control-allow-methods">>, <<"get, post, options">>}, {<<"access-control-allow-methods">>, <<"options">>}, {<<"access-control-expose-headers">>, <<"content-length">>}, {<<"access-control-request-headers">>, <<"content-type">>}, {<<"access-control-request-method">>, <<"get">>}, {<<"access-control-request-method">>, <<"post">>}, {<<"alt-svc">>, <<"clear">>}, {<<"authorization">>, undefined}, {<<"content-security-policy">>, <<"script-src 'none'; object-src 'none'; base-uri 'none'">>}, {<<"early-data">>, <<"1">>}, {<<"expect-ct">>, undefined}, {<<"forwarded">>, undefined}, {<<"if-range">>, undefined}, %% 90-98 {<<"origin">>, undefined}, {<<"purpose">>, <<"prefetch">>}, {<<"server">>, undefined}, {<<"timing-allow-origin">>, <<"*">>}, {<<"upgrade-insecure-requests">>, <<"1">>}, {<<"user-agent">>, undefined}, {<<"x-forwarded-for">>, undefined}, {<<"x-frame-options">>, <<"deny">>}, {<<"x-frame-options">>, <<"sameorigin">>} }). %% Static table field map for O(1) exact match lookup %% Only includes entries with non-undefined values -define(STATIC_FIELD_MAP, #{ {<<":path">>, <<"/">>} => 1, {<<":age">>, <<"0">>} => 2, {<<"content-length">>, <<"0">>} => 4, {<<":method">>, <<"CONNECT">>} => 15, {<<":method">>, <<"DELETE">>} => 16, {<<":method">>, <<"GET">>} => 17, {<<":method">>, <<"HEAD">>} => 18, {<<":method">>, <<"OPTIONS">>} => 19, {<<":method">>, <<"POST">>} => 20, {<<":method">>, <<"PUT">>} => 21, {<<":scheme">>, <<"http">>} => 22, {<<":scheme">>, <<"https">>} => 23, {<<":status">>, <<"103">>} => 24, {<<":status">>, <<"200">>} => 25, {<<":status">>, <<"304">>} => 26, {<<":status">>, <<"404">>} => 27, {<<":status">>, <<"503">>} => 28, {<<"accept">>, <<"*/*">>} => 29, {<<"accept">>, <<"application/dns-message">>} => 30, {<<"accept-encoding">>, <<"gzip, deflate, br">>} => 31, {<<"accept-ranges">>, <<"bytes">>} => 32, {<<"access-control-allow-headers">>, <<"cache-control">>} => 33, {<<"access-control-allow-headers">>, <<"content-type">>} => 34, {<<"access-control-allow-origin">>, <<"*">>} => 35, {<<"cache-control">>, <<"max-age=0">>} => 36, {<<"cache-control">>, <<"max-age=2592000">>} => 37, {<<"cache-control">>, <<"max-age=604800">>} => 38, {<<"cache-control">>, <<"no-cache">>} => 39, {<<"cache-control">>, <<"no-store">>} => 40, {<<"cache-control">>, <<"public, max-age=31536000">>} => 41, {<<"content-encoding">>, <<"br">>} => 42, {<<"content-encoding">>, <<"gzip">>} => 43, {<<"content-type">>, <<"application/dns-message">>} => 44, {<<"content-type">>, <<"application/javascript">>} => 45, {<<"content-type">>, <<"application/json">>} => 46, {<<"content-type">>, <<"application/x-www-form-urlencoded">>} => 47, {<<"content-type">>, <<"image/gif">>} => 48, {<<"content-type">>, <<"image/jpeg">>} => 49, {<<"content-type">>, <<"image/png">>} => 50, {<<"content-type">>, <<"text/css">>} => 51, {<<"content-type">>, <<"text/html; charset=utf-8">>} => 52, {<<"content-type">>, <<"text/plain">>} => 53, {<<"content-type">>, <<"text/plain;charset=utf-8">>} => 54, {<<"range">>, <<"bytes=0-">>} => 55, {<<"strict-transport-security">>, <<"max-age=31536000">>} => 56, {<<"strict-transport-security">>, <<"max-age=31536000; includesubdomains">>} => 57, {<<"strict-transport-security">>, <<"max-age=31536000; includesubdomains; preload">>} => 58, {<<"vary">>, <<"accept-encoding">>} => 59, {<<"vary">>, <<"origin">>} => 60, {<<"x-content-type-options">>, <<"nosniff">>} => 61, {<<"x-xss-protection">>, <<"1; mode=block">>} => 62, {<<":status">>, <<"100">>} => 63, {<<":status">>, <<"204">>} => 64, {<<":status">>, <<"206">>} => 65, {<<":status">>, <<"302">>} => 66, {<<":status">>, <<"400">>} => 67, {<<":status">>, <<"403">>} => 68, {<<":status">>, <<"421">>} => 69, {<<":status">>, <<"425">>} => 70, {<<":status">>, <<"500">>} => 71, {<<"access-control-allow-credentials">>, <<"FALSE">>} => 73, {<<"access-control-allow-credentials">>, <<"TRUE">>} => 74, {<<"access-control-allow-headers">>, <<"*">>} => 75, {<<"access-control-allow-methods">>, <<"get">>} => 76, {<<"access-control-allow-methods">>, <<"get, post, options">>} => 77, {<<"access-control-allow-methods">>, <<"options">>} => 78, {<<"access-control-expose-headers">>, <<"content-length">>} => 79, {<<"access-control-request-headers">>, <<"content-type">>} => 80, {<<"access-control-request-method">>, <<"get">>} => 81, {<<"access-control-request-method">>, <<"post">>} => 82, {<<"alt-svc">>, <<"clear">>} => 83, {<<"content-security-policy">>, <<"script-src 'none'; object-src 'none'; base-uri 'none'">>} => 85, {<<"early-data">>, <<"1">>} => 86, {<<"purpose">>, <<"prefetch">>} => 91, {<<"timing-allow-origin">>, <<"*">>} => 93, {<<"upgrade-insecure-requests">>, <<"1">>} => 94, {<<"x-frame-options">>, <<"deny">>} => 97, {<<"x-frame-options">>, <<"sameorigin">>} => 98 }). %% Static table name map for O(1) name-only lookup -define(STATIC_NAME_MAP, #{ <<":authority">> => 0, <<":path">> => 1, <<":age">> => 2, <<"content-disposition">> => 3, <<"content-length">> => 4, <<"cookie">> => 5, <<"date">> => 6, <<"etag">> => 7, <<"if-modified-since">> => 8, <<"if-none-match">> => 9, <<"last-modified">> => 10, <<"link">> => 11, <<"location">> => 12, <<"referer">> => 13, <<"set-cookie">> => 14, <<":method">> => 15, <<":scheme">> => 22, <<":status">> => 24, <<"accept">> => 29, <<"accept-encoding">> => 31, <<"accept-ranges">> => 32, <<"access-control-allow-headers">> => 33, <<"access-control-allow-origin">> => 35, <<"cache-control">> => 36, <<"content-encoding">> => 42, <<"content-type">> => 44, <<"range">> => 55, <<"strict-transport-security">> => 56, <<"vary">> => 59, <<"x-content-type-options">> => 61, <<"x-xss-protection">> => 62, <<"accept-language">> => 72, <<"access-control-allow-credentials">> => 73, <<"access-control-allow-methods">> => 76, <<"access-control-expose-headers">> => 79, <<"access-control-request-headers">> => 80, <<"access-control-request-method">> => 81, <<"alt-svc">> => 83, <<"authorization">> => 84, <<"content-security-policy">> => 85, <<"early-data">> => 86, <<"expect-ct">> => 87, <<"forwarded">> => 88, <<"if-range">> => 89, <<"origin">> => 90, <<"purpose">> => 91, <<"server">> => 92, <<"timing-allow-origin">> => 93, <<"upgrade-insecure-requests">> => 94, <<"user-agent">> => 95, <<"x-forwarded-for">> => 96, <<"x-frame-options">> => 97 }). %%==================================================================== %% API %%==================================================================== %% @doc Initialize QPACK state (static-only mode). -spec init() -> state(). init() -> #qpack_state{}. %% @doc Initialize QPACK state with options. %% Options: %% max_dynamic_size - Enable dynamic table with given max size (default: 0 = disabled) -spec init(#{atom() => term()}) -> state(). init(Opts) -> MaxDynSize = maps:get(max_dynamic_size, Opts, 0), #qpack_state{ use_dynamic = MaxDynSize > 0, dyn_max_size = MaxDynSize }. %% @doc Encode headers using QPACK (stateless, static-only). -spec encode([header()]) -> binary(). encode(Headers) -> {Encoded, _} = encode(Headers, #qpack_state{}), Encoded. %% @doc Encode headers using QPACK with state. -spec encode([header()], state()) -> {binary(), state()}. encode(Headers, State) -> %% Required Insert Count = 0 (no dynamic table refs from encoder) %% S bit = 0 (no sign), Delta Base = 0 Prefix = <<0, 0>>, {EncodedHeaders, NewState} = encode_headers(Headers, State, <<>>), {<>, NewState}. %% @doc Decode QPACK-encoded headers (stateless). -spec decode(binary()) -> {ok, [header()]} | {error, term()}. decode(Data) -> {Result, _} = decode(Data, #qpack_state{}), Result. %% @doc Decode QPACK-encoded headers with state. -spec decode(binary(), state()) -> {{ok, [header()]} | {error, term()}, state()}. decode(Data, State) -> try {_, Rest} = decode_prefix(Data), {Headers, NewState} = decode_headers(Rest, State, []), {{ok, Headers}, NewState} catch _:Reason -> {{error, Reason}, State} end. %%==================================================================== %% Internal - Encoding %%==================================================================== encode_headers([], State, Acc) -> {Acc, State}; encode_headers([Header | Rest], State, Acc) -> {Encoded, NewState} = encode_header(Header, State), encode_headers(Rest, NewState, <>). encode_header({Name, Value}, State) -> case find_static_match(Name, Value) of {exact, Index} -> %% Indexed Field Line (static) - 11xxxxxx {encode_indexed_static(Index), State}; {name, Index} -> %% Literal Field Line With Name Reference (static) {encode_literal_with_name_ref(Index, Value), State}; none -> %% Literal Field Line With Literal Name {encode_literal(Name, Value), State} end. %% Indexed Field Line - 11xxxxxx for static encode_indexed_static(Index) -> encode_prefixed_int(Index, 6, 2#11). %% Literal with name reference - 0101xxxx (N=0, T=1 for static) encode_literal_with_name_ref(Index, Value) -> NameRef = encode_prefixed_int(Index, 4, 2#0101), ValueEnc = encode_string(Value), <>. %% Literal with literal name - 0010xxxx (N=0, H=0 for no huffman) encode_literal(Name, Value) -> NameLen = byte_size(Name), ValueEnc = encode_string(Value), case NameLen < 7 of true -> FirstByte = 2#00100000 bor NameLen, <>; false -> FirstByte = 2#00100111, LenCont = encode_multi_byte_int(NameLen - 7), <> end. encode_string(Str) -> Len = byte_size(Str), LenEnc = encode_prefixed_int(Len, 7, 0), <>. encode_prefixed_int(Value, PrefixBits, Prefix) when Value < (1 bsl PrefixBits) - 1 -> <<(Prefix bsl PrefixBits bor Value)>>; encode_prefixed_int(Value, PrefixBits, Prefix) -> MaxPrefix = (1 bsl PrefixBits) - 1, FirstByte = Prefix bsl PrefixBits bor MaxPrefix, Remaining = Value - MaxPrefix, <>. encode_multi_byte_int(Value) when Value < 128 -> <>; encode_multi_byte_int(Value) -> <<(128 bor (Value band 127)), (encode_multi_byte_int(Value bsr 7))/binary>>. %%==================================================================== %% Internal - Decoding %%==================================================================== decode_prefix(<>) -> {{RIC, Base}, Rest}; decode_prefix(_) -> throw(invalid_prefix). decode_headers(<<>>, State, Acc) -> {lists:reverse(Acc), State}; decode_headers(<<2#11:2, _:6, _/binary>> = Data, State, Acc) -> %% Indexed Field Line (static) - 11xxxxxx {Index, Rest} = decode_prefixed_int(Data, 6), Header = get_static_entry(Index), decode_headers(Rest, State, [Header | Acc]); decode_headers(<<2#10:2, _:6, _/binary>> = Data, State, Acc) -> %% Indexed Field Line (dynamic) - 10xxxxxx {_Index, Rest} = decode_prefixed_int(Data, 6), decode_headers(Rest, State, Acc); decode_headers(<<2#01:2, _N:1, T:1, _:4, _/binary>> = Data, State, Acc) -> %% Literal Field Line with Name Reference - 01NTxxxx FirstByte = hd(binary_to_list(Data)), IndexBits = FirstByte band 16#0F, <<_, Rest0/binary>> = Data, {Index, Rest1} = case IndexBits < 15 of true -> {IndexBits, Rest0}; false -> decode_multi_byte_int(Rest0, IndexBits, 0) end, {Value, Rest2} = decode_string(Rest1), case T of 1 -> {Name, _} = get_static_entry(Index), decode_headers(Rest2, State, [{Name, Value} | Acc]); 0 -> decode_headers(Rest2, State, Acc) end; decode_headers(<<2#0010:4, H:1, NameLenPrefix:3, Rest0/binary>>, State, Acc) -> %% Literal with literal name - 0010Hxxx {NameLen, Rest1} = case NameLenPrefix < 7 of true -> {NameLenPrefix, Rest0}; false -> decode_multi_byte_int(Rest0, NameLenPrefix, 0) end, {Name, Rest2} = decode_string_with_huffman(H, NameLen, Rest1), {Value, Rest3} = decode_string(Rest2), decode_headers(Rest3, State, [{Name, Value} | Acc]); decode_headers(<<2#0011:4, H:1, NameLenPrefix:3, Rest0/binary>>, State, Acc) -> %% Literal with literal name, N=1 - 0011Hxxx {NameLen, Rest1} = case NameLenPrefix < 7 of true -> {NameLenPrefix, Rest0}; false -> decode_multi_byte_int(Rest0, NameLenPrefix, 0) end, {Name, Rest2} = decode_string_with_huffman(H, NameLen, Rest1), {Value, Rest3} = decode_string(Rest2), decode_headers(Rest3, State, [{Name, Value} | Acc]); decode_headers(<<2#0001:4, _:4, _/binary>> = Data, State, Acc) -> %% Indexed Header Field with Post-Base Index - 0001xxxx FirstByte = hd(binary_to_list(Data)), IndexBits = FirstByte band 16#0F, <<_, Rest0/binary>> = Data, {_Index, Rest1} = case IndexBits < 15 of true -> {IndexBits, Rest0}; false -> decode_multi_byte_int(Rest0, IndexBits, 0) end, decode_headers(Rest1, State, Acc); decode_headers(<<2#0000:4, _:4, _/binary>> = Data, State, Acc) -> %% Literal with post-base name reference - 0000Nxxx FirstByte = hd(binary_to_list(Data)), IndexBits = FirstByte band 16#07, <<_, Rest0/binary>> = Data, {_Index, Rest1} = case IndexBits < 7 of true -> {IndexBits, Rest0}; false -> decode_multi_byte_int(Rest0, IndexBits, 0) end, {_Value, Rest2} = decode_string(Rest1), decode_headers(Rest2, State, Acc); decode_headers(<>, _State, _Acc) -> throw({unknown_instruction, Byte}). decode_prefixed_int(Data, PrefixBits) -> MaxPrefix = (1 bsl PrefixBits) - 1, <> = Data, Value = First band MaxPrefix, case Value < MaxPrefix of true -> {Value, Rest}; false -> decode_multi_byte_int(Rest, Value, 0) end. decode_multi_byte_int(<>, Acc, Shift) -> NewAcc = Acc + ((Byte band 127) bsl Shift), case Byte band 128 of 0 -> {NewAcc, Rest}; _ -> decode_multi_byte_int(Rest, NewAcc, Shift + 7) end. decode_string(<<0:1, 127:7, Rest/binary>>) -> {ActualLen, Rest2} = decode_multi_byte_int(Rest, 127, 0), case byte_size(Rest2) >= ActualLen of true -> <> = Rest2, {Str, Rest3}; false -> throw({invalid_string, need_more_data}) end; decode_string(<<1:1, 127:7, Rest/binary>>) -> {ActualLen, Rest2} = decode_multi_byte_int(Rest, 127, 0), case byte_size(Rest2) >= ActualLen of true -> <> = Rest2, {Decoded, _} = dec_huffman(Encoded, ActualLen), {Decoded, Rest3}; false -> throw({invalid_string, need_more_data}) end; decode_string(<<0:1, Len:7, Rest/binary>>) when byte_size(Rest) >= Len -> <> = Rest, {Str, Rest2}; decode_string(<<1:1, Len:7, Rest/binary>>) when byte_size(Rest) >= Len -> <> = Rest, {Decoded, _} = dec_huffman(Encoded, Len), {Decoded, Rest2}; decode_string(Data) -> throw({invalid_string, byte_size(Data), Data}). decode_string_with_huffman(HuffFlag, Len, Data) when byte_size(Data) >= Len -> <> = Data, case HuffFlag of 1 -> {Decoded, _} = dec_huffman(Encoded, Len), {Decoded, Rest}; 0 -> {Encoded, Rest} end; decode_string_with_huffman(_HuffFlag, Len, Data) -> throw({invalid_string, need_more_data, Len, byte_size(Data)}). %% Huffman decoding using HPACK lookup table -include("hackney_hpack_huffman_dec.hrl"). dec_huffman(Data, Length) -> dec_huffman(Data, Length, 0, <<>>). dec_huffman(<>, Len, Huff0, Acc) when Len > 1 -> {_, CharA, Huff1} = dec_huffman_lookup(Huff0, A), {_, CharB, Huff} = dec_huffman_lookup(Huff1, B), case {CharA, CharB} of {undefined, undefined} -> dec_huffman(R, Len - 1, Huff, Acc); {CharA, undefined} -> dec_huffman(R, Len - 1, Huff, <>); {undefined, CharB} -> dec_huffman(R, Len - 1, Huff, <>); {CharA, CharB} -> dec_huffman(R, Len - 1, Huff, <>) end; dec_huffman(<>, 1, Huff0, Acc) -> {_, CharA, Huff} = dec_huffman_lookup(Huff0, A), case dec_huffman_lookup(Huff, B) of {ok, CharB, _} -> case {CharA, CharB} of {undefined, undefined} -> {Acc, Rest}; {CharA, undefined} -> {<>, Rest}; {undefined, CharB} -> {<>, Rest}; _ -> {<>, Rest} end; {more, _, _} -> case CharA of undefined -> {Acc, Rest}; _ -> {<>, Rest} end end; dec_huffman(Rest, 0, _, <<>>) -> {<<>>, Rest}; dec_huffman(Rest, 0, _, Acc) -> {Acc, Rest}. %%==================================================================== %% Internal - Static Table Lookup (O(1)) %%==================================================================== %% Find match in static table using maps - O(1) find_static_match(Name, Value) -> Header = {Name, Value}, case maps:find(Header, ?STATIC_FIELD_MAP) of {ok, Index} -> {exact, Index}; error -> case maps:find(Name, ?STATIC_NAME_MAP) of {ok, Index} -> {name, Index}; error -> none end end. %% Get static table entry by index - O(1) get_static_entry(Index) when Index >= 0, Index =< 98 -> element(Index + 1, ?STATIC_TABLE); get_static_entry(Index) -> throw({invalid_static_index, Index}).