Packages
hackney
3.2.1
4.7.2
4.7.1
4.7.0
4.6.1
4.6.0
4.5.2
4.5.1
4.5.0
4.4.5
4.4.3
4.4.2
4.4.1
4.4.0
4.3.0
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.3
4.0.2
4.0.1
4.0.0
3.2.1
3.2.0
3.1.2
3.1.1
3.1.0
3.0.3
3.0.2
3.0.1
3.0.0
retired
2.0.1
2.0.0
2.0.0-beta.1
1.25.0
1.24.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.2
1.18.1
1.18.0
1.17.4
1.17.3
1.17.2
1.17.1
1.17.0
1.16.0
1.15.2
1.15.1
1.15.0
1.14.3
1.14.2
1.14.0
1.13.0
1.12.1
1.12.0
1.11.0
1.10.1
1.10.0
1.9.0
1.8.6
1.8.5
1.8.4
1.8.3
1.8.2
1.8.0
1.7.1
1.7.0
1.6.6
retired
1.6.5
1.6.4
retired
1.6.3
1.6.2
1.6.1
1.6.0
1.5.7
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.10
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.0
1.1.0
1.0.6
1.0.5
1.0.2
1.0.1
0.15.2
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
Simple HTTP client with HTTP/1.1, HTTP/2, and HTTP/3 support
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
src/hackney_qpack.erl
%%% -*- 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, <<>>),
{<<Prefix/binary, EncodedHeaders/binary>>, 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, <<Acc/binary, Encoded/binary>>).
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),
<<NameRef/binary, ValueEnc/binary>>.
%% 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,
<<FirstByte, Name/binary, ValueEnc/binary>>;
false ->
FirstByte = 2#00100111,
LenCont = encode_multi_byte_int(NameLen - 7),
<<FirstByte, LenCont/binary, Name/binary, ValueEnc/binary>>
end.
encode_string(Str) ->
Len = byte_size(Str),
LenEnc = encode_prefixed_int(Len, 7, 0),
<<LenEnc/binary, Str/binary>>.
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,
<<FirstByte, (encode_multi_byte_int(Remaining))/binary>>.
encode_multi_byte_int(Value) when Value < 128 ->
<<Value>>;
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/binary>>) ->
{{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(<<Byte, _/binary>>, _State, _Acc) ->
throw({unknown_instruction, Byte}).
decode_prefixed_int(Data, PrefixBits) ->
MaxPrefix = (1 bsl PrefixBits) - 1,
<<First, Rest/binary>> = 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(<<Byte, Rest/binary>>, 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 ->
<<Str:ActualLen/binary, Rest3/binary>> = 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 ->
<<Encoded:ActualLen/binary, Rest3/binary>> = 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 ->
<<Str:Len/binary, Rest2/binary>> = Rest,
{Str, Rest2};
decode_string(<<1:1, Len:7, Rest/binary>>) when byte_size(Rest) >= Len ->
<<Encoded:Len/binary, Rest2/binary>> = 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 ->
<<Encoded:Len/binary, Rest/binary>> = 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(<<A:4, B:4, R/bits>>, 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, <<Acc/binary, CharA>>);
{undefined, CharB} -> dec_huffman(R, Len - 1, Huff, <<Acc/binary, CharB>>);
{CharA, CharB} -> dec_huffman(R, Len - 1, Huff, <<Acc/binary, CharA, CharB>>)
end;
dec_huffman(<<A:4, B:4, Rest/bits>>, 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} ->
{<<Acc/binary, CharA>>, Rest};
{undefined, CharB} ->
{<<Acc/binary, CharB>>, Rest};
_ ->
{<<Acc/binary, CharA, CharB>>, Rest}
end;
{more, _, _} ->
case CharA of
undefined -> {Acc, Rest};
_ -> {<<Acc/binary, CharA>>, 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}).