defmodule ExBencode do @moduledoc """ Documentation for ExBencode. """ def encode!(t) do case encode(t) do {:ok, b} -> b {:error, reason} -> raise reason end end def decode!(s) do case decode(s) do {:ok, t} -> t {:error, reason} -> raise reason end end @doc """ Decode the bencoded binary value. ## Examples Decoding integers iex> ExBencode.decode("i10e") {:ok, 10} iex> ExBencode.decode("i-10e") {:ok, -10} Doubles and scientific notation is **not** supported iex> ExBencode.decode("i4.2e") {:error, :invalid_integer} iex> ExBencode.decode("i1.5e7e") {:error, :invalid_integer} Decoding strings iex> ExBencode.decode("4:spam") {:ok, "spam"} iex> ExBencode.decode("4:too much spam") {:error, :unexpected_content, %{index: 6, unexpected: "much spam"}} Bytes are handled using the string type, with the preceding number representing the byte size, not the string length. iex> ExBencode.decode(<>) {:ok, <<1, 2, 3>>} iex> ExBencode.decode("7:hełło") {:ok, "hełło"} iex> ExBencode.decode("5:hełło") {:error, :unexpected_content, %{index: 7, unexpected: <<130, 111>>}} Decoding lists iex> ExBencode.decode("le") {:ok, []} iex> ExBencode.decode("l4:spam4:eggse") {:ok, ["spam", "eggs"]} Decoding Dictionaries iex> ExBencode.decode("de") {:ok, %{}} iex> ExBencode.decode("d3:cow3:mooe") {:ok, %{"cow" => "moo"}} iex> ExBencode.decode("d8:shoppingl4:eggs4:milkee") {:ok, %{"shopping" => ["eggs", "milk"]}} """ def decode(s) when is_binary(s) do case extract_next(s) do {:ok, body, ""} -> {:ok, body} # Fail if there's anything leftover after we parse {:ok, _, unexpected} -> { :error, :unexpected_content, %{ index: byte_size(s) - byte_size(unexpected), unexpected: unexpected } } {:error, msg} -> {:error, msg} {:error, msg, details} -> {:error, msg, details} end end defp extract_next(<<"i", _rest :: bits>> = s), do: extract_int(s) defp extract_next(<> = s) when i >= ?0 and i <= ?9 do with [len_bin | _] <- :binary.split(s, ":"), header_size <- byte_size(len_bin) + 1, str_and_rest <- after_n(s, header_size), {length, ""} <- Integer.parse(len_bin) do if byte_size(str_and_rest) < length do {:error, :invalid_string, %{expected_size: length, actual_size: byte_size(str_and_rest)}} else str = first_n(str_and_rest, length) rest = after_n(str_and_rest, length) if byte_size(str) != length do {:error, :invalid_string, %{expected_size: length, actual_size: byte_size(str)}} else {:ok, str, rest} end end else _ -> {:error, :invalid_string} end end defp extract_next(<<"l", rest :: bits>> = s) do extract_list_contents(rest) end defp extract_next(<<"d", tail :: bits>>) do with {:ok, contents, rest} <- extract_list_contents(tail) do mapcontents = contents |> Enum.chunk(2) |> Enum.map(fn [a, b] -> {a, b} end) |> Map.new {:ok, mapcontents, rest} else err -> err end end defp extract_next(_), do: {:error, :not_bencoded_form} defp extract_int(<<"i", rest :: bits>>) when byte_size(rest) > 1 do [intbin, afterint] = :binary.split(rest, "e", []) case Integer.parse(intbin) do {int, ""} -> {:ok, int, afterint} {_, _} -> {:error, :invalid_integer} end end defp extract_int(_) do {:error, :invalid_integer} end defp first_n(subject, n) when byte_size(subject) < n do :error end defp first_n(subject, n) do :binary.part(subject, 0, n) end defp after_n(subject, n) when byte_size(subject) < n do :error end defp after_n(subject, n) do :binary.part(subject, n, byte_size(subject)-n) end defp extract_list_contents(<>) do extract_list_contents({:ok, [], b}) end defp extract_list_contents({:ok, list, <>}) do {:ok, Enum.reverse(list), rest} end defp extract_list_contents({:ok, list, rest}) do with {:ok, next, rest} <- extract_next(rest) do extract_list_contents({:ok, [next|list], rest}) else err -> err end end defprotocol Bencode do @fallback_to_any true @doc "Encode an erlang term." def encode(term) end defimpl Bencode, for: Integer do def encode(term) do ["i", Integer.to_string(term), "e"] end end defimpl Bencode, for: BitString do def encode(term) do len = Integer.to_string byte_size(term) [len, ":", term] end end defimpl Bencode, for: List do def encode(term) do ["l", encode_contents(term), "e"] end defp encode_contents(term) when is_list(term) do Enum.map(term, &Bencode.encode/1) end end defimpl Bencode, for: Map do def encode(term) do ["d", encode_contents(term), "e"] end defp encode_contents(term) when is_map(term) do term |> Map.to_list |> List.keysort(0) |> Enum.map(&Tuple.to_list/1) |> Enum.map(&encode_contents/1) end defp encode_contents(term) when is_list(term) do Enum.map(term, &Bencode.encode/1) end end defimpl Bencode, for: Tuple do def encode(term) do term |> Tuple.to_list() |> Bencode.encode() end end defimpl Bencode, for: Any do def encode(term) do term |> to_string() |> Bencode.encode() end end @doc """ Encode an erlang term. ## Examples iex> ExBencode.encode(1) {:ok, "i1e"} iex> ExBencode.encode("hi!") {:ok, "3:hi!"} iex> ExBencode.encode([]) {:ok, "le"} iex> ExBencode.encode([1]) {:ok, "li1ee"} iex> ExBencode.encode(%{}) {:ok, "de"} iex> ExBencode.encode(%{"cow" => "moo"}) {:ok, "d3:cow3:mooe"} Note that a keyword list counts as a list of lists, so convert keyword lists to maps before encoding. Otherwise, an empty keyword list could either be encoded as an empty list or an empty dict, and the library avoids making that kind of arbitrary decision. iex> ExBencode.encode([cow: "moo"]) {:ok, "ll3:cow3:mooee"} Use `Enum.into/2` to convert a keyword list into a map iex> Enum.into [cow: "moo"], %{} %{cow: "moo"} iex> ExBencode.encode(%{cow: "moo"}) {:ok, "d3:cow3:mooe"} """ def encode(term) do {:ok, Bencode.encode(term) |> :erlang.iolist_to_binary()} end end