require 'bindata/base'
require 'bindata/sanitize'
module BinData
# A Struct is an ordered collection of named data objects.
#
# require 'bindata'
#
# class Tuple < BinData::MultiValue
# int8 :x
# int8 :y
# int8 :z
# end
#
# obj = BinData::Struct.new(:hide => :a,
# :fields => [ [:int32le, :a],
# [:int16le, :b],
# [:tuple, :s] ])
# obj.field_names =># ["b", "s"]
#
#
# == 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. 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.
# :endian:: Either :little or :big. This specifies the default
# endian of any numerics in this struct, or in any
# nested data objects.
class Struct < BinData::Base
# These reserved words may not be used as field names
RESERVED = (::Hash.instance_methods +
%w{alias and begin break case class def defined do else elsif
end ensure false for if in module next nil not or redo
rescue retry return self super then true undef unless until
when while yield }).uniq
# Register this class
register(self.name, self)
# A hash that can be accessed via attributes.
class Snapshot < Hash #:nodoc:
def method_missing(symbol, *args)
self[symbol.id2name] || super
end
end
class << self
#### DEPRECATION HACK to allow inheriting from BinData::Struct
#
def inherited(subclass) #:nodoc:
if subclass != MultiValue
# warn about deprecated method - remove before releasing 1.0
warn "warning: inheriting from BinData::Struct in deprecated. Inherit from BinData::MultiValue instead."
register(subclass.name, subclass)
end
end
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
def hide(*args)
# note that fields are stored in an instance variable not a class var
@hide ||= []
args.each do |name|
@hide << name.to_s
end
@hide
end
def fields
@fields || []
end
def method_missing(symbol, *args)
name, params = args
type = symbol
name = name.to_s
params ||= {}
# note that fields are stored in an instance variable not a class var
@fields ||= []
# check that type is known
unless Sanitizer.type_exists?(type, endian)
raise TypeError, "unknown type '#{type}' for #{self}", caller
end
# check for duplicate names
@fields.each do |t, n, p|
if n == name
raise SyntaxError, "duplicate field '#{name}' in #{self}", caller
end
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 self::RESERVED.include?(name)
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
def deprecated_hack!(params)
# possibly override endian
endian = params[:endian] || self.endian
params[:endian] = endian unless endian.nil?
params[:fields] = params[:fields] || self.fields
params[:hide] = params[:hide] || self.hide
end
#
#### DEPRECATION HACK to allow inheriting from BinData::Struct
# Ensures that +params+ is of the form expected by #initialize.
def sanitize_parameters!(sanitizer, params)
#### DEPRECATION HACK to allow inheriting from BinData::Struct
#
deprecated_hack!(params)
#
#### DEPRECATION HACK to allow inheriting from BinData::Struct
# possibly override endian
endian = params[:endian]
if endian != nil
unless [:little, :big].include?(endian)
raise ArgumentError, "unknown value for endian '#{endian}'"
end
params[:endian] = endian
end
if params.has_key?(:fields)
sanitizer.with_endian(endian) do
# ensure names of fields are strings and that params is sanitized
all_fields = params[:fields].collect do |ftype, fname, fparams|
fname = fname.to_s
klass, sanitized_fparams = sanitizer.sanitize(ftype, fparams)
[klass, fname, sanitized_fparams]
end
params[:fields] = all_fields
end
# now params are sanitized, check that parameter names are okay
field_names = []
instance_methods = self.instance_methods
reserved_names = RESERVED
params[:fields].each do |fklass, fname, fparams|
# check that name doesn't shadow an existing method
if instance_methods.include?(fname)
raise NameError.new("Rename field '#{fname}' in #{self}, " +
"as it shadows an existing method.", fname)
end
# check that name isn't reserved
if reserved_names.include?(fname)
raise NameError.new("Rename field '#{fname}' in #{self}, " +
"as it is a reserved name.", fname)
end
# check for multiple definitions
if field_names.include?(fname)
raise NameError.new("field '#{fname}' in #{self}, " +
"is defined multiple times.", fname)
end
field_names << fname
end
# collect all hidden names that correspond to a field name
hide = []
if params.has_key?(:hide)
hidden = (params[:hide] || []).collect { |h| h.to_s }
all_field_names = params[:fields].collect { |k,n,p| n }
hide = hidden & all_field_names
end
params[:hide] = hide
end
super(sanitizer, params)
end
end
# These are the parameters used by this class.
mandatory_parameter :fields
optional_parameters :endian, :hide
# Creates a new Struct.
def initialize(params = {}, env = nil)
super(params, env)
# extract field names but don't instantiate the fields
@field_names = param(:fields).collect { |k, n, p| n }
@field_objs = []
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?
@field_objs.each { |f| f.clear unless f.nil? }
else
obj = find_obj_for_name(name.to_s)
obj.clear unless obj.nil?
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?
@field_objs.each do |f|
return false unless f.nil? or f.clear?
end
true
else
obj = find_obj_for_name(name.to_s)
obj.nil? ? true : obj.clear?
end
end
# Returns whether this data object contains a single value. Single
# value data objects respond to #value and #value=.
def single_value?
return false
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)
# collect field names
names = []
hidden = param(:hide)
@field_names.each do |name|
if include_hidden or not hidden.include?(name)
names << name
end
end
names
end
# To be called after calling #read.
def done_read
@field_objs.each { |f| f.done_read unless f.nil? }
end
# Returns the data object that stores values for +name+.
def find_obj_for_name(name)
idx = @field_names.index(name)
if idx
instantiate_obj(idx)
@field_objs[idx]
else
nil
end
end
def offset_of(field)
idx = @field_names.index(field.to_s)
if idx
instantiate_all
offset = 0
(0...idx).each do |i|
this_offset = @field_objs[i].do_num_bytes
if ::Float === offset and ::Integer === this_offset
offset = offset.ceil
end
offset += this_offset
end
offset
else
nil
end
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
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
else
super
end
end
#---------------
private
# Instantiates all fields.
def instantiate_all
(0...@field_names.length).each { |idx| instantiate_obj(idx) }
end
# Instantiates the field object at position +idx+.
def instantiate_obj(idx)
if @field_objs[idx].nil?
fklass, fname, fparams = param(:fields)[idx]
@field_objs[idx] = fklass.new(fparams, create_env)
end
end
# Reads the values for all fields in this object from +io+.
def _do_read(io)
instantiate_all
@field_objs.each { |f| f.do_read(io) }
end
# Writes the values for all fields in this object to +io+.
def _do_write(io)
instantiate_all
@field_objs.each { |f| f.do_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 _do_num_bytes(name)
if name.nil?
instantiate_all
(@field_objs.inject(0) { |sum, f| sum + f.do_num_bytes }).ceil
else
obj = find_obj_for_name(name.to_s)
obj.nil? ? 0 : obj.do_num_bytes
end
end
# Returns a snapshot of this struct as a hash.
def _snapshot
hash = Snapshot.new
field_names.each do |name|
ss = find_obj_for_name(name).snapshot
hash[name] = ss unless ss.nil?
end
hash
end
end
end