-
Notifications
You must be signed in to change notification settings - Fork 55
AdvancedTopics
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.
BinData includes several features to make it easier to debug declarations.
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
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"
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"
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 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 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"
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
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)
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"