%%% @author Sergey %%% @copyright (C) 2018, Sergey %%% @doc %%% MTProto "full" packet format with padding %%% ``` %%% <> %%% ``` %%% @end %%% Created : 6 Jun 2018 by Sergey -module(mtp_full). -behaviour(mtp_codec). -export([new/0, new/2, try_decode_packet/2, encode_packet/2]). -export_type([codec/0]). -dialyzer(no_improper_lists). -record(full_st, {decode_buf = <<>> :: binary(), enc_seq_no :: integer(), dec_seq_no :: integer()}). -define(MIN_MSG_LEN, 12). -define(MAX_MSG_LEN, 16777216). %2^24 - 16mb -define(BLOCK_SIZE, 16). -define(PAD, <<4:32/little>>). -define(APP, mtproto_proxy). -opaque codec() :: #full_st{}. new() -> new(0, 0). new(EncSeqNo, DecSeqNo) -> #full_st{enc_seq_no = EncSeqNo, dec_seq_no = DecSeqNo}. try_decode_packet(<<4:32/little, Bin/binary>>, #full_st{decode_buf = <<>>} = S) -> %% Skip padding try_decode_packet(Bin, S); try_decode_packet(<> = Bin, #full_st{decode_buf = <<>>, dec_seq_no = SeqNo} = S) -> ((Len rem byte_size(?PAD)) == 0) orelse error({wrong_alignement, Len}), ((?MIN_MSG_LEN =< Len) and (Len =< ?MAX_MSG_LEN)) orelse error({wrong_msg_len, Len}), (SeqNo == PktSeqNo) orelse error({wrong_seq_no, SeqNo, PktSeqNo}), BodyLen = Len - 4 - 4 - 4, case Tail of <> -> PacketCrc = erlang:crc32([<> | Body]), (CRC == PacketCrc) orelse error({wrong_checksum, CRC, PacketCrc}), {ok, Body, S#full_st{decode_buf = Rest, dec_seq_no = SeqNo + 1}}; _ -> {incomplete, S#full_st{decode_buf = Bin}} end; try_decode_packet(Bin, #full_st{decode_buf = Buf} = S) when byte_size(Buf) > 0 -> try_decode_packet(<>, S#full_st{decode_buf = <<>>}); try_decode_packet(Bin, #full_st{decode_buf = <<>>} = S) -> {incomplete, S#full_st{decode_buf = Bin}}. encode_packet(Bin, #full_st{enc_seq_no = SeqNo} = S) -> BodySize = iolist_size(Bin), ((BodySize rem byte_size(?PAD)) == 0) orelse error({wrong_alignment, BodySize}), Len = BodySize + 4 + 4 + 4, MsgNoChecksum = [<> | Bin], CheckSum = erlang:crc32(MsgNoChecksum), FullMsg = [MsgNoChecksum | <>], Len = iolist_size(FullMsg), %% XXX: is there a cleaner way? PaddingSize = (?BLOCK_SIZE - (Len rem ?BLOCK_SIZE)) rem ?BLOCK_SIZE, NPaddings = PaddingSize div byte_size(?PAD), Padding = lists:duplicate(NPaddings, ?PAD), {[FullMsg | Padding], S#full_st{enc_seq_no = SeqNo + 1}}. -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). encode_nopadding_test() -> S = new(), {Enc, _S1} = encode_packet(<<1, 1, 1, 1>>, S), ?assertEqual( <<16,0,0,0, 0,0,0,0, 1,1,1,1, 22,39,175,160>>, iolist_to_binary(Enc)). encode_padding_test() -> S = new(), {Enc, _S1} = encode_packet(<<1,1,1,1,1,1,1,1>>, S), ?assertEqual( <<20,0,0,0,0,0,0,0, %size, seq no 1,1,1,1,1,1,1,1, %data 246,196,46,149, %CRC 4,0,0,0,4,0,0,0,4,0,0,0>>, %padding iolist_to_binary(Enc)). encode_padding_seq_test() -> S = new(), {Enc1, S1} = encode_packet(binary:copy(<<9>>, 8), S), ?assertEqual( <<20,0,0,0, 0,0,0,0, 9,9,9,9,9,9,9,9, 229,35,162,164, 4, 0,0,0,4,0,0,0,4,0,0,0>>, iolist_to_binary(Enc1)), {Enc2, _S2} = encode_packet(binary:copy(<<8>>, 8), S1), ?assertEqual( <<20,0,0,0, 1,0,0,0, 8,8,8,8,8,8,8,8, 48,146,132,116, 4,0,0,0,4,0,0,0,4,0,0,0>>, iolist_to_binary(Enc2)). decode_none_test() -> S = new(), ?assertEqual( {incomplete, S}, try_decode_packet(<<>>, S)). codec_test() -> S = new(), Packets = [ binary:copy(<<0>>, 4), %non-padded binary:copy(<<1>>, 8), %padded binary:copy(<<2>>, 4), %non-padded binary:copy(<<2>>, 100) %padded ], lists:foldl( fun(B, S1) -> {Encoded, S2} = encode_packet(B, S1), BinEncoded = iolist_to_binary(Encoded), {ok, Decoded, S3} = try_decode_packet(BinEncoded, S2), ?assertEqual(B, Decoded, {BinEncoded, S2, S3}), S3 end, S, Packets). codec_stream_test() -> S = new(), Packets = [ binary:copy(<<0>>, 4), %non-padded binary:copy(<<1>>, 8), %padded binary:copy(<<2>>, 4), %non-padded binary:copy(<<2>>, 100) %padded ], {Encoded, SS} = lists:foldl( fun(B, {Enc1, S1}) -> {Enc2, S2} = encode_packet(B, S1), {[Enc1 | Enc2], S2} end, {[], S}, Packets), lists:foldl( fun(B, {Enc, S1}) -> {ok, Dec, S2} = try_decode_packet(Enc, S1), ?assertEqual(B, Dec), {<<>>, S2} end, {iolist_to_binary(Encoded), SS}, Packets). -endif.