View Source Bitcraft.BitBlock behaviour (Bitcraft v0.1.1)

Defines a bit-block.

A bit-block is used to map a bitstring into an Elixir struct. The definition of the bit-block is possible through defblock/3.

defblock/3 is typically used to decode bitstring from a bit stream, usually a binary protocol (e.g.: TCP/IP), into Elixir structs and vice-versa (encoding Elixir structs into a bitstring).

example

Example

defmodule MyBlock do
  import Bitcraft.BitBlock

  defblock "my-static-block" do
    segment :h, 5, type: :binary
    segment :s1, 4, default: 1
    segment :s2, 8, default: 1, sign: :signed
    segment :t, 3, type: :binary
  end
end

The segment macro defines a segment in the bit-block with given name and size. Bit-blocks are regular structs and can be created and manipulated directly using Elixir's struct API:

iex> block = %MyBlock{h: "begin", s1: 3, s2: -3, t: "end"}
iex> %{block | h: "hello"}

By default, a bit-block will automatically generate the implementation for the callbacks encode/1 and decode/3. You can then encode the struct into a bitstring and then decode a bitstring into a Elixir struct, like so:

iex> bits = MyBlock.encode(block)
iex> data = MyBlock.decode(bits)

What we defined previously it is a static block, means a fixed size always. This is the easiest scenario because we don't need to provide any additional logic to the the encode/1 and decode/3 functions. For that reason, in the example above, we call decode function only with the input bitstring, the other arguments are not needed since they are ment to resolve the size for dynamic segments.

dynamic-segments

Dynamic Segments

There are other scenarios where the block of bits is dynamic, means the size of the block is variable and depends on other segment values to calculate the size, this makes it more complicated to decode it. For those variable blocks, we can define dynamic segments using the segment/3 API:

segment :var, :dynamic, type: :bits

As you can see, for the size argument we are passing :dynamic atom. In this way, the segment is marked as dynamic and its size is resolved later during the decoding process.

The following is a more elaborate example of block. We define an IPv4 datagram which has a static and a dynamic part. The dynamic part is basically the options and the data. The block can be defined as:

defmodule IpDatagram do
  @moduledoc false
  import Bitcraft.BitBlock

  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 segments, we can now use the other decode arguments to resolve the size for them during the decoding process:

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 segments, 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.

reflection

Reflection

Any bit-block module will generate the __bit_block__ function that can be used for runtime introspection of the bit-block:

  • __bit_block__(:name) - Returns the name or alias as given to defblock/3.
  • __bit_block__(:segments) - Returns a list of all segments names.
  • __schema__(:segment_info, segment) - Returns a map with the segment info.

working-with-typespecs

Working with typespecs

By default, the typespec t/0 is generated but in the simplest form:

@type t :: %__MODULE__{}

If you want to provide a more accurate typespec for you block adding the typespecs for each of the segments on it, you can set the option :typespec to false when defining the block, like so:

defblock "my-block", typespec: false do
  ...
end

Link to this section Summary

Types

Basic Segment types

Block's segment type definition

Resolver function for the size of dynamic segments.

Segment type

t()

Bitblock definition

Callbacks

Decodes the given bitstring into the corresponding data type.

Encodes the given data type into a bitstring.

Functions

This is a helper function used internally for building a block segment.

Same as segment/3, but automatically generates a dynamic segment with the type Bitcraft.BitBlock.Array.t().

This is a helper function used internally for building the encoding expressions.

Internal helper for decoding the block segments.

Defines a bit-block struct with a name and segment definitions.

Internal helper for encoding the block segments.

Defines a segment on the block with a given name and size.

Link to this section Types

@type base_seg_type() ::
  :integer
  | :float
  | :bitstring
  | :bits
  | :binary
  | :bytes
  | :utf8
  | :utf16
  | :utf32

Basic Segment types

@type block_segment() ::
  {:block_segment, name :: atom(), size :: integer() | :dynamic | nil,
   type :: segment_type(), sign :: atom(), endian :: atom(), default :: term()}

Block's segment type definition

Link to this type

dynamic_size_resolver()

View Source
@type dynamic_size_resolver() ::
  (t(), seg_name :: atom(), acc :: term() ->
     {size :: non_neg_integer(), acc :: term()})

Resolver function for the size of dynamic segments.

@type segment_type() :: base_seg_type() | Bitcraft.BitBlock.Array.t()

Segment type

@type t() :: %{optional(atom()) => any(), :__struct__ => atom()}

Bitblock definition

Link to this section Callbacks

Link to this callback

decode(input, acc, dynamic_size_resolver)

View Source
@callback decode(input :: bitstring(), acc :: term(), dynamic_size_resolver()) :: t()

Decodes the given bitstring into the corresponding data type.

example

Example

iex> block = %MyBlock{seg1: 1, seg: 2}
iex> bits = MyBlock.encode(block)
iex> MyBlock.decode(bits)
@callback encode(t()) :: bitstring()

Encodes the given data type into a bitstring.

example

Example

iex> block = %MyBlock{seg1: 1, seg: 2}
iex> MyBlock.encode(block)

Link to this section Functions

Link to this function

__segment__(mod, name, size, opts)

View Source
@spec __segment__(module(), atom(), non_neg_integer(), Keyword.t()) :: :ok

This is a helper function used internally for building a block segment.

Link to this macro

array(name, opts \\ [])

View Source (macro)

Same as segment/3, but automatically generates a dynamic segment with the type Bitcraft.BitBlock.Array.t().

The size of the array-type segment in bits has to be calculated dynamically during the decoding, and the length of the array will be segment_size/element_size. This process is performs automatically during the decoding. hence, it is important to set the right element_size and also implement properly the callback to calculate the segment size. See Bitcraft.BitBlock.dynamic_size_resolver().

options

Options

Options are the same as segment/3, and additionally:

  • :element_size - The size in bits of each array element. Defaults to 8.

NOTE: The :type is the same as segment/3 BUT it applies to the array element.

Link to this macro

block_segment(args \\ [])

View Source (macro)
Link to this macro

block_segment(record, args)

View Source (macro)
Link to this function

build_encoding_exprs(list, bin, map)

View Source
@spec build_encoding_exprs([block_segment()], String.t(), String.t()) ::
  {bin_expr_ast :: term(), map_expr_ast :: term()}

This is a helper function used internally for building the encoding expressions.

Link to this function

decode_segments(block_segments, struct, acc_in, fun)

View Source
@spec decode_segments([block_segment()], map(), term(), dynamic_size_resolver()) ::
  map()

Internal helper for decoding the block segments.

Link to this macro

defblock(name, opts \\ [], list)

View Source (macro)

Defines a bit-block struct with a name and segment definitions.

Link to this function

encode_segments(block_segments, data)

View Source
@spec encode_segments([block_segment()], map()) :: bitstring()

Internal helper for encoding the block segments.

Link to this macro

segment(name, size \\ nil, opts \\ [])

View Source (macro)

Defines a segment on the block with a given name and size.

See Kernel.SpecialForms.<<>>/1 for more information about the segment types, size, unit, and so on.

options

Options

  • :type - Defines the segment data type the set of bits will be mapped to. See Kernel.SpecialForms.<<>>/1 for more information about the segment data types. Defaults to :integer.

  • :sign - Applies only to integers and defines whether the integer is :signed or :unsigned. Defaults to :unsigned.

  • :endian - Applies to utf32, utf16, float, integer. Defines the endianness, :big or :little. Defaults to :big.

  • :default - Sets the default value on the block and the struct. The default value is calculated at compilation time, so don't use expressions for generating values dynamically as they would then be the same for all records. Defaults to nil.