%% @doc HTTP/2 Frame Encoding and Decoding (RFC 7540 Section 4) %% %% All frames begin with a fixed 9-octet header followed by a payload %% of variable length: %% %% +-----------------------------------------------+ %% | Length (24) | %% +---------------+---------------+---------------+ %% | Type (8) | Flags (8) | %% +-+-------------+---------------+-------------------------------+ %% |R| Stream Identifier (31) | %% +=+=============================================================+ %% | Frame Payload (0...) | %% +---------------------------------------------------------------+ %% -module(h2_frame). -export([encode/1, decode/1, decode/2, decode_header/1]). -export([data/3, data/4]). -export([headers/3, headers/4, headers/5]). -export([priority/4]). -export([rst_stream/2]). -export([settings/1, settings_ack/0]). -export([push_promise/4]). -export([ping/1, ping_ack/1]). -export([goaway/3]). -export([window_update/2]). -export([continuation/3]). -include("h2.hrl"). -define(FRAME_HEADER_SIZE, 9). %% Frame types -type frame_type() :: data | headers | priority | rst_stream | settings | push_promise | ping | goaway | window_update | continuation | {unknown, 0..255}. -type frame() :: #h2_frame{} | frame_data(). -type frame_data() :: {data, StreamId :: non_neg_integer(), Data :: binary(), EndStream :: boolean()} | {headers, StreamId :: non_neg_integer(), HeaderBlock :: binary(), EndStream :: boolean(), EndHeaders :: boolean()} | {headers, StreamId :: non_neg_integer(), HeaderBlock :: binary(), EndStream :: boolean(), EndHeaders :: boolean(), Priority :: {Exclusive :: boolean(), DependsOn :: non_neg_integer(), Weight :: 1..256}} | {priority, StreamId :: non_neg_integer(), Exclusive :: boolean(), DependsOn :: non_neg_integer(), Weight :: 1..256} | {rst_stream, StreamId :: non_neg_integer(), ErrorCode :: non_neg_integer()} | {settings, Settings :: [{non_neg_integer(), non_neg_integer()}]} | {settings_ack} | {push_promise, StreamId :: non_neg_integer(), PromisedId :: non_neg_integer(), HeaderBlock :: binary(), EndHeaders :: boolean()} | {ping, Data :: binary()} | {ping_ack, Data :: binary()} | {goaway, LastStreamId :: non_neg_integer(), ErrorCode :: non_neg_integer(), DebugData :: binary()} | {window_update, StreamId :: non_neg_integer(), Increment :: pos_integer()} | {continuation, StreamId :: non_neg_integer(), HeaderBlock :: binary(), EndHeaders :: boolean()}. -export_type([frame_type/0, frame/0, frame_data/0]). %% ============================================================================ %% Frame Constructors %% ============================================================================ %% @doc Create a DATA frame. -spec data(non_neg_integer(), binary(), boolean()) -> frame_data(). data(StreamId, Data, EndStream) -> {data, StreamId, Data, EndStream}. %% @doc Create a DATA frame with padding. -spec data(non_neg_integer(), binary(), boolean(), binary()) -> #h2_frame{}. data(StreamId, Data, EndStream, Padding) when byte_size(Padding) =< 255 -> PadLen = byte_size(Padding), Flags = flags([{?FLAG_END_STREAM, EndStream}, {?FLAG_PADDED, PadLen > 0}]), Payload = if PadLen > 0 -> <>; true -> Data end, #h2_frame{type = ?DATA, flags = Flags, stream_id = StreamId, payload = Payload}. %% @doc Create a HEADERS frame. -spec headers(non_neg_integer(), binary(), boolean()) -> frame_data(). headers(StreamId, HeaderBlock, EndStream) -> {headers, StreamId, HeaderBlock, EndStream, true}. %% @doc Create a HEADERS frame with end headers flag. -spec headers(non_neg_integer(), binary(), boolean(), boolean()) -> frame_data(). headers(StreamId, HeaderBlock, EndStream, EndHeaders) -> {headers, StreamId, HeaderBlock, EndStream, EndHeaders}. %% @doc Create a HEADERS frame with priority. -spec headers(non_neg_integer(), binary(), boolean(), boolean(), {boolean(), non_neg_integer(), 1..256}) -> frame_data(). headers(StreamId, HeaderBlock, EndStream, EndHeaders, Priority) -> {headers, StreamId, HeaderBlock, EndStream, EndHeaders, Priority}. %% @doc Create a PRIORITY frame. -spec priority(non_neg_integer(), boolean(), non_neg_integer(), 1..256) -> frame_data(). priority(StreamId, Exclusive, DependsOn, Weight) -> {priority, StreamId, Exclusive, DependsOn, Weight}. %% @doc Create a RST_STREAM frame. -spec rst_stream(non_neg_integer(), non_neg_integer() | atom()) -> frame_data(). rst_stream(StreamId, ErrorCode) when is_atom(ErrorCode) -> rst_stream(StreamId, h2_error:code(ErrorCode)); rst_stream(StreamId, ErrorCode) -> {rst_stream, StreamId, ErrorCode}. %% @doc Create a SETTINGS frame. -spec settings([{atom() | non_neg_integer(), non_neg_integer()}]) -> frame_data(). settings(Settings) -> {settings, Settings}. %% @doc Create a SETTINGS ACK frame. -spec settings_ack() -> frame_data(). settings_ack() -> {settings_ack}. %% @doc Create a PUSH_PROMISE frame. -spec push_promise(non_neg_integer(), non_neg_integer(), binary(), boolean()) -> frame_data(). push_promise(StreamId, PromisedId, HeaderBlock, EndHeaders) -> {push_promise, StreamId, PromisedId, HeaderBlock, EndHeaders}. %% @doc Create a PING frame. -spec ping(binary()) -> frame_data(). ping(Data) when byte_size(Data) =:= 8 -> {ping, Data}. %% @doc Create a PING ACK frame. -spec ping_ack(binary()) -> frame_data(). ping_ack(Data) when byte_size(Data) =:= 8 -> {ping_ack, Data}. %% @doc Create a GOAWAY frame. -spec goaway(non_neg_integer(), non_neg_integer() | atom(), binary()) -> frame_data(). goaway(LastStreamId, ErrorCode, DebugData) when is_atom(ErrorCode) -> goaway(LastStreamId, h2_error:code(ErrorCode), DebugData); goaway(LastStreamId, ErrorCode, DebugData) -> {goaway, LastStreamId, ErrorCode, DebugData}. %% @doc Create a WINDOW_UPDATE frame. -spec window_update(non_neg_integer(), pos_integer()) -> frame_data(). window_update(StreamId, Increment) when Increment > 0 -> {window_update, StreamId, Increment}. %% @doc Create a CONTINUATION frame. -spec continuation(non_neg_integer(), binary(), boolean()) -> frame_data(). continuation(StreamId, HeaderBlock, EndHeaders) -> {continuation, StreamId, HeaderBlock, EndHeaders}. %% ============================================================================ %% Encoding %% ============================================================================ %% @doc Encode a frame to binary. -spec encode(frame() | frame_data()) -> binary(). encode(#h2_frame{type = Type, flags = Flags, stream_id = StreamId, payload = Payload}) -> Length = byte_size(Payload), <>; encode({data, StreamId, Data, EndStream}) -> Flags = flags([{?FLAG_END_STREAM, EndStream}]), Length = byte_size(Data), <>; encode({headers, StreamId, HeaderBlock, EndStream, EndHeaders}) -> Flags = flags([{?FLAG_END_STREAM, EndStream}, {?FLAG_END_HEADERS, EndHeaders}]), Length = byte_size(HeaderBlock), <>; encode({headers, StreamId, HeaderBlock, EndStream, EndHeaders, {Exclusive, DependsOn, Weight}}) -> Flags = flags([{?FLAG_END_STREAM, EndStream}, {?FLAG_END_HEADERS, EndHeaders}, {?FLAG_PRIORITY, true}]), E = if Exclusive -> 1; true -> 0 end, Payload = <>, Length = byte_size(Payload), <>; encode({priority, StreamId, Exclusive, DependsOn, Weight}) -> E = if Exclusive -> 1; true -> 0 end, Payload = <>, <<5:24, ?PRIORITY:8, 0:8, 0:1, StreamId:31, Payload/binary>>; encode({rst_stream, StreamId, ErrorCode}) -> <<4:24, ?RST_STREAM:8, 0:8, 0:1, StreamId:31, ErrorCode:32>>; encode({settings, Settings}) -> Payload = encode_settings(Settings, <<>>), Length = byte_size(Payload), <>; encode({settings_ack}) -> <<0:24, ?SETTINGS:8, ?FLAG_ACK:8, 0:1, 0:31>>; encode({push_promise, StreamId, PromisedId, HeaderBlock, EndHeaders}) -> Flags = flags([{?FLAG_END_HEADERS, EndHeaders}]), Payload = <<0:1, PromisedId:31, HeaderBlock/binary>>, Length = byte_size(Payload), <>; encode({ping, Data}) -> <<8:24, ?PING:8, 0:8, 0:1, 0:31, Data:8/binary>>; encode({ping_ack, Data}) -> <<8:24, ?PING:8, ?FLAG_ACK:8, 0:1, 0:31, Data:8/binary>>; encode({goaway, LastStreamId, ErrorCode, DebugData}) -> Payload = <<0:1, LastStreamId:31, ErrorCode:32, DebugData/binary>>, Length = byte_size(Payload), <>; encode({window_update, StreamId, Increment}) -> <<4:24, ?WINDOW_UPDATE:8, 0:8, 0:1, StreamId:31, 0:1, Increment:31>>; encode({continuation, StreamId, HeaderBlock, EndHeaders}) -> Flags = flags([{?FLAG_END_HEADERS, EndHeaders}]), Length = byte_size(HeaderBlock), <>. encode_settings([], Acc) -> Acc; encode_settings([{Id, Value}|Rest], Acc) when is_integer(Id) -> encode_settings(Rest, <>); encode_settings([{Name, Value}|Rest], Acc) when is_atom(Name) -> Id = setting_id(Name), encode_settings(Rest, <>). setting_id(header_table_size) -> ?SETTINGS_HEADER_TABLE_SIZE; setting_id(enable_push) -> ?SETTINGS_ENABLE_PUSH; setting_id(max_concurrent_streams) -> ?SETTINGS_MAX_CONCURRENT_STREAMS; setting_id(initial_window_size) -> ?SETTINGS_INITIAL_WINDOW_SIZE; setting_id(max_frame_size) -> ?SETTINGS_MAX_FRAME_SIZE; setting_id(max_header_list_size) -> ?SETTINGS_MAX_HEADER_LIST_SIZE; setting_id(enable_connect_protocol) -> ?SETTINGS_ENABLE_CONNECT_PROTOCOL. flags(Flags) -> lists:foldl(fun ({Flag, true}, Acc) -> Acc bor Flag; ({_, false}, Acc) -> Acc end, 0, Flags). %% ============================================================================ %% Decoding %% ============================================================================ %% @doc Decode binary to frame. %% Returns {ok, Frame, Rest} | {more, N} | {error, Reason} -spec decode(binary()) -> {ok, frame_data(), binary()} | {more, non_neg_integer()} | {error, term()}. decode(Bin) -> decode(Bin, infinity). %% @doc Decode binary to frame, rejecting frames larger than MaxFrameSize %% with frame_size_error (RFC 7540 §4.2). -spec decode(binary(), pos_integer() | infinity) -> {ok, frame_data(), binary()} | {more, non_neg_integer()} | {error, term()}. decode(Bin, _MaxFrameSize) when byte_size(Bin) < ?FRAME_HEADER_SIZE -> {more, ?FRAME_HEADER_SIZE - byte_size(Bin)}; decode(<>, MaxFrameSize) when is_integer(MaxFrameSize), Length > MaxFrameSize -> {error, frame_size_error}; decode(<> = Bin, _MaxFrameSize) when byte_size(Rest) < Length -> {more, ?FRAME_HEADER_SIZE + Length - byte_size(Bin)}; decode(<>, _MaxFrameSize) -> case decode_frame(Type, Flags, StreamId, Payload) of {ok, Frame} -> {ok, Frame, Rest}; {error, _} = Err -> Err end. %% @doc Decode just the frame header (for peeking). -spec decode_header(binary()) -> {ok, {non_neg_integer(), frame_type(), 0..255, non_neg_integer()}, binary()} | {more, non_neg_integer()}. decode_header(Bin) when byte_size(Bin) < ?FRAME_HEADER_SIZE -> {more, ?FRAME_HEADER_SIZE - byte_size(Bin)}; decode_header(<>) -> {ok, {Length, type_name(Type), Flags, StreamId}, Rest}. decode_frame(?DATA, _Flags, 0, _Payload) -> {error, protocol_error}; %% DATA MUST have non-zero stream ID decode_frame(?DATA, Flags, StreamId, Payload) -> EndStream = (Flags band ?FLAG_END_STREAM) =/= 0, Padded = (Flags band ?FLAG_PADDED) =/= 0, case strip_padding(Padded, Payload) of {ok, Data} -> {ok, {data, StreamId, Data, EndStream}}; {error, _} = Err -> Err end; decode_frame(?HEADERS, _Flags, 0, _Payload) -> {error, protocol_error}; %% HEADERS MUST have non-zero stream ID decode_frame(?HEADERS, Flags, StreamId, Payload) -> EndStream = (Flags band ?FLAG_END_STREAM) =/= 0, EndHeaders = (Flags band ?FLAG_END_HEADERS) =/= 0, Padded = (Flags band ?FLAG_PADDED) =/= 0, HasPriority = (Flags band ?FLAG_PRIORITY) =/= 0, case strip_padding(Padded, Payload) of {ok, Data} -> case decode_headers_payload(HasPriority, Data) of {ok, HeaderBlock, undefined} -> {ok, {headers, StreamId, HeaderBlock, EndStream, EndHeaders}}; {ok, HeaderBlock, Priority} -> {ok, {headers, StreamId, HeaderBlock, EndStream, EndHeaders, Priority}}; {error, _} = Err -> Err end; {error, _} = Err -> Err end; decode_frame(?PRIORITY, _Flags, 0, _Payload) -> {error, protocol_error}; %% PRIORITY MUST have non-zero stream ID decode_frame(?PRIORITY, _Flags, StreamId, <>) -> {ok, {priority, StreamId, E =:= 1, DependsOn, Weight + 1}}; decode_frame(?PRIORITY, _Flags, _StreamId, _Payload) -> {error, frame_size_error}; decode_frame(?RST_STREAM, _Flags, 0, _Payload) -> {error, protocol_error}; %% RST_STREAM MUST have non-zero stream ID decode_frame(?RST_STREAM, _Flags, StreamId, <>) -> {ok, {rst_stream, StreamId, ErrorCode}}; decode_frame(?RST_STREAM, _Flags, _StreamId, _Payload) -> {error, frame_size_error}; decode_frame(?SETTINGS, Flags, 0, Payload) -> IsAck = (Flags band ?FLAG_ACK) =/= 0, if IsAck, byte_size(Payload) =:= 0 -> {ok, {settings_ack}}; IsAck -> {error, frame_size_error}; true -> case decode_settings(Payload) of {ok, Settings} -> {ok, {settings, Settings}}; {error, _} = Err -> Err end end; decode_frame(?SETTINGS, _Flags, _StreamId, _Payload) -> {error, protocol_error}; %% SETTINGS must be on stream 0 decode_frame(?PUSH_PROMISE, _Flags, 0, _Payload) -> {error, protocol_error}; %% PUSH_PROMISE MUST have non-zero stream ID decode_frame(?PUSH_PROMISE, Flags, StreamId, Payload) -> EndHeaders = (Flags band ?FLAG_END_HEADERS) =/= 0, Padded = (Flags band ?FLAG_PADDED) =/= 0, case strip_padding(Padded, Payload) of {ok, <<_:1, PromisedId:31, HeaderBlock/binary>>} -> {ok, {push_promise, StreamId, PromisedId, HeaderBlock, EndHeaders}}; {ok, _} -> {error, frame_size_error}; {error, _} = Err -> Err end; decode_frame(?PING, Flags, 0, Data) when byte_size(Data) =:= 8 -> IsAck = (Flags band ?FLAG_ACK) =/= 0, if IsAck -> {ok, {ping_ack, Data}}; true -> {ok, {ping, Data}} end; decode_frame(?PING, _Flags, 0, _Payload) -> {error, frame_size_error}; decode_frame(?PING, _Flags, _StreamId, _Payload) -> {error, protocol_error}; %% PING must be on stream 0 decode_frame(?GOAWAY, _Flags, 0, <<_:1, LastStreamId:31, ErrorCode:32, DebugData/binary>>) -> {ok, {goaway, LastStreamId, ErrorCode, DebugData}}; decode_frame(?GOAWAY, _Flags, 0, _Payload) -> {error, frame_size_error}; decode_frame(?GOAWAY, _Flags, _StreamId, _Payload) -> {error, protocol_error}; %% GOAWAY must be on stream 0 decode_frame(?WINDOW_UPDATE, _Flags, StreamId, <<_:1, Increment:31>>) when Increment > 0 -> {ok, {window_update, StreamId, Increment}}; decode_frame(?WINDOW_UPDATE, _Flags, _StreamId, <<_:1, 0:31>>) -> {error, protocol_error}; %% Zero increment is an error decode_frame(?WINDOW_UPDATE, _Flags, _StreamId, _Payload) -> {error, frame_size_error}; decode_frame(?CONTINUATION, _Flags, 0, _Payload) -> {error, protocol_error}; %% CONTINUATION MUST have non-zero stream ID decode_frame(?CONTINUATION, Flags, StreamId, HeaderBlock) -> EndHeaders = (Flags band ?FLAG_END_HEADERS) =/= 0, {ok, {continuation, StreamId, HeaderBlock, EndHeaders}}; decode_frame(Type, Flags, StreamId, Payload) -> %% Unknown frame types are ignored per RFC 7540 {ok, {unknown_frame, Type, Flags, StreamId, Payload}}. strip_padding(false, Data) -> {ok, Data}; strip_padding(true, <>) when PadLength =< byte_size(Rest) -> %% RFC 7540 §6.1: pad_length's value MUST be less than the frame payload %% length (which is 1 + byte_size(Rest) here since payload includes the %% pad-length byte). So PadLength =< byte_size(Rest) is the valid bound. DataLen = byte_size(Rest) - PadLength, <> = Rest, {ok, Data}; strip_padding(true, _) -> {error, protocol_error}. decode_headers_payload(false, HeaderBlock) -> {ok, HeaderBlock, undefined}; decode_headers_payload(true, <>) -> {ok, HeaderBlock, {E =:= 1, DependsOn, Weight + 1}}; decode_headers_payload(true, _) -> {error, frame_size_error}. decode_settings(Bin) -> decode_settings(Bin, []). decode_settings(<<>>, Acc) -> {ok, lists:reverse(Acc)}; decode_settings(<>, Acc) -> decode_settings(Rest, [{Id, Value}|Acc]); decode_settings(_, _) -> {error, frame_size_error}. type_name(?DATA) -> data; type_name(?HEADERS) -> headers; type_name(?PRIORITY) -> priority; type_name(?RST_STREAM) -> rst_stream; type_name(?SETTINGS) -> settings; type_name(?PUSH_PROMISE) -> push_promise; type_name(?PING) -> ping; type_name(?GOAWAY) -> goaway; type_name(?WINDOW_UPDATE) -> window_update; type_name(?CONTINUATION) -> continuation; type_name(N) -> {unknown, N}. -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). data_test() -> Frame = data(1, <<"hello">>, true), Bin = encode(Frame), {ok, Decoded, <<>>} = decode(Bin), ?assertEqual({data, 1, <<"hello">>, true}, Decoded). data_no_end_stream_test() -> Frame = data(1, <<"hello">>, false), Bin = encode(Frame), {ok, Decoded, <<>>} = decode(Bin), ?assertEqual({data, 1, <<"hello">>, false}, Decoded). headers_test() -> Frame = headers(1, <<"header_block">>, true), Bin = encode(Frame), {ok, Decoded, <<>>} = decode(Bin), ?assertEqual({headers, 1, <<"header_block">>, true, true}, Decoded). headers_with_priority_test() -> Frame = headers(1, <<"header_block">>, true, true, {true, 0, 16}), Bin = encode(Frame), {ok, Decoded, <<>>} = decode(Bin), ?assertEqual({headers, 1, <<"header_block">>, true, true, {true, 0, 16}}, Decoded). priority_test() -> Frame = priority(1, false, 0, 16), Bin = encode(Frame), {ok, Decoded, <<>>} = decode(Bin), ?assertEqual({priority, 1, false, 0, 16}, Decoded). rst_stream_test() -> Frame = rst_stream(1, 8), %% CANCEL Bin = encode(Frame), {ok, Decoded, <<>>} = decode(Bin), ?assertEqual({rst_stream, 1, 8}, Decoded). rst_stream_atom_test() -> Frame = rst_stream(1, cancel), Bin = encode(Frame), {ok, Decoded, <<>>} = decode(Bin), ?assertEqual({rst_stream, 1, 8}, Decoded). settings_test() -> Frame = settings([{1, 4096}, {4, 65535}]), Bin = encode(Frame), {ok, Decoded, <<>>} = decode(Bin), ?assertEqual({settings, [{1, 4096}, {4, 65535}]}, Decoded). settings_ack_test() -> Frame = settings_ack(), Bin = encode(Frame), {ok, Decoded, <<>>} = decode(Bin), ?assertEqual({settings_ack}, Decoded). ping_test() -> Data = <<1,2,3,4,5,6,7,8>>, Frame = ping(Data), Bin = encode(Frame), {ok, Decoded, <<>>} = decode(Bin), ?assertEqual({ping, Data}, Decoded). ping_ack_test() -> Data = <<1,2,3,4,5,6,7,8>>, Frame = ping_ack(Data), Bin = encode(Frame), {ok, Decoded, <<>>} = decode(Bin), ?assertEqual({ping_ack, Data}, Decoded). goaway_test() -> Frame = goaway(1, 0, <<"debug">>), Bin = encode(Frame), {ok, Decoded, <<>>} = decode(Bin), ?assertEqual({goaway, 1, 0, <<"debug">>}, Decoded). window_update_test() -> Frame = window_update(1, 1000), Bin = encode(Frame), {ok, Decoded, <<>>} = decode(Bin), ?assertEqual({window_update, 1, 1000}, Decoded). continuation_test() -> Frame = continuation(1, <<"more_headers">>, true), Bin = encode(Frame), {ok, Decoded, <<>>} = decode(Bin), ?assertEqual({continuation, 1, <<"more_headers">>, true}, Decoded). push_promise_test() -> Frame = push_promise(1, 2, <<"header_block">>, true), Bin = encode(Frame), {ok, Decoded, <<>>} = decode(Bin), ?assertEqual({push_promise, 1, 2, <<"header_block">>, true}, Decoded). decode_incomplete_test() -> %% Less than frame header ?assertEqual({more, 5}, decode(<<0,0,0,0>>)), %% Frame header but missing payload ?assertEqual({more, 5}, decode(<<0,0,10, 0, 0, 0,0,0,1, 1,2,3,4,5>>)). multiple_frames_test() -> Frame1 = encode(data(1, <<"hello">>, false)), Frame2 = encode(data(1, <<"world">>, true)), Combined = <>, {ok, D1, Rest} = decode(Combined), ?assertEqual({data, 1, <<"hello">>, false}, D1), {ok, D2, <<>>} = decode(Rest), ?assertEqual({data, 1, <<"world">>, true}, D2). %% ---- Spec-compliance: frame validation ---- max_frame_size_receive_test() -> Big = binary:copy(<<"x">>, 20000), Bin = encode(data(1, Big, true)), ?assertEqual({error, frame_size_error}, decode(Bin, 16384)), %% At the default settings-max, it decodes cleanly {ok, _, <<>>} = decode(Bin, 16#FFFFFF). data_on_stream_zero_rejected_test() -> Bin = encode(data(0, <<"oops">>, true)), ?assertEqual({error, protocol_error}, decode(Bin)). headers_on_stream_zero_rejected_test() -> Bin = encode(headers(0, <<"hdr">>, true)), ?assertEqual({error, protocol_error}, decode(Bin)). priority_on_stream_zero_rejected_test() -> Bin = encode(priority(0, false, 0, 16)), ?assertEqual({error, protocol_error}, decode(Bin)). rst_stream_on_stream_zero_rejected_test() -> Bin = encode(rst_stream(0, 0)), ?assertEqual({error, protocol_error}, decode(Bin)). push_promise_on_stream_zero_rejected_test() -> Bin = encode(push_promise(0, 2, <<"hdr">>, true)), ?assertEqual({error, protocol_error}, decode(Bin)). continuation_on_stream_zero_rejected_test() -> Bin = encode(continuation(0, <<"hdr">>, true)), ?assertEqual({error, protocol_error}, decode(Bin)). settings_on_stream_nonzero_rejected_test() -> %% Craft a SETTINGS frame on stream 1 manually Bin = <<0:24, ?SETTINGS:8, 0:8, 0:1, 1:31>>, ?assertEqual({error, protocol_error}, decode(Bin)). ping_on_stream_nonzero_rejected_test() -> Bin = <<8:24, ?PING:8, 0:8, 0:1, 1:31, 0,0,0,0,0,0,0,0>>, ?assertEqual({error, protocol_error}, decode(Bin)). goaway_on_stream_nonzero_rejected_test() -> Bin = <<8:24, ?GOAWAY:8, 0:8, 0:1, 1:31, 0:1, 0:31, 0:32>>, ?assertEqual({error, protocol_error}, decode(Bin)). padding_equal_payload_accepted_test() -> %% PadLength=5 with 0 data bytes and 5 padding bytes: payload size = 1+0+5 = 6 %% This is legal per RFC 7540 §6.1 since pad value (5) < payload length (6). Frame = data(1, <<>>, true, <<1,2,3,4,5>>), Bin = encode(Frame), {ok, {data, 1, <<>>, true}, <<>>} = decode(Bin). padding_over_payload_rejected_test() -> %% Manually craft a DATA frame where PadLength claims more bytes than follow. %% Payload = <<5, 1,2>> (PadLength=5 but only 2 bytes of data+padding). Payload = <<5, 1, 2>>, Bin = <<(byte_size(Payload)):24, ?DATA:8, ?FLAG_PADDED:8, 0:1, 1:31, Payload/binary>>, ?assertEqual({error, protocol_error}, decode(Bin)). settings_ack_with_payload_rejected_test() -> %% SETTINGS frame with ACK flag set MUST NOT have a payload (RFC 7540 §6.5). Payload = <<0,1, 0:32>>, Bin = <<(byte_size(Payload)):24, ?SETTINGS:8, ?FLAG_ACK:8, 0:1, 0:31, Payload/binary>>, ?assertEqual({error, frame_size_error}, decode(Bin)). unknown_frame_ignored_test() -> %% Type 0xFA is not defined; decode returns unknown_frame for upper layer %% to ignore (RFC 7540 §4.1). Payload = <<"anything">>, Bin = <<(byte_size(Payload)):24, 16#FA:8, 0:8, 0:1, 1:31, Payload/binary>>, {ok, {unknown_frame, 16#FA, 0, 1, Payload}, <<>>} = decode(Bin). -endif.