lib/bindata/sanitize.rb
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