%%%------------------------------------------------------------------- %%% @author Ralf Thomas Pietsch %%% @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 %%% BEP 3. %%% %%% Bencoding supports four data types: %%% %%% %%% 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 "). %% 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: %%% -spec encode(bencodable()) -> encode_result(). encode(<>) -> Len = integer_to_binary(byte_size(String)), {ok, <>}; 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: %%%
    %%%
  • `{error, negative_zero}' — for `<<"i-0e">>'
  • %%%
  • `{error, leading_zero}' — for `<<"i03e">>', `<<"i00e">>', `<<"i-03e">>' etc.
  • %%%
-spec decode(binary()) -> decode_result(). decode(<>) 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, <>) 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}, <>) 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, <>) -> {ok, Element, NextRest} = decode(Rest), decode_list([Element | List], NextRest). decode_dictionary(Dictionary, <<$e, Rest/binary>>) -> {ok, Dictionary, Rest}; decode_dictionary(Dictionary, <>) -> {ok, Key, NextRest} = decode(Rest), {ok, Value, NextNextRest} = decode(NextRest), decode_dictionary(maps:put(Key, Value, Dictionary), NextNextRest). decode_string({len, L}, <>) when N >= $0 andalso N =< $9 -> decode_string({len, (L * 10 + (N - $0))}, Rest); decode_string({len, L}, <<$:, Rest/binary>>) -> <> = Rest, {ok, Text, Remaining}.