%% @private -module(hpack_index). -export([ new/0, new/1, add/3, lookup/2, resize/2, table_size/1, max_table_size/1, match/2 ]). -type header_name() :: binary(). -type header_value():: binary(). -type header() :: {header_name(), header_value()}. -export_type([header/0]). -define(DYNAMIC_TABLE_MIN_INDEX, 62). -record(dynamic_table, { table = [] :: [{pos_integer(), header_name(), header_value()}], %% max_size is the size allowed by the protocol max_size = 4096 :: non_neg_integer(), %% size is how big this thing really is size = 0 :: non_neg_integer() }). -type dynamic_table() :: #dynamic_table{}. -spec new() -> dynamic_table(). new() -> #dynamic_table{}. -spec new(pos_integer()) -> dynamic_table(). new(S) -> #dynamic_table{max_size=S}. -spec entry_size({pos_integer(), header_name(), header_value()}) -> pos_integer(). entry_size({_, Name, Value}) -> entry_size(Name, Value). -spec entry_size(header_name(), header_value()) -> pos_integer(). entry_size(Name, Value) -> 32 + size(Name) + size(Value). -spec add(header_name(), header_value(), dynamic_table()) -> dynamic_table(). add(Name, Value, DT) -> add(Name, Value, entry_size(Name, Value), DT). -spec add(header_name(), header_value(), pos_integer(), dynamic_table()) -> dynamic_table(). add(Name, Value, EntrySize, DT=#dynamic_table{table=T, size=S, max_size=MS}) when EntrySize + S =< MS -> TPlus = lists:map(fun({N,H,V}) -> {N+1, H,V} end, T), DT#dynamic_table{size=S+EntrySize, table=[{?DYNAMIC_TABLE_MIN_INDEX, Name, Value}|TPlus]}; add(Name, Value, EntrySize, DT=#dynamic_table{size=S, max_size=MS}) when EntrySize + S > MS -> add(Name, Value, EntrySize, droplast(DT)). -spec droplast(dynamic_table()) -> dynamic_table(). droplast(DT=#dynamic_table{table=T, size=S}) -> [Last|NewTR] = lists:reverse(T), DT#dynamic_table{size=S-entry_size(Last), table=lists:reverse(NewTR)}. %% TODO: There's a problem here, where if you shink the dynamic table %% size, you can never increase it. %% resize/2 sets the max_table_size, and truncates the table if it %% needs to be smaller to fit in that maximum -spec resize(pos_integer(), dynamic_table()) -> dynamic_table(). resize(NewSize, DT=#dynamic_table{size=S}) when NewSize >= S -> DT#dynamic_table{max_size=NewSize}; resize(NewSize, DT) -> resize(NewSize, droplast(DT)). -spec table_size(dynamic_table()) -> pos_integer(). table_size(#dynamic_table{size=S}) -> S. -spec max_table_size(dynamic_table()) -> pos_integer(). max_table_size(#dynamic_table{max_size=S}) -> S. -spec lookup(pos_integer(), dynamic_table()) -> header() | undefined. lookup(1 , _) -> {<<":authority">>, <<>>}; lookup(2 , _) -> {<<":method">>, <<"GET">>}; lookup(3 , _) -> {<<":method">>, <<"POST">>}; lookup(4 , _) -> {<<":path">>, <<"/">>}; lookup(5 , _) -> {<<":path">>, <<"/index.html">>}; lookup(6 , _) -> {<<":scheme">>, <<"http">>}; lookup(7 , _) -> {<<":scheme">>, <<"https">>}; lookup(8 , _) -> {<<":status">>, <<"200">>}; lookup(9 , _) -> {<<":status">>, <<"204">>}; lookup(10, _) -> {<<":status">>, <<"206">>}; lookup(11, _) -> {<<":status">>, <<"304">>}; lookup(12, _) -> {<<":status">>, <<"400">>}; lookup(13, _) -> {<<":status">>, <<"404">>}; lookup(14, _) -> {<<":status">>, <<"500">>}; lookup(15, _) -> {<<"accept-charset">>, <<>>}; lookup(16, _) -> {<<"accept-encoding">>, <<"gzip, deflate">>}; lookup(17, _) -> {<<"accept-language">>, <<>>}; lookup(18, _) -> {<<"accept-ranges">>, <<>>}; lookup(19, _) -> {<<"accept">>, <<>>}; lookup(20, _) -> {<<"access-control-allow-origin">>, <<>>}; lookup(21, _) -> {<<"age">>, <<>>}; lookup(22, _) -> {<<"allow">>, <<>>}; lookup(23, _) -> {<<"authorization">>, <<>>}; lookup(24, _) -> {<<"cache-control">>, <<>>}; lookup(25, _) -> {<<"content-disposition">>, <<>>}; lookup(26, _) -> {<<"content-encoding">>, <<>>}; lookup(27, _) -> {<<"content-language">>, <<>>}; lookup(28, _) -> {<<"content-length">>, <<>>}; lookup(29, _) -> {<<"content-location">>, <<>>}; lookup(30, _) -> {<<"content-range">>, <<>>}; lookup(31, _) -> {<<"content-type">>, <<>>}; lookup(32, _) -> {<<"cookie">>, <<>>}; lookup(33, _) -> {<<"date">>, <<>>}; lookup(34, _) -> {<<"etag">>, <<>>}; lookup(35, _) -> {<<"expect">>, <<>>}; lookup(36, _) -> {<<"expires">>, <<>>}; lookup(37, _) -> {<<"from">>, <<>>}; lookup(38, _) -> {<<"host">>, <<>>}; lookup(39, _) -> {<<"if-match">>, <<>>}; lookup(40, _) -> {<<"if-modified-since">>, <<>>}; lookup(41, _) -> {<<"if-none-match">>, <<>>}; lookup(42, _) -> {<<"if-range">>, <<>>}; lookup(43, _) -> {<<"if-unmodified-since">>, <<>>}; lookup(44, _) -> {<<"last-modified">>, <<>>}; lookup(45, _) -> {<<"link">>, <<>>}; lookup(46, _) -> {<<"location">>, <<>>}; lookup(47, _) -> {<<"max-forwards">>, <<>>}; lookup(48, _) -> {<<"proxy-authenticate">>, <<>>}; lookup(49, _) -> {<<"proxy-authorization">>, <<>>}; lookup(50, _) -> {<<"range">>, <<>>}; lookup(51, _) -> {<<"referer">>, <<>>}; lookup(52, _) -> {<<"refresh">>, <<>>}; lookup(53, _) -> {<<"retry-after">>, <<>>}; lookup(54, _) -> {<<"server">>, <<>>}; lookup(55, _) -> {<<"set-cookie">>, <<>>}; lookup(56, _) -> {<<"strict-transport-security">>, <<>>}; lookup(57, _) -> {<<"transfer-encoding">>, <<>>}; lookup(58, _) -> {<<"user-agent">>, <<>>}; lookup(59, _) -> {<<"vary">>, <<>>}; lookup(60, _) -> {<<"via">>, <<>>}; lookup(61, _) -> {<<"www-authenticate">>, <<>>}; lookup(Idx, #dynamic_table{table=T}) -> case lists:keyfind(Idx, 1, T) of false -> undefined; {Idx, Name, V} -> {Name, V} end. -spec match(header(), dynamic_table()) -> {atom(), pos_integer()|undefined}. match({<<":authority">>, <<>>}, _) -> {indexed, 1 }; match({<<":method">>, <<"GET">>}, _) -> {indexed, 2 }; match({<<":method">>, <<"POST">>}, _) -> {indexed, 3 }; match({<<":path">>, <<"/">>}, _) -> {indexed, 4 }; match({<<":path">>, <<"/index.html">>}, _) -> {indexed, 5 }; match({<<":scheme">>, <<"http">>}, _) -> {indexed, 6 }; match({<<":scheme">>, <<"https">>}, _) -> {indexed, 7 }; match({<<":status">>, <<"200">>}, _) -> {indexed, 8 }; match({<<":status">>, <<"204">>}, _) -> {indexed, 9 }; match({<<":status">>, <<"206">>}, _) -> {indexed, 10}; match({<<":status">>, <<"304">>}, _) -> {indexed, 11}; match({<<":status">>, <<"400">>}, _) -> {indexed, 12}; match({<<":status">>, <<"404">>}, _) -> {indexed, 13}; match({<<":status">>, <<"500">>}, _) -> {indexed, 14}; match({<<"accept-charset">>, <<>>}, _) -> {indexed, 15}; match({<<"accept-encoding">>, <<"gzip, deflate">>}, _) -> {indexed, 16}; match({<<"accept-language">>, <<>>}, _) -> {indexed, 17}; match({<<"accept-ranges">>, <<>>}, _) -> {indexed, 18}; match({<<"accept">>, <<>>}, _) -> {indexed, 19}; match({<<"access-control-allow-origin">>, <<>>}, _) -> {indexed, 20}; match({<<"age">>, <<>>}, _) -> {indexed, 21}; match({<<"allow">>, <<>>}, _) -> {indexed, 22}; match({<<"authorization">>, <<>>}, _) -> {indexed, 23}; match({<<"cache-control">>, <<>>}, _) -> {indexed, 24}; match({<<"content-disposition">>, <<>>}, _) -> {indexed, 25}; match({<<"content-encoding">>, <<>>}, _) -> {indexed, 26}; match({<<"content-language">>, <<>>}, _) -> {indexed, 27}; match({<<"content-length">>, <<>>}, _) -> {indexed, 28}; match({<<"content-location">>, <<>>}, _) -> {indexed, 29}; match({<<"content-range">>, <<>>}, _) -> {indexed, 30}; match({<<"content-type">>, <<>>}, _) -> {indexed, 31}; match({<<"cookie">>, <<>>}, _) -> {indexed, 32}; match({<<"date">>, <<>>}, _) -> {indexed, 33}; match({<<"etag">>, <<>>}, _) -> {indexed, 34}; match({<<"expect">>, <<>>}, _) -> {indexed, 35}; match({<<"expires">>, <<>>}, _) -> {indexed, 36}; match({<<"from">>, <<>>}, _) -> {indexed, 37}; match({<<"host">>, <<>>}, _) -> {indexed, 38}; match({<<"if-match">>, <<>>}, _) -> {indexed, 39}; match({<<"if-modified-since">>, <<>>}, _) -> {indexed, 40}; match({<<"if-none-match">>, <<>>}, _) -> {indexed, 41}; match({<<"if-range">>, <<>>}, _) -> {indexed, 42}; match({<<"if-unmodified-since">>, <<>>}, _) -> {indexed, 43}; match({<<"last-modified">>, <<>>}, _) -> {indexed, 44}; match({<<"link">>, <<>>}, _) -> {indexed, 45}; match({<<"location">>, <<>>}, _) -> {indexed, 46}; match({<<"max-forwards">>, <<>>}, _) -> {indexed, 47}; match({<<"proxy-authenticate">>, <<>>}, _) -> {indexed, 48}; match({<<"proxy-authorization">>, <<>>}, _) -> {indexed, 49}; match({<<"range">>, <<>>}, _) -> {indexed, 50}; match({<<"referer">>, <<>>}, _) -> {indexed, 51}; match({<<"refresh">>, <<>>}, _) -> {indexed, 52}; match({<<"retry-after">>, <<>>}, _) -> {indexed, 53}; match({<<"server">>, <<>>}, _) -> {indexed, 54}; match({<<"set-cookie">>, <<>>}, _) -> {indexed, 55}; match({<<"strict-transport-security">>, <<>>}, _) -> {indexed, 56}; match({<<"transfer-encoding">>, <<>>}, _) -> {indexed, 57}; match({<<"user-agent">>, <<>>}, _) -> {indexed, 58}; match({<<"vary">>, <<>>}, _) -> {indexed, 59}; match({<<"via">>, <<>>}, _) -> {indexed, 60}; match({<<"www-authenticate">>, <<>>}, _) -> {indexed, 61}; match({Name, Value}, #dynamic_table{table=T}) -> %% If there's a N/V match in the DT, return {indexed, Int} %% If there's a N match, make sure there's not also a static_match %% if so, use it, otherwise use the first element in the first match %% If not Found = lists:filter(fun({_,N,_}) -> N =:= Name end, T), ExactFound = lists:filter(fun({_,_,V}) -> V =:= Value end, Found), case {ExactFound, Found, static_match(Name)} of {[{I,Name,Value}|_], _, _} -> {indexed, I}; {[], [{I,Name,_}|_], undefined} -> {literal_with_indexing, I}; {[], _, I} when is_integer(I) -> {literal_with_indexing, I}; {[], [], undefined} -> {literal_wo_indexing, undefined} end. -spec static_match(header_name()) -> pos_integer() | undefined. static_match(<<":authority">>) -> 1 ; static_match(<<":method">>) -> 2 ; static_match(<<":path">>) -> 4 ; static_match(<<":scheme">>) -> 6 ; static_match(<<":status">>) -> 8 ; static_match(<<"accept-charset">>) -> 15; static_match(<<"accept-encoding">>) -> 16; static_match(<<"accept-language">>) -> 17; static_match(<<"accept-ranges">>) -> 18; static_match(<<"accept">>) -> 19; static_match(<<"access-control-allow-origin">>) -> 20; static_match(<<"age">>) -> 21; static_match(<<"allow">>) -> 22; static_match(<<"authorization">>) -> 23; static_match(<<"cache-control">>) -> 24; static_match(<<"content-disposition">>) -> 25; static_match(<<"content-encoding">>) -> 26; static_match(<<"content-language">>) -> 27; static_match(<<"content-length">>) -> 28; static_match(<<"content-location">>) -> 29; static_match(<<"content-range">>) -> 30; static_match(<<"content-type">>) -> 31; static_match(<<"cookie">>) -> 32; static_match(<<"date">>) -> 33; static_match(<<"etag">>) -> 34; static_match(<<"expect">>) -> 35; static_match(<<"expires">>) -> 36; static_match(<<"from">>) -> 37; static_match(<<"host">>) -> 38; static_match(<<"if-match">>) -> 39; static_match(<<"if-modified-since">>) -> 40; static_match(<<"if-none-match">>) -> 41; static_match(<<"if-range">>) -> 42; static_match(<<"if-unmodified-since">>) -> 43; static_match(<<"last-modified">>) -> 44; static_match(<<"link">>) -> 45; static_match(<<"location">>) -> 46; static_match(<<"max-forwards">>) -> 47; static_match(<<"proxy-authenticate">>) -> 48; static_match(<<"proxy-authorization">>) -> 49; static_match(<<"range">>) -> 50; static_match(<<"referer">>) -> 51; static_match(<<"refresh">>) -> 52; static_match(<<"retry-after">>) -> 53; static_match(<<"server">>) -> 54; static_match(<<"set-cookie">>) -> 55; static_match(<<"strict-transport-security">>) -> 56; static_match(<<"transfer-encoding">>) -> 57; static_match(<<"user-agent">>) -> 58; static_match(<<"vary">>) -> 59; static_match(<<"via">>) -> 60; static_match(<<"www-authenticate">>) -> 61; static_match(_) -> undefined.