require 'bindata/lazy'
require 'bindata/registry'
module BinData
# Error raised when unexpected results occur when reading data from IO.
class ValidityError < StandardError ; end
# This is the abstract base class for all data objects.
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params are:
#
# [:readwrite] If false, calls to #read or #write will
# not perform any I/O. Default is true.
# [:check_offset] Raise an error if the current IO offset doesn't
# meet this criteria. A boolean return indicates
# success or failure. Any other return is compared
# to the current offset. The variable +offset+
# is made available to any lambda assigned to
# this parameter. This parameter is only checked
# before reading.
class Base
class << self
# Returns the mandatory parameters used by this class. Any given args
# are appended to the parameters list. The parameters for a class will
# include the parameters of its ancestors.
def mandatory_parameters(*args)
unless defined? @mandatory_parameters
@mandatory_parameters = []
ancestors[1..-1].each do |parent|
if parent.respond_to?(:mandatory_parameters)
@mandatory_parameters.concat(parent.mandatory_parameters)
end
end
end
if not args.empty?
args.each { |arg| @mandatory_parameters << arg.to_sym }
@mandatory_parameters.uniq!
end
@mandatory_parameters
end
alias_method :mandatory_parameter, :mandatory_parameters
# Returns the optional parameters used by this class. Any given args
# are appended to the parameters list. The parameters for a class will
# include the parameters of its ancestors.
def optional_parameters(*args)
unless defined? @optional_parameters
@optional_parameters = []
ancestors[1..-1].each do |parent|
if parent.respond_to?(:optional_parameters)
@optional_parameters.concat(parent.optional_parameters)
end
end
end
if not args.empty?
args.each { |arg| @optional_parameters << arg.to_sym }
@optional_parameters.uniq!
end
@optional_parameters
end
alias_method :optional_parameter, :optional_parameters
# Returns both the mandatory and optional parameters used by this class.
def parameters
(mandatory_parameters + optional_parameters).uniq
end
# Instantiates this class and reads from +io+. For single value objects
# just the value is returned, otherwise the newly created data object is
# returned.
def read(io)
data = self.new
data.read(io)
data.single_value? ? data.value : data
end
# Registers the mapping of +name+ to +klass+.
def register(name, klass)
Registry.instance.register(name, klass)
end
private :register
# Returns the class matching a previously registered +name+.
def lookup(name)
klass = Registry.instance.lookup(name)
if klass.nil?
# lookup failed so attempt endian lookup
if self.respond_to?(:endian) and self.endian != nil
name = name.to_s
if /^u?int\d\d?$/ =~ name
new_name = name + ((self.endian == :little) ? "le" : "be")
klass = Registry.instance.lookup(new_name)
elsif ["float", "double"].include?(name)
new_name = name + ((self.endian == :little) ? "_le" : "_be")
klass = Registry.instance.lookup(new_name)
end
end
end
klass
end
end
# Define the parameters we use in this class.
optional_parameters :check_offset, :readwrite
# Creates a new data object.
#
# +params+ is a hash containing symbol keys. Some params may
# reference callable objects (methods or procs). +env+ is the
# environment that these callable objects are evaluated in.
def initialize(params = {}, env = nil)
# default :readwrite param to true if unspecified
if not params.has_key?(:readwrite)
params = params.dup
params[:readwrite] = true
end
# ensure mandatory parameters exist
self.class.mandatory_parameters.each do |prm|
if not params.has_key?(prm)
raise ArgumentError, "parameter ':#{prm}' must be specified " +
"in #{self}"
end
end
known_params = self.class.parameters
# partition parameters into known and extra parameters
@params = {}
extra = {}
params.each do |k,v|
k = k.to_sym
raise ArgumentError, "parameter :#{k} is nil in #{self}" if v.nil?
if known_params.include?(k)
@params[k] = v.freeze
else
extra[k] = v.freeze
end
end
# set up the environment
@env = env || LazyEvalEnv.new
@env.params = extra
@env.data_object = self
end
# Returns the class matching a previously registered +name+.
def klass_lookup(name)
klass = self.class.lookup(name)
if klass.nil? and @env.parent_data_object != nil
# lookup failed so retry in the context of the parent data object
klass = @env.parent_data_object.klass_lookup(name)
end
klass
end
# Reads data into this bin object by calling #do_read then #done_read.
def read(io)
# remember the current position in the IO object
io.instance_eval "def mark; #{io.pos}; end"
do_read(io)
done_read
end
# Reads the value for this data from +io+.
def do_read(io)
clear
check_offset(io)
_do_read(io) if eval_param(:readwrite) != false
end
# Writes the value for this data to +io+.
def write(io)
_write(io) if eval_param(:readwrite) != false
end
# Returns the number of bytes it will take to write this data.
def num_bytes(what = nil)
(eval_param(:readwrite) != false) ? _num_bytes(what) : 0
end
# Returns whether this data object contains a single value. Single
# value data objects respond to #value and #value=.
def single_value?
respond_to? :value
end
#---------------
private
# Creates a new LazyEvalEnv for use by a child data object.
def create_env
LazyEvalEnv.new(@env)
end
# Returns the value of the evaluated parameter. +key+ references a
# parameter from the +params+ hash used when creating the data object.
# +values+ contains data that may be accessed when evaluating +key+.
# Returns nil if +key+ does not refer to any parameter.
def eval_param(key, values = {})
@env.lazy_eval(@params[key], values)
end
# Returns the parameter from the +params+ hash referenced by +key+.
# Use this method if you are sure the parameter is not to be evaluated.
# You most likely want #eval_param.
def param(key)
@params[key]
end
# Returns whether +key+ exists in the +params+ hash used when creating
# this data object.
def has_param?(key)
@params.has_key?(key.to_sym)
end
# Raise an error if +param1+ and +param2+ are both given as params.
def ensure_mutual_exclusion(param1, param2)
if has_param?(param1) and has_param?(param2)
raise ArgumentError, "params #{param1} and #{param2} " +
"are mutually exclusive"
end
end
# Checks that the current offset of +io+ is as expected. This should
# be called from #do_read before performing the reading.
def check_offset(io)
if has_param?(:check_offset)
actual_offset = io.pos - io.mark
expected = eval_param(:check_offset, :offset => actual_offset)
if not expected
raise ValidityError, "offset not as expected"
elsif actual_offset != expected and expected != true
raise ValidityError, "offset is '#{actual_offset}' but " +
"expected '#{expected}'"
end
end
end
=begin
# To be implemented by subclasses
# Resets the internal state to that of a newly created object.
def clear
raise NotImplementedError
end
# Reads the data for this data object from +io+.
def _do_read(io)
raise NotImplementedError
end
# To be called after calling #do_read.
def done_read
raise NotImplementedError
end
# Writes the value for this data to +io+.
def _write(io)
raise NotImplementedError
end
# Returns the number of bytes it will take to write this data.
def _num_bytes
raise NotImplementedError
end
# Returns a snapshot of this data object.
def snapshot
raise NotImplementedError
end
# Returns a list of the names of all fields accessible through this
# object.
def field_names
raise NotImplementedError
end
# To be implemented by subclasses
=end
end
end