require 'bindata/io'
require 'bindata/lazy'
require 'bindata/params'
require 'bindata/registry'
require 'bindata/sanitize'
require 'stringio'
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] Deprecated. An alias for :onlyif.
# [:onlyif] Used to indicate a data object is optional.
# if false, calls to #read or #write will not
# perform any I/O, #num_bytes will return 0 and
# #snapshot will return nil. 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.
# [:adjust_offset] Ensures that the current IO offset is at this
# position before reading. This is like
# :check_offset, except that it will
# adjust the IO offset instead of raising an error.
class Base
class << self
extend Parameters
# Define methods for:
# bindata_mandatory_parameters
# bindata_optional_parameters
# bindata_default_parameters
# bindata_mutually_exclusive_parameters
define_x_parameters(:bindata_mandatory, []) do |array, args|
args.each { |arg| array << arg.to_sym }
array.uniq!
end
define_x_parameters(:bindata_optional, []) do |array, args|
args.each { |arg| array << arg.to_sym }
array.uniq!
end
define_x_parameters(:bindata_default, {}) do |hash, args|
params = args.length > 0 ? args[0] : {}
hash.merge!(params)
end
define_x_parameters(:bindata_mutually_exclusive, []) do |array, args|
array << [args[0].to_sym, args[1].to_sym]
end
# Returns a list of internal parameters that are accepted by this object
def internal_parameters
(bindata_mandatory_parameters + bindata_optional_parameters +
bindata_default_parameters.keys).uniq
end
# Ensures that +params+ is of the form expected by #initialize.
def sanitize_parameters!(sanitizer, params)
# replace :readwrite with :onlyif
if params.has_key?(:readwrite)
warn ":readwrite is deprecated. Replacing with :onlyif"
params[:onlyif] = params.delete(:readwrite)
end
# add default parameters
bindata_default_parameters.each do |k,v|
params[k] = v unless params.has_key?(k)
end
# ensure mandatory parameters exist
bindata_mandatory_parameters.each do |prm|
if not params.has_key?(prm)
raise ArgumentError, "parameter ':#{prm}' must be specified " +
"in #{self}"
end
end
# ensure mutual exclusion
bindata_mutually_exclusive_parameters.each do |param1, param2|
if params.has_key?(param1) and params.has_key?(param2)
raise ArgumentError, "params #{param1} and #{param2} " +
"are mutually exclusive"
end
end
end
# Can this data object self reference itself?
def recursive?
false
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
end
# Define the parameters we use in this class.
bindata_optional_parameters :check_offset, :adjust_offset
bindata_default_parameters :onlyif => true
bindata_mutually_exclusive_parameters :check_offset, :adjust_offset
# Creates a new data object.
#
# +params+ is a hash containing symbol keys. Some params may
# reference callable objects (methods or procs). +parent+ is the
# parent data object (e.g. struct, array, choice) this object resides
# under.
def initialize(params = {}, parent = nil)
@params = Sanitizer.sanitize(self.class, params)
@parent = parent
end
# The parent data object.
attr_accessor :parent
# Returns all the custom parameters supplied to this data object.
def parameters
@params.extra_parameters
end
# Reads data into this data object by calling #do_read then #done_read.
def read(io)
io = BinData::IO.new(io) unless BinData::IO === io
do_read(io)
done_read
self
end
# Reads the value for this data from +io+.
def do_read(io)
raise ArgumentError, "io must be a BinData::IO" unless BinData::IO === io
clear
check_offset(io)
if eval_param(:onlyif)
_do_read(io)
end
end
# Writes the value for this data to +io+ by calling #do_write.
def write(io)
io = BinData::IO.new(io) unless BinData::IO === io
do_write(io)
io.flush
self
end
# Writes the value for this data to +io+.
def do_write(io)
raise ArgumentError, "io must be a BinData::IO" unless BinData::IO === io
if eval_param(:onlyif)
_do_write(io)
end
end
# Returns the number of bytes it will take to write this data by calling
# #do_num_bytes.
def num_bytes(what = nil)
num = do_num_bytes(what)
num.ceil
end
# Returns the number of bytes it will take to write this data.
def do_num_bytes(what = nil)
if eval_param(:onlyif)
_do_num_bytes(what)
else
0
end
end
# Returns a snapshot of this data object.
# Returns nil if :onlyif is false
def snapshot
if eval_param(:onlyif)
_snapshot
else
nil
end
end
# Returns the string representation of this data object.
def to_s
io = StringIO.new
write(io)
io.rewind
io.read
end
# Return a human readable representation of this object.
def inspect
snapshot.inspect
end
# Returns the object this object represents.
def obj
self
end
#---------------
private
# 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 = nil)
LazyEvaluator.eval(no_eval_param(key), self, 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 no_eval_param(key)
@params.internal_parameters[key]
end
# Returns whether +key+ exists in the +params+ hash used when creating
# this data object.
def has_param?(key)
@params.internal_parameters.has_key?(key)
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.offset
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
elsif has_param?(:adjust_offset)
actual_offset = io.offset
expected = eval_param(:adjust_offset)
if actual_offset != expected
begin
seek = expected - actual_offset
io.seekbytes(seek)
warn "adjusting stream position by #{seek} bytes" if $VERBOSE
rescue
# could not seek so raise an error
raise ValidityError, "offset is '#{actual_offset}' but " +
"couldn't seek to expected '#{expected}'"
end
end
end
end
###########################################################################
# To be implemented by subclasses
# Resets the internal state to that of a newly created object.
def clear
raise NotImplementedError
end
# Returns true if the object has not been changed since creation.
def clear?(*args)
raise NotImplementedError
end
# Returns whether this data object contains a single value. Single
# value data objects respond to #value and #value=.
def single_value?
raise NotImplementedError
end
# To be called after calling #do_read.
def done_read
raise NotImplementedError
end
# Reads the data for this data object from +io+.
def _do_read(io)
raise NotImplementedError
end
# Writes the value for this data to +io+.
def _do_write(io)
raise NotImplementedError
end
# Returns the number of bytes it will take to write this data.
def _do_num_bytes
raise NotImplementedError
end
# Returns a snapshot of this data object.
def _snapshot
raise NotImplementedError
end
# Set visibility requirements of methods to implement
public :clear, :clear?, :single_value?, :done_read
private :_do_read, :_do_write, :_do_num_bytes, :_snapshot
# End To be implemented by subclasses
###########################################################################
end
end