require 'bindata/framework' require 'bindata/io' require 'bindata/lazy' require 'bindata/name' require 'bindata/params' require 'bindata/registry' require 'bindata/sanitize' module BinData # This is the abstract base class for all data objects. class Base extend AcceptedParametersPlugin include Framework include RegisterNamePlugin class << self # Instantiates this class and reads from +io+, returning the newly # created data object. +args+ will be used when instantiating. def read(io, *args, &block) obj = self.new(*args) obj.read(io, &block) obj end # The arg processor for this class. def arg_processor(name = nil) @arg_processor ||= nil if name @arg_processor = "#{name}_arg_processor".gsub(/(?:^|_)(.)/) { $1.upcase }.to_sym elsif @arg_processor.is_a? Symbol @arg_processor = BinData.const_get(@arg_processor).new elsif @arg_processor.nil? @arg_processor = superclass.arg_processor else @arg_processor end end # The name of this class as used by Records, Arrays etc. def bindata_name RegisteredClasses.underscore_name(name) end # Call this method if this class is abstract and not to be used. def unregister_self RegisteredClasses.unregister(name) end # Registers all subclasses of this class for use def register_subclasses #:nodoc: singleton_class.send(:undef_method, :inherited) define_singleton_method(:inherited) do |subclass| RegisteredClasses.register(subclass.name, subclass) register_subclasses end end private :unregister_self, :register_subclasses end # Register all subclasses of this class. register_subclasses # Set the initial arg processor. arg_processor :base # Creates a new data object. # # Args are optional, but if present, must be in the following order. # # +value+ is a value that is +assign+ed immediately after initialization. # # +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(*args) value, @params, @parent = extract_args(args) initialize_shared_instance initialize_instance assign(value) if value end attr_accessor :parent protected :parent= # Creates a new data object based on this instance. # # All parameters will be be duplicated. Use this method # when creating multiple objects with the same parameters. def new(value = nil, parent = nil) obj = clone obj.parent = parent if parent obj.initialize_instance obj.assign(value) if value obj end # 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 = nil) value = get_parameter(key) if value.is_a?(Symbol) || value.respond_to?(:arity) lazy_evaluator.lazy_eval(value, overrides) else value end end # Returns a lazy evaluator for this object. def lazy_evaluator #:nodoc: @lazy ||= LazyEvaluator.new(self) 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 # Resets the internal state to that of a newly created object. def clear initialize_instance end # Reads data into this data object. def read(io, &block) io = BinData::IO::Read.new(io) unless BinData::IO::Read === io start_read do clear do_read(io) end block.call(self) if block_given? self end # Writes the value for this data object to +io+. def write(io, &block) io = BinData::IO::Write.new(io) unless BinData::IO::Write === io do_write(io) io.flush block.call(self) if block_given? self end # Returns the number of bytes it will take to write this data object. def num_bytes do_num_bytes.ceil end # Returns the string representation of this data object. def to_binary_s(&block) io = BinData::IO.create_string_io write(io, &block) io.rewind io.read end # Returns the hexadecimal string representation of this data object. def to_hex(&block) to_binary_s(&block).unpack('H*')[0] 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 # Override and delegate =~ as it is defined in Object. def =~(other) snapshot =~ other 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 (in bytes) of this object with respect to its most # distant ancestor. def abs_offset if @parent @parent.abs_offset + @parent.offset_of(self) else 0 end end # Returns the offset (in bytes) of this object with respect to its parent. def rel_offset if @parent @parent.offset_of(self) else 0 end end def ==(other) #:nodoc: # double dispatch other == snapshot end # A version of +respond_to?+ used by the lazy evaluator. It doesn't # reinvoke the evaluator so as to avoid infinite evaluation loops. def safe_respond_to?(symbol, include_private = false) #:nodoc: base_respond_to?(symbol, include_private) end alias base_respond_to? respond_to? #--------------- private def extract_args(args) self.class.arg_processor.extract_args(self.class, args) end def start_read top_level_set(:in_read, true) yield ensure top_level_set(:in_read, false) end # Is this object tree currently being read? Used by BasePrimitive. def reading? top_level_get(:in_read) end def top_level_set(sym, value) top_level.instance_variable_set("@tl_#{sym}", value) end def top_level_get(sym) tl = top_level tl.instance_variable_defined?("@tl_#{sym}") && tl.instance_variable_get("@tl_#{sym}") end def top_level if parent.nil? tl = self else tl = parent tl = tl.parent while tl.parent end tl end def binary_string(str) str.to_s.dup.force_encoding(Encoding::BINARY) end end # ArgProcessors process the arguments passed to BinData::Base.new into # the form required to initialise the BinData object. # # Any passed parameters are sanitized so the BinData object doesn't # need to perform error checking on the parameters. class BaseArgProcessor @@empty_hash = Hash.new.freeze # Takes the arguments passed to BinData::Base.new and # extracts [value, sanitized_parameters, parent]. def extract_args(obj_class, obj_args) value, params, parent = separate_args(obj_class, obj_args) sanitized_params = SanitizedParameters.sanitize(params, obj_class) [value, sanitized_params, parent] end # Separates the arguments passed to BinData::Base.new into # [value, parameters, parent]. Called by #extract_args. def separate_args(_obj_class, obj_args) args = obj_args.dup value = parameters = parent = nil if args.length > 1 && args.last.is_a?(BinData::Base) parent = args.pop end if args.length > 0 && args.last.is_a?(Hash) parameters = args.pop end if args.length > 0 value = args.pop end parameters ||= @@empty_hash [value, parameters, parent] end # Performs sanity checks on the given parameters. # This method converts the parameters to the form expected # by the data object. def sanitize_parameters!(obj_class, obj_params) end end end