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

Navigation


Namespaces

Namespaces are a common solution to the problem of too many names. Here is how BinData addresses the problem.

BinData types are named based on their class name. Any module hierarchy is ignored, which results in a flat namespace. Any desired namespace hierarchy must be explicitly encoded into the classname by the user.

class Ns1Foo < BinData::Record
end

class Ns1Bar < BinData::Record
end

class Ns2Foo < BinData::Record
end

The obvious downside to this is a verbose class declaration.

class MyClass < BinData::Record
  ns2_foo :a
  ns1_bar :b
  ns2_foo :c
end

BinData provides the search_prefix keyword to help DRY up the verbosity.

class MyClass < BinData::Record
  search_prefix ns2, ns1
  foo :a   # searches for ns2_foo then ns1_foo => resolves to ns2_foo
  bar :b   # searches for ns2_bar then ns1_bar => resolves to ns1_bar
  foo :c
end

search_prefix provides a similar function to the endian keyword. Whereas endian allows the suffix of a type to be omitted, search_prefix allows omission of the prefix.

Debugging

BinData includes several features to make it easier to debug declarations.

Tracing

BinData has the ability to trace the results of reading a data structure.

class A < BinData::Record
  int8  :a
  bit4  :b
  bit2  :c
  array :d, initial_length: 6, type: :bit1
end

BinData::trace_reading do
  A.read("\373\225\220")
end

Results in the following being written to STDERR.

obj.a => -5
obj.b => 9
obj.c => 1
obj.d[0] => 0
obj.d[1] => 1
obj.d[2] => 1
obj.d[3] => 0
obj.d[4] => 0
obj.d[5] => 1

Rest

The rest keyword will consume the input stream from the current position to the end of the stream.

class A < BinData::Record
  string :a, read_length: 5
  rest   :rest
end

obj = A.read("abcdefghij")
obj.a #=> "abcde"
obj.rest #=> "fghij"

Hidden fields

The typical way to view the contents of a BinData record is to call #snapshot or #inspect. This gives all fields and their values. The hide keyword can be used to prevent certain fields from appearing in this output. This removes clutter and allows the developer to focus on what they are currently interested in.

class Testing < BinData::Record
  hide :a, :b
  string :a, read_length: 10
  string :b, read_length: 10
  string :c, read_length: 10
end

obj = Testing.read(("a" * 10) + ("b" * 10) + ("c" * 10))
obj.snapshot #=> {"c"=>"cccccccccc"}
obj.to_binary_s #=> "aaaaaaaaaabbbbbbbbbbcccccccccc"

Parameterizing User Defined Types

All BinData types have parameters that allow the behaviour of an object to be specified at initialization time. User defined types may also specify parameters. There are two types of parameters: mandatory and default.

Mandatory Parameters

Mandatory parameters must be specified when creating an instance of the type.

class Polygon < BinData::Record
  mandatory_parameter :num_vertices

  uint8 :num, value: -> { vertices.length }
  array :vertices, initial_length: :num_vertices do
    int8 :x
    int8 :y
  end
end

triangle = Polygon.new
    #=> raises ArgumentError: parameter 'num_vertices' must be specified in Polygon

triangle = Polygon.new(num_vertices: 3)
triangle.snapshot #=> {"num" => 3, "vertices" =>
                         [{"x"=>0, "y"=>0}, {"x"=>0, "y"=>0}, {"x"=>0, "y"=>0}]}

Default Parameters

Default parameters are optional. These parameters have a default value that may be overridden when an instance of the type is created.

class Phrase < BinData::Primitive
  default_parameter number: "three"
  default_parameter adjective: "blind"
  default_parameter noun: "mice"

  stringz :a, initial_value: :number
  stringz :b, initial_value: :adjective
  stringz :c, initial_value: :noun

  def get; "#{a} #{b} #{c}"; end
  def set(v)
    if /(.*) (.*) (.*)/ =~ v
      self.a, self.b, self.c = $1, $2, $3
    end
  end
end

obj = Phrase.new(number: "two", adjective: "deaf")
obj.to_s #=> "two deaf mice"

Extending existing Types

Sometimes you wish to create a new type that is simply an existing type with some predefined parameters. Examples could be an array with a specified type, or an integer with an initial value.

This can be achieved by subclassing the existing type and providing default parameters. These parameters can of course be overridden at initialisation time.

Here we define an array that contains big endian 16 bit integers. The array has a preferred initial length.

class IntArray < BinData::Array
  default_parameters type: :uint16be, initial_length: 5
end

arr = IntArray.new
arr.size #=> 5

The initial length can be overridden at initialisation time.

arr = IntArray.new(initial_length: 8)
arr.size #=> 8

We can also use the block form syntax:

class IntArray < BinData::Array
  endian :big
  default_parameter initial_length: 5

  uint16
end

Dynamically creating Types

Sometimes the format of a record is not known until runtime. You can use the BinData::Struct class to dynamically create a new type. To be able to reuse this type, you can give it a name.

# Dynamically create my_new_type
BinData::Struct.new(name: :my_new_type,
                    fields: [ [:int8, :a], [:int8, :b] ])

# Create an array of these types
array = BinData::Array.new(type: :my_new_type)

Advanced Bitfields

Most types in a record are byte oriented. Bitfields allow access to individual bits in an octet stream.

Sometimes a bitfield has unused elements such as

class RecordWithBitfield < BinData::Record
  bit1 :foo
  bit1 :bar
  bit1 :baz
  bit5 :unused

  stringz :qux
end

The problem with specifying an unused field is that the size of this field must be manually counted. This is a potential source of errors.

BinData provides a shortcut to skip to the next byte boundary with the resume_byte_alignment keyword.

class RecordWithBitfield < BinData::Record
  bit1 :foo
  bit1 :bar
  bit1 :baz
  resume_byte_alignment

  stringz :qux
end

Occasionally you will come across a format where primitive types (string and numerics) are not aligned on byte boundaries but are to be packed in the bit stream.

class PackedRecord < BinData::Record
  bit4     :a
  string   :b, length: 2  # note: byte-aligned
  bit1     :c
  int16le  :d                # note: byte-aligned
  bit3     :e
end

obj = PackedRecord.read("\xff" * 10)
obj.to_binary_s #=> "\360\377\377\200\377\377\340"

The above declaration does not work as expected because BinData's internal strings and integers are byte-aligned. We need bit-aligned versions of string and int16le.

class BitString < BinData::String
  bit_aligned
end

class BitInt16le < BinData::Int16le
  bit_aligned
end

class PackedRecord < BinData::Record
  bit4        :a
  bit_string  :b, length: 2
  bit1        :c
  bit_int16le :d
  bit3        :e
end

obj = PackedRecord.read("\xff" * 10)
obj.to_binary_s #=> "\377\377\377\377\377"