diff --git a/lib/macho/structure.rb b/lib/macho/structure.rb index 64787ec53..eb8ebbc90 100644 --- a/lib/macho/structure.rb +++ b/lib/macho/structure.rb @@ -52,151 +52,212 @@ module Fields :tool_entries => "L=", }.freeze - # a list of classes that must be initialized separately - # in the constructor - CLASS_LIST = %i[lcstr two_level_hints_table tool_entries].freeze + # A list of classes that must get initialized + # To add a new class append it here and add the init method to the def_class_reader method + # @api private + CLASS_LIST = %i[lcstr tool_entries two_level_hints_table].freeze end - # map of field names to field types - @type_map = {} + # map of field names to indices + @field_idxs = {} - # array of field name in definition order - @field_list = [] + # array of fields sizes + @size_list = [] - # map of field options - @option_map = {} + # array of field format codes + @fmt_list = [] # minimum number of required arguments @min_args = 0 - class << self - # Public getters - attr_reader :type_map, :field_list, :option_map, :min_args - - private - - # Private setters - attr_writer :type_map, :field_list, :option_map, :min_args - end - # Used to dynamically create an instance of the inherited class # according to the defined fields. # @param args [Array[Value]] list of field parameters def initialize(*args) raise ArgumentError, "Invalid number of arguments" if args.size < self.class.min_args - # Set up all instance variables - self.class.field_list.zip(args).each do |field, value| - # TODO: Find a better way to specialize initialization for certain types + @values = args + end + + # @return [Hash] a hash representation of this {MachOStructure}. + def to_h + { + "structure" => { + "format" => self.class.format, + "bytesize" => self.class.bytesize, + }, + } + end + + class << self + attr_reader :min_args + + # @param endianness [Symbol] either `:big` or `:little` + # @param bin [String] the string to be unpacked into the new structure + # @return [MachO::MachOStructure] the resulting structure + # @api private + def new_from_bin(endianness, bin) + format = Utils.specialize_format(self.format, endianness) + + new(*bin.unpack(format)) + end + + def format + @format ||= @fmt_list.join + end + + def bytesize + @bytesize ||= @size_list.sum + end + + private + + # @param subclass [Class] subclass type + # @api private + def inherited(subclass) # rubocop:disable Lint/MissingSuper + # Clone all class instance variables + field_idxs = @field_idxs.dup + size_list = @size_list.dup + fmt_list = @fmt_list.dup + min_args = @min_args.dup + + # Add those values to the inheriting class + subclass.class_eval do + @field_idxs = field_idxs + @size_list = size_list + @fmt_list = fmt_list + @min_args = min_args + end + end + + # @param name [Symbol] name of internal field + # @param type [Symbol] type of field in terms of binary size + # @param options [Hash] set of additonal options + # Expected options + # :size [Integer] size in bytes + # :mask [Integer] bitmask + # :unpack [String] string format + # :default [Value] default value + # @api private + def field(name, type, **options) + raise ArgumentError, "Invalid field type #{type}" unless Fields::FORMAT_CODE.key?(type) + + idx = if @field_idxs.key?(name) + @field_idxs[name] + else + @min_args += 1 unless options.key?(:default) + @field_idxs[name] = @field_idxs.size + @size_list << nil + @fmt_list << nil + @field_idxs.size - 1 + end + + @size_list[idx] = Fields::BYTE_SIZE[type] || options[:size] + @fmt_list[idx] = Fields::FORMAT_CODE[type] + @fmt_list[idx] += options[:size].to_s if options.key?(:size) - # Handle special cases - type = self.class.type_map[field] + # Generate methods if Fields::CLASS_LIST.include?(type) - case type - when :lcstr - value = LoadCommands::LoadCommand::LCStr.new(self, value) - when :two_level_hints_table - value = LoadCommands::TwolevelHintsCommand::TwolevelHintsTable.new(view, htoffset, nhints) - when :tool_entries - value = LoadCommands::BuildVersionCommand::ToolEntries.new(view, value) + def_class_reader(name, type, idx) + elsif options.key?(:mask) + def_mask_reader(name, idx, options[:mask]) + elsif options.key?(:unpack) + def_unpack_reader(name, idx, options[:unpack]) + elsif options.key?(:default) + def_default_reader(name, idx, options[:default]) + else + def_reader(name, idx) + end + end + + # + # Method Generators + # + + # Generates a reader method for classes that need to be initialized. + # These classes are defined in the Fields::CLASS_LIST array. + # @param name [Symbol] name of internal field + # @param type [Symbol] type of field in terms of binary size + # @param idx [Integer] the index of the field value in the @values array + # @api private + def def_class_reader(name, type, idx) + case type + when :lcstr + define_method(name) do + instance_variable_defined?("@#{name}") || + instance_variable_set("@#{name}", LoadCommands::LoadCommand::LCStr.new(self, @values[idx])) + + instance_variable_get("@#{name}") end - elsif self.class.option_map.key?(field) - options = self.class.option_map[field] - - if options.key?(:mask) - value &= ~options[:mask] - elsif options.key?(:unpack) - value = value.unpack(options[:unpack]) - elsif value.nil? && options.key?(:default) - value = options[:default] + when :two_level_hints_table + define_method(name) do + instance_variable_defined?("@#{name}") || + instance_variable_set("@#{name}", LoadCommands::TwolevelHintsCommand::TwolevelHintsTable.new(view, htoffset, nhints)) + + instance_variable_get("@#{name}") end - end + when :tool_entries + define_method(name) do + instance_variable_defined?("@#{name}") || + instance_variable_set("@#{name}", LoadCommands::BuildVersionCommand::ToolEntries.new(view, @values[idx])) - instance_variable_set("@#{field}", value) + instance_variable_get("@#{name}") + end + end end - end - # @param subclass [Class] subclass type - # @api private - def self.inherited(subclass) # rubocop:disable Lint/MissingSuper - # Clone all class instance variables - type_map = @type_map.dup - field_list = @field_list.dup - option_map = @option_map.dup - min_args = @min_args.dup - - # Add those values to the inheriting class - subclass.class_eval do - @type_map = type_map - @field_list = field_list - @option_map = option_map - @min_args = min_args - end - end + # Generates a reader method for fields that need to be bitmasked. + # @param name [Symbol] name of internal field + # @param idx [Integer] the index of the field value in the @values array + # @param mask [Integer] the bitmask + # @api private + def def_mask_reader(name, idx, mask) + define_method(name) do + instance_variable_defined?("@#{name}") || + instance_variable_set("@#{name}", @values[idx] & ~mask) - # @param name [Symbol] name of internal field - # @param type [Symbol] type of field in terms of binary size - # @param options [Hash] set of additonal options - # Expected options - # :size [Integer] size in bytes - # :mask [Integer] bitmask - # :unpack [String] string format - # :default [Value] default value - # @api private - def self.field(name, type, **options) - raise ArgumentError, "Invalid field type #{type}" unless Fields::FORMAT_CODE.key?(type) - - if type_map.key?(name) - @min_args -= 1 unless @option_map.dig(name, :default) - - @option_map.delete(name) if options.empty? - else - attr_reader name - - # TODO: Should be able to generate #to_s based on presence of LCStr which is the 90% case - # TODO: Could try generating #to_h for the 90% perecent case - # Might be best to make another functional called maybe generate - - @field_list << name + instance_variable_get("@#{name}") + end end - @option_map[name] = options unless options.empty? - @min_args += 1 unless options.key?(:default) - @type_map[name] = type - end - - # @param endianness [Symbol] either `:big` or `:little` - # @param bin [String] the string to be unpacked into the new structure - # @return [MachO::MachOStructure] the resulting structure - # @api private - def self.new_from_bin(endianness, bin) - format = Utils.specialize_format(self.format, endianness) + # Generates a reader method for fields that need further unpacking. + # @param name [Symbol] name of internal field + # @param idx [Integer] the index of the field value in the @values array + # @param unpack [String] the format code used for futher binary unpacking + # @api private + def def_unpack_reader(name, idx, unpack) + define_method(name) do + instance_variable_defined?("@#{name}") || + instance_variable_set("@#{name}", @values[idx].unpack(unpack)) - new(*bin.unpack(format)) - end + instance_variable_get("@#{name}") + end + end - def self.format - @format ||= field_list.map do |field| - Fields::FORMAT_CODE[type_map[field]] + - option_map.dig(field, :size).to_s - end.join - end + # Generates a reader method for fields that have default values. + # @param name [Symbol] name of internal field + # @param idx [Integer] the index of the field value in the @values array + # @param default [Value] the default value + # @api private + def def_default_reader(name, idx, default) + define_method(name) do + instance_variable_defined?("@#{name}") || + instance_variable_set("@#{name}", @values.size > idx ? @values[idx] : default) - def self.bytesize - @bytesize ||= field_list.map do |field| - Fields::BYTE_SIZE[type_map[field]] || - option_map.dig(field, :size) - end.sum - end + instance_variable_get("@#{name}") + end + end - # @return [Hash] a hash representation of this {MachOStructure}. - def to_h - { - "structure" => { - "format" => self.class.format, - "bytesize" => self.class.bytesize, - }, - } + # Generates a reader method for fields that need to be bitmasked. + # @param name [Symbol] name of internal field + # @param type [Symbol] type of field in terms of binary size + # @param idx [Integer] the index of the field value in the @values array + # @api private + def def_reader(name, idx) + define_method(name) do + @values[idx] + end + end end end end