ruby-macho 3.0.0 → 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
 
@@ -413,22 +411,37 @@ module MachO
413
411
 
414
412
  # Delete the given runtime path from the Mach-O.
415
413
  # @example
416
- # file.rpaths # => ["/lib"]
417
- # file.delete_rpath("/lib")
418
- # file.rpaths # => []
414
+ # file1.rpaths # => ["/lib", "/usr/lib", "/lib"]
415
+ # file1.delete_rpath("/lib")
416
+ # file1.rpaths # => ["/usr/lib", "/lib"]
417
+ # file2.rpaths # => ["foo", "foo"]
418
+ # file2.delete_rpath("foo", :uniq => true)
419
+ # file2.rpaths # => []
420
+ # file3.rpaths # => ["foo", "bar", "foo"]
421
+ # file3.delete_rpath("foo", :last => true)
422
+ # file3.rpaths # => ["foo", "bar"]
419
423
  # @param path [String] the runtime path to delete
420
424
  # @param options [Hash]
421
425
  # @option options [Boolean] :uniq (false) if true, also delete
422
426
  # duplicates of the requested path. If false, delete the first
423
- # instance (by offset) of the requested path.
427
+ # instance (by offset) of the requested path, unless :last is true.
428
+ # Incompatible with :last.
429
+ # @option options [Boolean] :last (false) if true, delete the last
430
+ # instance (by offset) of the requested path. Incompatible with :uniq.
424
431
  # @return void
425
432
  # @raise [RpathUnknownError] if no such runtime path exists
433
+ # @raise [ArgumentError] if both :uniq and :last are true
426
434
  def delete_rpath(path, options = {})
427
435
  uniq = options.fetch(:uniq, false)
428
- search_method = uniq ? :select : :find
436
+ last = options.fetch(:last, false)
437
+ raise ArgumentError, "Cannot set both :uniq and :last to true" if uniq && last
438
+
439
+ search_method = uniq || last ? :select : :find
440
+ rpath_cmds = command(:LC_RPATH).public_send(search_method) { |r| r.path.to_s == path }
441
+ rpath_cmds = rpath_cmds.last if last
429
442
 
430
443
  # 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 })
444
+ rpath_cmds = Array(rpath_cmds)
432
445
  raise RpathUnknownError, path if rpath_cmds.empty?
433
446
 
434
447
  # delete the commands in reverse order, offset descending.
@@ -592,7 +605,7 @@ module MachO
592
605
  LoadCommands::LoadCommand
593
606
  end
594
607
 
595
- view = MachOView.new(@raw_data, endianness, offset)
608
+ view = MachOView.new(self, @raw_data, endianness, offset)
596
609
  command = klass.new_from_bin(view)
597
610
 
598
611
  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 additional 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 further 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.1"
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.1
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: 2024-02-16 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.