Skip to content

Commit

Permalink
Make it so 🚀
Browse files Browse the repository at this point in the history
  • Loading branch information
cabol committed Sep 8, 2020
1 parent 002ccc9 commit 5d75d50
Show file tree
Hide file tree
Showing 17 changed files with 1,518 additions and 25 deletions.
31 changes: 31 additions & 0 deletions .credo.exs
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}
]
}
]
}
1 change: 1 addition & 0 deletions .dialyzer_ignore.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
4 changes: 4 additions & 0 deletions .formatter.exs
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}"]
]
74 changes: 74 additions & 0 deletions .github/workflows/ci.yml
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
32 changes: 30 additions & 2 deletions .gitignore
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
21 changes: 0 additions & 21 deletions LICENSE

This file was deleted.

212 changes: 210 additions & 2 deletions README.md
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).
Loading

0 comments on commit 5d75d50

Please sign in to comment.