ruby-macho 0.2.4 → 0.2.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 555d8c0f3cd81252ac1319053a95d643536387a2
4
- data.tar.gz: 23a91b4980829b2b8d2133f226aeefbc4bd950b4
3
+ metadata.gz: 14255fd4c8aaab675981bcf982893a8be8b6ba0c
4
+ data.tar.gz: e89c2ff64ebe885089b1b3b3ffa3c80418070fdd
5
5
  SHA512:
6
- metadata.gz: 37f23f18f8a4a4c07933978d7be6bcb65cc6655f778496a8b2b5e2932cfa199f3230bb82399f1f964c06469ec74cece3fd9fb2ef4c6d39c2ef933eaa185ae96c
7
- data.tar.gz: 39bfb1a708519820a4afbbccb6a47da8c7ef3ad2641d7ad5c7969709f50f4ce7ffc3369918238848da2786f49fd9e10209d0e7e0b6bbba96a2a9620b6f6bdfc6
6
+ metadata.gz: f8796363fb1de467d352e6ed808865d83a84b8cbca747037c542f788feb316cc6e733d03817d01d4182d5049b90b557365e8c4bd9e24ef2f4eab15cca212c92a
7
+ data.tar.gz: efc4df8def9305d15edb6273ea72baf8d8da778151697bae975be45750da6f7eec6c541b9852875b8ce4f469ee05bf8b380816ea0fe79ea5e66e04a1e710565e
data/README.md CHANGED
@@ -14,6 +14,9 @@ executables, dynamic libraries, and so forth.
14
14
 
15
15
  ### Documentation
16
16
 
17
+ **Important**: ruby-macho does not have a stable API (yet). Use it with this
18
+ in mind.
19
+
17
20
  Full documentation is available on [RubyDoc](http://www.rubydoc.info/gems/ruby-macho/).
18
21
 
19
22
  A quick example of what ruby-macho can do:
@@ -23,8 +26,8 @@ require 'macho'
23
26
 
24
27
  file = MachO::MachOFile.new("/path/to/my/binary")
25
28
 
26
- # get the file's type (MH_OBJECT, MH_DYLIB, MH_EXECUTE, etc)
27
- file.filetype # => "MH_EXECUTE"
29
+ # get the file's type (object, dynamic lib, executable, etc)
30
+ file.filetype # => :execute
28
31
 
29
32
  # get all load commands in the file and print their offsets:
30
33
  file.load_commands.each do |lc|
@@ -32,7 +35,7 @@ file.load_commands.each do |lc|
32
35
  end
33
36
 
34
37
  # access a specific load command
35
- lc_vers = file['LC_VERSION_MIN_MACOSX'].first
38
+ lc_vers = file[:LC_VERSION_MIN_MACOSX].first
36
39
  puts lc_vers.version_string # => "10.10.0"
37
40
  ```
38
41
 
@@ -41,16 +44,12 @@ puts lc_vers.version_string # => "10.10.0"
41
44
  * Reading data from x86/x86_64/PPC Mach-O files
42
45
  * Changing the IDs of Mach-O and Fat dylibs
43
46
  * Changing install names in Mach-O and Fat files
44
-
45
- ### What doesn't work yet?
46
-
47
- * Adding, deleting, or modifying rpaths.
47
+ * Adding, deleting, and modifying rpaths.
48
48
 
49
49
  ### What needs to be done?
50
50
 
51
51
  * Documentation.
52
- * Rpath modification.
53
- * Many, many things.
52
+ * Unit and performance testing.
54
53
 
55
54
  Attribution:
56
55
 
data/lib/macho.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require "#{File.dirname(__FILE__)}/macho/structure"
2
+ require "#{File.dirname(__FILE__)}/macho/view"
2
3
  require "#{File.dirname(__FILE__)}/macho/headers"
3
4
  require "#{File.dirname(__FILE__)}/macho/load_commands"
4
5
  require "#{File.dirname(__FILE__)}/macho/sections"
@@ -12,5 +13,5 @@ require "#{File.dirname(__FILE__)}/macho/tools"
12
13
  # The primary namespace for ruby-macho.
13
14
  module MachO
14
15
  # release version
15
- VERSION = "0.2.4".freeze
16
+ VERSION = "0.2.5".freeze
16
17
  end
@@ -3,6 +3,26 @@ module MachO
3
3
  class MachOError < RuntimeError
4
4
  end
5
5
 
6
+ # Raised when a Mach-O file modification fails.
7
+ class ModificationError < MachOError
8
+ end
9
+
10
+ # Raised when a Mach-O file modification fails but can be recovered when
11
+ # operating on multiple Mach-O slices of a fat binary in non-strict mode.
12
+ class RecoverableModificationError < ModificationError
13
+ # @return [Fixnum, nil] The index of the Mach-O slice of a fat binary for
14
+ # which modification failed or `nil` if not a fat binary. This is used to
15
+ # make the error message more useful.
16
+ attr_accessor :macho_slice
17
+
18
+ # @return [String] The exception message.
19
+ def to_s
20
+ s = super.to_s
21
+ s = "While modifying Mach-O slice #{@macho_slice}: #{s}" if @macho_slice
22
+ s
23
+ end
24
+ end
25
+
6
26
  # Raised when a file is not a Mach-O.
7
27
  class NotAMachOError < MachOError
8
28
  # @param error [String] the error in question
@@ -80,32 +100,89 @@ module MachO
80
100
  end
81
101
  end
82
102
 
103
+ # Raised when a load command can't be created manually.
104
+ class LoadCommandNotCreatableError < MachOError
105
+ # @param cmd_sym [Symbol] the uncreatable load command's symbol
106
+ def initialize(cmd_sym)
107
+ super "Load commands of type #{cmd_sym} cannot be created manually"
108
+ end
109
+ end
110
+
111
+ # Raised when the number of arguments used to create a load command manually is wrong.
112
+ class LoadCommandCreationArityError < MachOError
113
+ # @param cmd_sym [Symbol] the load command's symbol
114
+ # @param expected_arity [Fixnum] the number of arguments expected
115
+ # @param actual_arity [Fixnum] the number of arguments received
116
+ def initialize(cmd_sym, expected_arity, actual_arity)
117
+ super "Expected #{expected_arity} arguments for #{cmd_sym} creation, got #{actual_arity}"
118
+ end
119
+ end
120
+
121
+ # Raised when a load command can't be serialized.
122
+ class LoadCommandNotSerializableError < MachOError
123
+ # @param cmd_sym [Symbol] the load command's symbol
124
+ def initialize(cmd_sym)
125
+ super "Load commands of type #{cmd_sym} cannot be serialized"
126
+ end
127
+ end
128
+
129
+ # Raised when a load command string is malformed in some way.
130
+ class LCStrMalformedError < MachOError
131
+ # @param lc [MachO::LoadCommand] the load command containing the string
132
+ def initialize(lc)
133
+ super "Load command #{lc.type} at offset #{lc.view.offset} contains a malformed string"
134
+ end
135
+ end
136
+
137
+ # Raised when a change at an offset is not valid.
138
+ class OffsetInsertionError < ModificationError
139
+ # @param offset [Fixnum] the invalid offset
140
+ def initialize(offset)
141
+ super "Insertion at offset #{offset} is not valid"
142
+ end
143
+ end
144
+
83
145
  # Raised when load commands are too large to fit in the current file.
84
- class HeaderPadError < MachOError
146
+ class HeaderPadError < ModificationError
85
147
  # @param filename [String] the filename
86
148
  def initialize(filename)
87
- super "Updated load commands do not fit in the header of " +
88
- "#{filename}. #{filename} needs to be relinked, possibly with " +
89
- "-headerpad or -headerpad_max_install_names"
149
+ super "Updated load commands do not fit in the header of " \
150
+ "#{filename}. #{filename} needs to be relinked, possibly with " \
151
+ "-headerpad or -headerpad_max_install_names"
90
152
  end
91
153
  end
92
154
 
93
155
  # Raised when attempting to change a dylib name that doesn't exist.
94
- class DylibUnknownError < MachOError
156
+ class DylibUnknownError < RecoverableModificationError
95
157
  # @param dylib [String] the unknown shared library name
96
158
  def initialize(dylib)
97
159
  super "No such dylib name: #{dylib}"
98
160
  end
99
161
  end
100
162
 
163
+ # Raised when a dylib is missing an ID
164
+ class DylibIdMissingError < RecoverableModificationError
165
+ def initialize
166
+ super "Dylib is missing a dylib ID"
167
+ end
168
+ end
169
+
101
170
  # Raised when attempting to change an rpath that doesn't exist.
102
- class RpathUnknownError < MachOError
171
+ class RpathUnknownError < RecoverableModificationError
103
172
  # @param path [String] the unknown runtime path
104
173
  def initialize(path)
105
174
  super "No such runtime path: #{path}"
106
175
  end
107
176
  end
108
177
 
178
+ # Raised when attempting to add an rpath that already exists.
179
+ class RpathExistsError < RecoverableModificationError
180
+ # @param path [String] the extant path
181
+ def initialize(path)
182
+ super "#{path} already exists"
183
+ end
184
+ end
185
+
109
186
  # Raised whenever unfinished code is called.
110
187
  class UnimplementedError < MachOError
111
188
  # @param thing [String] the thing that is unimplemented
@@ -30,22 +30,24 @@ module MachO
30
30
  # @param filename [String] the fat file to load from
31
31
  # @raise [ArgumentError] if the given file does not exist
32
32
  def initialize(filename)
33
- raise ArgumentError.new("#{filename}: no such file") unless File.file?(filename)
33
+ raise ArgumentError, "#{filename}: no such file" unless File.file?(filename)
34
34
 
35
35
  @filename = filename
36
- @raw_data = File.open(@filename, "rb") { |f| f.read }
37
- @header = get_fat_header
38
- @fat_archs = get_fat_archs
39
- @machos = get_machos
36
+ @raw_data = File.open(@filename, "rb", &:read)
37
+ @header = populate_fat_header
38
+ @fat_archs = populate_fat_archs
39
+ @machos = populate_machos
40
40
  end
41
41
 
42
+ # Initializes a new FatFile instance from a binary string.
43
+ # @see MachO::FatFile.new_from_bin
42
44
  # @api private
43
45
  def initialize_from_bin(bin)
44
46
  @filename = nil
45
47
  @raw_data = bin
46
- @header = get_fat_header
47
- @fat_archs = get_fat_archs
48
- @machos = get_machos
48
+ @header = populate_fat_header
49
+ @fat_archs = populate_fat_archs
50
+ @machos = populate_machos
49
51
  end
50
52
 
51
53
  # The file's raw fat data.
@@ -115,7 +117,7 @@ module MachO
115
117
  end
116
118
 
117
119
  # The file's type. Assumed to be the same for every Mach-O within.
118
- # @return [String] the filetype
120
+ # @return [Symbol] the filetype
119
121
  def filetype
120
122
  machos.first.filetype
121
123
  end
@@ -124,34 +126,37 @@ module MachO
124
126
  # @example
125
127
  # file.dylib_id # => 'libBar.dylib'
126
128
  # @return [String, nil] the file's dylib ID
129
+ # @see MachO::MachOFile#linked_dylibs
127
130
  def dylib_id
128
131
  machos.first.dylib_id
129
132
  end
130
133
 
131
134
  # Changes the file's dylib ID to `new_id`. If the file is not a dylib, does nothing.
132
135
  # @example
133
- # file.dylib_id = 'libFoo.dylib'
136
+ # file.change_dylib_id('libFoo.dylib')
134
137
  # @param new_id [String] the new dylib ID
138
+ # @param options [Hash]
139
+ # @option options [Boolean] :strict (true) if true, fail if one slice fails.
140
+ # if false, fail only if all slices fail.
135
141
  # @return [void]
136
142
  # @raise [ArgumentError] if `new_id` is not a String
137
- def dylib_id=(new_id)
138
- if !new_id.is_a?(String)
139
- raise ArgumentError.new("argument must be a String")
140
- end
141
-
142
- if !machos.all?(&:dylib?)
143
- return nil
144
- end
143
+ # @see MachO::MachOFile#linked_dylibs
144
+ def change_dylib_id(new_id, options = {})
145
+ raise ArgumentError, "argument must be a String" unless new_id.is_a?(String)
146
+ return unless machos.all?(&:dylib?)
145
147
 
146
- machos.each do |macho|
147
- macho.dylib_id = new_id
148
+ each_macho(options) do |macho|
149
+ macho.change_dylib_id(new_id, options)
148
150
  end
149
151
 
150
152
  synchronize_raw_data
151
153
  end
152
154
 
155
+ alias dylib_id= change_dylib_id
156
+
153
157
  # All shared libraries linked to the file's Mach-Os.
154
158
  # @return [Array<String>] an array of all shared libraries
159
+ # @see MachO::MachOFile#linked_dylibs
155
160
  def linked_dylibs
156
161
  # Individual architectures in a fat binary can link to different subsets
157
162
  # of libraries, but at this point we want to have the full picture, i.e.
@@ -165,16 +170,74 @@ module MachO
165
170
  # file.change_install_name('/usr/lib/libFoo.dylib', '/usr/lib/libBar.dylib')
166
171
  # @param old_name [String] the shared library name being changed
167
172
  # @param new_name [String] the new name
168
- # @todo incomplete
169
- def change_install_name(old_name, new_name)
170
- machos.each do |macho|
171
- macho.change_install_name(old_name, new_name)
173
+ # @param options [Hash]
174
+ # @option options [Boolean] :strict (true) if true, fail if one slice fails.
175
+ # if false, fail only if all slices fail.
176
+ # @return [void]
177
+ # @see MachO::MachOFile#change_install_name
178
+ def change_install_name(old_name, new_name, options = {})
179
+ each_macho(options) do |macho|
180
+ macho.change_install_name(old_name, new_name, options)
181
+ end
182
+
183
+ synchronize_raw_data
184
+ end
185
+
186
+ alias change_dylib change_install_name
187
+
188
+ # All runtime paths associated with the file's Mach-Os.
189
+ # @return [Array<String>] an array of all runtime paths
190
+ # @see MachO::MachOFile#rpaths
191
+ def rpaths
192
+ # Can individual architectures have different runtime paths?
193
+ machos.map(&:rpaths).flatten.uniq
194
+ end
195
+
196
+ # Change the runtime path `old_path` to `new_path` in the file's Mach-Os.
197
+ # @param old_path [String] the old runtime path
198
+ # @param new_path [String] the new runtime path
199
+ # @param options [Hash]
200
+ # @option options [Boolean] :strict (true) if true, fail if one slice fails.
201
+ # if false, fail only if all slices fail.
202
+ # @return [void]
203
+ # @see MachO::MachOFile#change_rpath
204
+ def change_rpath(old_path, new_path, options = {})
205
+ each_macho(options) do |macho|
206
+ macho.change_rpath(old_path, new_path, options)
207
+ end
208
+
209
+ synchronize_raw_data
210
+ end
211
+
212
+ # Add the given runtime path to the file's Mach-Os.
213
+ # @param path [String] the new runtime path
214
+ # @param options [Hash]
215
+ # @option options [Boolean] :strict (true) if true, fail if one slice fails.
216
+ # if false, fail only if all slices fail.
217
+ # @return [void]
218
+ # @see MachO::MachOFile#add_rpath
219
+ def add_rpath(path, options = {})
220
+ each_macho(options) do |macho|
221
+ macho.add_rpath(path, options)
172
222
  end
173
223
 
174
224
  synchronize_raw_data
175
225
  end
176
226
 
177
- alias :change_dylib :change_install_name
227
+ # Delete the given runtime path from the file's Mach-Os.
228
+ # @param path [String] the runtime path to delete
229
+ # @param options [Hash]
230
+ # @option options [Boolean] :strict (true) if true, fail if one slice fails.
231
+ # if false, fail only if all slices fail.
232
+ # @return void
233
+ # @see MachO::MachOFile#delete_rpath
234
+ def delete_rpath(path, options = {})
235
+ each_macho(options) do |macho|
236
+ macho.delete_rpath(path, options)
237
+ end
238
+
239
+ synchronize_raw_data
240
+ end
178
241
 
179
242
  # Extract a Mach-O with the given CPU type from the file.
180
243
  # @example
@@ -197,7 +260,7 @@ module MachO
197
260
  # @note Overwrites all data in the file!
198
261
  def write!
199
262
  if filename.nil?
200
- raise MachOError.new("cannot write to a default file when initialized from a binary string")
263
+ raise MachOError, "cannot write to a default file when initialized from a binary string"
201
264
  else
202
265
  File.open(@filename, "wb") { |f| f.write(@raw_data) }
203
266
  end
@@ -211,15 +274,15 @@ module MachO
211
274
  # @raise [MachO::MagicError] if the magic is not valid Mach-O magic
212
275
  # @raise [MachO::MachOBinaryError] if the magic is for a non-fat Mach-O file
213
276
  # @raise [MachO::JavaClassFileError] if the file is a Java classfile
214
- # @private
215
- def get_fat_header
277
+ # @api private
278
+ def populate_fat_header
216
279
  # the smallest fat Mach-O header is 8 bytes
217
- raise TruncatedFileError.new if @raw_data.size < 8
280
+ raise TruncatedFileError if @raw_data.size < 8
218
281
 
219
282
  fh = FatHeader.new_from_bin(:big, @raw_data[0, FatHeader.bytesize])
220
283
 
221
- raise MagicError.new(fh.magic) unless MachO.magic?(fh.magic)
222
- raise MachOBinaryError.new unless MachO.fat_magic?(fh.magic)
284
+ raise MagicError, fh.magic unless Utils.magic?(fh.magic)
285
+ raise MachOBinaryError unless Utils.fat_magic?(fh.magic)
223
286
 
224
287
  # Rationale: Java classfiles have the same magic as big-endian fat
225
288
  # Mach-Os. Classfiles encode their version at the same offset as
@@ -228,15 +291,15 @@ module MachO
228
291
  # technically possible for a fat Mach-O to have over 30 architectures,
229
292
  # but this is extremely unlikely and in practice distinguishes the two
230
293
  # formats.
231
- raise JavaClassFileError.new if fh.nfat_arch > 30
294
+ raise JavaClassFileError if fh.nfat_arch > 30
232
295
 
233
296
  fh
234
297
  end
235
298
 
236
299
  # Obtain an array of fat architectures from raw file data.
237
300
  # @return [Array<MachO::FatArch>] an array of fat architectures
238
- # @private
239
- def get_fat_archs
301
+ # @api private
302
+ def populate_fat_archs
240
303
  archs = []
241
304
 
242
305
  fa_off = FatHeader.bytesize
@@ -250,8 +313,8 @@ module MachO
250
313
 
251
314
  # Obtain an array of Mach-O blobs from raw file data.
252
315
  # @return [Array<MachO::MachOFile>] an array of Mach-Os
253
- # @private
254
- def get_machos
316
+ # @api private
317
+ def populate_machos
255
318
  machos = []
256
319
 
257
320
  fat_archs.each do |arch|
@@ -261,9 +324,37 @@ module MachO
261
324
  machos
262
325
  end
263
326
 
264
- # @todo this needs to be redesigned. arch[:offset] and arch[:size] are
265
- # already out-of-date, and the header needs to be synchronized as well.
266
- # @private
327
+ # Yield each Mach-O object in the file, rescuing and accumulating errors.
328
+ # @param options [Hash]
329
+ # @option options [Boolean] :strict (true) whether or not to fail loudly
330
+ # with an exception if at least one Mach-O raises an exception. If false,
331
+ # only raises an exception if *all* Mach-Os raise exceptions.
332
+ # @raise [MachO::RecoverableModificationError] under the conditions of
333
+ # the `:strict` option above.
334
+ # @api private
335
+ def each_macho(options = {})
336
+ strict = options.fetch(:strict, true)
337
+ errors = []
338
+
339
+ machos.each_with_index do |macho, index|
340
+ begin
341
+ yield macho
342
+ rescue RecoverableModificationError => error
343
+ error.macho_slice = index
344
+
345
+ # Strict mode: Immediately re-raise. Otherwise: Retain, check later.
346
+ raise error if strict
347
+ errors << error
348
+ end
349
+ end
350
+
351
+ # Non-strict mode: Raise first error if *all* Mach-O slices failed.
352
+ raise errors.first if errors.size == machos.size
353
+ end
354
+
355
+ # Synchronize the raw file data with each internal Mach-O object.
356
+ # @return [void]
357
+ # @api private
267
358
  def synchronize_raw_data
268
359
  machos.each_with_index do |macho, i|
269
360
  arch = fat_archs[i]