Skip to content
Dion Mendel edited this page Jun 25, 2023 · 11 revisions

Navigation


Compound types contain more that a single value. These types are Records, Arrays and Choices.

Arrays

A BinData array is a list of data objects of the same type. It behaves much the same as the standard Ruby array, supporting most of the common methods.

Array syntax

When instantiating an array, the type of object it contains must be specified. The two different ways of declaring this are the :type parameter and the block form.

class A < BinData::Record
  array :numbers, type: :uint8, initial_length: 3
end
              -vs-

class A < BinData::Record
  array :numbers, initial_length: 3 do
    uint8
  end
end

For the simple case, the :type parameter is usually clearer. When the array type has parameters, the block form becomes easier to read.

class A < BinData::Record
   array :numbers, type: [:uint8, {initial_value: :index}],
                   initial_length: 3
end
              -vs-

class A < BinData::Record
  array :numbers, initial_length: 3 do
    uint8 initial_value: :index
  end
end

An array can also be declared as a custom type by moving the contents of the block into a custom class. The above example could alternatively be declared as:

class NumberArray < BinData::Array
  uint8 initial_value: :index
end

class A < BinData::Record
  number_array :numbers, initial_length: 3
end

If the block form has multiple types declared, they are interpreted as the contents of an anonymous struct. To illustrate this, consider the following representation of a polygon.

class Polygon < BinData::Record
  endian :little
  uint8 :num_points, value: -> { points.length }
  array :points, initial_length: :num_points do
    double :x
    double :y
  end
end

triangle = Polygon.new
triangle.points[0].assign(x: 1, y: 2)
triangle.points[1].x = 3
triangle.points[1].y = 4
triangle.points << {x: 5, y: 6}

Array parameters

There are two different parameters that specify the length of the array.

:initial_length

Specifies the initial length of a newly instantiated array. The array may grow as elements are inserted.

obj = BinData::Array.new(type: :int8, initial_length: 4)
obj.read("\002\003\004\005\006\007")
obj.snapshot #=> [2, 3, 4, 5]

:read_until

While reading, elements are read until this condition is true. This is typically used to read an array until a sentinel value is found. The variables index, element and array are made available to any lambda assigned to this parameter. If the value of this parameter is the symbol :eof, then the array will read as much data from the stream as possible.

obj = BinData::Array.new(type: :int8,
                         read_until: -> { index == 1 })
obj.read("\002\003\004\005\006\007")
obj.snapshot #=> [2, 3]

obj = BinData::Array.new(type: :int8,
                         read_until: -> { element >= 3.5 })
obj.read("\002\003\004\005\006\007")
obj.snapshot #=> [2, 3, 4]

obj = BinData::Array.new(type: :int8,
        read_until: -> { array[index] + array[index - 1] == 9 })
obj.read("\002\003\004\005\006\007")
obj.snapshot #=> [2, 3, 4, 5]

obj = BinData::Array.new(type: :int8, read_until: :eof)
obj.read("\002\003\004\005\006\007")
obj.snapshot #=> [2, 3, 4, 5, 6, 7]

Byte arrays

There is a performance enhancement for byte arrays.

BinData::Uint8Array is a (mostly) drop in replacement for BinData::Array.new(type: :uint8).

When you have

obj = BinData::Array.new(type: :uint8, initial_length: 10)

you can replace it with the following for increased performance.

obj = BinData::Uint8Array.new(initial_length: 10)

Choices

A Choice is a collection of data objects of which only one is active at any particular time. Method calls will be delegated to the active choice. The possible types of objects that a choice contains is controlled by the :choices parameter, while the :selection parameter specifies the active choice.

Choice syntax

Choices have two ways of specifying the possible data objects they can contain. The :choices parameter or the block form. The block form is usually clearer and is prefered.

class MyInt16 < BinData::Record
  uint8  :e, assert: -> { value == 0 || value == 1 }
  choice :int, selection: :e,
               choices: {0 => :int16be, 1 => :int16le}
end
              -vs-

class MyInt16 < BinData::Record
  uint8  :e, assert: -> { value == 0 || value == 1 }
  choice :int, selection: :e do
    int16be 0
    int16le 1
  end
end

Like all compound types, a choice can be declared as its own type. The above example can be declared as:

class BigLittleInt16 < BinData::Choice
  int16be 0
  int16le 1
end

class MyInt16 < BinData::Record
  uint8  :e, assert: -> { value == 0 || value == 1 }
  big_little_int_16 :int, selection: :e
end

The general form of the choice is

class MyRecord < BinData::Record
  choice :name, selection: -> { ... } do
    type key, param1: "foo", param2: "bar" ... # option 1
    type key, param1: "foo", param2: "bar" ... # option 2
  end
end

type

is the name of a supplied type (e.g. uint32be, string) or a user defined type. This is the same as for Records.

key

is the value that :selection will return to specify that this choice is currently active. The key can be any ruby type (String, Numeric etc) except Symbol.

Choice parameters

:choices

Either an array or a hash specifying the possible data objects. The format of the array/hash.values is a list of symbols representing the data object type. If a choice is to have params passed to it, then it should be provided as [type_symbol, hash_params]. An implementation constraint is that the hash may not contain symbols as keys.

:selection

An index/key into the :choices array/hash which specifies the currently active choice.

:copy_on_change

If set to true, copy the value of the previous selection to the current selection whenever the selection changes. Default is false.

Examples

type1 = [:string, {value: "Type1"}]
type2 = [:string, {value: "Type2"}]

choices = {5 => type1, 17 => type2}
obj = BinData::Choice.new(choices: choices, selection: 5)
obj # => "Type1"

choices = [ type1, type2 ]
obj = BinData::Choice.new(choices: choices, selection: 1)
obj # => "Type2"

class MyNumber < BinData::Record
  int8 :is_big_endian
  choice :data, selection: -> { is_big_endian != 0 },
                copy_on_change: true do
    int32le false
    int32be true
  end
end

obj = MyNumber.new
obj.is_big_endian = 1
obj.data = 5
obj.to_binary_s #=> "\001\000\000\000\005"

obj.is_big_endian = 0
obj.to_binary_s #=> "\000\005\000\000\000"

Default selection

A key of :default can be specified as a default selection. If the value of the selection isn't specified then the :default will be used. The previous MyNumber example used a flag for endian. Zero is little endian while any other value is big endian. This can be concisely written as:

class MyNumber < BinData::Record
  int8 :is_big_endian
  choice :data, selection: :is_big_endian,
                copy_on_change: true do
    int32le 0          # zero is little endian
    int32be :default   # anything else is big endian
  end
end