Skip to content

Commit

Permalink
Add array data type for segments
Browse files Browse the repository at this point in the history
  • Loading branch information
cabol committed Sep 11, 2020
1 parent 203ac0c commit 87b8e46
Show file tree
Hide file tree
Showing 11 changed files with 376 additions and 87 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ jobs:
- uses: actions/cache@v1
with:
path: priv/plts
key: ${{ runner.os }}-plt-v1-${{ env.MIX_ENV }}
key: ${{ runner.os }}-plt-v2-${{ env.MIX_ENV }}
restore-keys: |
${{ runner.os }}-plt-v1
${{ runner.os }}-plt-v2
- name: Dialyzer
run: mix dialyzer --format short
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ iex> MyBlock.decode(bits)
%MyBlock{header: "begin", leftover: "", s1: 3, s2: -3, tail: "end"}
```

> See `Bitcraft.BitBlock.defblock/3` and `Bitcraft.BitBlock.segment/3`
for more information.

### Working with dynamic blocks

For this example let's define an IPv4 datagram, which has a dynamic part:
Expand Down Expand Up @@ -199,6 +202,66 @@ iex> IpDatagram.decode(bits, :erlang.bit_size(bits), &IpDatagram.calc_size/3)
}
```

## Arrays

Sometimes we may also want to parse a segment of bits as an array, for example,
suppose we have a dynamic segment but we want to parse it as a list of integers
of 16 bits, what variates is the amount of them (the length of the array).
So, if the size of the segment is calculated as 64 bits, we expect an array
of 4 integers of 16 bits.

First of all, let us define a block with an array-type segment:

```elixir
defmodule TestBlock do
import Bitcraft.BitBlock

defblock "test-block" do
segment(:a, 8)
segment(:b, 8)
array(:list, type: :integer, element_size: 16, sign: :signed)
end

# Size resolver for dynamic segments invoked during the decoding
def calc_size(%__MODULE__{a: a, b: b}, :list, acc) do
{a + b, acc}
end
end
```

As you may notice, the field `:list` is defined as array-type in the form:

```elixir
array(:list, type: :integer, element_size: 16, sign: :signed)
```

The first argument is the name of the segment, then we pass the options.
The `type: :integer` defines the the type for the array elements, and
`element_size: 16` defines that each element of the array must have
16 bits size. The rest of the options are the same and apply to the
array elements.

> See `Bitcraft.BitBlock.array/2` and `Bitcraft.BitBlock.segment/3`.
Now, let's try it out:

```elixir
iex> data = %TestBlock{
...> a: 32,
...> b: 32,
...> list: %Bitcraft.BitBlock.DynamicSegment{value: [1, 2, 3, 4], size: 64}
}
iex> encoded = TestBlock.encode(data)
<<32, 32, 0, 1, 0, 2, 0, 3, 0, 4>>
iex> TestBlock.decode(encoded, %{}, &TestBlock.calc_size/3)
%TestBlock{
a: 32,
b: 32,
leftover: "",
list: %Bitcraft.BitBlock.DynamicSegment{size: 64, value: [1, 2, 3, 4]}
}
```

## Contributing

Contributions to Bitcraft are very welcome and appreciated!
Expand Down
1 change: 1 addition & 0 deletions coveralls.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

"skip_files": [
"lib/bitcraft/helpers.ex",
"lib/bitcraft/bit_block.ex",
"test/support/*"
]
}
136 changes: 131 additions & 5 deletions lib/bitcraft.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,68 @@ defmodule Bitcraft do
use Bitwise
use Bitcraft.Helpers

@typedoc "Codable data types"
@type bit_data :: integer | float | binary | bitstring | byte | char
# Base data types for binaries
@type base_type ::
integer
| float
| binary
| bitstring
| byte
| char

@typedoc "Segment type"
@type segment_type :: base_type | Bitcraft.BitBlock.Array.t()

@typedoc "Codable segment type"
@type codable_segment_type :: base_type | [base_type]

## API

@spec encode_bits(bit_data, Keyword.t()) :: bitstring
def encode_bits(input, opts \\ []) do
@doc """
Encodes the given `input` into a bitstring.
## Options
* `:size` - The size in bits for the input to encode. The default
value depend on the type, for integer is 8, for float is 63, and for
other data types is `nil`. If the `input` is a list, this option is
skipped, since it is handled as array and the size will be
`array_length * element_size`.
* `:type` - The segment type given by `Bitcraft.segment_type()`.
Defaults to `:integer`.
* `:sign` - If the input is an integer, defines if it is `:signed`
or `:unsigned`. Defaults to `:unsigned`.
* `:endian` - Applies to `utf32`, `utf16`, `float`, `integer`.
Defines the endianness, `:big` or `:little`. Defaults to `:big`.
## Example
iex> Bitcraft.encode_segment(15)
<<15>>
iex> Bitcraft.encode_segment(255, size: 4)
<<15::size(4)>>
iex> Bitcraft.encode_segment(-3.3, size: 64, type: :float)
<<192, 10, 102, 102, 102, 102, 102, 102>>
iex> Bitcraft.encode_segment("hello", type: :binary)
"hello"
iex> Bitcraft.encode_segment(<<1, 2, 3>>, type: :bits)
<<1, 2, 3>>
iex> Bitcraft.encode_segment([1, -2, 3], type: %Bitcraft.BitBlock.Array{
...> type: :integer, element_size: 4},
...> sign: :signed
...> )
<<30, 3::size(4)>>
"""
@spec encode_segment(codable_segment_type, Keyword.t()) :: bitstring
def encode_segment(input, opts \\ []) do
type = Keyword.get(opts, :type, :integer)
sign = Keyword.get(opts, :sign, :unsigned)
endian = Keyword.get(opts, :endian, :big)
Expand All @@ -25,9 +80,69 @@ defmodule Bitcraft do
true -> size
end

encode_bits(input, size, type, sign, endian)
encode_segment(input, size, type, sign, endian)
end

@doc """
Returns a tuple `{decoded value, leftover}` where the first element is the
decoded value from the given `input` (according to the given `otps` too)
and the second element is the leftover.
## Options
* `:size` - The size in bits to decode. Defaults to `byte_size(input) * 8`.
If the type is `Bitcraft.BitBlock.Array.()`, the size should match with
`array_length * element_size`.
* `:type` - The segment type given by `Bitcraft.segment_type()`.
Defaults to `:integer`.
* `:sign` - If the input is an integer, defines if it is `:signed`
or `:unsigned`. Defaults to `:unsigned`.
* `:endian` - Applies to `utf32`, `utf16`, `float`, `integer`.
Defines the endianness, `:big` or `:little`. Defaults to `:big`.
## Example
iex> 3
...> |> Bitcraft.encode_segment(size: 4)
...> |> Bitcraft.decode_segment(size: 4)
{3, ""}
iex> -3.3
...> |> Bitcraft.encode_segment(size: 64, type: :float, sign: :signed)
...> |> Bitcraft.decode_segment(size: 64, type: :float, sign: :signed)
{-3.3, ""}
iex> "test"
...> |> Bitcraft.encode_segment(type: :binary)
...> |> Bitcraft.decode_segment(size: 4, type: :binary)
{"test", ""}
iex> <<1, 2, 3, 4>>
...> |> Bitcraft.encode_segment(type: :bits)
...> |> Bitcraft.decode_segment(size: 32, type: :bits)
{<<1, 2, 3, 4>>, ""}
iex> alias Bitcraft.BitBlock.Array
iex> [1, 2]
...> |> Bitcraft.encode_segment(type: %Array{})
...> |> Bitcraft.decode_segment(size: 16, type: %Array{})
{[1, 2], ""}
iex> [3.3, -7.7, 9.9]
...> |> Bitcraft.encode_segment(
...> type: %Array{type: :float, element_size: 64},
...> sign: :signed
...> )
...> |> Bitcraft.decode_segment(
...> size: 192,
...> type: %Array{type: :float, element_size: 64},
...> sign: :signed
...> )
{[3.3, -7.7, 9.9], ""}
"""
@spec decode_segment(bitstring, Keyword.t()) :: {codable_segment_type, bitstring}
def decode_segment(input, opts \\ []) do
type = Keyword.get(opts, :type, :integer)
sign = Keyword.get(opts, :sign, :unsigned)
Expand All @@ -37,6 +152,17 @@ defmodule Bitcraft do
decode_segment(input, size, type, sign, endian)
end

@doc """
Returns the number of `1`s in binary representation of the given `integer`.
## Example
iex> Bitcraft.count_ones(15)
4
iex> Bitcraft.count_ones(255)
8
"""
@spec count_ones(integer) :: integer
def count_ones(integer) when is_integer(integer) do
count_ones(integer, 0)
Expand Down
Loading

0 comments on commit 87b8e46

Please sign in to comment.