-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
1,518 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
%{ | ||
configs: [ | ||
%{ | ||
name: "default", | ||
files: %{ | ||
included: ["lib/", "src/", "test/", "benchmarks/"], | ||
excluded: [~r"/_build/", ~r"/deps/"] | ||
}, | ||
color: true, | ||
checks: [ | ||
## Design Checks | ||
{Credo.Check.Design.AliasUsage, priority: :low}, | ||
|
||
# Deactivate due to they're not compatible with current Elixir version | ||
{Credo.Check.Refactor.MapInto, false}, | ||
{Credo.Check.Warning.LazyLogging, false}, | ||
|
||
## Readability Checks | ||
{Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 100}, | ||
|
||
## Refactoring Opportunities | ||
{Credo.Check.Refactor.LongQuoteBlocks, false}, | ||
{Credo.Check.Refactor.CyclomaticComplexity, max_complexity: 15}, | ||
|
||
## TODO and FIXME do not cause the build to fail | ||
{Credo.Check.Design.TagTODO, exit_status: 0}, | ||
{Credo.Check.Design.TagFIXME, exit_status: 0} | ||
] | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
[] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Used by "mix format" | ||
[ | ||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
name: CI | ||
|
||
on: | ||
push: | ||
branches: [ master ] | ||
pull_request: | ||
branches: [ master ] | ||
|
||
jobs: | ||
nebulex_test: | ||
name: 'Bitcraft Test (Elixir ${{ matrix.elixir }} OTP ${{ matrix.otp }})' | ||
|
||
strategy: | ||
matrix: | ||
elixir: | ||
- '1.10.x' | ||
- '1.9.x' | ||
otp: | ||
- '22.x' | ||
|
||
runs-on: ubuntu-latest | ||
|
||
env: | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
MIX_ENV: test | ||
|
||
steps: | ||
- uses: actions/checkout@v2 | ||
|
||
- uses: actions/setup-elixir@v1 | ||
with: | ||
otp-version: '${{ matrix.otp }}' | ||
elixir-version: '${{ matrix.elixir }}' | ||
|
||
- uses: actions/cache@v1 | ||
with: | ||
path: deps | ||
key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} | ||
restore-keys: | | ||
${{ runner.os }}-mix- | ||
- uses: actions/cache@v1 | ||
with: | ||
path: _build | ||
key: ${{ runner.os }}-build-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} | ||
restore-keys: | | ||
${{ runner.os }}-build- | ||
- name: Install Dependencies | ||
run: mix deps.get | ||
|
||
- name: Compile Code | ||
run: mix compile --warnings-as-errors | ||
|
||
- name: Check Format | ||
run: mix format --check-formatted | ||
|
||
- name: Check Style | ||
run: mix credo --strict | ||
|
||
- name: Tests and Coverage | ||
run: | | ||
epmd -daemon | ||
mix coveralls.github | ||
- uses: actions/cache@v1 | ||
with: | ||
path: priv/plts | ||
key: ${{ runner.os }}-plt-v1-${{ env.MIX_ENV }} | ||
restore-keys: | | ||
${{ runner.os }}-plt-v1 | ||
- name: Dialyzer | ||
run: mix dialyzer --format short |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,38 @@ | ||
# The directory Mix will write compiled artifacts to. | ||
/_build | ||
|
||
# If you run "mix test --cover", coverage assets end up here. | ||
/cover | ||
|
||
# The directory Mix downloads your dependencies sources to. | ||
/deps | ||
|
||
# Where 3rd-party dependencies like ExDoc output generated docs. | ||
/doc | ||
/docs | ||
/benchmarks | ||
!/benchmarks/benchmark.exs | ||
|
||
# Ignore .fetch files in case you like to edit your project deps locally. | ||
/.fetch | ||
|
||
# Dialyzer | ||
/priv | ||
|
||
# If the VM crashes, it generates a dump, let's ignore it too. | ||
erl_crash.dump | ||
|
||
# Also ignore archive artifacts (built via "mix archive.build"). | ||
*.ez | ||
|
||
# Others | ||
*.o | ||
*.beam | ||
/config/*.secret.exs | ||
.elixir_ls/ | ||
*.plt | ||
erl_crash.dump | ||
.DS_Store | ||
._* | ||
/tmp* | ||
.elixir* | ||
.vs* | ||
/priv |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,210 @@ | ||
# ex_bits | ||
Toolkit and DSL for encoding/decoding bitstring in Elixir | ||
# Bitcraft | ||
### Toolkit and DSL for defining and parsing bitstring and/or binary blocks. | ||
|
||
![CI](https://github.com/cabol/bitcraft/workflows/CI/badge.svg) | ||
|
||
When working with binary protocols we usually have to implement encoding and | ||
decoding functions for the different type of messages the protocol supports. | ||
Despite parsing binary protocols is relatively easy in Elixir/Erlang using | ||
binary pattern-matching (and one of the greatest features in Elixir/Erlang), | ||
it might be tedius implement X number of parsing functions to support the | ||
protocol messages, we may ending up with a lot of similar binary matching | ||
all over the code, which is not bad, but what if we could avoid it? | ||
What if we had a toolkit like **Ecto** to define parseable bit-blocks, | ||
commonly used in binary protocols? This is where **Bitcraft** comes in! | ||
|
||
**Bitcraft** provides a DSL for defining parseable binary blocks or messages. | ||
You just need to define the bit-block for your message, adding the segments | ||
with their names, sizes and properties, and then **Bitcraft** generates the | ||
encoding and decoding functions automatically. | ||
|
||
## Installation | ||
|
||
You need to add `bitcraft` as a dependency to your `mix.exs` file. | ||
|
||
```elixir | ||
def deps do | ||
[ | ||
{:bitcraft, "~> 0.1.0"} | ||
] | ||
end | ||
``` | ||
|
||
## Getting started | ||
|
||
Let's start defining the bit-block for a simple message: | ||
|
||
```elixir | ||
defmodule MyBlock do | ||
import Bitcraft.BitBlock | ||
|
||
defblock "my-block" do | ||
segment(:header, 5, type: :binary) | ||
segment(:s1, 4, default: 1) | ||
segment(:s2, 8, default: 1, sign: :signed) | ||
segment(:tail, 3, type: :binary) | ||
end | ||
end | ||
``` | ||
|
||
After compile your code, you will be able to run: | ||
|
||
```elixir | ||
iex> data = %MyBlock{header: "begin", s1: 3, s2: -3, tail: "end"} | ||
iex> bits = MyBlock.encode(block) | ||
<<98, 101, 103, 105, 110, 63, 214, 86, 230, 4::size(4)>> | ||
|
||
iex> MyBlock.decode(bits) | ||
%MyBlock{header: "begin", leftover: "", s1: 3, s2: -3, tail: "end"} | ||
``` | ||
|
||
### Working with dynamic blocks | ||
|
||
For this example let's define an IPv4 datagram, which has a dynamic part: | ||
|
||
```elixir | ||
defmodule IpDatagram do | ||
import Bitcraft.BitBlock | ||
alias Bitcraft.BitBlock.DynamicSegment | ||
|
||
defblock "IP-datagram" do | ||
segment(:vsn, 4) | ||
segment(:hlen, 4) | ||
segment(:srvc_type, 8) | ||
segment(:tot_len, 16) | ||
segment(:id, 16) | ||
segment(:flags, 3) | ||
segment(:frag_off, 13) | ||
segment(:ttl, 8) | ||
segment(:proto, 8) | ||
segment(:hdr_chksum, 16, type: :bits) | ||
segment(:src_ip, 32, type: :bits) | ||
segment(:dst_ip, 32, type: :bits) | ||
segment(:opts, :dynamic, type: :bits) | ||
segment(:data, :dynamic, type: :bits) | ||
end | ||
|
||
# Size resolver for dynamic segments invoked during the decoding | ||
def calc_size(%__MODULE__{hlen: hlen}, :opts, dgram_s) | ||
when hlen >= 5 and 4 * hlen <= dgram_s do | ||
opts_s = 4 * (hlen - 5) | ||
{opts_s * 8, dgram_s} | ||
end | ||
|
||
def calc_size(%__MODULE__{leftover: leftover}, :data, dgram_s) do | ||
data_s = :erlang.bit_size(leftover) | ||
{data_s, dgram_s} | ||
end | ||
end | ||
``` | ||
|
||
Here, the segment corresponding to the `:opts` segment has a type modifier, | ||
specifying that `:opts` is to bind to a bitstring (or binary). All other | ||
segments have the default type equal to unsigned integer. | ||
|
||
An IP datagram header is of variable length. This length is measured in the | ||
number of 32-bit words and is given in the segment corresponding to `:hlen`. | ||
The minimum value of `:hlen` is 5. It is the segment corresponding to | ||
`:opts` that is variable, so if `:hlen` is equal to 5, `:opts` becomes | ||
an empty binary. Finally, the tail segment `:data` bind to bitstring. | ||
|
||
The decoding of the datagram fails if one of the following occurs: | ||
|
||
* The first 4-bits segment of datagram is not equal to 4. | ||
* `:hlen` is less than 5. | ||
* The size of the datagram is less than `4*hlen`. | ||
|
||
Since this block has dynamic fields, we can now use the other decode | ||
arguments to resolve the size for them during the decoding process: | ||
|
||
```elixir | ||
IpDatagram.decode(bits, :erlang.bit_size(bits), &IpDatagram.calc_size/3) | ||
``` | ||
|
||
Where: | ||
|
||
* The first argument is the input IPv4 datagram (bitstring). | ||
* The second argument is is the accumulator to the callback function | ||
(third argument), in this case is the total number of bits in the | ||
datagram. | ||
* And the third argument is the function callback or dynamic size resolver | ||
that will be invoked by the decoder for each dynamic segment. The callback | ||
functions receives the data struct with the current decoded fields, the | ||
segment name (to be pattern-matched and resolve its size), and the | ||
accumulator that can be used to pass metadata during the dynamic | ||
segments evaluation. | ||
|
||
Let's try it out: | ||
|
||
```elixir | ||
iex> dgram = %IpDatagram{ | ||
...> vsn: 4, | ||
...> hlen: 6, | ||
...> srvc_type: 8, | ||
...> tot_len: 100, | ||
...> id: 1, | ||
...> flags: 1, | ||
...> frag_off: 1, | ||
...> ttl: 32, | ||
...> proto: 6, | ||
...> hdr_chksum: <<1, 1>>, | ||
...> src_ip: <<10, 10, 10, 5>>, | ||
...> dst_ip: <<10, 10, 10, 6>>, | ||
...> opts: %Bitcraft.BitBlock.DynamicSegment{ | ||
...> value: <<10, 10, 10, 1>>, | ||
...> size: 32 | ||
...> }, | ||
...> data: %Bitcraft.BitBlock.DynamicSegment{ | ||
...> value: "ping", | ||
...> size: 32 | ||
...> } | ||
...> } | ||
iex> bits = IpDatagram.encode(dgram) | ||
<<70, 8, 0, 100, 0, 1, 32, 1, 32, 6, 1, 1, 10, 10, 10, 5, 10, 10, 10, 6, 10, 10, | ||
10, 1, 112, 105, 110, 103>> | ||
iex> IpDatagram.decode(bits, :erlang.bit_size(bits), &IpDatagram.calc_size/3) | ||
%IpDatagram{ | ||
data: %Bitcraft.BitBlock.DynamicSegment{size: 32, value: "ping"}, | ||
dst_ip: <<10, 10, 10, 6>>, | ||
flags: 1, | ||
frag_off: 1, | ||
hdr_chksum: <<1, 1>>, | ||
hlen: 6, | ||
id: 1, | ||
leftover: "", | ||
opts: %Bitcraft.BitBlock.DynamicSegment{size: 32, value: <<10, 10, 10, 1>>}, | ||
proto: 6, | ||
src_ip: <<10, 10, 10, 5>>, | ||
srvc_type: 8, | ||
tot_len: 100, | ||
ttl: 32, | ||
vsn: 4 | ||
} | ||
``` | ||
|
||
## Contributing | ||
|
||
Contributions to Bitcraft are very welcome and appreciated! | ||
|
||
Use the [issue tracker](https://github.com/cabol/bitcraft/issues) for bug reports | ||
or feature requests. Open a [pull request](https://github.com/cabol/bitcraft/pulls) | ||
when you are ready to contribute. | ||
|
||
When submitting a pull request you should not update the [CHANGELOG.md](CHANGELOG.md), | ||
and also make sure you test your changes thoroughly, include unit tests | ||
alongside new or changed code. | ||
|
||
Before to submit a PR it is highly recommended to run: | ||
|
||
* `mix format` to format the code properly. | ||
* `MIX_ENV=test mix credo --strict` to find code style issues. | ||
* `mix coveralls.html && open cover/excoveralls.html` to run tests and check | ||
out code coverage (expected 100%). | ||
* `MIX_ENV=test mix dialyzer` to run dialyzer for type checking; might take a | ||
while on the first invocation. | ||
|
||
## Copyright and License | ||
|
||
Copyright (c) 2020, Carlos Bolaños. | ||
|
||
Bitcraft source code is licensed under the [MIT License](LICENSE). |
Oops, something went wrong.