Class: Depix::Binary::Structure

Inherits:
Object
  • Object
show all
Defined in:
lib/depix/binary/structure.rb

Overview

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

Constant Summary collapse

DEF_OPTS =
{ :req => false, :desc => nil }

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.apply!(string) ⇒ Object

Apply this structure to data in the string, returning an instance of this structure with fields completed



143
144
145
# File 'lib/depix/binary/structure.rb', line 143

def self.apply!(string)
  consume!(string.unpack(pattern))
end

.apply_le!(string) ⇒ Object

Apply this structure to data in the string, returning an instance of this structure with fields completed assume little-endian fields



149
150
151
# File 'lib/depix/binary/structure.rb', line 149

def self.apply_le!(string)
  consume!(string.unpack(pattern_le))
end

.array(name, mapped_to, *extras) {|a.members| ... } ⇒ Object

Define an array of values

Yields:

  • (a.members)


89
90
91
92
93
94
95
96
97
98
99
100
101
# 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

.blanking(name, *extras) ⇒ Object

Define a blanking field (it’s return value is always nil)



68
69
70
71
72
# 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

.byteify_string(string) ⇒ Object

Only relevant for 1.9



183
184
185
# File 'lib/depix/binary/structure.rb', line 183

def self.byteify_string(string)
  string.force_encoding("ASCII-8BIT")
end

.char(name, *extras) ⇒ Object

Define a char field



111
112
113
114
115
# 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

.const_missing(c) ⇒ Object

Allows us to use field names from Fields module



37
38
39
# File 'lib/depix/binary/structure.rb', line 37

def self.const_missing(c)
  Depix::Binary::Fields.const_get(c)
end

.consume!(stack_of_unpacked_values) ⇒ Object

Consume a stack of unpacked values, letting each field decide how many to consume



134
135
136
137
138
139
140
# 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

.fieldsObject

Get the array of fields defined in this struct



42
43
44
# File 'lib/depix/binary/structure.rb', line 42

def self.fields
  @fields ||= []
end

.fillerObject

Get an opaque struct based on this one, that will consume exactly as many bytes as this structure would occupy, but discard them instead



178
179
180
# File 'lib/depix/binary/structure.rb', line 178

def self.filler
  only([])
end

.inner(name, mapped_to, *extras) ⇒ Object

Define a nested struct



104
105
106
107
108
# 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

.lengthObject

How many bytes are needed to complete this structure



129
130
131
# File 'lib/depix/binary/structure.rb', line 129

def self.length
  fields.inject(0){|_, s| _ + s.length.to_i }
end

.only(*field_names) ⇒ Object

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



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# 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(instance, buffer = nil) ⇒ Object

Pack the instance of this struct



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# 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

.patternObject

Get the pattern that will be used to unpack this structure and all of it’s descendants



118
119
120
# File 'lib/depix/binary/structure.rb', line 118

def self.pattern
  fields.map{|f| f.pattern }.join
end

.pattern_leObject

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



124
125
126
# File 'lib/depix/binary/structure.rb', line 124

def self.pattern_le
  pattern.tr("gN", "eV")
end

.r32(name, *extras) ⇒ Object

Define a real number



82
83
84
85
86
# 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

.u16(name, *extras) ⇒ Object

Define a double-width unsigned integer



61
62
63
64
65
# 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

.u32(name, *extras) ⇒ Object

Define a 4-byte unsigned integer



54
55
56
57
58
# 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

.u8(name, *extras) ⇒ Object

Define a small unsigned integer



75
76
77
78
79
# 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!(instance) ⇒ Object

Validate a passed instance



47
48
49
50
51
# 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

Instance Method Details

#[](field) ⇒ Object



241
242
243
# File 'lib/depix/binary/structure.rb', line 241

def [](field)
  send(field)
end

#[]=(field, value) ⇒ Object



237
238
239
# File 'lib/depix/binary/structure.rb', line 237

def []=(field, value)
  send("#{field}=", value)
end