-
Notifications
You must be signed in to change notification settings - Fork 54
CompoundTypes
Compound types contain more that a single value. These types are Records, Arrays and Choices.
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.
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}
There are two different parameters that specify the length of the array.
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]
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]
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)
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.
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
is the name of a supplied type (e.g. uint32be
, string
)
or a user defined type. This is the same as for Records.
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
.
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.
An index/key into the :choices
array/hash which specifies the
currently active choice.
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"
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