class Depix::Binary::Structure
A basic C structs library (only works by value). Here's the basic mode of operation:
1) You define a struct, with a number of fields in it. This hould be a subclass of Dict within which you create Field objects which are saved in a class variable 3) Each created Field instance knows how big it is and how to produce a pattern to get it's value from the byte stream by using Ruby's "pack/unpack". Each field thus provides an unpack pattern, and patterns are ordered into a stack, starting with the first unpack pattern 4) When you parse some bytes using the struct: - An unpack pattern will be compiled from all of the fields composing the struct, and it will be a single string. The string gets applied to the bytes passed to parse() - An array of unpacked values returned by unpack is then passed to the struct's consumption engine, which lets each field take as many items off the stack as it needs. A field might happily produce 4 items for unpacking and then take the same 4 items off the stack of parsed values. Or not. - A new structure gets created and for every named field it defines an attr_accessor. When consuming, the values returned by Field objects get set using the accessors (so accessors can be overridden too!) 5) When you save out the struct roughly the same happens but in reverse (readers are called per field, then it's checked whether the data can be packed and fits into the alloted number of bytes, and then one big array of values is composed and passed on to Array#pack)
For example
class OneIntegerAndOneFloat < Structure uint32 :identifier, :description => "This is the important ID", :required => true real :value, :description => "The value that we store" end ready_struct = OneIntegerAndOneFloat.new ready_struct.identifier = 23 # Plain Ruby assignment ready_struct.value = 45.0 binary_file.write(OneIntegerAndOneFloat.pack(ready_struct)) # dumps the packed struct with paddings
Constants
- DEF_OPTS
Public Class Methods
Apply this structure to data in the string, returning an instance of this structure with fields completed
# File lib/depix/binary/structure.rb, line 143 def self.apply!(string) consume!(string.unpack(pattern)) end
Apply this structure to data in the string, returning an instance of this structure with fields completed assume little-endian fields
# File lib/depix/binary/structure.rb, line 149 def self.apply_le!(string) consume!(string.unpack(pattern_le)) end
Define an array of values
# File lib/depix/binary/structure.rb, line 89 def self.array(name, mapped_to, *extras) count, opts = count_and_opts_from(extras) attr_accessor name a = ArrayField.new({:name => name}.merge(opts)) a.members = if mapped_to.is_a?(Class) # Array of structs [InnerField.new(:cast => mapped_to)] * count else c = Depix::Binary::Fields.const_get("#{mapped_to.to_s.upcase}Field") [c.new] * count end yield a.members if block_given? fields << a end
Define a blanking field (it's return value is always nil)
# File lib/depix/binary/structure.rb, line 68 def self.blanking(name, *extras) length, opts = count_and_opts_from(extras) attr_accessor name fields << BlankingField.new( {:name => name, :length => length}.merge(opts) ) end
Only relevant for 1.9
# File lib/depix/binary/structure.rb, line 183 def self.byteify_string(string) string.force_encoding("ASCII-8BIT") end
Define a char field
# File lib/depix/binary/structure.rb, line 111 def self.char(name, *extras) count, opts = count_and_opts_from(extras) attr_accessor name fields << CharField.new( {:name => name, :length => count}.merge(opts) ) end
Allows us to use field names from Fields module
# File lib/depix/binary/structure.rb, line 37 def self.const_missing(c) Depix::Binary::Fields.const_get(c) end
Consume a stack of unpacked values, letting each field decide how many to consume
# File lib/depix/binary/structure.rb, line 134 def self.consume!(stack_of_unpacked_values) new_item = new @fields.each do | field | new_item.send("#{field.name}=", field.consume!(stack_of_unpacked_values)) unless field.name.nil? end new_item end
Get the array of fields defined in this struct
# File lib/depix/binary/structure.rb, line 42 def self.fields @fields ||= [] end
Get an opaque struct based on this one, that will consume exactly as many bytes as this structure would occupy, but discard them instead
# File lib/depix/binary/structure.rb, line 178 def self.filler only([]) end
Define a nested struct
# File lib/depix/binary/structure.rb, line 104 def self.inner(name, mapped_to, *extras) count, opts = count_and_opts_from(extras) attr_accessor name fields << InnerField.new({:name => name, :cast => mapped_to}.merge(opts)) end
How many bytes are needed to complete this structure
# File lib/depix/binary/structure.rb, line 129 def self.length fields.inject(0){|_, s| _ + s.length.to_i } end
Get a class that would parse just the same, preserving only the fields passed in the array. This speeds up parsing because we only extract and conform the fields that we need
# File lib/depix/binary/structure.rb, line 155 def self.only(*field_names) distillate = fields.inject([]) do | m, f | if field_names.include?(f.name) # preserve m.push(f) else # create filler unless m[-1].is_a?(Filler) m.push(Filler.new(:length => f.length)) else m[-1].length += f.length end m end end anon = Class.new(self) anon.fields.replace(distillate) only_items = distillate.map{|n| n.name } anon end
Pack the instance of this struct
# File lib/depix/binary/structure.rb, line 188 def self.pack(instance, buffer = nil) # Preallocate a buffer just as big as me since we want everything to remain at fixed offsets buffer ||= (0xFF.chr * length) # We need to enforce ASCII-8bit encoding which in Ruby parlance is actually "bytestream" byteify_string(buffer) unless RUBY_VERSION < '1.9.0' # If the instance is nil return pure padding return buffer if instance.nil? # Now for the important stuff. For each field that we have, replace a piece at offsets in the buffer # with the packed results, skipping fillers fields.each_with_index do | f, i | # Skip blanking, we just dont touch it. TODO - test! next if f.is_a?(Filler) # Where should we put that value? offset = fields[0...i].inject(0){|_, s| _ + s.length } val = instance.send(f.name) # Validate the passed value using the format the field supports f.validate!(val) packed = f.pack(val) # Signal offset violation raise "Improper length for #{f.name} - packed #{packed.length} bytes but #{f.length} is required to fill the slot" if packed.length != f.length # See above, byt we need to do this with the packed string as well byteify_string(packed) unless RUBY_VERSION < '1.9.0' buffer[offset...(offset+f.length)] = packed end raise "Resulting buffer not the same length, expected #{length} bytes but compued #{buffer.length}" if buffer.length != length buffer end
Get the pattern that will be used to unpack this structure and all of it's descendants
# File lib/depix/binary/structure.rb, line 118 def self.pattern fields.map{|f| f.pattern }.join end
Get the pattern that will be used to unpack this structure and all of it's descendants from a buffer with pieces in little-endian byte order
# File lib/depix/binary/structure.rb, line 124 def self.pattern_le pattern.tr("gN", "eV") end
Define a real number
# File lib/depix/binary/structure.rb, line 82 def self.r32(name, *extras) count, opts = count_and_opts_from(extras) attr_accessor name fields << R32Field.new( {:name => name}.merge(opts) ) end
Define a double-width unsigned integer
# File lib/depix/binary/structure.rb, line 61 def self.u16(name, *extras) count, opts = count_and_opts_from(extras) attr_accessor name fields << U16Field.new( {:name => name }.merge(opts) ) end
Define a 4-byte unsigned integer
# File lib/depix/binary/structure.rb, line 54 def self.u32(name, *extras) count, opts = count_and_opts_from(extras) attr_accessor name fields << U32Field.new( {:name => name }.merge(opts) ) end
Define a small unsigned integer
# File lib/depix/binary/structure.rb, line 75 def self.u8(name, *extras) count, opts = count_and_opts_from(extras) attr_accessor name fields << U8Field.new( {:name => name }.merge(opts) ) end
Validate a passed instance
# File lib/depix/binary/structure.rb, line 47 def self.validate!(instance) fields.each do | f | f.validate!(instance.send(f.name)) if f.name end end
Private Class Methods
extract_options! on a diet
# File lib/depix/binary/structure.rb, line 230 def self.count_and_opts_from(args) options, count = (args[-1].is_a?(Hash) ? DEF_OPTS.merge(args.pop) : DEF_OPTS), (args.shift || 1) [count, options] end
Public Instance Methods
# File lib/depix/binary/structure.rb, line 241 def [](field) send(field) end
# File lib/depix/binary/structure.rb, line 237 def []=(field, value) send("#{field}=", value) end