├── .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 | [](https://github.com/gausby/bencode/blob/master/LICENSE)
4 | [](https://hex.pm/packages/bencode)
5 | [
](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 |
--------------------------------------------------------------------------------