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 +4 -4
- data/README.md +8 -9
- data/lib/macho.rb +2 -1
- data/lib/macho/exceptions.rb +83 -6
- data/lib/macho/fat_file.rb +130 -39
- data/lib/macho/headers.rb +131 -25
- data/lib/macho/load_commands.rb +518 -177
- data/lib/macho/macho_file.rb +254 -172
- data/lib/macho/open.rb +5 -5
- data/lib/macho/sections.rb +20 -9
- data/lib/macho/structure.rb +8 -16
- data/lib/macho/tools.rb +34 -15
- data/lib/macho/utils.rb +87 -39
- data/lib/macho/view.rb +23 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 14255fd4c8aaab675981bcf982893a8be8b6ba0c
|
4
|
+
data.tar.gz: e89c2ff64ebe885089b1b3b3ffa3c80418070fdd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 (
|
27
|
-
file.filetype # =>
|
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[
|
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
|
-
*
|
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.
|
16
|
+
VERSION = "0.2.5".freeze
|
16
17
|
end
|
data/lib/macho/exceptions.rb
CHANGED
@@ -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 <
|
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
|
-
|
89
|
-
|
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 <
|
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 <
|
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
|
data/lib/macho/fat_file.rb
CHANGED
@@ -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
|
33
|
+
raise ArgumentError, "#{filename}: no such file" unless File.file?(filename)
|
34
34
|
|
35
35
|
@filename = filename
|
36
|
-
@raw_data = File.open(@filename, "rb"
|
37
|
-
@header =
|
38
|
-
@fat_archs =
|
39
|
-
@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 =
|
47
|
-
@fat_archs =
|
48
|
-
@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 [
|
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.
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
147
|
-
macho.
|
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
|
-
# @
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
-
|
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
|
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
|
277
|
+
# @api private
|
278
|
+
def populate_fat_header
|
216
279
|
# the smallest fat Mach-O header is 8 bytes
|
217
|
-
raise TruncatedFileError
|
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
|
222
|
-
raise MachOBinaryError
|
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
|
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
|
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
|
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
|
-
#
|
265
|
-
#
|
266
|
-
# @
|
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]
|