require 'bindata/io'
require 'bindata/lazy'
require 'bindata/params'
require 'bindata/registry'
require 'bindata/sanitize'
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 parameters are:
#
# [: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
include AcceptedParametersMixin
optional_parameters :check_offset, :adjust_offset
optional_parameter :onlyif # Used by Struct
mutually_exclusive_parameters :check_offset, :adjust_offset
class << self
# Instantiates this class and reads from +io+, returning the newly
# created data object.
def read(io)
data = self.new
data.read(io)
data
end
# Registers this class for use.
def register_self
register_class(self)
end
# Registers all subclasses of this class for use
def register_subclasses
class << self
define_method(:inherited) do |subclass|
register_class(subclass)
end
end
end
def register_class(class_to_register) #:nodoc:
RegisteredClasses.register(class_to_register.name, class_to_register)
end
private :register_self, :register_subclasses, :register_class
end
# Creates a new data object.
#
# +parameters+ is a hash containing symbol keys. Some parameters may
# reference callable objects (methods or procs).
#
# +parent+ is the parent data object (e.g. struct, array, choice) this
# object resides under.
def initialize(parameters = {}, parent = nil)
@params = Sanitizer.sanitize(parameters, self.class)
@parent = parent
end
attr_reader :parent
# Returns the result of evaluating the parameter identified by +key+.
#
# +overrides+ is an optional +parameters+ like hash that allow the
# parameters given at object construction to be overridden.
#
# Returns nil if +key+ does not refer to any parameter.
def eval_parameter(key, overrides = {})
LazyEvaluator.eval(self, get_parameter(key), overrides)
end
# Returns the parameter referenced by +key+.
# Use this method if you are sure the parameter is not to be evaluated.
# You most likely want #eval_parameter.
def get_parameter(key)
@params[key]
end
# Returns whether +key+ exists in the +parameters+ hash.
def has_parameter?(key)
@params.has_parameter?(key)
end
# Reads data into this data object.
def read(io)
io = BinData::IO.new(io) unless BinData::IO === io
do_read(io)
done_read
self
end
def do_read(io) #:nodoc:
check_or_adjust_offset(io)
clear
_do_read(io)
end
def done_read #:nodoc:
_done_read
end
protected :do_read, :done_read
# Writes the value for this data object to +io+.
def write(io)
io = BinData::IO.new(io) unless BinData::IO === io
do_write(io)
io.flush
self
end
def do_write(io) #:nodoc:
_do_write(io)
end
protected :do_write
# Returns the number of bytes it will take to write this data object.
def num_bytes
do_num_bytes.ceil
end
def do_num_bytes #:nodoc:
_do_num_bytes
end
protected :do_num_bytes
# Assigns the value of +val+ to this data object. Note that +val+ will
# always be deep copied to ensure no aliasing problems can occur.
def assign(val)
_assign(val)
end
# Returns a snapshot of this data object.
def snapshot
_snapshot
end
# Returns the string representation of this data object.
def to_binary_s
io = BinData::IO.create_string_io
write(io)
io.rewind
io.read
end
# Return a human readable representation of this data object.
def inspect
snapshot.inspect
end
# Return a string representing this data object.
def to_s
snapshot.to_s
end
# Work with Ruby's pretty-printer library.
def pretty_print(pp) #:nodoc:
pp.pp(snapshot)
end
# Returns a user friendly name of this object for debugging purposes.
def debug_name
if @parent
@parent.debug_name_of(self)
else
"obj"
end
end
# Returns the offset of this object wrt to its most distant ancestor.
def offset
if @parent
@parent.offset + @parent.offset_of(self)
else
0
end
end
# Returns the offset of this object wrt to its parent.
def rel_offset
if @parent
@parent.offset_of(self)
else
0
end
end
def ==(other) #:nodoc:
# double dispatch
other == snapshot
end
#---------------
private
def check_or_adjust_offset(io)
if has_parameter?(:check_offset)
check_offset(io)
elsif has_parameter?(:adjust_offset)
adjust_offset(io)
end
end
def check_offset(io)
actual_offset = io.offset
expected = eval_parameter(:check_offset, :offset => actual_offset)
if not expected
raise ValidityError, "offset not as expected for #{debug_name}"
elsif actual_offset != expected and expected != true
raise ValidityError,
"offset is '#{actual_offset}' but " +
"expected '#{expected}' for #{debug_name}"
end
end
def adjust_offset(io)
actual_offset = io.offset
expected = eval_parameter(:adjust_offset)
if actual_offset != expected
begin
seek = expected - actual_offset
io.seekbytes(seek)
warn "adjusting stream position by #{seek} bytes" if $VERBOSE
rescue
raise ValidityError,
"offset is '#{actual_offset}' but couldn't seek to " +
"expected '#{expected}' for #{debug_name}"
end
end
end
###########################################################################
# To be implemented by subclasses
# Performs sanity checks on the given parameters. This method converts
# the parameters to the form expected by this data object.
def self.sanitize_parameters!(parameters, sanitizer) #:nodoc:
end
# 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?
raise NotImplementedError
end
# Returns the debug name of +child+. This only needs to be implemented
# by objects that contain child objects.
def debug_name_of(child) #:nodoc:
debug_name
end
# Returns the offset of +child+. This only needs to be implemented
# by objects that contain child objects.
def offset_of(child) #:nodoc:
0
end
# Reads the data for this data object from +io+.
def _do_read(io)
raise NotImplementedError
end
# Trigger function that is called after #do_read.
def _done_read
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
# Assigns the value of +val+ to this data object. Note that +val+ will
# always be deep copied to ensure no aliasing problems can occur.
def _assign(val)
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?, :debug_name_of, :offset_of
private :_do_read, :_done_read, :_do_write, :_do_num_bytes, :_assign, :_snapshot
# End To be implemented by subclasses
###########################################################################
end
end