Class: BinData::Struct

Inherits:
Base
  • Object
show all
Defined in:
lib/bindata/struct.rb

Overview

A Struct is an ordered collection of named data objects.

require 'bindata'

class Tuple < BinData::Struct
  int8  :x
  int8  :y
  int8  :z
end

class SomeStruct < BinData::Struct
  hide 'a'

  int32le :a
  int16le :b
  tuple   nil
end

obj = SomeStruct.new
obj.field_names   =># ["b", "x", "y", "z"]

class PascalString < BinData::Struct
  delegate :data

  uint8  :len, :value => lambda { data.length }
  string :data, :read_length => :len
end

str = PascalString.new
str.value = "a test string"
str.single_value?   =># true
str.len         =># 13
str.num_bytes   =># 17

Parameters

Parameters may be provided at initialisation to control the behaviour of an object. These params are:

:fields

An array specifying the fields for this struct. Each element of the array is of the form [type, name, params]. Type is a symbol representing a registered type. Name is the name of this field. Name may be nil as in the example above. Params is an optional hash of parameters to pass to this field when instantiating it.

:hide

A list of the names of fields that are to be hidden from the outside world. Hidden fields don’t appear in #snapshot or #field_names but are still accessible by name.

:delegate

Forwards unknown methods calls and unknown params to this field.

Defined Under Namespace

Classes: Snapshot

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#do_read, #inspect, #klass_lookup, lookup, mandatory_parameters, #num_bytes, optional_parameters, parameters, #read, read, register, #write

Constructor Details

#initialize(params = {}, env = nil) ⇒ Struct

Creates a new Struct.



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
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
# File 'lib/bindata/struct.rb', line 157

def initialize(params = {}, env = nil)
  super(cleaned_params(params), env)

  all_methods = methods
  all_reserved_methods = nil
  delegate_name = param(:delegate)

  # get all reserved field names
  res = param(:fields).find { |type, name, params| name == delegate_name }
  if res
    # all methods and field_names of the delegate are reserved.
    klass_name = res[0]
    delegate_klass = klass_lookup(klass_name)
    if delegate_klass.nil?
      raise TypeError, "unknown type '#{klass_name} for #{self}"
    end

    delegate_params = res[2]
    delegate = delegate_klass.new(delegate_params, create_env)
    all_reserved_methods = delegate.methods + delegate.field_names -
                             all_methods

    # move accepted params from this object to the delegate object
    env_params = @env.params.dup
    delegate.accepted_parameters.each do |p|
      if (v = env_params.delete(p))
        delegate_params[p] = v
      end
    end
    @env.params = env_params
  else
    # no delegate so all instance methods of Hash are reserved
    all_reserved_methods = Hash.instance_methods - all_methods
  end

  # check if field names conflict with any reserved names
  field_names = param(:fields).collect { |f| f[1] }
  field_names_okay = (all_methods & field_names).empty? &&
                       (all_reserved_methods & field_names).empty?

  # create instances of the fields
  @fields = param(:fields).collect do |type, name, params|
    klass = klass_lookup(type)
    raise TypeError, "unknown type '#{type}' for #{self}" if klass.nil?
    if not field_names_okay
      # at least one field names conflicts so test them all.
      # rationale - #include? is expensive so we avoid it if possible.
      if all_methods.include?(name)
        raise NameError.new("field '#{name}' shadows an existing method",name)
      end
      if all_reserved_methods.include?(name)
        raise NameError.new("field '#{name}' is a reserved name",name)
      end
    end
    [name, klass.new(params, create_env)]
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(symbol, *args, &block) ⇒ Object



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/bindata/struct.rb', line 349

def method_missing(symbol, *args, &block)
  name = symbol.id2name

  is_writer = (name[-1, 1] == "=")
  name.chomp!("=")

  # find the object that is responsible for name
  if (obj = find_obj_for_name(name))
    # pass on the request
    if obj.single_value? and is_writer
      obj.value = *args
    elsif obj.single_value?
      obj.value
    else
      obj
    end
  elsif delegate_object.respond_to?(symbol)
    delegate_object.__send__(symbol, *args, &block)
  else
    super
  end
end

Class Method Details

.delegate(name = nil) ⇒ Object

Returns the name of the delegate field for this struct. The delegate is set to name if given.



89
90
91
92
93
94
95
# File 'lib/bindata/struct.rb', line 89

def delegate(name=nil)
  @delegate ||= nil
  if name != nil
    @delegate = name.to_s
  end
  @delegate
end

.endian(endian = nil) ⇒ Object

Returns or sets the endianess of numerics used in this stucture. Endianess is applied to the fields of this structure. Valid values are :little and :big.



77
78
79
80
81
82
83
84
85
# File 'lib/bindata/struct.rb', line 77

def endian(endian = nil)
  @endian ||= nil
  if [:little, :big].include?(endian)
    @endian = endian
  elsif endian != nil
    raise ArgumentError, "unknown value for endian '#{endian}'"
  end
  @endian
end

.fieldsObject

Returns all stored fields. Should only be called by #cleaned_params.



147
148
149
# File 'lib/bindata/struct.rb', line 147

def fields
  @fields || []
end

.hide(*args) ⇒ Object

Returns the names of any hidden fields in this struct. Any given args are appended to the hidden list.



99
100
101
102
103
104
105
106
107
# File 'lib/bindata/struct.rb', line 99

def hide(*args)
  # note that fields are stored in an instance variable not a class var
  @hide ||= []
  args.each do |name|
    next if name.nil?
    @hide << name.to_s
  end
  @hide
end

.inherited(subclass) ⇒ Object

Register the names of all subclasses of this class.



70
71
72
# File 'lib/bindata/struct.rb', line 70

def inherited(subclass) #:nodoc:
  register(subclass.name, subclass)
end

.method_missing(symbol, *args) ⇒ Object

Used to define fields for this structure.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/bindata/struct.rb', line 110

def method_missing(symbol, *args)
  name, params = args

  type = symbol
  name = name.to_s unless name.nil?
  params ||= {}

  if lookup(type).nil?
    raise TypeError, "unknown type '#{type}' for #{self}", caller
  end

  # note that fields are stored in an instance variable not a class var

  # check for duplicate names
  @fields ||= []
  if @fields.detect { |t, n, p| n == name and n != nil }
    raise SyntaxError, "duplicate field '#{name}' in #{self}", caller
  end

  # check that name doesn't shadow an existing method
  if self.instance_methods.include?(name)
    raise NameError.new("", name),
          "field '#{name}' shadows an existing method", caller
  end

  # check that name isn't reserved
  if Hash.instance_methods.include?(name) and delegate.nil?
    raise NameError.new("", name),
          "field '#{name}' is a reserved name", caller
  end

  # remember this field.  These fields will be recalled upon creating
  # an instance of this class
  @fields.push([type, name, params])
end

Instance Method Details

#_do_read(io) ⇒ Object

Reads the values for all fields in this object from io.



246
247
248
# File 'lib/bindata/struct.rb', line 246

def _do_read(io)
  bindata_objects.each { |f| f.do_read(io) }
end

#_num_bytes(name) ⇒ Object

Returns the number of bytes it will take to write the field represented by name. If name is nil then returns the number of bytes required to write all fields.



263
264
265
266
267
268
269
# File 'lib/bindata/struct.rb', line 263

def _num_bytes(name)
  if name.nil?
    bindata_objects.inject(0) { |sum, f| sum + f.num_bytes }
  else
    find_obj_for_name(name.to_s).num_bytes
  end
end

#_write(io) ⇒ Object

Writes the values for all fields in this object to io.



256
257
258
# File 'lib/bindata/struct.rb', line 256

def _write(io)
  bindata_objects.each { |f| f.write(io) }
end

#accepted_parametersObject

Returns a list of parameters that are accepted by this object



216
217
218
219
220
221
222
# File 'lib/bindata/struct.rb', line 216

def accepted_parameters
  if delegate_object != nil
    delegate_object.accepted_parameters
  else
    super
  end
end

#clear(name = nil) ⇒ Object

Clears the field represented by name. If no name is given, clears all fields in the struct.



226
227
228
229
230
231
232
# File 'lib/bindata/struct.rb', line 226

def clear(name = nil)
  if name.nil?
    bindata_objects.each { |f| f.clear }
  else
    find_obj_for_name(name.to_s).clear
  end
end

#clear?(name = nil) ⇒ Boolean

Returns if the field represented by name is clear?. If no name is given, returns whether all fields are clear.

Returns:

  • (Boolean)


236
237
238
239
240
241
242
243
# File 'lib/bindata/struct.rb', line 236

def clear?(name = nil)
  if name.nil?
    bindata_objects.each { |f| return false if not f.clear? }
    true
  else
    find_obj_for_name(name.to_s).clear?
  end
end

#done_readObject

To be called after calling #read.



251
252
253
# File 'lib/bindata/struct.rb', line 251

def done_read
  bindata_objects.each { |f| f.done_read }
end

#field_names(include_hidden = false) ⇒ Object

Returns a list of the names of all fields accessible through this object. include_hidden specifies whether to include hidden names in the listing.



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/bindata/struct.rb', line 287

def field_names(include_hidden = false)
  if delegate_object != nil and !include_hidden
    # delegate if possible
    delegate_object.field_names
  else
    # collect field names
    names = []
    hidden = param(:hide)
    @fields.each do |name, obj|
      if name != ""
        if include_hidden or not hidden.include?(name)
          names << name
        end
      else
        names.concat(obj.field_names)
      end
    end
    names
  end
end

#find_obj_for_name(name) ⇒ Object

Returns the data object that stores values for name.



309
310
311
312
313
314
315
316
317
318
# File 'lib/bindata/struct.rb', line 309

def find_obj_for_name(name)
  @fields.each do |n, o|
    if n == name
      return o
    elsif n == "" and o.field_names.include?(name)
      return o.find_obj_for_name(name)
    end
  end
  nil
end

#offset_of(field) ⇒ Object



320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/bindata/struct.rb', line 320

def offset_of(field)
  field_name = field.to_s
  offset = 0
  @fields.each do |name, obj|
    if name != ""
      break if name == field_name
      offset += obj.num_bytes
    elsif obj.field_names.include?(field_name)
      offset += obj.offset_of(field)
      break
    end
  end
  offset
end

#orig_respond_to?Object

Override to include field names and delegate methods.



336
# File 'lib/bindata/struct.rb', line 336

alias_method :orig_respond_to?, :respond_to?

#respond_to?(symbol, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


337
338
339
340
341
# File 'lib/bindata/struct.rb', line 337

def respond_to?(symbol, include_private = false)
  orig_respond_to?(symbol, include_private) ||
    field_names(true).include?(symbol.id2name.chomp("=")) ||
      delegate_object.respond_to?(symbol, include_private)
end

#single_value?Boolean

Returns whether this data object contains a single value. Single value data objects respond to #value and #value=.

Returns:

  • (Boolean)


345
346
347
# File 'lib/bindata/struct.rb', line 345

def single_value?
  delegate_object ? delegate_object.single_value? : false
end

#snapshotObject

Returns a snapshot of this struct as a hash.



272
273
274
275
276
277
278
279
280
281
282
# File 'lib/bindata/struct.rb', line 272

def snapshot
  if delegate_object != nil
    delegate_object.snapshot
  else
    hash = Snapshot.new
    field_names.each do |name|
      hash[name] = find_obj_for_name(name).snapshot
    end
    hash
  end
end