require 'bindata/base'
module BinData
# 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"]
#
# == 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.
class Struct < Base
# A hash that can be accessed via attributes.
class Snapshot < Hash #:nodoc:
def method_missing(symbol, *args)
self[symbol.id2name] || super
end
end
# Register this class
register(self.name, self)
class << self
# Register the names of all subclasses of this class.
def inherited(subclass) #:nodoc:
register(subclass.name, subclass)
end
# Returns the names of any hidden fields in this struct. Any given args
# are appended to the hidden list.
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
# Used to define fields for this structure.
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
# remember this field. These fields will be recalled upon creating
# an instance of this class
@fields.push([type, name, params])
end
# Returns all stored fields. Should only be called by #cleaned_params.
def fields
@fields || []
end
end
# These are the parameters used by this class.
mandatory_parameter :fields
optional_parameter :hide
# Creates a new Struct.
def initialize(params = {}, env = nil)
super(cleaned_params(params), env)
# create instances of the fields
@fields = param(:fields).collect do |type, name, params|
klass = self.class.lookup(type)
raise TypeError, "unknown type '#{type}' for #{self}" if klass.nil?
if methods.include?(name)
raise NameError.new("field '#{name}' shadows an existing method",name)
end
[name, klass.new(params, create_env)]
end
end
# Clears the field represented by +name+. If no +name+
# is given, clears all fields in the struct.
def clear(name = nil)
if name.nil?
bindata_objects.each { |f| f.clear }
else
find_obj_for_name(name.to_s).clear
end
end
# Returns if the field represented by +name+ is clear?. If no +name+
# is given, returns whether all fields are clear.
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
# Reads the values for all fields in this object from +io+.
def _do_read(io)
bindata_objects.each { |f| f.do_read(io) }
end
# To be called after calling #read.
def done_read
bindata_objects.each { |f| f.done_read }
end
# Writes the values for all fields in this object to +io+.
def _write(io)
bindata_objects.each { |f| f.write(io) }
end
# 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.
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
# Returns a snapshot of this struct as a hash.
def snapshot
# allow structs to fake single value
return value if single_value?
hash = Snapshot.new
field_names.each do |name|
hash[name] = find_obj_for_name(name).snapshot
end
hash
end
# Returns a list of the names of all fields accessible through this
# object. +include_hidden+ specifies whether to include hidden names
# in the listing.
def field_names(include_hidden = false)
# single values don't have any fields
return [] if single_value?
names = []
@fields.each do |name, obj|
if name != ""
names << name unless (param(:hide).include?(name) and !include_hidden)
else
names.concat(obj.field_names)
end
end
names
end
# Returns the data object that stores values for +name+.
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
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
# Override to include field names.
alias_method :orig_respond_to?, :respond_to?
def respond_to?(symbol, include_private = false)
orig_respond_to?(symbol, include_private) ||
field_names(true).include?(symbol.id2name.chomp("="))
end
# Returns whether this data object contains a single value. Single
# value data objects respond to #value and #value=.
def single_value?
# need to use original respond_to? to prevent infinite recursion
orig_respond_to?(:value)
end
def method_missing(symbol, *args)
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
else
super
end
end
#---------------
private
# Returns a list of all the bindata objects for this struct.
def bindata_objects
@fields.collect { |f| f[1] }
end
# Returns a hash of cleaned +params+. Cleaning means that param
# values are converted to a desired format.
def cleaned_params(params)
new_params = params.dup
# use fields defined in this class if no fields are passed as params
fields = new_params[:fields] || self.class.fields
# ensure the names of fields are strings and that params is a hash
new_params[:fields] = fields.collect do |t, n, p|
[t, n.to_s, (p || {}).dup]
end
# collect all non blank field names
field_names = new_params[:fields].collect { |f| f[1] }
field_names = field_names.delete_if { |n| n == "" }
# collect all hidden names that correspond to a field name
hide = []
(new_params[:hide] || self.class.hide).each do |h|
h = h.to_s
hide << h if field_names.include?(h)
end
new_params[:hide] = hide
new_params
end
end
end