ruby-macho 0.2.4 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
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]