Skip to content

Commit

Permalink
Moved logic out of the constructor
Browse files Browse the repository at this point in the history
All of the logic in the constructor except for one
runtime check has been moved to the self.field() method.
This in turn calls one of a bunch of def_reader() methods
that act like attr_reader but lazily define variables
internally.

I also made all of the class methods private that are
used by the dsl.
  • Loading branch information
apainintheneck committed Apr 3, 2022
1 parent 2e8c3a9 commit 2b70368
Showing 1 changed file with 178 additions and 117 deletions.
295 changes: 178 additions & 117 deletions lib/macho/structure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 2b70368

Please sign in to comment.