lib/bindata/sanitize.rb



require 'forwardable'

module BinData

  # A BinData object accepts arbitrary parameters.  This class only contains
  # parameters that have been sanitized, and categorizes them according to
  # whether they are BinData::Base.internal_parameters or are extra.
  class SanitizedParameters
    extend Forwardable

    # Sanitize the given parameters.
    def initialize(klass, params)
      @hash = params
      @internal_parameters = {}
      @extra_parameters = {}

      # partition parameters into known and extra parameters
      @hash.each do |k,v|
        k = k.to_sym
        if v.nil?
          raise ArgumentError, "parameter :#{k} has nil value in #{klass}"
        end

        if klass.internal_parameters.include?(k)
          @internal_parameters[k] = v
        else
          @extra_parameters[k] = v
        end
      end
    end

    attr_reader :internal_parameters, :extra_parameters

    def_delegators :@hash, :[], :has_key?, :include?, :keys
  end

  # The Sanitizer sanitizes the parameters that are passed when creating a
  # BinData object.  Sanitizing consists of checking for mandatory, optional
  # and default parameters and ensuring the values of known parameters are
  # valid.
  class Sanitizer

    class << self

      # Sanitize +params+ for +obj+.
      # Returns sanitized parameters.
      def sanitize(klass, params)
        if SanitizedParameters === params
          params
        else
          sanitizer = self.new
          sanitizer.sanitize_params(klass, params)
        end
      end

      # Returns true if +type+ is registered.
      def type_exists?(type, endian = nil)
        lookup(type, endian) != nil
      end

      # Returns the class matching a previously registered +name+.
      def lookup(name, endian)
        name = name.to_s
        klass = Registry.instance.lookup(name)
        if klass.nil? and endian != nil
          # lookup failed so attempt endian lookup
          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

    # Create a new Sanitizer.
    def initialize
      @seen   = []
      @endian = nil
    end

    # Executes the given block with +endian+ set as the current endian.
    def with_endian(endian, &block)
      if endian != nil
        saved_endian = @endian
        @endian = endian
        yield
        @endian = saved_endian
      else
        yield
      end
    end

    # Converts +type+ into the appropriate class.
    def lookup_klass(type)
      klass = self.class.lookup(type, @endian)
      raise TypeError, "unknown type '#{type}'" if klass.nil?
      klass
    end

    # Sanitizes +params+ for +klass+.
    # Returns +sanitized_params+.
    def sanitize_params(klass, params)
      new_params = params.nil? ? {} : params.dup

      if klass.recursive? and @seen.include?(klass)
        # This klass is defined recursively.  Remember the current endian
        # and delay sanitizing the parameters until later.
        new_params[:endian] = @endian if can_store_endian?(klass, new_params)
        ret_val = new_params
      else
        @seen.push(klass)

        # Sanitize new_params.  This may recursively call this method again.
        klass.sanitize_parameters!(self, new_params)
        ret_val = SanitizedParameters.new(klass, new_params)

        @seen.pop
      end

      ret_val
    end

    #---------------
    private

    # Can we store the current endian for later?
    def can_store_endian?(klass, params)
      (@endian != nil and klass.internal_parameters.include?(:endian) and
       not params.has_key?(:endian))
    end
  end
end