ruby-macho 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -381,11 +381,9 @@ module MachO
381
381
  # rpaths simultaneously.
382
382
  # @return [void]
383
383
  # @raise [RpathUnknownError] if no such old runtime path exists
384
- # @raise [RpathExistsError] if the new runtime path already exists
385
384
  def change_rpath(old_path, new_path, options = {})
386
385
  old_lc = command(:LC_RPATH).find { |r| r.path.to_s == old_path }
387
386
  raise RpathUnknownError, old_path if old_lc.nil?
388
- raise RpathExistsError, new_path if rpaths.include?(new_path)
389
387
 
390
388
  new_lc = LoadCommands::LoadCommand.create(:LC_RPATH, new_path)
391
389
 
@@ -420,15 +418,24 @@ module MachO
420
418
  # @param options [Hash]
421
419
  # @option options [Boolean] :uniq (false) if true, also delete
422
420
  # duplicates of the requested path. If false, delete the first
423
- # instance (by offset) of the requested path.
421
+ # instance (by offset) of the requested path, unless :last is true.
422
+ # Incompatible with :last.
423
+ # @option options [Boolean] :last (false) if true, delete the last
424
+ # instance (by offset) of the requested path. Incompatible with :uniq.
424
425
  # @return void
425
426
  # @raise [RpathUnknownError] if no such runtime path exists
427
+ # @raise [ArgumentError] if both :uniq and :last are true
426
428
  def delete_rpath(path, options = {})
427
429
  uniq = options.fetch(:uniq, false)
428
- search_method = uniq ? :select : :find
430
+ last = options.fetch(:last, false)
431
+ raise ArgumentError, "Cannot set both :uniq and :last to true" if uniq && last
432
+
433
+ search_method = uniq || last ? :select : :find
434
+ rpath_cmds = command(:LC_RPATH).public_send(search_method) { |r| r.path.to_s == path }
435
+ rpath_cmds = rpath_cmds.last if last
429
436
 
430
437
  # Cast rpath_cmds into an Array so we can handle the uniq and non-uniq cases the same way
431
- rpath_cmds = Array(command(:LC_RPATH).method(search_method).call { |r| r.path.to_s == path })
438
+ rpath_cmds = Array(rpath_cmds)
432
439
  raise RpathUnknownError, path if rpath_cmds.empty?
433
440
 
434
441
  # delete the commands in reverse order, offset descending.
@@ -592,7 +599,7 @@ module MachO
592
599
  LoadCommands::LoadCommand
593
600
  end
594
601
 
595
- view = MachOView.new(@raw_data, endianness, offset)
602
+ view = MachOView.new(self, @raw_data, endianness, offset)
596
603
  command = klass.new_from_bin(view)
597
604
 
598
605
  load_commands << command
@@ -89,61 +89,38 @@ module MachO
89
89
  # Represents a section of a segment for 32-bit architectures.
90
90
  class Section < MachOStructure
91
91
  # @return [String] the name of the section, including null pad bytes
92
- attr_reader :sectname
92
+ field :sectname, :string, :padding => :null, :size => 16
93
93
 
94
94
  # @return [String] the name of the segment's section, including null
95
95
  # pad bytes
96
- attr_reader :segname
96
+ field :segname, :string, :padding => :null, :size => 16
97
97
 
98
98
  # @return [Integer] the memory address of the section
99
- attr_reader :addr
99
+ field :addr, :uint32
100
100
 
101
101
  # @return [Integer] the size, in bytes, of the section
102
- attr_reader :size
102
+ field :size, :uint32
103
103
 
104
104
  # @return [Integer] the file offset of the section
105
- attr_reader :offset
105
+ field :offset, :uint32
106
106
 
107
107
  # @return [Integer] the section alignment (power of 2) of the section
108
- attr_reader :align
108
+ field :align, :uint32
109
109
 
110
110
  # @return [Integer] the file offset of the section's relocation entries
111
- attr_reader :reloff
111
+ field :reloff, :uint32
112
112
 
113
113
  # @return [Integer] the number of relocation entries
114
- attr_reader :nreloc
114
+ field :nreloc, :uint32
115
115
 
116
116
  # @return [Integer] flags for type and attributes of the section
117
- attr_reader :flags
117
+ field :flags, :uint32
118
118
 
119
119
  # @return [void] reserved (for offset or index)
120
- attr_reader :reserved1
120
+ field :reserved1, :uint32
121
121
 
122
122
  # @return [void] reserved (for count or sizeof)
123
- attr_reader :reserved2
124
-
125
- # @see MachOStructure::FORMAT
126
- FORMAT = "Z16Z16L=9"
127
-
128
- # @see MachOStructure::SIZEOF
129
- SIZEOF = 68
130
-
131
- # @api private
132
- def initialize(sectname, segname, addr, size, offset, align, reloff,
133
- nreloc, flags, reserved1, reserved2)
134
- super()
135
- @sectname = sectname
136
- @segname = segname
137
- @addr = addr
138
- @size = size
139
- @offset = offset
140
- @align = align
141
- @reloff = reloff
142
- @nreloc = nreloc
143
- @flags = flags
144
- @reserved1 = reserved1
145
- @reserved2 = reserved2
146
- end
123
+ field :reserved2, :uint32
147
124
 
148
125
  # @return [String] the section's name
149
126
  def section_name
@@ -219,22 +196,14 @@ module MachO
219
196
 
220
197
  # Represents a section of a segment for 64-bit architectures.
221
198
  class Section64 < Section
222
- # @return [void] reserved
223
- attr_reader :reserved3
224
-
225
- # @see MachOStructure::FORMAT
226
- FORMAT = "Z16Z16Q=2L=8"
199
+ # @return [Integer] the memory address of the section
200
+ field :addr, :uint64
227
201
 
228
- # @see MachOStructure::SIZEOF
229
- SIZEOF = 80
202
+ # @return [Integer] the size, in bytes, of the section
203
+ field :size, :uint64
230
204
 
231
- # @api private
232
- def initialize(sectname, segname, addr, size, offset, align, reloff,
233
- nreloc, flags, reserved1, reserved2, reserved3)
234
- super(sectname, segname, addr, size, offset, align, reloff,
235
- nreloc, flags, reserved1, reserved2)
236
- @reserved3 = reserved3
237
- end
205
+ # @return [void] reserved
206
+ field :reserved3, :uint32
238
207
 
239
208
  # @return [Hash] a hash representation of this {Section64}
240
209
  def to_h
@@ -1,42 +1,284 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MachO
4
- # A general purpose pseudo-structure.
4
+ # A general purpose pseudo-structure. Described in detail in docs/machostructure-dsl.md.
5
5
  # @abstract
6
6
  class MachOStructure
7
- # The String#unpack format of the data structure.
8
- # @return [String] the unpacking format
9
- # @api private
10
- FORMAT = ""
11
-
12
- # The size of the data structure, in bytes.
13
- # @return [Integer] the size, in bytes
14
- # @api private
15
- SIZEOF = 0
16
-
17
- # @return [Integer] the size, in bytes, of the represented structure.
18
- def self.bytesize
19
- self::SIZEOF
7
+ # Constants used for parsing MachOStructure fields
8
+ module Fields
9
+ # 1. All fields with empty strings and zeros aren't used
10
+ # to calculate the format and sizeof variables.
11
+ # 2. All fields with nil should provide those values manually
12
+ # via the :size parameter.
13
+
14
+ # association of field types to byte size
15
+ # @api private
16
+ BYTE_SIZE = {
17
+ # Binary slices
18
+ :string => nil,
19
+ :null_padded_string => nil,
20
+ :int32 => 4,
21
+ :uint32 => 4,
22
+ :uint64 => 8,
23
+ # Classes
24
+ :view => 0,
25
+ :lcstr => 4,
26
+ :two_level_hints_table => 0,
27
+ :tool_entries => 4,
28
+ }.freeze
29
+
30
+ # association of field types with ruby format codes
31
+ # Binary format codes can be found here:
32
+ # https://docs.ruby-lang.org/en/2.6.0/String.html#method-i-unpack
33
+ #
34
+ # The equals sign is used to manually change endianness using
35
+ # the Utils#specialize_format() method.
36
+ # @api private
37
+ FORMAT_CODE = {
38
+ # Binary slices
39
+ :string => "a",
40
+ :null_padded_string => "Z",
41
+ :int32 => "l=",
42
+ :uint32 => "L=",
43
+ :uint64 => "Q=",
44
+ # Classes
45
+ :view => "",
46
+ :lcstr => "L=",
47
+ :two_level_hints_table => "",
48
+ :tool_entries => "L=",
49
+ }.freeze
50
+
51
+ # A list of classes that must get initialized
52
+ # To add a new class append it here and add the init method to the def_class_reader method
53
+ # @api private
54
+ CLASSES_TO_INIT = %i[lcstr tool_entries two_level_hints_table].freeze
55
+
56
+ # A list of fields that don't require arguments in the initializer
57
+ # Used to calculate MachOStructure#min_args
58
+ # @api private
59
+ NO_ARG_REQUIRED = %i[two_level_hints_table].freeze
20
60
  end
21
61
 
22
- # @param endianness [Symbol] either `:big` or `:little`
23
- # @param bin [String] the string to be unpacked into the new structure
24
- # @return [MachO::MachOStructure] the resulting structure
25
- # @api private
26
- def self.new_from_bin(endianness, bin)
27
- format = Utils.specialize_format(self::FORMAT, endianness)
62
+ # map of field names to indices
63
+ @field_idxs = {}
64
+
65
+ # array of fields sizes
66
+ @size_list = []
28
67
 
29
- new(*bin.unpack(format))
68
+ # array of field format codes
69
+ @fmt_list = []
70
+
71
+ # minimum number of required arguments
72
+ @min_args = 0
73
+
74
+ # @param args [Array[Value]] list of field parameters
75
+ def initialize(*args)
76
+ raise ArgumentError, "Invalid number of arguments" if args.size < self.class.min_args
77
+
78
+ @values = args
30
79
  end
31
80
 
32
81
  # @return [Hash] a hash representation of this {MachOStructure}.
33
82
  def to_h
34
83
  {
35
84
  "structure" => {
36
- "format" => self.class::FORMAT,
85
+ "format" => self.class.format,
37
86
  "bytesize" => self.class.bytesize,
38
87
  },
39
88
  }
40
89
  end
90
+
91
+ class << self
92
+ attr_reader :min_args
93
+
94
+ # @param endianness [Symbol] either `:big` or `:little`
95
+ # @param bin [String] the string to be unpacked into the new structure
96
+ # @return [MachO::MachOStructure] the resulting structure
97
+ # @api private
98
+ def new_from_bin(endianness, bin)
99
+ format = Utils.specialize_format(self.format, endianness)
100
+
101
+ new(*bin.unpack(format))
102
+ end
103
+
104
+ def format
105
+ @format ||= @fmt_list.join
106
+ end
107
+
108
+ def bytesize
109
+ @bytesize ||= @size_list.sum
110
+ end
111
+
112
+ private
113
+
114
+ # @param subclass [Class] subclass type
115
+ # @api private
116
+ def inherited(subclass) # rubocop:disable Lint/MissingSuper
117
+ # Clone all class instance variables
118
+ field_idxs = @field_idxs.dup
119
+ size_list = @size_list.dup
120
+ fmt_list = @fmt_list.dup
121
+ min_args = @min_args.dup
122
+
123
+ # Add those values to the inheriting class
124
+ subclass.class_eval do
125
+ @field_idxs = field_idxs
126
+ @size_list = size_list
127
+ @fmt_list = fmt_list
128
+ @min_args = min_args
129
+ end
130
+ end
131
+
132
+ # @param name [Symbol] name of internal field
133
+ # @param type [Symbol] type of field in terms of binary size
134
+ # @param options [Hash] set of additonal options
135
+ # Expected options
136
+ # :size [Integer] size in bytes
137
+ # :mask [Integer] bitmask
138
+ # :unpack [String] string format
139
+ # :default [Value] default value
140
+ # :to_s [Boolean] flag for generating #to_s
141
+ # :endian [Symbol] optionally specify :big or :little endian
142
+ # :padding [Symbol] optionally specify :null padding
143
+ # @api private
144
+ def field(name, type, **options)
145
+ raise ArgumentError, "Invalid field type #{type}" unless Fields::FORMAT_CODE.key?(type)
146
+
147
+ # Get field idx for size_list and fmt_list
148
+ idx = if @field_idxs.key?(name)
149
+ @field_idxs[name]
150
+ else
151
+ @min_args += 1 unless options.key?(:default) || Fields::NO_ARG_REQUIRED.include?(type)
152
+ @field_idxs[name] = @field_idxs.size
153
+ @size_list << nil
154
+ @fmt_list << nil
155
+ @field_idxs.size - 1
156
+ end
157
+
158
+ # Update string type if padding is specified
159
+ type = :null_padded_string if type == :string && options[:padding] == :null
160
+
161
+ # Add to size_list and fmt_list
162
+ @size_list[idx] = Fields::BYTE_SIZE[type] || options[:size]
163
+ @fmt_list[idx] = if options[:endian]
164
+ Utils.specialize_format(Fields::FORMAT_CODE[type], options[:endian])
165
+ else
166
+ Fields::FORMAT_CODE[type]
167
+ end
168
+ @fmt_list[idx] += options[:size].to_s if options.key?(:size)
169
+
170
+ # Generate methods
171
+ if Fields::CLASSES_TO_INIT.include?(type)
172
+ def_class_reader(name, type, idx)
173
+ elsif options.key?(:mask)
174
+ def_mask_reader(name, idx, options[:mask])
175
+ elsif options.key?(:unpack)
176
+ def_unpack_reader(name, idx, options[:unpack])
177
+ elsif options.key?(:default)
178
+ def_default_reader(name, idx, options[:default])
179
+ else
180
+ def_reader(name, idx)
181
+ end
182
+
183
+ def_to_s(name) if options[:to_s]
184
+ end
185
+
186
+ #
187
+ # Method Generators
188
+ #
189
+
190
+ # Generates a reader method for classes that need to be initialized.
191
+ # These classes are defined in the Fields::CLASSES_TO_INIT array.
192
+ # @param name [Symbol] name of internal field
193
+ # @param type [Symbol] type of field in terms of binary size
194
+ # @param idx [Integer] the index of the field value in the @values array
195
+ # @api private
196
+ def def_class_reader(name, type, idx)
197
+ case type
198
+ when :lcstr
199
+ define_method(name) do
200
+ instance_variable_defined?("@#{name}") ||
201
+ instance_variable_set("@#{name}", LoadCommands::LoadCommand::LCStr.new(self, @values[idx]))
202
+
203
+ instance_variable_get("@#{name}")
204
+ end
205
+ when :two_level_hints_table
206
+ define_method(name) do
207
+ instance_variable_defined?("@#{name}") ||
208
+ instance_variable_set("@#{name}", LoadCommands::TwolevelHintsCommand::TwolevelHintsTable.new(view, htoffset, nhints))
209
+
210
+ instance_variable_get("@#{name}")
211
+ end
212
+ when :tool_entries
213
+ define_method(name) do
214
+ instance_variable_defined?("@#{name}") ||
215
+ instance_variable_set("@#{name}", LoadCommands::BuildVersionCommand::ToolEntries.new(view, @values[idx]))
216
+
217
+ instance_variable_get("@#{name}")
218
+ end
219
+ end
220
+ end
221
+
222
+ # Generates a reader method for fields that need to be bitmasked.
223
+ # @param name [Symbol] name of internal field
224
+ # @param idx [Integer] the index of the field value in the @values array
225
+ # @param mask [Integer] the bitmask
226
+ # @api private
227
+ def def_mask_reader(name, idx, mask)
228
+ define_method(name) do
229
+ instance_variable_defined?("@#{name}") ||
230
+ instance_variable_set("@#{name}", @values[idx] & ~mask)
231
+
232
+ instance_variable_get("@#{name}")
233
+ end
234
+ end
235
+
236
+ # Generates a reader method for fields that need further unpacking.
237
+ # @param name [Symbol] name of internal field
238
+ # @param idx [Integer] the index of the field value in the @values array
239
+ # @param unpack [String] the format code used for futher binary unpacking
240
+ # @api private
241
+ def def_unpack_reader(name, idx, unpack)
242
+ define_method(name) do
243
+ instance_variable_defined?("@#{name}") ||
244
+ instance_variable_set("@#{name}", @values[idx].unpack(unpack))
245
+
246
+ instance_variable_get("@#{name}")
247
+ end
248
+ end
249
+
250
+ # Generates a reader method for fields that have default values.
251
+ # @param name [Symbol] name of internal field
252
+ # @param idx [Integer] the index of the field value in the @values array
253
+ # @param default [Value] the default value
254
+ # @api private
255
+ def def_default_reader(name, idx, default)
256
+ define_method(name) do
257
+ instance_variable_defined?("@#{name}") ||
258
+ instance_variable_set("@#{name}", @values.size > idx ? @values[idx] : default)
259
+
260
+ instance_variable_get("@#{name}")
261
+ end
262
+ end
263
+
264
+ # Generates an attr_reader like method for a field.
265
+ # @param name [Symbol] name of internal field
266
+ # @param idx [Integer] the index of the field value in the @values array
267
+ # @api private
268
+ def def_reader(name, idx)
269
+ define_method(name) do
270
+ @values[idx]
271
+ end
272
+ end
273
+
274
+ # Generates the to_s method based on the named field.
275
+ # @param name [Symbol] name of the field
276
+ # @api private
277
+ def def_to_s(name)
278
+ define_method(:to_s) do
279
+ send(name).to_s
280
+ end
281
+ end
282
+ end
41
283
  end
42
284
  end
data/lib/macho/view.rb CHANGED
@@ -3,6 +3,9 @@
3
3
  module MachO
4
4
  # A representation of some unspecified Mach-O data.
5
5
  class MachOView
6
+ # @return [MachOFile] that this view belongs to
7
+ attr_reader :macho_file
8
+
6
9
  # @return [String] the raw Mach-O data
7
10
  attr_reader :raw_data
8
11
 
@@ -13,10 +16,12 @@ module MachO
13
16
  attr_reader :offset
14
17
 
15
18
  # Creates a new MachOView.
19
+ # @param macho_file [MachOFile] the file this view slice is from
16
20
  # @param raw_data [String] the raw Mach-O data
17
21
  # @param endianness [Symbol] the endianness of the data
18
22
  # @param offset [Integer] the offset of the relevant data
19
- def initialize(raw_data, endianness, offset)
23
+ def initialize(macho_file, raw_data, endianness, offset)
24
+ @macho_file = macho_file
20
25
  @raw_data = raw_data
21
26
  @endianness = endianness
22
27
  @offset = offset
@@ -29,5 +34,9 @@ module MachO
29
34
  "offset" => offset,
30
35
  }
31
36
  end
37
+
38
+ def inspect
39
+ "#<#{self.class}:0x#{(object_id << 1).to_s(16)} @endianness=#{@endianness.inspect}, @offset=#{@offset.inspect}, length=#{@raw_data.length}>"
40
+ end
32
41
  end
33
42
  end
data/lib/macho.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "open3"
4
4
 
5
+ require_relative "macho/utils"
5
6
  require_relative "macho/structure"
6
7
  require_relative "macho/view"
7
8
  require_relative "macho/headers"
@@ -10,13 +11,12 @@ require_relative "macho/sections"
10
11
  require_relative "macho/macho_file"
11
12
  require_relative "macho/fat_file"
12
13
  require_relative "macho/exceptions"
13
- require_relative "macho/utils"
14
14
  require_relative "macho/tools"
15
15
 
16
16
  # The primary namespace for ruby-macho.
17
17
  module MachO
18
18
  # release version
19
- VERSION = "3.0.0"
19
+ VERSION = "4.0.0"
20
20
 
21
21
  # Opens the given filename as a MachOFile or FatFile, depending on its magic.
22
22
  # @param filename [String] the file being opened
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-macho
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - William Woodruff
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-11 00:00:00.000000000 Z
11
+ date: 2023-07-25 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A library for viewing and manipulating Mach-O files in Ruby.
14
14
  email: william@yossarian.net
@@ -33,7 +33,8 @@ files:
33
33
  homepage: https://github.com/Homebrew/ruby-macho
34
34
  licenses:
35
35
  - MIT
36
- metadata: {}
36
+ metadata:
37
+ rubygems_mfa_required: 'true'
37
38
  post_install_message:
38
39
  rdoc_options: []
39
40
  require_paths:
@@ -49,7 +50,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
49
50
  - !ruby/object:Gem::Version
50
51
  version: '0'
51
52
  requirements: []
52
- rubygems_version: 3.2.32
53
+ rubygems_version: 3.4.10
53
54
  signing_key:
54
55
  specification_version: 4
55
56
  summary: ruby-macho - Mach-O file analyzer.