lib/bindata/base.rb in bindata-0.8.1 vs lib/bindata/base.rb in bindata-0.9.0
- old
+ new
@@ -1,7 +1,9 @@
require 'bindata/lazy'
+require 'bindata/sanitize'
require 'bindata/registry'
+require 'stringio'
module BinData
# Error raised when unexpected results occur when reading data from IO.
class ValidityError < StandardError ; end
@@ -19,10 +21,14 @@
# 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.
+ # [<tt>:adjust_offset</tt>] Ensures that the current IO offset is at this
+ # position before reading. This is like
+ # <tt>:check_offset</tt>, except that it will
+ # adjust the IO offset instead of raising an error.
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.
@@ -61,17 +67,81 @@
end
@optional_parameters
end
alias_method :optional_parameter, :optional_parameters
- # Returns both the mandatory and optional parameters used by this class.
- def parameters
- # warn about deprecated method - remove before releasing 1.0
- warn "warning: #parameters is deprecated."
- (mandatory_parameters + optional_parameters).uniq
+ # Returns the default 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 default_parameters(params = {})
+ unless defined? @default_parameters
+ @default_parameters = {}
+ ancestors[1..-1].each do |parent|
+ if parent.respond_to?(:default_parameters)
+ @default_parameters = @default_parameters.merge(parent.default_parameters)
+ end
+ end
+ end
+ if not params.empty?
+ @default_parameters = @default_parameters.merge(params)
+ end
+ @default_parameters
end
+ alias_method :default_parameter, :default_parameters
+ # Returns the pairs of mutually exclusive 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 mutually_exclusive_parameters(*args)
+ unless defined? @mutually_exclusive_parameters
+ @mutually_exclusive_parameters = []
+ ancestors[1..-1].each do |parent|
+ if parent.respond_to?(:mutually_exclusive_parameters)
+ @mutually_exclusive_parameters.concat(parent.mutually_exclusive_parameters)
+ end
+ end
+ end
+ if not args.empty?
+ @mutually_exclusive_parameters << [args[0].to_sym, args[1].to_sym]
+ end
+ @mutually_exclusive_parameters
+ end
+
+ # Returns a list of parameters that are accepted by this object
+ def accepted_parameters
+ (mandatory_parameters + optional_parameters + default_parameters.keys).uniq
+ end
+
+ # Returns a sanitized +params+ that is of the form expected
+ # by #initialize.
+ def sanitize_parameters(params, *args)
+ params = params.dup
+
+ # add default parameters
+ default_parameters.each do |k,v|
+ params[k] = v unless params.has_key?(k)
+ end
+
+ # ensure mandatory parameters exist
+ 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
+ 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
+
+ params
+ 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
@@ -84,100 +154,58 @@
Registry.instance.register(name, klass)
end
private :register
# Returns the class matching a previously registered +name+.
- def lookup(name)
+ def lookup(name, endian = nil)
+ name = name.to_s
klass = Registry.instance.lookup(name)
- if klass.nil?
+ if klass.nil? and endian != 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
+ if /^u?int\d{1,3}$/ =~ name
+ new_name = name + ((endian == :little) ? "le" : "be")
+ klass = Registry.instance.lookup(new_name)
+ elsif ["float", "double"].include?(name)
+ new_name = name + ((endian == :little) ? "_le" : "_be")
+ klass = Registry.instance.lookup(new_name)
end
end
klass
end
end
# Define the parameters we use in this class.
- optional_parameters :check_offset, :readwrite
+ optional_parameters :check_offset, :adjust_offset
+ default_parameters :readwrite => true
+ 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). +env+ is the
# environment that these callable objects are evaluated in.
def initialize(params = {}, env = nil)
- # all known parameters
- mandatory = self.class.mandatory_parameters
- optional = self.class.optional_parameters
-
- # default :readwrite param to true if unspecified
- if not params.has_key?(:readwrite)
- params = params.dup
- params[:readwrite] = true
+ unless SanitizedParameters === params
+ params = SanitizedParameters.new(self.class, params)
end
- # ensure mandatory parameters exist
- mandatory.each do |prm|
- if not params.has_key?(prm)
- raise ArgumentError, "parameter ':#{prm}' must be specified " +
- "in #{self}"
- end
- end
+ @params = params.accepted_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 mandatory.include?(k) or optional.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.params = params.extra_parameters
@env.data_object = self
end
- # Returns the class matching a previously registered +name+.
- def klass_lookup(name)
- @cache ||= {}
- klass = @cache[name]
- if klass.nil?
- 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
- @cache[name] = klass
- end
- klass
- end
-
- # Returns a list of parameters that are accepted by this object
- def accepted_parameters
- (self.class.mandatory_parameters + self.class.optional_parameters).uniq
- end
-
- # Reads data into this bin object by calling #do_read then #done_read.
+ # Reads data into this data object by calling #do_read then #done_read.
def read(io)
+ # wrap strings in a StringIO
+ io = StringIO.new(io) if io.respond_to?(:to_str)
+
# remove previous method to prevent warnings
class << io
- undef_method(:bindata_mark) if method_defined?(:bindata_mark)
+ remove_method(:bindata_mark) if method_defined?(:bindata_mark)
end
# remember the current position in the IO object
io.instance_eval "def bindata_mark; #{io.pos}; end"
@@ -196,21 +224,23 @@
# Writes the value for this data to +io+.
def write(io)
_write(io) if eval_param(:readwrite) != false
end
+ # Returns the string representation of this data object.
+ def to_s
+ io = StringIO.new
+ write(io)
+ io.rewind
+ io.read
+ 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 <tt>#value</tt> and <tt>#value=</tt>.
- def single_value?
- respond_to? :value
- end
-
# Return a human readable representation of this object.
def inspect
snapshot.inspect
end
@@ -241,18 +271,10 @@
# 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.bindata_mark
@@ -262,15 +284,36 @@
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.pos - io.bindata_mark
+ expected = eval_param(:adjust_offset)
+ if actual_offset != expected
+ begin
+ seek = expected - actual_offset
+ io.seek(seek, IO::SEEK_CUR)
+ 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
+ # Returns a list of the names of all possible field names for an object
+ # created with +sanitized_params+.
+ def self.all_possible_field_names(sanitized_params)
+ raise NotImplementedError
+ end
+
# Resets the internal state to that of a newly created object.
def clear
raise NotImplementedError
end
@@ -297,18 +340,25 @@
# Returns a snapshot of this data object.
def snapshot
raise NotImplementedError
end
+ # Returns whether this data object contains a single value. Single
+ # value data objects respond to <tt>#value</tt> and <tt>#value=</tt>.
+ def single_value?
+ raise NotImplementedError
+ end
+
# Returns a list of the names of all fields accessible through this
# object.
def field_names
raise NotImplementedError
end
# Set visibility requirements of methods to implement
- public :clear, :done_read, :snapshot, :field_names
+ public :clear, :done_read, :snapshot, :single_value?, :field_names
private :_do_read, :_write, :_num_bytes
# End To be implemented by subclasses
+ ###########################################################################
end
end