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