Skip to content

Commit f8cd221

Browse files
Moved logic out of the constructor
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.
1 parent 2e8c3a9 commit f8cd221

File tree

1 file changed

+177
-117
lines changed

1 file changed

+177
-117
lines changed

lib/macho/structure.rb

Lines changed: 177 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -52,151 +52,211 @@ module Fields
5252
:tool_entries => "L=",
5353
}.freeze
5454

55-
# a list of classes that must be initialized separately
56-
# in the constructor
57-
CLASS_LIST = %i[lcstr two_level_hints_table tool_entries].freeze
55+
# A list of classes that must get initialized
56+
# To add a new class append it here and add the init method to the def_class_reader method
57+
# @api private
58+
CLASS_LIST = %i[lcstr tool_entries two_level_hints_table].freeze
5859
end
5960

60-
# map of field names to field types
61-
@type_map = {}
61+
# map of field names to indices
62+
@field_idxs = {}
6263

63-
# array of field name in definition order
64-
@field_list = []
64+
# array of fields sizes
65+
@size_list = []
6566

66-
# map of field options
67-
@option_map = {}
67+
# array of field format codes
68+
@fmt_list = []
6869

6970
# minimum number of required arguments
7071
@min_args = 0
7172

72-
class << self
73-
# Public getters
74-
attr_reader :type_map, :field_list, :option_map, :min_args
75-
76-
private
77-
78-
# Private setters
79-
attr_writer :type_map, :field_list, :option_map, :min_args
80-
end
81-
8273
# Used to dynamically create an instance of the inherited class
8374
# according to the defined fields.
8475
# @param args [Array[Value]] list of field parameters
8576
def initialize(*args)
8677
raise ArgumentError, "Invalid number of arguments" if args.size < self.class.min_args
8778

88-
# Set up all instance variables
89-
self.class.field_list.zip(args).each do |field, value|
90-
# TODO: Find a better way to specialize initialization for certain types
79+
@values = args
80+
end
81+
82+
# @return [Hash] a hash representation of this {MachOStructure}.
83+
def to_h
84+
{
85+
"structure" => {
86+
"format" => self.class.format,
87+
"bytesize" => self.class.bytesize,
88+
},
89+
}
90+
end
91+
92+
class << self
93+
attr_reader :min_args
94+
95+
# @param endianness [Symbol] either `:big` or `:little`
96+
# @param bin [String] the string to be unpacked into the new structure
97+
# @return [MachO::MachOStructure] the resulting structure
98+
# @api private
99+
def new_from_bin(endianness, bin)
100+
format = Utils.specialize_format(self.format, endianness)
101+
102+
new(*bin.unpack(format))
103+
end
104+
105+
def format
106+
@format ||= @fmt_list.join
107+
end
108+
109+
def bytesize
110+
@bytesize ||= @size_list.sum
111+
end
112+
113+
private
114+
115+
# @param subclass [Class] subclass type
116+
# @api private
117+
def inherited(subclass) # rubocop:disable Lint/MissingSuper
118+
# Clone all class instance variables
119+
field_idxs = @field_idxs.dup
120+
size_list = @size_list.dup
121+
fmt_list = @fmt_list.dup
122+
min_args = @min_args.dup
123+
124+
# Add those values to the inheriting class
125+
subclass.class_eval do
126+
@field_idxs = field_idxs
127+
@size_list = size_list
128+
@fmt_list = fmt_list
129+
@min_args = min_args
130+
end
131+
end
132+
133+
# @param name [Symbol] name of internal field
134+
# @param type [Symbol] type of field in terms of binary size
135+
# @param options [Hash] set of additonal options
136+
# Expected options
137+
# :size [Integer] size in bytes
138+
# :mask [Integer] bitmask
139+
# :unpack [String] string format
140+
# :default [Value] default value
141+
# @api private
142+
def field(name, type, **options)
143+
raise ArgumentError, "Invalid field type #{type}" unless Fields::FORMAT_CODE.key?(type)
144+
145+
idx = if @field_idxs.key?(name)
146+
@field_idxs[name]
147+
else
148+
@min_args += 1 unless options.key?(:default)
149+
@field_idxs[name] = @field_idxs.size
150+
@size_list << nil
151+
@fmt_list << nil
152+
@field_idxs.size - 1
153+
end
154+
155+
@size_list[idx] = Fields::BYTE_SIZE[type] || options[:size]
156+
@fmt_list[idx] = Fields::FORMAT_CODE[type]
157+
@fmt_list[idx] += options[:size].to_s if options.key?(:size)
91158

92-
# Handle special cases
93-
type = self.class.type_map[field]
159+
# Generate methods
94160
if Fields::CLASS_LIST.include?(type)
95-
case type
96-
when :lcstr
97-
value = LoadCommands::LoadCommand::LCStr.new(self, value)
98-
when :two_level_hints_table
99-
value = LoadCommands::TwolevelHintsCommand::TwolevelHintsTable.new(view, htoffset, nhints)
100-
when :tool_entries
101-
value = LoadCommands::BuildVersionCommand::ToolEntries.new(view, value)
161+
def_class_reader(name, type, idx)
162+
elsif options.key?(:mask)
163+
def_mask_reader(name, idx, options[:mask])
164+
elsif options.key?(:unpack)
165+
def_unpack_reader(name, idx, options[:unpack])
166+
elsif options.key?(:default)
167+
def_default_reader(name, idx, options[:default])
168+
else
169+
def_reader(name, idx)
170+
end
171+
end
172+
173+
#
174+
# Method Generators
175+
#
176+
177+
# Generates a reader method for classes that need to be initialized.
178+
# These classes are defined in the Fields::CLASS_LIST array.
179+
# @param name [Symbol] name of internal field
180+
# @param type [Symbol] type of field in terms of binary size
181+
# @param idx [Integer] the index of the field value in the @values array
182+
# @api private
183+
def def_class_reader(name, type, idx)
184+
case type
185+
when :lcstr
186+
define_method(name) do
187+
instance_variable_defined?("@#{name}") ||
188+
instance_variable_set("@#{name}", LoadCommands::LoadCommand::LCStr.new(self, @values[idx]))
189+
190+
instance_variable_get("@#{name}")
102191
end
103-
elsif self.class.option_map.key?(field)
104-
options = self.class.option_map[field]
105-
106-
if options.key?(:mask)
107-
value &= ~options[:mask]
108-
elsif options.key?(:unpack)
109-
value = value.unpack(options[:unpack])
110-
elsif value.nil? && options.key?(:default)
111-
value = options[:default]
192+
when :two_level_hints_table
193+
define_method(name) do
194+
instance_variable_defined?("@#{name}") ||
195+
instance_variable_set("@#{name}", LoadCommands::TwolevelHintsCommand::TwolevelHintsTable.new(view, htoffset, nhints))
196+
197+
instance_variable_get("@#{name}")
112198
end
113-
end
199+
when :tool_entries
200+
define_method(name) do
201+
instance_variable_defined?("@#{name}") ||
202+
instance_variable_set("@#{name}", LoadCommands::BuildVersionCommand::ToolEntries.new(view, @values[idx]))
114203

115-
instance_variable_set("@#{field}", value)
204+
instance_variable_get("@#{name}")
205+
end
206+
end
116207
end
117-
end
118208

119-
# @param subclass [Class] subclass type
120-
# @api private
121-
def self.inherited(subclass) # rubocop:disable Lint/MissingSuper
122-
# Clone all class instance variables
123-
type_map = @type_map.dup
124-
field_list = @field_list.dup
125-
option_map = @option_map.dup
126-
min_args = @min_args.dup
127-
128-
# Add those values to the inheriting class
129-
subclass.class_eval do
130-
@type_map = type_map
131-
@field_list = field_list
132-
@option_map = option_map
133-
@min_args = min_args
134-
end
135-
end
209+
# Generates a reader method for fields that need to be bitmasked.
210+
# @param name [Symbol] name of internal field
211+
# @param idx [Integer] the index of the field value in the @values array
212+
# @param mask [Integer] the bitmask
213+
# @api private
214+
def def_mask_reader(name, idx, mask)
215+
define_method(name) do
216+
instance_variable_defined?("@#{name}") ||
217+
instance_variable_set("@#{name}", @values[idx] & ~mask)
136218

137-
# @param name [Symbol] name of internal field
138-
# @param type [Symbol] type of field in terms of binary size
139-
# @param options [Hash] set of additonal options
140-
# Expected options
141-
# :size [Integer] size in bytes
142-
# :mask [Integer] bitmask
143-
# :unpack [String] string format
144-
# :default [Value] default value
145-
# @api private
146-
def self.field(name, type, **options)
147-
raise ArgumentError, "Invalid field type #{type}" unless Fields::FORMAT_CODE.key?(type)
148-
149-
if type_map.key?(name)
150-
@min_args -= 1 unless @option_map.dig(name, :default)
151-
152-
@option_map.delete(name) if options.empty?
153-
else
154-
attr_reader name
155-
156-
# TODO: Should be able to generate #to_s based on presence of LCStr which is the 90% case
157-
# TODO: Could try generating #to_h for the 90% perecent case
158-
# Might be best to make another functional called maybe generate
159-
160-
@field_list << name
219+
instance_variable_get("@#{name}")
220+
end
161221
end
162222

163-
@option_map[name] = options unless options.empty?
164-
@min_args += 1 unless options.key?(:default)
165-
@type_map[name] = type
166-
end
167-
168-
# @param endianness [Symbol] either `:big` or `:little`
169-
# @param bin [String] the string to be unpacked into the new structure
170-
# @return [MachO::MachOStructure] the resulting structure
171-
# @api private
172-
def self.new_from_bin(endianness, bin)
173-
format = Utils.specialize_format(self.format, endianness)
223+
# Generates a reader method for fields that need further unpacking.
224+
# @param name [Symbol] name of internal field
225+
# @param idx [Integer] the index of the field value in the @values array
226+
# @param unpack [String] the format code used for futher binary unpacking
227+
# @api private
228+
def def_unpack_reader(name, idx, unpack)
229+
define_method(name) do
230+
instance_variable_defined?("@#{name}") ||
231+
instance_variable_set("@#{name}", @values[idx].unpack(unpack))
174232

175-
new(*bin.unpack(format))
176-
end
233+
instance_variable_get("@#{name}")
234+
end
235+
end
177236

178-
def self.format
179-
@format ||= field_list.map do |field|
180-
Fields::FORMAT_CODE[type_map[field]] +
181-
option_map.dig(field, :size).to_s
182-
end.join
183-
end
237+
# Generates a reader method for fields that have default values.
238+
# @param name [Symbol] name of internal field
239+
# @param idx [Integer] the index of the field value in the @values array
240+
# @param default [Value] the default value
241+
# @api private
242+
def def_default_reader(name, idx, default)
243+
define_method(name) do
244+
instance_variable_defined?("@#{name}") ||
245+
instance_variable_set("@#{name}", @values.size > idx ? @values[idx] : default)
184246

185-
def self.bytesize
186-
@bytesize ||= field_list.map do |field|
187-
Fields::BYTE_SIZE[type_map[field]] ||
188-
option_map.dig(field, :size)
189-
end.sum
190-
end
247+
instance_variable_get("@#{name}")
248+
end
249+
end
191250

192-
# @return [Hash] a hash representation of this {MachOStructure}.
193-
def to_h
194-
{
195-
"structure" => {
196-
"format" => self.class.format,
197-
"bytesize" => self.class.bytesize,
198-
},
199-
}
251+
# Generates an attr_reader like method for a field.
252+
# @param name [Symbol] name of internal field
253+
# @param idx [Integer] the index of the field value in the @values array
254+
# @api private
255+
def def_reader(name, idx)
256+
define_method(name) do
257+
@values[idx]
258+
end
259+
end
200260
end
201261
end
202262
end

0 commit comments

Comments
 (0)