commit bfc020332e9bc540816631e02c6dd3ea15a2efd5 Author: niamtokik Date: Wed May 10 21:11:14 2017 +0200 Initial working commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87be8c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.rebar3 +_* +.eunit +*.o +*.beam +*.plt +*.swp +*.swo +.erlang.cookie +ebin +log +erl_crash.dump +.rebar +logs +_build +.idea +rebar3.crashdump +*~ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..60d4636 --- /dev/null +++ b/LICENSE @@ -0,0 +1,35 @@ +Copyright (c) 2017, Mathieu Kerjouan +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +3. All advertising materials mentioning features or use of this + software must display the following acknowledgement: This + product includes software developed by the . + +4. Neither the name of the nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba4f518 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +#bencode + +Bencode Erlang Library Implementation + +## Build + + $ rebar3 compile + +## Usage + +You can encode Erlang terms to Bencoded string: + + > {ok, E} = bencode:encode([1, 2, 3, mystring]). + {ok,<<"li1ei2ei3e8:mystringe">>} + > E. + <<"li1ei2ei3e8:mystringe">> + +You can also decode Bencoded string to Erlang terms: + + > {ok, D} = bencode_decode(B). + {ok,[1,2,3,<<"mystring">>]} + > D. + [1,2,3,<<"mystring">>] + +If you want to parse torrent file: + + > {ok, Torrent} = file:read_file("/path/to/my.torrent"). + > {ok, Decoded} = bencode:decode(Torrent). + > Decoded. + +## Todo + + - Decoding options + - Encoding options + - Benchmarking + - More documentation + - More Test Unit + - More Common Test + - Find better way to order keys in dictionary + +## References + + - https://wiki.theory.org/BitTorrentSpecification#Bencoding + - http://fileformats.wikia.com/wiki/Torrent_file + - https://github.com/jlouis/benc + - https://en.wikipedia.org/wiki/Bencode diff --git a/rebar.config b/rebar.config new file mode 100644 index 0000000..2656fd5 --- /dev/null +++ b/rebar.config @@ -0,0 +1,2 @@ +{erl_opts, [debug_info]}. +{deps, []}. diff --git a/src/bencode.app.src b/src/bencode.app.src new file mode 100644 index 0000000..89f328c --- /dev/null +++ b/src/bencode.app.src @@ -0,0 +1,15 @@ +{application, bencode, + [{description, "Bencode Erlang Library"}, + {vsn, "0.1.0"}, + {registered, []}, + {applications, + [kernel, + stdlib + ]}, + {env,[]}, + {modules, []}, + + {maintainers, ["Mathieu Kerjouan "]}, + {licenses, ["BSD-4"]}, + {links, ["https://github.com/niamtokik/bencode"]} + ]}. diff --git a/src/bencode.erl b/src/bencode.erl new file mode 100644 index 0000000..70a15a1 --- /dev/null +++ b/src/bencode.erl @@ -0,0 +1,155 @@ +%%%------------------------------------------------------------------- +%%% Copyright (c) 2017, Mathieu Kerjouan +%%% All rights reserved. +%%% +%%% Redistribution and use in source and binary forms, with or without +%%% modification, are permitted provided that the following conditions +%%% are met: +%%% +%%% 1. Redistributions of source code must retain the above copyright +%%% notice, this list of conditions and the following disclaimer. +%%% +%%% 2. Redistributions in binary form must reproduce the above +%%% copyright notice, this list of conditions and the following +%%% disclaimer in the documentation and/or other materials provided +%%% with the distribution. +%%% +%%% 3. All advertising materials mentioning features or use of this +%%% software must display the following acknowledgement: This +%%% product includes software developed by the . +%%% +%%% 4. Neither the name of the nor the names of its +%%% contributors may be used to endorse or promote products derived +%%% from this software without specific prior written permission. +%%% +%%% THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY +%%% EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +%%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +%%% PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE +%%% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +%%% OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +%%% PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +%%% PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +%%% THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +%%% TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +%%% THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +%%% SUCH DAMAGE. +%%% +%%% ------------------------------------------------------------------ +%%% +%%% Bencoding FSM schema: +%%% +%%% _____________________ ________________________ +%%% | | +------>| | +%%% | integer: | | | string: | +%%% | <<$i,Char,$e>> | | | <> | +%%% | where integer(Char) | | | where integer(Char) | +%%% |_____________________|<------+ | bitstring(Bytes) | +%%% /_\ | | |________________________| +%%% | | | /_\ +%%% | | | | +%%% | | | | +%%% ___|_________________ | | _______________|________ +%%% | |--+ +--| | +%%% | list: | | dictionary: | +%%% | <<$l,Content,$e>> |--------->| <<$d,Content,$e>> | +%%% |_____________________|<---------|________________________| +%%% +%%% ------------------------------------------------------------------ +%%% +%%% @author Mathieu Kerjouan +%%% @copyright (c) 2017, Mathieu Kerjouan +%%% @doc +%%% @end +%%% +%%%------------------------------------------------------------------- + +-module(bencode). +-export([encode/1, encode/2, encode_options/0]). +-export([decode/1, decode/2, decode_options/0]). + +-include_lib("eunit/include/eunit.hrl"). + +%%-------------------------------------------------------------------- +%% +%%-------------------------------------------------------------------- +-spec decode_options() -> list(). +decode_options() -> + [integer_as_string + ,integer_as_bitstring + ,string_as_list + ,dictionary_as_proplists + ,dictionary_as_map]. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%% @param bencoded string as bitstring or binary term +%% @returns erlang term based on bencode schema. +%%-------------------------------------------------------------------- +-spec decode(bitstring() | list()) + -> list() | map() | integer() | bitstring(). +decode(Bitstring) -> + decode(Bitstring, []). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%% @param bencoded string as bitstring or binary term +%% @param options list +%% @returns erlang term based on bencode schema. +%%-------------------------------------------------------------------- +-spec decode(bitstring() | list(), list()) + -> list() | map() | integer() | bitstring(). +decode(List, Opts) + when is_list(List) -> + decode(erlang:list_to_bitstring(List), Opts); +decode(Bitstring, _) -> + bencode_decode:switch(Bitstring). + +%%-------------------------------------------------------------------- +%% +%%-------------------------------------------------------------------- +-spec encode_options() -> list(). +encode_options() -> + [integer_as_string + ,integer_as_bitstring + ,string_as_list + ,dictionary_as_proplists + ,dictionary_as_map]. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +encode_test() -> + ?assertEqual(encode([1,2,3,[1,2,3]]), + {ok, <<"li1ei2ei3eli1ei2ei3eee">>}), + ?assertEqual(encode([#{a => 1}, 1, 23]), + {ok, <<"ld1:ai1eei1ei23ee">>}). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec encode(integer() | bitstring() | list() | map()) + -> {ok, bitstring()}. +encode(Data) + when is_integer(Data) -> + bencode_encode:integer(Data); +encode(Data) + when is_bitstring(Data) -> + bencode_encode:string(Data); +encode(Data) + when is_list(Data) -> + bencode_encode:list(Data); +encode(Data) + when is_map(Data) -> + bencode_encode:dictionary(Data). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +encode(_,_) -> + ok. diff --git a/src/bencode_decode.erl b/src/bencode_decode.erl new file mode 100644 index 0000000..faefb11 --- /dev/null +++ b/src/bencode_decode.erl @@ -0,0 +1,370 @@ +%%%------------------------------------------------------------------- +%%% Copyright (c) 2017, Mathieu Kerjouan +%%% All rights reserved. +%%% +%%% Redistribution and use in source and binary forms, with or without +%%% modification, are permitted provided that the following conditions +%%% are met: +%%% +%%% 1. Redistributions of source code must retain the above copyright +%%% notice, this list of conditions and the following disclaimer. +%%% +%%% 2. Redistributions in binary form must reproduce the above +%%% copyright notice, this list of conditions and the following +%%% disclaimer in the documentation and/or other materials provided +%%% with the distribution. +%%% +%%% 3. All advertising materials mentioning features or use of this +%%% software must display the following acknowledgement: This +%%% product includes software developed by the . +%%% +%%% 4. Neither the name of the nor the names of its +%%% contributors may be used to endorse or promote products derived +%%% from this software without specific prior written permission. +%%% +%%% THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY +%%% EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +%%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +%%% PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE +%%% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +%%% OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +%%% PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +%%% PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +%%% THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +%%% TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +%%% THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +%%% SUCH DAMAGE. +%%% +%%% ------------------------------------------------------------------ +%%% +%%% @author Mathieu Kerjouan +%%% @copyright (c) 2017, Mathieu Kerjouan +%%% @doc +%%% @end +%%% +%%%------------------------------------------------------------------- + +-module(bencode_decode). +-export([integer/1]). +-export([string/1]). +-export([list/1]). +-export([dictionary/1]). +-export([switch/1]). + +-include_lib("eunit/include/eunit.hrl"). + +%%-------------------------------------------------------------------- +%% @doc switch/1 helper function will route bitstring parameter +%% based on pattern matching. +%% @end +%% @param bencoded string as bitstring. +%% @return erlang term +%%-------------------------------------------------------------------- +switch(Bitstring) -> + case Bitstring of + <<"i", _/bitstring>> -> + switch_integer(Bitstring); + <<"l", _/bitstring>> -> + switch_list(Bitstring); + <<"d", _/bitstring>> -> + switch_dictionary(Bitstring); + <> + when Char >= $0 andalso Char =< $9 -> + switch_string(Bitstring); + _Else -> + {error, not_bencoded} + end. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%% @param +%%-------------------------------------------------------------------- +switch_integer(Bitstring) -> + integer(Bitstring). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%% @param +%%-------------------------------------------------------------------- +switch_list(Bitstring) -> + list(Bitstring). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +switch_dictionary(Bitstring) -> + dictionary(Bitstring). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +switch_string(Bitstring) -> + string(Bitstring). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +integer_test() -> + ?assertEqual(integer(<<"test">>), {not_integer, <<"test">>}), + ?assertEqual(integer(<<"ie">>), {error, invalid}), + ?assertEqual(integer(<<"i0e">>), {ok, 0}), + ?assertEqual(integer(<<"i0etest">>), {ok, 0, <<"test">>}), + ?assertEqual(integer(<<"i-0e">>), {error, invalid}), + ?assertEqual(integer(<<"i-00001e">>), {error, invalid}), + ?assertEqual(integer(<<"i000001e">>), {error, invalid}), + ?assertEqual(integer(<<"i1e">>), {ok, 1}), + ?assertEqual(integer(<<"i3e">>), {ok, 3}), + ?assertEqual(integer(<<"i-3e">>), {ok, -3}), + ?assertEqual(integer(<<"i03e">>), {error, invalid}), + ?assertEqual(integer(<<"i1etest">>), {ok, 1, <<"test">>}), + % 64bit value + ?assertEqual(integer(<<"i18446744073709551616e">>), + {ok, 18446744073709551616}), + % 64bit negative value + ?assertEqual(integer(<<"i-18446744073709551616e">>), + {ok, (-18446744073709551616)}), + % 128bit value + ?assertEqual(integer(<<"i340282366920938463463374607431768211456e">>), + {ok, 340282366920938463463374607431768211456}), + % 128bit negative value + ?assertEqual(integer(<<"i-340282366920938463463374607431768211456e">>), + {ok, (-340282366920938463463374607431768211456)}). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%% @param bencoded string as bitstring or binary. +%% @returns tuple with atom() tag and erlang integer term. +%%-------------------------------------------------------------------- +-spec integer(bitstring() | list()) + -> {ok, integer()} | + {ok, integer(), bitstring()} | + {error, term()}. +integer(List) when is_list(List) -> + integer(erlang:list_to_bitstring(List)); +integer(<<"ie">>) -> + {error, invalid}; +integer(<<"i0e">>) -> + {ok, 0}; +integer(<<"i0e", Rest/bitstring>>) -> + {ok, 0, Rest}; +integer(<<$i, Rest/bitstring>>) -> + integer(Rest, <<>>); +integer(_Else) + when is_bitstring(_Else) -> + {not_integer, _Else}. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%% @param +%% @returns +%%-------------------------------------------------------------------- +-spec integer(bitstring(), bitstring()) + -> {ok, integer()} | + {ok, integer(), bitstring()} | + {error, term()}. +integer(<<$0, _/bitstring>>, <<>>) -> + {error, invalid}; +integer(<<$-, $0, _/bitstring>>, <<>>) -> + {error, invalid}; +integer(<>, <<>>) + when (Number >= $1 andalso Number =< $9) orelse Number =:= $- -> + integer(Rest, <>); +integer(<>, Buf) + when Number >= $0 andalso Number =< $9 -> + integer(Rest, <>); +integer(<<$e>>, Buf) -> + {ok, erlang:binary_to_integer(Buf)}; +integer(<<$e, Rest/bitstring>>, Buf) -> + {ok, erlang:binary_to_integer(Buf), Rest}; +integer(_, _) -> + {error, invalid}. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +string_test() -> + ?assertEqual(string(<<"0:">>), {ok, <<"">>}), + ?assertEqual(string(<<"1:a">>), {ok, <<"a">>}), + ?assertEqual(string(<<"2:be">>), {ok, <<"be">>}), + ?assertEqual(string(<<"4:test">>), {ok, <<"test">>}), + ?assertEqual(string(<<"5:">>), {error, bad_length}), + ?assertEqual(string(<<"6:jijoja4:test">>), + {ok, <<"jijoja">>, <<"4:test">>}). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec string(bitstring() | list()) + -> {ok, bitstring()} | + {ok, bitstring(), bitstring()} | + {error, bad_length} | + {not_string, bitstring()}. + +string(List) + when is_list(List) -> + string(erlang:list_to_bitstring(List)); +string(<<"0:">>) -> + {ok, <<"">>}; +string(<>) + when Number >= $1 andalso Number =< $9 -> + string(Rest, <>); +string(_Else) -> + {not_string, _Else}. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec string(bitstring(), bitstring()) + -> {ok, bitstring()} | + {ok, bitstring(), bitstring()} | + {error, bad_length}. + +string(<<$:, Rest/bitstring>>, Length) -> + string(Rest, erlang:binary_to_integer(Length), <<>>); +string(<>, Length) + when Number >= $0 andalso Number =< $9 -> + string(Rest, <>). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec string(bitstring(), non_neg_integer(), bitstring()) + ->{ok, bitstring()} | + {ok, bitstring(), bitstring()} | + {error, bad_length}. + +string(<<>>, 0, String) -> + {ok, String}; +string(<<>>, _, _) -> + {error, bad_length}; +string(Rest, 0, String) -> + {ok, String, Rest}; +string(<>, Length, String) -> + string(Rest, Length-1, <>). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +list_test() -> + ?assertEqual(list(<<"l4:spam4:eggse">>), + {ok, [<<"spam">>, <<"eggs">>]}), + ?assertEqual(list(<<"le">>), + {ok, []}), + ?assertEqual(list(<<"ldee">>), {ok, [#{}]}), + ?assertEqual(list(<<"llleee">>), {ok, [[[]]]}). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec list(bitstring()) + -> {ok, list()} | + {ok, list(), bitstring()} | + {error, no_ending_delimiter} | + {not_list, bitstring()}. + +list(<<$l, Rest/bitstring>>) -> + list(Rest, []); +list(_Else) -> + {not_list, _Else}. + +-spec list(bitstring(), list()) + -> {ok, list()} | + {ok, list(), bitstring()} | + {error, term()}. +list(<<$e>>, List) -> + {ok, lists:reverse(List)}; +list(<<$e, Rest/bitstring>>, List) -> + {ok, lists:reverse(List), Rest}; +list(Bitstring, Buf) -> + case switch(Bitstring) of + {ok, _} -> + {error, no_ending_delimiter}; + {ok, Data, Rest} -> + list(Rest, [Data] ++ Buf) + end. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +dictionary_test() -> + ?assertEqual(dictionary(<<"de">>), {ok, #{}}). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec dictionary(bitstring()) + -> {ok, map()} | + {ok, map(), bitstring()} | + {error, no_value} | + {error, no_ending_delimiter} | + {not_dictionary, bitstring()}. + +dictionary(<<"de">>) -> + {ok, #{}}; +dictionary(<<"de", Rest/bitstring>>) -> + {ok, #{}, Rest}; +dictionary(<<$d, Rest/bitstring>>) -> + dictionary(Rest, #{}, {}); +dictionary(_Else) -> + {not_dictionary, _Else}. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec dictionary(bitstring(), map()) + -> {ok, map()} | + {ok, map(), bitstring()} | + {error, term()}. +dictionary(<<$e>>, Dict) -> + {ok, Dict}; +dictionary(<<$e, Rest/bitstring>>, Dict) -> + {ok, Dict, Rest}; +dictionary(Bitstring, Dict) -> + dictionary(Bitstring, Dict, {}). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec dictionary(bitstring(), map(), tuple()) + -> {ok, map()} | + {ok, map(), bitstring()} | + {error, term()}. +dictionary(<<>>, _, {_}) -> + {error, no_value}; +dictionary(<<>>, _, {_, _}) -> + {error, no_ending_delimiter}; +dictionary(<<$e, _/bitstring>>, _, {_}) -> + {error, no_value}; +dictionary(Bitstring, Dict, {}) -> + case switch(Bitstring) of + {ok, _} -> + {error, no_ending_delimiter}; + {ok, Key, Rest} -> + dictionary(Rest, Dict, {Key}) + end; +dictionary(Bitstring, Dict, {Key}) -> + case switch(Bitstring) of + {ok, _} -> + {error, no_ending_delimiter}; + {ok, Value, Rest} -> + dictionary(Rest, Dict, {Key, Value}) + end; +dictionary(Bitstring, Dict, {Key, Value}) -> + dictionary(Bitstring, maps:put(Key, Value, Dict)). diff --git a/src/bencode_encode.erl b/src/bencode_encode.erl new file mode 100644 index 0000000..529db22 --- /dev/null +++ b/src/bencode_encode.erl @@ -0,0 +1,289 @@ +%%%------------------------------------------------------------------- +%%% Copyright (c) 2017, Mathieu Kerjouan +%%% All rights reserved. +%%% +%%% Redistribution and use in source and binary forms, with or without +%%% modification, are permitted provided that the following conditions +%%% are met: +%%% +%%% 1. Redistributions of source code must retain the above copyright +%%% notice, this list of conditions and the following disclaimer. +%%% +%%% 2. Redistributions in binary form must reproduce the above +%%% copyright notice, this list of conditions and the following +%%% disclaimer in the documentation and/or other materials provided +%%% with the distribution. +%%% +%%% 3. All advertising materials mentioning features or use of this +%%% software must display the following acknowledgement: This +%%% product includes software developed by the . +%%% +%%% 4. Neither the name of the nor the names of its +%%% contributors may be used to endorse or promote products derived +%%% from this software without specific prior written permission. +%%% +%%% THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY +%%% EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +%%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +%%% PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE +%%% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +%%% OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +%%% PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +%%% PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +%%% THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +%%% TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +%%% THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +%%% SUCH DAMAGE. +%%% +%%% ------------------------------------------------------------------ +%%% +%%% @author Mathieu Kerjouan +%%% @copyright (c) 2017, Mathieu Kerjouan +%%% @doc +%%% @end +%%% +%%%------------------------------------------------------------------- + +-module(bencode_encode). +-export([integer/1]). +-export([string/1]). +-export([list/1]). +-export([dictionary/1]). + +-include_lib("eunit/include/eunit.hrl"). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +integer_test() -> + ?assertEqual(integer(1), {ok, <<"i1e">>}), + ?assertEqual(integer(-1), {ok, <<"i-1e">>}), + ?assertEqual(integer(0), {ok, <<"i0e">>}), + ?assertEqual(integer(1000), {ok, <<"i1000e">>}), + ?assertEqual(integer(-1000), {ok, <<"i-1000e">>}). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec integer(integer()) -> {ok, bitstring()}. +integer(Integer) + when is_integer(Integer) -> + Bitstring = erlang:integer_to_binary(Integer), + {ok, <<$i, Bitstring/bitstring, $e>>}. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +string_test() -> + ?assertEqual(string(<<"test">>), {ok, <<"4:test">>}), + ?assertEqual(string(<<"">>), {ok, <<"0:">>}), + ?assertEqual(string(a), {ok, <<"1:a">>}), + ?assertEqual(string(test), {ok, <<"4:test">>}), + ?assertEqual(string(<<"dumped">>), {ok, <<"6:dumped">>}). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec string(bitstring()) -> {ok, bitstring()}. +string(List) + when is_list(List) -> + string(erlang:list_to_bitstring(List)); +string(Atom) when is_atom(Atom) -> + string(erlang:atom_to_binary(Atom, utf8)); +string(<<>>) -> + {ok, <<"0:">>}; +string(Bitstring) -> + Length = erlang:byte_size(Bitstring), + LengthBitstring = erlang:integer_to_binary(Length), + {ok, <>}. + + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +list_test() -> + ?assertEqual(list([1,2,3]), + {ok, <<"li1ei2ei3ee">>}), + ?assertEqual(list([<<"a">>,<<"b">>,<<"c">>]), + {ok, <<"l1:a1:b1:ce">>}), + ?assertEqual(list([a,b,c]), + {ok, <<"l1:a1:b1:ce">>}), + ?assertEqual(list([#{}]), + {ok, <<"ldee">>}), + ?assertEqual(list([[]]), + {ok, <<"llee">>}), + ?assertEqual(list([1,2,3,[a,b,c]]), + {ok,<<"li1ei2ei3el1:a1:b1:cee">>}). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec list(list()) -> {ok, bitstring()}. +list([]) -> + {ok, <<"le">>}; +list(List) + when is_list(List) -> + list(List, <<>>). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec list(list(), bitstring()) -> {ok, bitstring()}. +list([], Buf) -> + {ok, <<$l, Buf/bitstring, $e>>}; +list([H|T], Buf) + when is_integer(H)-> + {ok, Integer} = integer(H), + list(T, <>); +list([H|T], Buf) + when is_atom(H) -> + {ok, String} = string(H), + list(T, <>); +list([H|T], Buf) + when is_list(H) -> + {ok, String} = list(H), + list(T, <>); +list([H|T], Buf) + when is_bitstring(H) -> + {ok, String} = string(H), + list(T, <>); +list([{Key, Value}|T], Buf) + when is_bitstring(Key) -> + {wip, proplist}; +list([H|T], Buf) + when is_map(H) -> + {ok, Dictionary} = dictionary(H), + list(T, <>). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +dictionary_test() -> + ?assertEqual(dictionary(#{<<"test">> => []}), + {ok, <<"d4:testlee">>}), + ?assertEqual(dictionary(#{a => #{ b => c }}), + {ok,<<"d1:ad1:b1:cee">>}), + ?assertEqual(dictionary(#{aaa => 3, bb => 2, c => 1 }), + {ok,<<"d1:ci1e2:bbi2e3:aaai3ee">>}). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec dictionary(map()) -> {ok, bitstring()}. +dictionary(Map) + when is_map(Map), map_size(Map) =:= 0 -> + {ok, <<"de">>}; +dictionary(Map) + when is_map(Map), map_size(Map) >= 1 -> + Keys = sort_map_keys(Map), + Enc = fun(K, M) -> + V = maps:get(K, M), + key_value(K, V) + end, + Dict = << (Enc(Key, Map)) || Key <- Keys >>, + {ok, <<$d, Dict/bitstring, $e>>}. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +sort_map_keys_test() -> + ?assertEqual(sort_map_keys(#{aaa => 3, bb => 2, c => 1}), + [c, bb, aaa]), + ?assertEqual(sort_map_keys(#{<<"aaa">> => 3, <<"bb">> => 2, <<"c">> => 1 }), + [<<"c">>,<<"bb">>,<<"aaa">>]). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec sort_map_keys(map()) -> list(). +sort_map_keys(Map) + when is_map(Map) -> + Keys = maps:keys(Map), + Fun = fun F(X) when is_atom(X) -> + erlang:atom_to_binary(X, utf8); + F(X) when is_list(X) -> + erlang:list_to_bitstring(X); + F(X) when is_integer(X) -> + erlang:integer_to_binary(X); + F(X) -> X + end, + Zip = [ {erlang:byte_size(Fun(X)), X} || X <- Keys ], + SortedZip = lists:sort(Zip), + {_, Sort} = lists:unzip(SortedZip), + Sort. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +key_test() -> + ?assertEqual(key(a), <<"1:a">>), + ?assertEqual(key(1), <<"1:1">>), + ?assertEqual(key(<<"a">>), <<"1:a">>). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec key(integer() | atom() | list() | bitstring()) + -> bitstring(). +key(Key) when is_integer(Key) -> + key(erlang:integer_to_binary(Key)); +key(Key) when is_list(Key) -> + key(erlang:list_to_bitstring(Key)); +key(Key) when is_atom(Key) -> + key(erlang:atom_to_binary(Key, utf8)); +key(Key) when is_bitstring(Key) -> + {ok, KeyBitstring} = string(Key), + KeyBitstring. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +value_test() -> + ok. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec value(integer() | atom() | list() | bitstring()) + -> bitstring(). +value(Value) when is_integer(Value) -> + {ok, ValueBitstring} = integer(Value), + ValueBitstring; +value(Value) when is_bitstring(Value) -> + {ok, ValueBitstring} = string(Value), + ValueBitstring; +value(Value) when is_atom(Value) -> + {ok, ValueBitstring} = string(Value), + ValueBitstring; +value(Value) when is_list(Value) -> + {ok, ValueBitstring} = list(Value), + ValueBitstring; +value(Value) when is_map(Value) -> + {ok, ValueBitstring} = dictionary(Value), + ValueBitstring. + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +-spec key_value(bitstring(), integer() | list() | bitstring() | map() ) + -> bitstring(). +key_value(Key, Value) -> + K = key(Key), + V = value(Value), + <>.