├── .eqc_ci ├── .gitignore ├── EQC_CI_LICENCE.txt ├── LICENSE ├── README.md ├── lib ├── bencode.ex └── bencode │ ├── decoder.ex │ ├── decoder │ ├── error.ex │ └── options.ex │ └── encoder.ex ├── mix.exs ├── mix.lock └── test ├── bencode ├── decode_eqc.exs └── encode_eqc.exs ├── bencode_eqc.exs ├── bencode_test.exs └── test_helper.exs /.eqc_ci: -------------------------------------------------------------------------------- 1 | {build, "MIX_ENV=test mix deps.get; MIX_ENV=test mix eqcCI"}. 2 | {test_path, "_build/test/lib/bencode/ebin"}. 3 | {deps, "_build/test/lib/eqc_ex/ebin"}. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | *.eqc-info 7 | *.eqc -------------------------------------------------------------------------------- /EQC_CI_LICENCE.txt: -------------------------------------------------------------------------------- 1 | This file is an agreement between Quviq AB ("Quviq"), Sven Hultins 2 | Gata 9, Gothenburg, Sweden, and the committers to the github 3 | repository in which the file appears ("the owner"). By placing this 4 | file in a github repository, the owner agrees to the terms below. 5 | 6 | The purpose of the agreement is to enable Quviq AB to provide a 7 | continuous integration service to the owner, whereby the code in the 8 | repository ("the source code") is tested using Quviq's test tools, and 9 | the test results are made available on the web. The test results 10 | include test output, generated test cases, and a copy of the source 11 | code in the repository annotated with coverage information ("the test 12 | results"). 13 | 14 | The owner agrees that Quviq may run the tests in the source code and 15 | display the test results on the web, without obligation. 16 | 17 | The owner warrants that running the tests in the source code and 18 | displaying the test results on the web violates no laws, licences or other 19 | agreements. In the event of such a violation, the owner accepts full 20 | responsibility. 21 | 22 | The owner warrants that the source code is not malicious, and will not 23 | mount an attack on either Quviq's server or any other server--for 24 | example by taking part in a denial of service attack, or by attempting 25 | to send unsolicited emails. 26 | 27 | The owner warrants that the source code does not attempt to reverse 28 | engineer Quviq's code. 29 | 30 | Quviq reserves the right to exclude repositories that break this 31 | agreement from its continuous integration service. 32 | 33 | Any dispute arising from the use of Quviq's service will be resolved 34 | under Swedish law. 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Martin Gausby 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bencode 2 | 3 | [![Hex.pm](https://img.shields.io/hexpm/l/bencode.svg "Apache 2.0 Licensed")](https://github.com/gausby/bencode/blob/master/LICENSE) 4 | [![Hex version](https://img.shields.io/hexpm/v/bencode.svg "Hex version")](https://hex.pm/packages/bencode) 5 | [QuickCheck CI Status](http://quickcheck-ci.com/p/gausby/bencode) 6 | 7 | A complete and correct Bencode encoder and decoder written in pure Elixir. 8 | 9 | The encoder is implemented as a protocol, allowing implementations for custom structs. 10 | 11 | The decoder should handle malformed data, either by raising an error, or returning an 2-tuple with status, both containing detailed information about the error. The decoder is also capable of calculating the hash of the info dictionary. 12 | 13 | ## API 14 | 15 | * `Bencode.encode/1` will encode a given data structure to the b-code representation and return a `{:ok, data}`-tuple on success, or an `{:error, reason}`-tuple if the data was invalid. 16 | 17 | * `Bencode.encode!/1` will encode a given data structure to the b-code representation; it will raise an error if the given input is invalid. 18 | 19 | * `Bencode.decode/1` will decode a b-code encoded string and return a 2-tuple; containing the status (`:ok`) and its Elixir data structure representation. Should the data be invalid a 2-tuple will get returned with `{:error, reason}` 20 | 21 | * `Bencode.decode!/1` will decode a b-code encoded string and return the decoded result as a Elixir data structure; if the input is invalid an it will raise with the reason. 22 | 23 | * `Bencode.decode_with_info_hash/1` will decode a b-code encoded string and return a 3-tuple; containing the status (`:ok`), its Elixir data structure representation along with the checksum of the info dictionary. If no info-dictionary was found the last value will be `nil`. `{:error, reason}` will get returned if the input data was invalid b-code. 24 | 25 | * `Bencode.decode_with_info_hash!/1` will do the same as the `Bencode.decode_with_info_hash/1` function, but will raise if the given input is malformed. 26 | 27 | ## Installation 28 | 29 | Bencode is [available in Hex](https://hex.pm/packages/bencode), and can be installed by adding it to the list of dependencies in `mix.exs`: 30 | 31 | ``` elixir 32 | def deps do 33 | [{:bencode, "~> 0.3.0"}] 34 | end 35 | ``` 36 | 37 | Notice that there are other bencode implementations on [hex](https://hex.pm/). Please check them out: 38 | 39 | * [bencodex](https://hex.pm/packages/bencodex) by [Patrick Gombert](https://github.com/patrickgombert/) 40 | 41 | * [bencoder](https://hex.pm/packages/bencoder) by [Alexander Ivanov](https://github.com/alehander42) 42 | 43 | * [elixir_bencode](https://hex.pm/packages/elixir_bencode) by [Anton Fagerberg](https://github.com/AntonFagerberg/) 44 | 45 | * [bento](https://hex.pm/packages/bento) by [Rodney Folz](https://github.com/folz/) 46 | 47 | Bento is the fastest option, but Bencode has arguably better error reporting during decoding. 48 | 49 | ## Thanks 50 | Thanks to the following for their contributions to the project: 51 | 52 | * [preciz](https://github.com/preciz) 53 | 54 | 55 | ## License 56 | 57 | Copyright 2016 Martin Gausby 58 | 59 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 60 | 61 | http://www.apache.org/licenses/LICENSE-2.0 62 | 63 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 64 | -------------------------------------------------------------------------------- /lib/bencode.ex: -------------------------------------------------------------------------------- 1 | defmodule Bencode do 2 | @type encodable :: binary | atom | Map | List | Integer 3 | 4 | @spec encode(encodable) :: {:ok, binary} | {:error, binary} 5 | def encode(data) do 6 | try do 7 | Bencode.Encoder.encode!(data) 8 | rescue 9 | e in Protocol.UndefinedError -> 10 | {:error, "protocol Bencode.Encoder is not implemented for #{inspect e.value}"} 11 | else 12 | result -> 13 | {:ok, result} 14 | end 15 | end 16 | 17 | defdelegate encode!(data), 18 | to: Bencode.Encoder 19 | 20 | defdelegate decode(data), 21 | to: Bencode.Decoder 22 | 23 | defdelegate decode!(data), 24 | to: Bencode.Decoder 25 | 26 | defdelegate decode_with_info_hash(data), 27 | to: Bencode.Decoder 28 | 29 | defdelegate decode_with_info_hash!(data), 30 | to: Bencode.Decoder 31 | end 32 | -------------------------------------------------------------------------------- /lib/bencode/decoder.ex: -------------------------------------------------------------------------------- 1 | defmodule Bencode.Decoder do 2 | @type encodable :: binary | atom | Map | List | Integer 3 | 4 | @moduledoc false 5 | alias Bencode.Decoder.Error 6 | alias Bencode.Decoder.Options 7 | 8 | @type t :: %Bencode.Decoder{ 9 | rest: binary, 10 | position: non_neg_integer, 11 | checksum: binary | nil, 12 | data: binary | nil, 13 | opts: Bencode.Decoder.Options.t 14 | } 15 | defstruct( 16 | rest: "", 17 | position: 0, 18 | checksum: nil, 19 | data: nil, 20 | opts: %Options{} 21 | ) 22 | alias Bencode.Decoder, as: State 23 | 24 | @spec decode(binary) :: {:ok, encodable} | {:error, binary} 25 | def decode(data) do 26 | case do_decode(%State{rest: data}) do 27 | %State{data: data, rest: ""} -> 28 | {:ok, data} 29 | 30 | %State{rest: <>, position: position} -> 31 | {:error, "unexpected character at #{position}, expected no more data, got: \"#{[char]}\""} 32 | 33 | {:error, _} = error -> 34 | error 35 | end 36 | end 37 | 38 | @spec decode!(binary) :: encodable | no_return 39 | def decode!(data) do 40 | case decode(data) do 41 | {:ok, result} -> 42 | result 43 | 44 | {:error, reason} -> 45 | raise Error, reason: reason, action: "decode data", data: data 46 | end 47 | end 48 | 49 | @spec decode_with_info_hash(binary) :: {:ok, encodable, binary | nil} | {:error, binary} 50 | def decode_with_info_hash(data) do 51 | case do_decode(%State{rest: data, opts: %Options{calculate_info_hash: true}}) do 52 | %State{data: data, rest: "", checksum: checksum} -> 53 | {:ok, data, checksum} 54 | 55 | %State{rest: <>, position: position} -> 56 | {:error, "unexpected character at #{position}, expected no more data, got: \"#{[char]}\""} 57 | 58 | {:error, _} = error -> 59 | error 60 | end 61 | end 62 | 63 | @spec decode_with_info_hash!(binary) :: {encodable, binary | nil} | no_return 64 | def decode_with_info_hash!(data) do 65 | case decode_with_info_hash(data) do 66 | {:ok, data, checksum} -> 67 | {data, checksum} 68 | {:error, reason} -> 69 | raise Error, reason: reason, action: "decode data", data: data 70 | end 71 | end 72 | 73 | # handle integers 74 | defp do_decode(%State{rest: <<"i", data::binary>>} = state) do 75 | %State{state|rest: data} 76 | |> advance_position 77 | |> decode_integer 78 | end 79 | # handle lists 80 | defp do_decode(%State{rest: <<"l", data::binary>>} = state) do 81 | %State{state|rest: data} 82 | |> advance_position 83 | |> decode_list 84 | end 85 | # handle dictionaries 86 | defp do_decode(%State{rest: <<"d", data::binary>>} = state) do 87 | %State{state|rest: data} 88 | |> advance_position 89 | |> decode_dictionary 90 | end 91 | # handle info dictionary, if present the checksum should get calculated 92 | # from the verbatim info data; not all benencoders are build the same 93 | defp do_decode(%State{rest: <<"4:infod", data::binary>>, opts: %{calculate_info_hash: true}} = state) do 94 | case get_raw_source_data("d" <> data) do 95 | {:ok, raw_info_directory} -> 96 | checksum = :crypto.hash(:sha, raw_info_directory) 97 | # continue parsing the string 98 | decode_string(%State{state|checksum: checksum}) 99 | 100 | {:error, _} -> 101 | # the data is faulty, but we still attempt to decode it to get the 102 | # exact reason for the failure using the regular parser 103 | decode_string(state) 104 | end 105 | end 106 | # handle strings 107 | defp do_decode(%State{rest: <>} = state) when first in ?0..?9 do 108 | decode_string(state) 109 | end 110 | defp do_decode(%State{rest: <>, position: position}) do 111 | {:error, "unexpected character at #{position}, expected a string; an integer; a list; or a dictionary, got: \"#{[char]}\""} 112 | end 113 | # handle empty strings 114 | defp do_decode(%State{rest: <<>>} = state), 115 | do: state 116 | 117 | #=integers ----------------------------------------------------------- 118 | defp decode_integer(state, acc \\ []) 119 | defp decode_integer(%State{rest: <<"e", rest::binary>>} = state, acc) when length(acc) > 0 do 120 | %State{state|rest: rest, data: prepare_integer(acc)} 121 | |> advance_position 122 | end 123 | defp decode_integer(%State{rest: <>} = state, acc) when current == ?- or current in ?0..?9 do 124 | %State{state|rest: rest} 125 | |> advance_position 126 | |> decode_integer([current|acc]) 127 | end 128 | # errors 129 | defp decode_integer(%State{rest: <<"e", _::binary>>} = state, []), 130 | do: {:error, "empty integer starting at #{state.position - 1}"} 131 | defp decode_integer(%State{rest: <>, position: position}, _), 132 | do: {:error, "unexpected character at #{position}, expected a number or an `e`, got: \"#{[char]}\""} 133 | 134 | #=strings ------------------------------------------------------------ 135 | defp decode_string(state, acc \\ []) 136 | defp decode_string(%State{rest: <<":", data::binary>>} = state, acc) do 137 | len = prepare_integer acc 138 | case data do 139 | <> -> 140 | %State{state|rest: rest,data: string} 141 | |> advance_position(1 + len) 142 | 143 | _ -> 144 | {:error, "expected a string of length #{len} at #{state.position + 1} but got out of bounds"} 145 | end 146 | end 147 | defp decode_string(%State{rest: <>} = state, acc) when number in ?0..?9 do 148 | %State{state|rest: rest} 149 | |> advance_position 150 | |> decode_string([number|acc]) 151 | end 152 | defp decode_string(%State{rest: <>, position: position}, _) do 153 | {:error, "unexpected character at #{position}, expected a number or an `:`, got: \"#{[char]}\""} 154 | end 155 | 156 | #=lists -------------------------------------------------------------- 157 | defp decode_list(state, acc \\ []) 158 | defp decode_list(%State{rest: <<"e", rest::binary>>} = state, acc) do 159 | %State{state|rest: rest, data: acc |> Enum.reverse} 160 | |> advance_position 161 | end 162 | defp decode_list(%State{rest: data} = state, acc) when data != "" do 163 | with %State{data: list_item} = new_state <- do_decode(state) do 164 | decode_list(new_state, [list_item|acc]) 165 | end 166 | end 167 | # errors 168 | defp decode_list(%State{rest: <<>>, position: position}, _) do 169 | {:error, "unexpected character at #{position}, expected data or an end character, got end of data"} 170 | end 171 | 172 | #=dictionaries ------------------------------------------------------- 173 | defp decode_dictionary(state, acc \\ %{}) 174 | defp decode_dictionary(%State{rest: <<"e", rest::binary>>} = state, acc) do 175 | %State{state|rest: rest, data: acc} 176 | |> advance_position 177 | end 178 | defp decode_dictionary(%State{rest: rest} = state, acc) when rest != "" do 179 | with %State{data: key} = state <- do_decode(state), 180 | %State{data: value} = state <- do_decode(state) do 181 | decode_dictionary(state, Map.put_new(acc, key, value)) 182 | end 183 | end 184 | # errors 185 | defp decode_dictionary(%State{rest: <<>>, position: position}, _) do 186 | {:error, "unexpected character at #{position}, expected data or an end character, got end of data"} 187 | end 188 | 189 | #=helpers ============================================================ 190 | defp prepare_integer(list) do 191 | list 192 | |> Enum.reverse 193 | |> List.to_integer 194 | end 195 | 196 | defp advance_position(%State{position: current} = state, increment \\ 1) do 197 | %State{state|position: current + increment} 198 | end 199 | 200 | defp get_raw_source_data(data) do 201 | with {:ok, _, len} <- do_scan(data, 0), 202 | <> <- data do 203 | {:ok, raw_source_data} 204 | end 205 | end 206 | 207 | #=scan =============================================================== 208 | defp do_scan(<<"i", rest::binary>>, offset), 209 | do: do_scan_integer(rest, offset + 1) 210 | defp do_scan(<<"l", rest::binary>>, offset), 211 | do: do_scan_list(rest, offset + 1) 212 | defp do_scan(<<"d", rest::binary>>, offset), 213 | do: do_scan_dictionary(rest, offset + 1) 214 | defp do_scan(<> = data, offset) when first in ?0..?9, 215 | do: do_scan_string(data, offset) 216 | defp do_scan(_, _), 217 | do: {:error, "faulty info dictionary"} 218 | 219 | # scan integers 220 | defp do_scan_integer(<<"e", rest::binary>>, offset), 221 | do: {:ok, rest, offset + 1} 222 | defp do_scan_integer(<>, offset) when number in ?0..?9, 223 | do: do_scan_integer(rest, offset + 1) 224 | defp do_scan_integer(_, _offset), 225 | do: {:error, "faulty info dictionary"} 226 | 227 | # scan strings 228 | defp do_scan_string(data, acc \\ [], offset) 229 | defp do_scan_string(<<":", data::binary>>, acc, offset) do 230 | len = prepare_integer(acc) 231 | case data do 232 | <<_::binary-size(len), rest::binary>> -> 233 | {:ok, rest, offset + len + 1} 234 | 235 | _ -> 236 | {:error, "faulty info dictionary"} 237 | end 238 | end 239 | defp do_scan_string(<>, acc, offset) when number in ?0..?9, 240 | do: do_scan_string(rest, [number|acc], offset + 1) 241 | defp do_scan_string(_, _acc, _offset), 242 | do: {:error, "faulty info dictionary"} 243 | 244 | # scan lists 245 | defp do_scan_list(<<"e", rest::binary>>, offset), 246 | do: {:ok, rest, offset + 1} 247 | defp do_scan_list(data, offset) when data != "" do 248 | with {:ok, rest, offset} <- do_scan(data, offset) do 249 | do_scan_list(rest, offset) 250 | end 251 | end 252 | defp do_scan_list(<<>>, _offset), 253 | do: {:error, "faulty info dictionary"} 254 | 255 | # scan dictionary 256 | defp do_scan_dictionary(<<"e", data::binary>>, offset), 257 | do: {:ok, data, offset + 1} 258 | defp do_scan_dictionary(data, offset) when data != "" do 259 | # first scan key, then the value 260 | with {:ok, rest, offset} <- do_scan(data, offset), 261 | {:ok, rest, offset} <- do_scan(rest, offset) do 262 | do_scan_dictionary(rest, offset) 263 | end 264 | end 265 | defp do_scan_dictionary(<<>>, _offset), 266 | do: {:error, "faulty info dictionary"} 267 | end 268 | -------------------------------------------------------------------------------- /lib/bencode/decoder/error.ex: -------------------------------------------------------------------------------- 1 | defmodule Bencode.Decoder.Error do 2 | @moduledoc false 3 | 4 | defexception( 5 | reason: nil, 6 | action: "", 7 | data: nil 8 | ) 9 | 10 | def message(exception) do 11 | "could not #{exception.action} #{exception.data}: #{exception.reason}" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/bencode/decoder/options.ex: -------------------------------------------------------------------------------- 1 | defmodule Bencode.Decoder.Options do 2 | @moduledoc """ 3 | A struct for defining option keys and default values for the 4 | bencode decoder. 5 | 6 | The following options are defined: 7 | 8 | * `calculate_info_hash` (Boolean), default: false 9 | The sha sum of the info dictionary will be returned along 10 | with the content of the bencode if this option is set to 11 | true. `nil` will be returned if no *info*-dictionary was 12 | found doing decoding. 13 | 14 | """ 15 | 16 | @opaque t :: %Bencode.Decoder.Options{ 17 | calculate_info_hash: boolean 18 | } 19 | defstruct( 20 | calculate_info_hash: false 21 | ) 22 | end 23 | -------------------------------------------------------------------------------- /lib/bencode/encoder.ex: -------------------------------------------------------------------------------- 1 | defprotocol Bencode.Encoder do 2 | @type encodable :: binary | atom | Map | List | Integer 3 | 4 | @spec encode!(encodable) :: binary | no_return 5 | def encode!(data) 6 | end 7 | 8 | defimpl Bencode.Encoder, for: Atom do 9 | def encode!(atom), 10 | do: Bencode.Encoder.encode!(to_string(atom)) 11 | end 12 | 13 | defimpl Bencode.Encoder, for: Integer do 14 | def encode!(number), 15 | do: "i#{number}e" 16 | end 17 | 18 | defimpl Bencode.Encoder, for: BitString do 19 | def encode!(string), 20 | do: "#{byte_size string}:#{string}" 21 | end 22 | 23 | defimpl Bencode.Encoder, for: List do 24 | def encode!(data), 25 | do: "l#{Enum.map_join(data, &Bencode.Encoder.encode!/1)}e" 26 | end 27 | 28 | defimpl Bencode.Encoder, for: Map do 29 | def encode!(data), 30 | do: "d#{Enum.map_join(data, &encode_pair/1)}e" 31 | 32 | defp encode_pair({key, value}) when is_bitstring(key), 33 | do: "#{Bencode.Encoder.encode! key}#{Bencode.Encoder.encode! value}" 34 | 35 | defp encode_pair({key, value}) when is_atom(key) do 36 | key_string = Atom.to_string key 37 | "#{Bencode.Encoder.encode! key_string}#{Bencode.Encoder.encode! value}" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Bencode.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :bencode, 6 | version: "0.3.2", 7 | elixir: "~> 1.2", 8 | test_pattern: "*_{test,eqc}.exs", 9 | build_embedded: Mix.env == :prod, 10 | start_permanent: Mix.env == :prod, 11 | description: description(), 12 | package: package(), 13 | deps: deps()] 14 | end 15 | 16 | def application do 17 | [applications: [:logger]] 18 | end 19 | 20 | defp description do 21 | """ 22 | A complete and correct Bencode encoder and decoder written in pure Elixir. 23 | 24 | The decoder will return the info hash with along with the decoded data, and 25 | the encoder is implemented as a protocol, allowing any data structure to be 26 | bcode encoded. 27 | """ 28 | end 29 | 30 | def package do 31 | [files: ["lib", "mix.exs", "README*", "LICENSE"], 32 | maintainers: ["Martin Gausby"], 33 | licenses: ["Apache 2.0"], 34 | links: %{"GitHub" => "https://github.com/gausby/bencode", 35 | "Issues" => "https://github.com/gausby/bencode/issues", 36 | "Contributors" => "https://github.com/gausby/bencode/graphs/contributors"}] 37 | end 38 | 39 | defp deps do 40 | [{:eqc_ex, "~> 1.3.0"}, 41 | {:credo, "~> 0.6.1", only: [:dev, :test]}] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []}, 2 | "credo": {:hex, :credo, "0.6.1", "a941e2591bd2bd2055dc92b810c174650b40b8290459c89a835af9d59ac4a5f8", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, optional: false]}]}, 3 | "eqc_ex": {:hex, :eqc_ex, "1.3.0", "04dc4481319d2e2ff1ce136507078cdbaf5a124d6a05ce32d5d642db28b053e4", [:mix], []}} 4 | -------------------------------------------------------------------------------- /test/bencode/decode_eqc.exs: -------------------------------------------------------------------------------- 1 | defmodule Bencode.DecodeEQC do 2 | use ExUnit.Case, async: true 3 | use EQC.ExUnit 4 | 5 | # Decode 6 | property "decode numbers" do 7 | forall input <- int() do 8 | encoded_input = "i#{input}e" 9 | {:ok, decoded_result} = Bencode.decode(encoded_input) 10 | ensure decoded_result == input 11 | end 12 | end 13 | 14 | property "decode strings" do 15 | forall input <- utf8() do 16 | encoded_input = "#{byte_size input}:#{input}" 17 | {:ok, decoded_result} = Bencode.decode(encoded_input) 18 | ensure decoded_result == input 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/bencode/encode_eqc.exs: -------------------------------------------------------------------------------- 1 | defmodule Bencode.EncodeEQC do 2 | use ExUnit.Case, async: true 3 | use EQC.ExUnit 4 | 5 | # numbers 6 | property "encode numbers" do 7 | forall input <- int() do 8 | ensure Bencode.encode!(input) == "i#{input}e" 9 | end 10 | end 11 | 12 | property "encode strings" do 13 | forall input <- utf8() do 14 | ensure Bencode.encode!(input) == "#{byte_size input}:#{input}" 15 | end 16 | end 17 | 18 | # figure out a way to model lists and property test them 19 | # figure out a way to model maps and property test them 20 | end 21 | -------------------------------------------------------------------------------- /test/bencode_eqc.exs: -------------------------------------------------------------------------------- 1 | defmodule BencodeEQC do 2 | use ExUnit.Case, async: true 3 | use EQC.ExUnit 4 | 5 | # encode + decode 6 | property "lists" do 7 | forall input <- list(int()) do 8 | encoded_input = Bencode.encode!(input) 9 | {:ok, decoded_result} = Bencode.decode(encoded_input) 10 | ensure decoded_result == input 11 | end 12 | end 13 | 14 | property "maps" do 15 | forall input <- map(utf8(), int()) do 16 | encoded_input = Bencode.encode!(input) 17 | {:ok, decoded_result} = Bencode.decode(encoded_input) 18 | ensure decoded_result == input 19 | end 20 | end 21 | 22 | # ints 23 | property "Output of encoding ints followed by a decode should result in the input" do 24 | forall input <- int() do 25 | encoded_input = Bencode.encode!(input) 26 | {:ok, decoded_result} = Bencode.decode(encoded_input) 27 | ensure decoded_result == input 28 | end 29 | end 30 | 31 | property "Output of encoding lists of ints followed by a decode should result in the input" do 32 | forall input <- list(int()) do 33 | encoded_input = Bencode.encode!(input) 34 | {:ok, decoded_result} = Bencode.decode(encoded_input) 35 | ensure decoded_result == input 36 | end 37 | end 38 | 39 | # strings 40 | property "Encoding strings followed by a decode should result in the input" do 41 | forall input <- utf8() do 42 | encoded_input = Bencode.encode!(input) 43 | {:ok, decoded_result} = Bencode.decode(encoded_input) 44 | ensure decoded_result == input 45 | end 46 | end 47 | 48 | property "Encoding lists of strings followed by a decode should result in the input" do 49 | forall input <- list(utf8()) do 50 | encoded_input = Bencode.encode!(input) 51 | {:ok, decoded_result} = Bencode.decode(encoded_input) 52 | ensure decoded_result == input 53 | end 54 | end 55 | 56 | property "Encoded maps should decode to the input" do 57 | forall input <- map(utf8(), utf8()) do 58 | encoded_input = Bencode.encode!(input) 59 | {:ok, decoded_result} = Bencode.decode(encoded_input) 60 | ensure decoded_result == input 61 | end 62 | end 63 | 64 | # misc 65 | property "random nested data structures" do 66 | structure = 67 | frequency( 68 | [{1, list( 69 | frequency( 70 | [{1, utf8()}, 71 | {1, int()}, 72 | {1, list(frequency([{1, utf8()}, {1, int()}]))}, 73 | {1, map(utf8(), frequency([{1, utf8()}, {1, int()}]))}]))}, 74 | {1, map( 75 | utf8(), frequency( 76 | [{1, utf8()}, 77 | {1, int()}, 78 | {1, list( 79 | frequency( 80 | [{1, utf8()}, 81 | {1, int()}, 82 | {1, list(frequency([{1, utf8()}, {1, int()}]))}, 83 | {1, map(utf8(), frequency([{1, utf8()}, {1, int()}]))}]))}, 84 | {1, map(utf8(), frequency([{1, utf8()}, {1, int()}]))}]))}]) 85 | 86 | forall input <- structure do 87 | encoded_input = Bencode.encode!(input) 88 | {:ok, decoded_result} = Bencode.decode(encoded_input) 89 | ensure decoded_result == input 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/bencode_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BencodeTest do 2 | use ExUnit.Case, async: true 3 | doctest Bencode 4 | 5 | test "calculate checksum of info directory when decoding" do 6 | input = %{"info" => %{"foo" => "bar"}} 7 | {:ok, data, checksum} = Bencode.decode_with_info_hash(Bencode.encode!(input)) 8 | assert data == input 9 | assert checksum == <<109, 34, 98, 18, 111, 235, 110, 199, 189, 52, 100, 147, 80, 37, 200, 198, 9, 192, 17, 157>> 10 | end 11 | 12 | test "calculate checksum of info directory when decoding!" do 13 | input = %{"info" => %{"foo" => "bar"}} 14 | {data, checksum} = Bencode.decode_with_info_hash!(Bencode.encode!(input)) 15 | assert data == input 16 | assert checksum == <<109, 34, 98, 18, 111, 235, 110, 199, 189, 52, 100, 147, 80, 37, 200, 198, 9, 192, 17, 157>> 17 | end 18 | 19 | test "returning error tuples on faulty input containing only integers" do 20 | # unexpected character 21 | {:error, reason} = Bencode.decode("i1be") 22 | assert reason =~ "character at 2" 23 | 24 | {:error, reason} = Bencode.decode("ie") 25 | assert reason =~ "empty integer" 26 | assert reason =~ "starting at 0" 27 | end 28 | 29 | test "returning error tuples on faulty input containing only strings" do 30 | # too short of a string 31 | {:error, reason} = Bencode.decode("3:fo") 32 | assert reason =~ "at 2 " 33 | assert reason =~ "out of bounds" 34 | end 35 | 36 | test "returning error tuples on faulty input containing lists with integers" do 37 | {:error, reason} = Bencode.decode("li28e") # missing `e` at end of data 38 | assert reason =~ "character at 5," 39 | assert reason =~ "end of data" 40 | 41 | {:error, reason} = Bencode.decode("li42eiee") 42 | assert reason =~ "empty integer" 43 | assert reason =~ "starting at 5" 44 | end 45 | 46 | test "returning error tuples on faulty input containing dictionaries with integers" do 47 | {:error, reason} = Bencode.decode("d3:fooi28e") # missing `e` at end of data 48 | assert reason =~ "character at 10," 49 | assert reason =~ "end of data" 50 | 51 | {:error, reason} = Bencode.decode("d3:fooiee") # empty integer as value 52 | assert reason =~ "empty integer" 53 | assert reason =~ "at 6" 54 | end 55 | 56 | test "returning error tuples on faulty input containing dictionaries with strings" do 57 | {:error, reason} = Bencode.decode("d3:foo2:bare") # too short of a string as value 58 | assert reason =~ "unexpected character" 59 | assert reason =~ "at 10" 60 | 61 | {:error, reason} = Bencode.decode("d1:foo2:bare") # faulty string as key 62 | assert reason =~ "unexpected character" 63 | assert reason =~ "at 4" 64 | end 65 | 66 | test "faulty data at top level" do 67 | {:error, reason} = Bencode.decode("e") 68 | assert reason =~ "unexpected character at 0" 69 | 70 | {:error, reason} = Bencode.decode("i1ei2e") 71 | assert reason =~ "unexpected character" 72 | assert reason =~ "expected no more data" 73 | end 74 | 75 | test "empty data should return nil" do 76 | assert {:ok, nil} = Bencode.decode("") 77 | end 78 | 79 | defp info_wrap(data), 80 | do: "d4:infod#{data}ee" 81 | 82 | test "faulty data inside info dictionaries when scanning for length" do 83 | # failure in integers 84 | assert {:error, _} = Bencode.decode_with_info_hash(info_wrap("3:fooi4_e")) 85 | 86 | # failure in strings 87 | assert {:error, _} = Bencode.decode_with_info_hash(info_wrap("3:foo2:bar")) 88 | assert {:error, _} = Bencode.decode_with_info_hash(info_wrap("3:foo14:bar")) 89 | 90 | # failure in dictionaries 91 | # - faulty key 92 | assert {:error, _} = Bencode.decode_with_info_hash(info_wrap("3:food4:bar3:baze")) 93 | assert {:error, _} = Bencode.decode_with_info_hash(info_wrap("3:food40:bar3:baze")) 94 | # - faulty value, strings 95 | assert {:error, _} = Bencode.decode_with_info_hash(info_wrap("3:food3:bar2:baze")) 96 | assert {:error, _} = Bencode.decode_with_info_hash(info_wrap("3:food3:bar30:baze")) 97 | # - faulty value, integers 98 | assert {:error, _} = Bencode.decode_with_info_hash(info_wrap("3:food3:bari4_ee")) 99 | assert {:error, _} = Bencode.decode_with_info_hash(info_wrap("3:food3:bariee")) 100 | 101 | # faulty in lists 102 | # - faulty value, integers 103 | assert {:error, _} = Bencode.decode_with_info_hash(info_wrap("3:fooli4_ee")) 104 | # - faulty value, strings 105 | assert {:error, _} = Bencode.decode_with_info_hash(info_wrap("3:fool30:bare")) 106 | end 107 | 108 | defmodule CustomStructTest do 109 | defstruct foo: nil 110 | end 111 | test "should return an error-tuple if unknown structs are encoded" do 112 | err = "protocol Bencode.Encoder is not implemented for %BencodeTest.CustomStructTest{foo: \"bar\"}" 113 | assert {:error, err} == Bencode.encode(%__MODULE__.CustomStructTest{foo: "bar"}) 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------