Initial working commit.

This commit is contained in:
2017-05-10 21:11:14 +02:00
commit bfc020332e
8 changed files with 930 additions and 0 deletions

18
.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
.rebar3
_*
.eunit
*.o
*.beam
*.plt
*.swp
*.swo
.erlang.cookie
ebin
log
erl_crash.dump
.rebar
logs
_build
.idea
rebar3.crashdump
*~

35
LICENSE Normal file
View File

@@ -0,0 +1,35 @@
Copyright (c) 2017, Mathieu Kerjouan <contact [at] steepath.eu>
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 <organization>.
4. Neither the name of the <organization> 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 <COPYRIGHT HOLDER> ''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 <COPYRIGHT HOLDER> 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.

46
README.md Normal file
View File

@@ -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

2
rebar.config Normal file
View File

@@ -0,0 +1,2 @@
{erl_opts, [debug_info]}.
{deps, []}.

15
src/bencode.app.src Normal file
View File

@@ -0,0 +1,15 @@
{application, bencode,
[{description, "Bencode Erlang Library"},
{vsn, "0.1.0"},
{registered, []},
{applications,
[kernel,
stdlib
]},
{env,[]},
{modules, []},
{maintainers, ["Mathieu Kerjouan <contact [at] steepath.eu>"]},
{licenses, ["BSD-4"]},
{links, ["https://github.com/niamtokik/bencode"]}
]}.

155
src/bencode.erl Normal file
View File

@@ -0,0 +1,155 @@
%%%-------------------------------------------------------------------
%%% Copyright (c) 2017, Mathieu Kerjouan <contact [at] steepath.eu>
%%% 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 <organization>.
%%%
%%% 4. Neither the name of the <organization> 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 <COPYRIGHT HOLDER> ''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 <COPYRIGHT HOLDER> 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>> | | | <<Char,$:,Bytes>> |
%%% | where integer(Char) | | | where integer(Char) |
%%% |_____________________|<------+ | bitstring(Bytes) |
%%% /_\ | | |________________________|
%%% | | | /_\
%%% | | | |
%%% | | | |
%%% ___|_________________ | | _______________|________
%%% | |--+ +--| |
%%% | list: | | dictionary: |
%%% | <<$l,Content,$e>> |--------->| <<$d,Content,$e>> |
%%% |_____________________|<---------|________________________|
%%%
%%% ------------------------------------------------------------------
%%%
%%% @author Mathieu Kerjouan
%%% @copyright (c) 2017, Mathieu Kerjouan <contact [at] steepath.eu>
%%% @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.

370
src/bencode_decode.erl Normal file
View File

@@ -0,0 +1,370 @@
%%%-------------------------------------------------------------------
%%% Copyright (c) 2017, Mathieu Kerjouan <contact [at] steepath.eu>
%%% 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 <organization>.
%%%
%%% 4. Neither the name of the <organization> 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 <COPYRIGHT HOLDER> ''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 <COPYRIGHT HOLDER> 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 <contact [at] steepath.eu>
%%% @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);
<<Char, _/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(<<Number, Rest/bitstring>>, <<>>)
when (Number >= $1 andalso Number =< $9) orelse Number =:= $- ->
integer(Rest, <<Number>>);
integer(<<Number, Rest/bitstring>>, Buf)
when Number >= $0 andalso Number =< $9 ->
integer(Rest, <<Buf/bitstring, Number>>);
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(<<Number, Rest/bitstring>>)
when Number >= $1 andalso Number =< $9 ->
string(Rest, <<Number>>);
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(<<Number, Rest/bitstring>>, Length)
when Number >= $0 andalso Number =< $9 ->
string(Rest, <<Length/bitstring, Number>>).
%%--------------------------------------------------------------------
%% @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(<<Char, Rest/bitstring>>, Length, String) ->
string(Rest, Length-1, <<String/bitstring, Char>>).
%%--------------------------------------------------------------------
%% @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)).

289
src/bencode_encode.erl Normal file
View File

@@ -0,0 +1,289 @@
%%%-------------------------------------------------------------------
%%% Copyright (c) 2017, Mathieu Kerjouan <contact [at] steepath.eu>
%%% 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 <organization>.
%%%
%%% 4. Neither the name of the <organization> 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 <COPYRIGHT HOLDER> ''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 <COPYRIGHT HOLDER> 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 <contact [at] steepath.eu>
%%% @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, <<LengthBitstring/bitstring,$:,Bitstring/bitstring>>}.
%%--------------------------------------------------------------------
%% @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, <<Buf/bitstring, Integer/bitstring>>);
list([H|T], Buf)
when is_atom(H) ->
{ok, String} = string(H),
list(T, <<Buf/bitstring, String/bitstring>>);
list([H|T], Buf)
when is_list(H) ->
{ok, String} = list(H),
list(T, <<Buf/bitstring, String/bitstring>>);
list([H|T], Buf)
when is_bitstring(H) ->
{ok, String} = string(H),
list(T, <<Buf/bitstring, String/bitstring>>);
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, <<Buf/bitstring, Dictionary/bitstring>>).
%%--------------------------------------------------------------------
%% @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),
<<K/bitstring, V/bitstring>>.