require 'bindata/registry' module BinData # Subclasses of this are sanitized class SanitizedParameter; end class SanitizedPrototype < SanitizedParameter def initialize(obj_type, obj_params, hints) raw_hints = hints.dup if raw_hints[:endian].respond_to?(:endian) raw_hints[:endian] = raw_hints[:endian].endian end obj_params ||= {} if BinData::Base === obj_type obj_class = obj_type else obj_class = RegisteredClasses.lookup(obj_type, raw_hints) end if BinData::Base === obj_class @factory = obj_class else @obj_class = obj_class @obj_params = SanitizedParameters.new(obj_params, @obj_class, hints) end end def has_parameter?(param) if defined? @factory @factory.has_parameter?(param) else @obj_params.has_parameter?(param) end end def instantiate(value = nil, parent = nil) @factory ||= @obj_class.new(@obj_params) @factory.new(value, parent) end end #---------------------------------------------------------------------------- class SanitizedField < SanitizedParameter def initialize(name, field_type, field_params, hints) @name = name @prototype = SanitizedPrototype.new(field_type, field_params, hints) end attr_reader :prototype, :name def name_as_sym @name&.to_sym end def has_parameter?(param) @prototype.has_parameter?(param) end def instantiate(value = nil, parent = nil) @prototype.instantiate(value, parent) end end #---------------------------------------------------------------------------- class SanitizedFields < SanitizedParameter include Enumerable def initialize(hints, base_fields = nil) @hints = hints @fields = base_fields ? base_fields.raw_fields : [] end def add_field(type, name, params) name = nil if name == "" @fields << SanitizedField.new(name, type, params, @hints) end def raw_fields @fields.dup end def [](idx) @fields[idx] end def empty? @fields.empty? end def length @fields.length end def each(&block) @fields.each(&block) end def field_names @fields.collect(&:name_as_sym) end def field_name?(name) @fields.detect { |f| f.name_as_sym == name.to_sym } end def all_field_names_blank? @fields.all? { |f| f.name.nil? } end def no_field_names_blank? @fields.all? { |f| f.name != nil } end def any_field_has_parameter?(parameter) @fields.any? { |f| f.has_parameter?(parameter) } end end #---------------------------------------------------------------------------- class SanitizedChoices < SanitizedParameter def initialize(choices, hints) @choices = {} choices.each_pair do |key, val| if SanitizedParameter === val prototype = val else type, param = val prototype = SanitizedPrototype.new(type, param, hints) end if key == :default @choices.default = prototype else @choices[key] = prototype end end end def [](key) @choices[key] end end #---------------------------------------------------------------------------- class SanitizedBigEndian < SanitizedParameter def endian :big end end class SanitizedLittleEndian < SanitizedParameter def endian :little end end #---------------------------------------------------------------------------- # BinData objects are instantiated with parameters to determine their # behaviour. These parameters must be sanitized to ensure their values # are valid. When instantiating many objects with identical parameters, # such as an array of records, there is much duplicated sanitizing. # # The purpose of the sanitizing code is to eliminate the duplicated # validation. # # SanitizedParameters is a hash-like collection of parameters. Its purpose # is to recursively sanitize the parameters of an entire BinData object chain # at a single time. class SanitizedParameters < Hash # Memoized constants BIG_ENDIAN = SanitizedBigEndian.new LITTLE_ENDIAN = SanitizedLittleEndian.new class << self def sanitize(parameters, the_class) if SanitizedParameters === parameters parameters else SanitizedParameters.new(parameters, the_class, {}) end end end def initialize(parameters, the_class, hints) parameters.each_pair { |key, value| self[key.to_sym] = value } @the_class = the_class if hints[:endian] self[:endian] ||= hints[:endian] end if hints[:search_prefix] && !hints[:search_prefix].empty? self[:search_prefix] = Array(self[:search_prefix]).concat(Array(hints[:search_prefix])) end sanitize! end alias has_parameter? key? def has_at_least_one_of?(*keys) keys.each do |key| return true if has_parameter?(key) end false end def warn_replacement_parameter(bad_key, suggested_key) if has_parameter?(bad_key) Kernel.warn ":#{bad_key} is not used with #{@the_class}. " \ "You probably want to change this to :#{suggested_key}" end end # def warn_renamed_parameter(old_key, new_key) # val = delete(old_key) # if val # self[new_key] = val # Kernel.warn ":#{old_key} has been renamed to :#{new_key} in #{@the_class}. " \ # "Using :#{old_key} is now deprecated and will be removed in the future" # end # end def must_be_integer(*keys) keys.each do |key| if has_parameter?(key) parameter = self[key] unless Symbol === parameter || parameter.respond_to?(:arity) || parameter.respond_to?(:to_int) raise ArgumentError, "parameter '#{key}' in #{@the_class} must " \ "evaluate to an integer, got #{parameter.class}" end end end end def rename_parameter(old_key, new_key) if has_parameter?(old_key) self[new_key] = delete(old_key) end end def sanitize_object_prototype(key) sanitize(key) do |obj_type, obj_params| create_sanitized_object_prototype(obj_type, obj_params) end end def sanitize_fields(key, &block) sanitize(key) do |fields| sanitized_fields = create_sanitized_fields yield(fields, sanitized_fields) sanitized_fields end end def sanitize_choices(key, &block) sanitize(key) do |obj| create_sanitized_choices(yield(obj)) end end def sanitize_endian(key) sanitize(key) { |endian| create_sanitized_endian(endian) } end def sanitize(key, &block) if needs_sanitizing?(key) self[key] = yield(self[key]) end end def create_sanitized_params(params, the_class) SanitizedParameters.new(params, the_class, hints) end def hints { endian: self[:endian], search_prefix: self[:search_prefix] } end #--------------- private def sanitize! ensure_no_nil_values merge_default_parameters! @the_class.arg_processor.sanitize_parameters!(@the_class, self) ensure_mandatory_parameters_exist ensure_mutual_exclusion_of_parameters end def needs_sanitizing?(key) has_parameter?(key) && !self[key].is_a?(SanitizedParameter) end def ensure_no_nil_values each do |key, value| if value.nil? raise ArgumentError, "parameter '#{key}' has nil value in #{@the_class}" end end end def merge_default_parameters! @the_class.default_parameters.each do |key, value| self[key] = value unless has_parameter?(key) end end def ensure_mandatory_parameters_exist @the_class.mandatory_parameters.each do |key| unless has_parameter?(key) raise ArgumentError, "parameter '#{key}' must be specified in #{@the_class}" end end end def ensure_mutual_exclusion_of_parameters return if length < 2 @the_class.mutually_exclusive_parameters.each do |key1, key2| if has_parameter?(key1) && has_parameter?(key2) raise ArgumentError, "params '#{key1}' and '#{key2}' " \ "are mutually exclusive in #{@the_class}" end end end def create_sanitized_endian(endian) if endian == :big BIG_ENDIAN elsif endian == :little LITTLE_ENDIAN elsif endian == :big_and_little raise ArgumentError, "endian: :big or endian: :little is required" else raise ArgumentError, "unknown value for endian '#{endian}'" end end def create_sanitized_choices(choices) SanitizedChoices.new(choices, hints) end def create_sanitized_fields SanitizedFields.new(hints) end def create_sanitized_object_prototype(obj_type, obj_params) SanitizedPrototype.new(obj_type, obj_params, hints) end end #---------------------------------------------------------------------------- end