Current section
Files
Jump to
Current section
Files
src/bencoding.erl
%%%-------------------------------------------------------------------
%%% @author Ralf Thomas Pietsch <ratopi@abwesend.de>
%%% @copyright (C) 2016-2026, Ralf Th. Pietsch
%%% @doc Encoder and decoder for the Bencoding data format used by BitTorrent.
%%%
%%% Implements the Bencoding serialization format as specified in
%%% <a href="https://www.bittorrent.org/beps/bep_0003.html">BEP 3</a>.
%%%
%%% Bencoding supports four data types:
%%% <ul>
%%% <li><b>Integers</b> — mapped to Erlang `integer()'</li>
%%% <li><b>Byte Strings</b> — mapped to Erlang `binary()'</li>
%%% <li><b>Lists</b> — mapped to Erlang `list()'</li>
%%% <li><b>Dictionaries</b> — mapped to Erlang `map()' with binary keys</li>
%%% </ul>
%%%
%%% Dictionary keys are encoded in sorted order (sorted as raw strings,
%%% not alphanumerics) as required by BEP 3. Encoding and decoding are
%%% roundtrip-safe.
%%%
%%% == Examples ==
%%%
%%% Encoding:
%%% ```
%%% > bencoding:encode(<<"spam">>).
%%% {ok, <<"4:spam">>}
%%%
%%% > bencoding:encode(42).
%%% {ok, <<"i42e">>}
%%%
%%% > bencoding:encode([<<"spam">>, <<"eggs">>]).
%%% {ok, <<"l4:spam4:eggse">>}
%%%
%%% > bencoding:encode(#{<<"cow">> => <<"moo">>}).
%%% {ok, <<"d3:cow3:mooe">>}
%%% '''
%%%
%%% Decoding:
%%% ```
%%% > bencoding:decode(<<"4:spam">>).
%%% {ok, <<"spam">>, <<>>}
%%%
%%% > bencoding:decode(<<"i42e">>).
%%% {ok, 42, <<>>}
%%%
%%% > bencoding:decode(<<"l4:spam4:eggse">>).
%%% {ok, [<<"spam">>, <<"eggs">>], <<>>}
%%%
%%% > bencoding:decode(<<"d3:cow3:mooe">>).
%%% {ok, #{<<"cow">> => <<"moo">>}, <<>>}
%%% '''
%%%
%%% Invalid integers are rejected:
%%% ```
%%% > bencoding:decode(<<"i-0e">>).
%%% {error, negative_zero}
%%%
%%% > bencoding:decode(<<"i03e">>).
%%% {error, leading_zero}
%%% '''
%%%
%%% @end
%%% Created : 2016-12-29 17:43
%%%-------------------------------------------------------------------
-module(bencoding).
-author("Ralf Th. Pietsch <ratopi@abwesend.de>").
%% API
-export([encode/1, decode/1]).
-type bencodable() :: binary() | integer() | list(bencodable()) | #{binary() => bencodable()}.
-type encode_result() :: {ok, binary()}.
-type decode_result() :: {ok, bencodable(), binary()} | {error, negative_zero | leading_zero}.
-export_type([bencodable/0, encode_result/0, decode_result/0]).
%%%===================================================================
%%% API
%%%===================================================================
%%% @doc Encode an Erlang term into a bencoded binary.
%%%
%%% Supported input types:
%%% <ul>
%%% <li>`binary()' — encoded as bencoded byte string</li>
%%% <li>`integer()' — encoded as bencoded integer</li>
%%% <li>`list()' — encoded as bencoded list</li>
%%% <li>`map()' — encoded as bencoded dictionary (keys sorted as raw strings)</li>
%%% </ul>
-spec encode(bencodable()) -> encode_result().
encode(<<String/binary>>) ->
Len = integer_to_binary(byte_size(String)),
{ok, <<Len/binary, $:, String/binary>>};
encode(N) when is_integer(N) ->
Bin = integer_to_binary(N),
{ok, <<$i, Bin/binary, $e>>};
encode(L) when is_list(L) ->
encode_list(start, L);
encode(M) when is_map(M) ->
encode_dictionary(start, M).
%%% @doc Decode a bencoded binary into an Erlang term.
%%%
%%% Returns `{ok, Value, Rest}' where `Value' is the decoded term and
%%% `Rest' is the remaining binary data that was not consumed.
%%%
%%% Returns `{error, Reason}' for invalid integers:
%%% <ul>
%%% <li>`{error, negative_zero}' — for `<<"i-0e">>'</li>
%%% <li>`{error, leading_zero}' — for `<<"i03e">>', `<<"i00e">>', `<<"i-03e">>' etc.</li>
%%% </ul>
-spec decode(binary()) -> decode_result().
decode(<<L, Rest/binary>>) when L >= $0, L =< $9 ->
decode_string({len, L - $0}, Rest);
decode(<<$i, Rest/binary>>) ->
decode_int(start, Rest);
decode(<<$l, Rest/binary>>) ->
decode_list([], Rest);
decode(<<$d, Rest/binary>>) ->
decode_dictionary(#{}, Rest).
%%%===================================================================
%%% Internal functions
%%%===================================================================
encode_list(start, L) ->
encode_list([$l], L);
encode_list(Output, []) ->
{ok, list_to_binary([Output, $e])};
encode_list(Output, [H | T]) ->
{ok, Element} = encode(H),
encode_list([Output, Element], T).
encode_dictionary(start, M) ->
encode_dictionary({[$d], lists:sort(maps:keys(M))}, M);
encode_dictionary({Output, []}, _) ->
{ok, list_to_binary([Output, $e])};
encode_dictionary({Output, [Key | Keys]}, Map) ->
{ok, EncodedKey} = encode(Key),
{ok, EncodedValue} = encode(maps:get(Key, Map)),
encode_dictionary(
{
[Output, EncodedKey, EncodedValue],
Keys
},
Map
).
%%
%%
%%
%% start state: first character after 'i'
decode_int(start, <<$0, $e, Rest/binary>>) ->
{ok, 0, Rest};
decode_int(start, <<$0, _Rest/binary>>) ->
{error, leading_zero};
decode_int(start, <<$-, $0, $e, _Rest/binary>>) ->
{error, negative_zero};
decode_int(start, <<$-, $0, _Rest/binary>>) ->
{error, leading_zero};
decode_int(start, <<$-, Rest/binary>>) ->
decode_int({negative, 0}, Rest);
decode_int(start, <<L, Rest/binary>>) when L >= $1, L =< $9 ->
decode_int({positive, L - $0}, Rest);
%% accumulating digits
decode_int({negative, N}, <<$e, Rest/binary>>) ->
{ok, -N, Rest};
decode_int({positive, N}, <<$e, Rest/binary>>) ->
{ok, N, Rest};
decode_int({PosNeg, N}, <<L, Rest/binary>>) when L >= $0, L =< $9 ->
decode_int({PosNeg, 10 * N + (L - $0)}, Rest).
decode_list(List, <<$e, Rest/binary>>) ->
{ok, lists:reverse(List), Rest};
decode_list(List, <<Rest/binary>>) ->
{ok, Element, NextRest} = decode(Rest),
decode_list([Element | List], NextRest).
decode_dictionary(Dictionary, <<$e, Rest/binary>>) ->
{ok, Dictionary, Rest};
decode_dictionary(Dictionary, <<Rest/binary>>) ->
{ok, Key, NextRest} = decode(Rest),
{ok, Value, NextNextRest} = decode(NextRest),
decode_dictionary(maps:put(Key, Value, Dictionary), NextNextRest).
decode_string({len, L}, <<N, Rest/binary>>) when N >= $0 andalso N =< $9 ->
decode_string({len, (L * 10 + (N - $0))}, Rest);
decode_string({len, L}, <<$:, Rest/binary>>) ->
<<Text:L/binary, Remaining/binary>> = Rest,
{ok, Text, Remaining}.