defmodule Bitcraft do
@moduledoc """
The following are the main Bitcraft components:
* `Bitcraft.BitBlock` - This is the main Bitcraft component. It provides
a DSL that allows to define bit-blocks with their segments (useful for
building binary protocols) and automatically injects encoding and decoding
functions for them.
* `Bitcraft` - This is a helper module that provides utility functions to
work with bit strings and binaries.
"""
use Bitwise
use Bitcraft.Helpers
# 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
@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)
size = Keyword.get(opts, :size)
size =
cond do
is_nil(size) and is_integer(input) -> 8
is_nil(size) and is_float(input) -> 64
true -> size
end
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)
endian = Keyword.get(opts, :endian, :big)
size = opts[:size] || byte_size(input) * 8
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)
end
defp count_ones(0, count), do: count
defp count_ones(integer, count) do
count_ones(integer &&& integer - 1, count + 1)
end
end