module BinData # Extracts args for Records and Buffers. # # Foo.new(bar: "baz) is ambiguous as to whether :bar is a value or parameter. # # BaseArgExtractor always assumes :bar is parameter. This extractor correctly # identifies it as value or parameter. module MultiFieldArgSeparator def separate_args(obj_class, obj_args) value, parameters, parent = super(obj_class, obj_args) if parameters_is_value?(obj_class, value, parameters) value = parameters parameters = {} end [value, parameters, parent] end def parameters_is_value?(obj_class, value, parameters) if value.nil? && !parameters.empty? field_names_in_parameters?(obj_class, parameters) else false end end def field_names_in_parameters?(obj_class, parameters) field_names = obj_class.fields.field_names param_keys = parameters.keys !(field_names & param_keys).empty? end end # BinData classes that are part of the DSL must be extended by this. module DSLMixin def dsl_parser(parser_type = nil) @dsl_parser ||= begin parser_type ||= superclass.dsl_parser.parser_type DSLParser.new(self, parser_type) end end def method_missing(symbol, *args, &block) #:nodoc: dsl_parser.__send__(symbol, *args, &block) end # Assert object is not an array or string. def to_ary; nil; end def to_str; nil; end # A DSLParser parses and accumulates field definitions of the form # # type name, params # # where: # * +type+ is the under_scored name of a registered type # * +name+ is the (possible optional) name of the field # * +params+ is a hash containing any parameters # class DSLParser def initialize(the_class, parser_type) raise "unknown parser type #{parser_type}" unless parser_abilities[parser_type] @the_class = the_class @parser_type = parser_type @validator = DSLFieldValidator.new(the_class, self) @endian = nil end attr_reader :parser_type def endian(endian = nil) if endian set_endian(endian) elsif @endian.nil? set_endian(parent_attribute(:endian)) end @endian end def search_prefix(*args) @search_prefix ||= parent_attribute(:search_prefix, []).dup prefix = args.collect(&:to_sym).compact unless prefix.empty? if fields? dsl_raise SyntaxError, "search_prefix must be called before defining fields" end @search_prefix = prefix.concat(@search_prefix) end @search_prefix end def hide(*args) if option?(:hidden_fields) @hide ||= parent_attribute(:hide, []).dup hidden = args.collect(&:to_sym).compact @hide.concat(hidden) @hide end end def fields @fields ||= begin SanitizedFields.new(hints).tap do |san_fields| san_fields.copy_fields(parent_fields) if parent_fields end end end def dsl_params abilities = parser_abilities[@parser_type] send(abilities.at(0), abilities.at(1)) end def method_missing(*args, &block) ensure_hints parse_and_append_field(*args, &block) end #------------- private def parser_abilities @abilities ||= { struct: [:to_struct_params, :struct, [:multiple_fields, :optional_fieldnames, :hidden_fields]], array: [:to_object_params, :type, [:multiple_fields, :optional_fieldnames]], buffer: [:to_object_params, :type, [:multiple_fields, :optional_fieldnames, :hidden_fields]], choice: [:to_choice_params, :choices, [:multiple_fields, :all_or_none_fieldnames, :fieldnames_are_values]], delayed_io: [:to_object_params, :type, [:multiple_fields, :optional_fieldnames, :hidden_fields]], primitive: [:to_struct_params, :struct, [:multiple_fields, :optional_fieldnames]], skip: [:to_object_params, :until_valid, [:multiple_fields, :optional_fieldnames]], } end def option?(opt) parser_abilities[@parser_type].at(2).include?(opt) end def ensure_hints endian search_prefix end def hints { endian: endian, search_prefix: search_prefix } end def set_endian(endian) if endian if fields? dsl_raise SyntaxError, "endian must be called before defining fields" end if !valid_endian?(endian) dsl_raise ArgumentError, "unknown value for endian '#{endian}'" end if endian == :big_and_little DSLBigAndLittleEndianHandler.handle(@the_class) end @endian = endian end end def valid_endian?(endian) [:big, :little, :big_and_little].include?(endian) end def parent_fields parent_attribute(:fields) end def fields? defined?(@fields) && !@fields.empty? end def parse_and_append_field(*args, &block) parser = DSLFieldParser.new(hints, *args, &block) begin @validator.validate_field(parser.name) append_field(parser.type, parser.name, parser.params) rescue Exception => err dsl_raise err.class, err.message end end def append_field(type, name, params) fields.add_field(type, name, params) rescue BinData::UnRegisteredTypeError => err raise TypeError, "unknown type '#{err.message}'" end def parent_attribute(attr, default = nil) parent = @the_class.superclass parser = parent.respond_to?(:dsl_parser) ? parent.dsl_parser : nil if parser && parser.respond_to?(attr) parser.send(attr) else default end end def dsl_raise(exception, msg) backtrace = caller backtrace.shift while %r{bindata/dsl.rb} =~ backtrace.first raise exception, "#{msg} in #{@the_class}", backtrace end def to_object_params(key) case fields.length when 0 {} when 1 {key => fields[0].prototype} else {key=> [:struct, to_struct_params]} end end def to_choice_params(key) if fields.empty? {} elsif fields.all_field_names_blank? {key => fields.collect(&:prototype)} else choices = {} fields.each { |f| choices[f.name] = f.prototype } {key => choices} end end def to_struct_params(*unused) result = {fields: fields} if !endian.nil? result[:endian] = endian end if !search_prefix.empty? result[:search_prefix] = search_prefix end if option?(:hidden_fields) && !hide.empty? result[:hide] = hide end result end end # Handles the :big_and_little endian option. # This option creates two subclasses, each handling # :big or :little endian. class DSLBigAndLittleEndianHandler class << self def handle(bnl_class) make_class_abstract(bnl_class) create_subclasses_with_endian(bnl_class) override_new_in_class(bnl_class) delegate_field_creation(bnl_class) fixup_subclass_hierarchy(bnl_class) end def make_class_abstract(bnl_class) bnl_class.send(:unregister_self) end def create_subclasses_with_endian(bnl_class) instance_eval "class ::#{bnl_class}Be < ::#{bnl_class}; endian :big; end" instance_eval "class ::#{bnl_class}Le < ::#{bnl_class}; endian :little; end" end def override_new_in_class(bnl_class) endian_classes = { big: class_with_endian(bnl_class, :big), little: class_with_endian(bnl_class, :little), } bnl_class.define_singleton_method(:new) do |*args| if self == bnl_class _, options, _ = arg_processor.separate_args(self, args) delegate = endian_classes[options[:endian]] return delegate.new(*args) if delegate end super(*args) end end def delegate_field_creation(bnl_class) endian_classes = { big: class_with_endian(bnl_class, :big), little: class_with_endian(bnl_class, :little), } parser = bnl_class.dsl_parser parser.define_singleton_method(:parse_and_append_field) do |*args, &block| endian_classes[:big].send(*args, &block) endian_classes[:little].send(*args, &block) end end def fixup_subclass_hierarchy(bnl_class) parent = bnl_class.superclass if obj_attribute(parent, :endian) == :big_and_little be_subclass = class_with_endian(bnl_class, :big) be_parent = class_with_endian(parent, :big) be_fields = obj_attribute(be_parent, :fields) le_subclass = class_with_endian(bnl_class, :little) le_parent = class_with_endian(parent, :little) le_fields = obj_attribute(le_parent, :fields) be_subclass.dsl_parser.define_singleton_method(:parent_fields) do be_fields end le_subclass.dsl_parser.define_singleton_method(:parent_fields) do le_fields end end end def class_with_endian(class_name, endian) hints = { endian: endian, search_prefix: class_name.dsl_parser.search_prefix, } RegisteredClasses.lookup(class_name, hints) end def obj_attribute(obj, attr) obj.dsl_parser.send(attr) end end end # Extracts the details from a field declaration. class DSLFieldParser def initialize(hints, symbol, *args, &block) @hints = hints @type = symbol @name = name_from_field_declaration(args) @params = params_from_field_declaration(args, &block) end attr_reader :type, :name, :params def name_from_field_declaration(args) name, _ = args if name == "" || name.is_a?(Hash) nil else name end end def params_from_field_declaration(args, &block) params = params_from_args(args) if block_given? params.merge(params_from_block(&block)) else params end end def params_from_args(args) name, params = args params = name if name.is_a?(Hash) params || {} end def params_from_block(&block) bindata_classes = { array: BinData::Array, buffer: BinData::Buffer, choice: BinData::Choice, delayed_io: BinData::DelayedIO, skip: BinData::Skip, struct: BinData::Struct, } if bindata_classes.include?(@type) parser = DSLParser.new(bindata_classes[@type], @type) parser.endian(@hints[:endian]) parser.search_prefix(*@hints[:search_prefix]) parser.instance_eval(&block) parser.dsl_params else {} end end end # Validates a field defined in a DSLMixin. class DSLFieldValidator def initialize(the_class, parser) @the_class = the_class @dsl_parser = parser end def validate_field(name) if must_not_have_a_name_failed?(name) raise SyntaxError, "field must not have a name" end if all_or_none_names_failed?(name) raise SyntaxError, "fields must either all have names, or none must have names" end if must_have_a_name_failed?(name) raise SyntaxError, "field must have a name" end ensure_valid_name(name) end def ensure_valid_name(name) if name && !option?(:fieldnames_are_values) if malformed_name?(name) raise NameError.new("", name), "field '#{name}' is an illegal fieldname" end if duplicate_name?(name) raise SyntaxError, "duplicate field '#{name}'" end if name_shadows_method?(name) raise NameError.new("", name), "field '#{name}' shadows an existing method" end if name_is_reserved?(name) raise NameError.new("", name), "field '#{name}' is a reserved name" end end end def must_not_have_a_name_failed?(name) option?(:no_fieldnames) && !name.nil? end def must_have_a_name_failed?(name) option?(:mandatory_fieldnames) && name.nil? end def all_or_none_names_failed?(name) if option?(:all_or_none_fieldnames) && !fields.empty? all_names_blank = fields.all_field_names_blank? no_names_blank = fields.no_field_names_blank? (!name.nil? && all_names_blank) || (name.nil? && no_names_blank) else false end end def malformed_name?(name) /^[a-z_]\w*$/ !~ name.to_s end def duplicate_name?(name) fields.field_name?(name) end def name_shadows_method?(name) @the_class.method_defined?(name) end def name_is_reserved?(name) BinData::Struct::RESERVED.include?(name.to_sym) end def fields @dsl_parser.fields end def option?(opt) @dsl_parser.send(:option?, opt) end end end end