ruby-macho 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,531 +1,511 @@
1
1
  module MachO
2
- # Represents a Mach-O file, which contains a header and load commands
3
- # as well as binary executable instructions. Mach-O binaries are
4
- # architecture specific.
5
- # @see https://en.wikipedia.org/wiki/Mach-O
6
- # @see MachO::FatFile
7
- class MachOFile
8
- # @return [MachO::MachHeader] if the Mach-O is 32-bit
9
- # @return [MachO::MachHeader64] if the Mach-O is 64-bit
10
- attr_reader :header
11
-
12
- # @return [Array<MachO::LoadCommand>] an array of the file's load commands
13
- attr_reader :load_commands
14
-
15
- # Creates a new MachOFile instance from a binary string.
16
- # @param bin [String] a binary string containing raw Mach-O data
17
- # @return [MachO::MachOFile] a new MachOFile
18
- def self.new_from_bin(bin)
19
- instance = allocate
20
- instance.initialize_from_bin(bin)
21
-
22
- instance
23
- end
24
-
25
- # Creates a new FatFile from the given filename.
26
- # @param filename [String] the Mach-O file to load from
27
- # @raise [ArgumentError] if the given filename does not exist
28
- def initialize(filename)
29
- raise ArgumentError.new("#{filetype}: no such file") unless File.exist?(filename)
30
-
31
- @filename = filename
32
- @raw_data = open(@filename, "rb") { |f| f.read }
33
- @header = get_mach_header
34
- @load_commands = get_load_commands
35
- end
36
-
37
- # @api private
38
- def initialize_from_bin(bin)
39
- @filename = nil
40
- @raw_data = bin
41
- @header = get_mach_header
42
- @load_commands = get_load_commands
43
- end
44
-
45
- # The file's raw Mach-O data.
46
- # @return [String] the raw Mach-O data
47
- def serialize
48
- @raw_data
49
- end
50
-
51
- # @return [Boolean] true if the Mach-O has 32-bit magic, false otherwise
52
- def magic32?
53
- MachO.magic32?(header.magic)
54
- end
55
-
56
- # @return [Boolean] true if the Mach-O has 64-bit magic, false otherwise
57
- def magic64?
58
- MachO.magic64?(header.magic)
59
- end
60
-
61
- # @return [Boolean] true if the file is of type `MH_OBJECT`, false otherwise
62
- def object?
63
- header.filetype == MH_OBJECT
64
- end
65
-
66
- # @return [Boolean] true if the file is of type `MH_EXECUTE`, false otherwise
67
- def executable?
68
- header.filetype == MH_EXECUTE
69
- end
70
-
71
- # @return [Boolean] true if the file is of type `MH_FVMLIB`, false otherwise
72
- def fvmlib?
73
- header.filetype == MH_FVMLIB
74
- end
75
-
76
- # @return [Boolean] true if the file is of type `MH_CORE`, false otherwise
77
- def core?
78
- header.filetype == MH_CORE
79
- end
80
-
81
- # @return [Boolean] true if the file is of type `MH_PRELOAD`, false otherwise
82
- def preload?
83
- header.filetype == MH_PRELOAD
84
- end
85
-
86
- # @return [Boolean] true if the file is of type `MH_DYLIB`, false otherwise
87
- def dylib?
88
- header.filetype == MH_DYLIB
89
- end
90
-
91
- # @return [Boolean] true if the file is of type `MH_DYLINKER`, false otherwise
92
- def dylinker?
93
- header.filetype == MH_DYLINKER
94
- end
95
-
96
- # @return [Boolean] true if the file is of type `MH_BUNDLE`, false otherwise
97
- def bundle?
98
- header.filetype == MH_BUNDLE
99
- end
100
-
101
- # @return [Boolean] true if the file is of type `MH_DSYM`, false otherwise
102
- def dsym?
103
- header.filetype == MH_DSYM
104
- end
105
-
106
- # @return [Boolean] true if the file is of type `MH_KEXT_BUNDLE`, false otherwise
107
- def kext?
108
- header.filetype == MH_KEXT_BUNDLE
109
- end
110
-
111
- # @return [Fixnum] the file's magic number
112
- def magic
113
- header.magic
114
- end
115
-
116
- # @return [String] a string representation of the file's magic number
117
- def magic_string
118
- MH_MAGICS[magic]
119
- end
120
-
121
- # @return [String] a string representation of the Mach-O's filetype
122
- def filetype
123
- MH_FILETYPES[header.filetype]
124
- end
125
-
126
- # @return [String] a string representation of the Mach-O's CPU type
127
- def cputype
128
- CPU_TYPES[header.cputype]
129
- end
130
-
131
- # @return [String] a string representation of the Mach-O's CPU subtype
132
- def cpusubtype
133
- CPU_SUBTYPES[header.cpusubtype]
134
- end
135
-
136
- # @return [Fixnum] the number of load commands in the Mach-O's header
137
- def ncmds
138
- header.ncmds
139
- end
140
-
141
- # @return [Fixnum] the size of all load commands, in bytes
142
- def sizeofcmds
143
- header.sizeofcmds
144
- end
145
-
146
- # @return [Fixnum] execution flags set by the linker
147
- def flags
148
- header.flags
149
- end
150
-
151
- # All load commands of a given name.
152
- # @example
153
- # file.command("LC_LOAD_DYLIB")
154
- # file[:LC_LOAD_DYLIB]
155
- # @param [String, Symbol] name the load command ID
156
- # @return [Array<MachO::LoadCommand>] an array of LoadCommands corresponding to `name`
157
- def command(name)
158
- load_commands.select { |lc| lc.type == name.to_sym }
159
- end
160
-
161
- alias :[] :command
162
-
163
- # All load commands responsible for loading dylibs.
164
- # @return [Array<MachO::DylibCommand>] an array of DylibCommands
165
- def dylib_load_commands
166
- load_commands.select { |lc| DYLIB_LOAD_COMMANDS.include?(lc.type) }
167
- end
168
-
169
- # All segment load commands in the Mach-O.
170
- # @return [Array<MachO::SegmentCommand>] if the Mach-O is 32-bit
171
- # @return [Array<MachO::SegmentCommand64>] if the Mach-O is 64-bit
172
- def segments
173
- if magic32?
174
- command(:LC_SEGMENT)
175
- else
176
- command(:LC_SEGMENT_64)
177
- end
178
- end
179
-
180
- # The Mach-O's dylib ID, or `nil` if not a dylib.
181
- # @example
182
- # file.dylib_id # => 'libBar.dylib'
183
- # @return [String, nil] the Mach-O's dylib ID
184
- def dylib_id
185
- if !dylib?
186
- return nil
187
- end
188
-
189
- dylib_id_cmd = command(:LC_ID_DYLIB).first
190
-
191
- dylib_id_cmd.name.to_s
192
- end
193
-
194
- # Changes the Mach-O's dylib ID to `new_id`. Does nothing if not a dylib.
195
- # @example
196
- # file.dylib_id = "libFoo.dylib"
197
- # @param new_id [String] the dylib's new ID
198
- # @return [void]
199
- # @raise [ArgumentError] if `new_id` is not a String
200
- def dylib_id=(new_id)
201
- if !new_id.is_a?(String)
202
- raise ArgumentError.new("argument must be a String")
203
- end
204
-
205
- if !dylib?
206
- return nil
207
- end
208
-
209
- dylib_cmd = command(:LC_ID_DYLIB).first
210
- old_id = dylib_id
211
-
212
- set_name_in_dylib(dylib_cmd, old_id, new_id)
213
- end
214
-
215
- # All shared libraries linked to the Mach-O.
216
- # @return [Array<String>] an array of all shared libraries
217
- def linked_dylibs
218
- dylib_load_commands.map(&:name).map(&:to_s)
219
- end
220
-
221
- # Changes the shared library `old_name` to `new_name`
222
- # @example
223
- # file.change_install_name("/usr/lib/libWhatever.dylib", "/usr/local/lib/libWhatever2.dylib")
224
- # @param old_name [String] the shared library's old name
225
- # @param new_name [String] the shared library's new name
226
- # @return [void]
227
- # @raise [MachO::DylibUnknownError] if no shared library has the old name
228
- def change_install_name(old_name, new_name)
229
- dylib_cmd = dylib_load_commands.find { |d| d.name.to_s == old_name }
230
- raise DylibUnknownError.new(old_name) if dylib_cmd.nil?
231
-
232
- set_name_in_dylib(dylib_cmd, old_name, new_name)
233
- end
234
-
235
- alias :change_dylib :change_install_name
236
-
237
- # All runtime paths searched by the dynamic linker for the Mach-O.
238
- # @return [Array<String>] an array of all runtime paths
239
- def rpaths
240
- command(:LC_RPATH).map(&:path).map(&:to_s)
241
- end
242
-
243
- # Changes the runtime path `old_path` to `new_path`
244
- # @example
245
- # file.change_rpath("/usr/lib", "/usr/local/lib")
246
- # @param old_path [String] the old runtime path
247
- # @param new_path [String] the new runtime path
248
- # @return [void]
249
- # @raise [MachO::RpathUnknownError] if no such old runtime path exists
250
- # @api private
251
- def change_rpath(old_path, new_path)
252
- rpath_cmd = command(:LC_RPATH).find { |r| r.path.to_s == old_path }
253
- raise RpathUnknownError.new(old_path) if rpath_cmd.nil?
254
-
255
- set_path_in_rpath(rpath_cmd, old_path, new_path)
256
- end
257
-
258
- # All sections of the segment `segment`.
259
- # @param segment [MachO::SegmentCommand, MachO::SegmentCommand64] the segment being inspected
260
- # @return [Array<MachO::Section>] if the Mach-O is 32-bit
261
- # @return [Array<MachO::Section64>] if the Mach-O is 64-bit
262
- def sections(segment)
263
- sections = []
264
-
265
- if !segment.is_a?(SegmentCommand) && !segment.is_a?(SegmentCommand64)
266
- raise ArgumentError.new("not a valid segment")
267
- end
268
-
269
- if segment.nsects.zero?
270
- return sections
271
- end
272
-
273
- offset = segment.offset + segment.class.bytesize
274
-
275
- segment.nsects.times do
276
- if segment.is_a? SegmentCommand
277
- sections << Section.new_from_bin(@raw_data.slice(offset, Section.bytesize))
278
- offset += Section.bytesize
279
- else
280
- sections << Section64.new_from_bin(@raw_data.slice(offset, Section64.bytesize))
281
- offset += Section64.bytesize
282
- end
283
- end
284
-
285
- sections
286
- end
287
-
288
- # Write all Mach-O data to the given filename.
289
- # @param filename [String] the file to write to
290
- # @return [void]
291
- def write(filename)
292
- File.open(filename, "wb") { |f| f.write(@raw_data) }
293
- end
294
-
295
- # Write all Mach-O data to the file used to initialize the instance.
296
- # @raise [MachOError] if the instance was created from a binary string
297
- # @return [void]
298
- # @raise [MachO::MachOError] if the instance was initialized without a file
299
- # @note Overwrites all data in the file!
300
- def write!
301
- if @filename.nil?
302
- raise MachOError.new("cannot write to a default file when initialized from a binary string")
303
- else
304
- File.open(@filename, "wb") { |f| f.write(@raw_data) }
305
- end
306
- end
307
-
308
- private
309
-
310
- # The file's Mach-O header structure.
311
- # @return [MachO::MachHeader] if the Mach-O is 32-bit
312
- # @return [MachO::MachHeader64] if the Mach-O is 64-bit
313
- # @private
314
- def get_mach_header
315
- magic = get_magic
316
- cputype = get_cputype
317
- cpusubtype = get_cpusubtype
318
- filetype = get_filetype
319
- ncmds = get_ncmds
320
- sizeofcmds = get_sizeofcmds
321
- flags = get_flags
322
-
323
- if MachO.magic32?(magic)
324
- MachHeader.new(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags)
325
- else
326
- # the reserved field is...reserved, so just fill it with 0
327
- MachHeader64.new(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags, 0)
328
- end
329
- end
330
-
331
- # The file's magic number.
332
- # @return [Fixnum] the magic
333
- # @raise [MachO::MagicError] if the magic is not valid Mach-O magic
334
- # @raise [MachO::FatBinaryError] if the magic is for a Fat file
335
- # @private
336
- def get_magic
337
- magic = @raw_data[0..3].unpack("N").first
338
-
339
- raise MagicError.new(magic) unless MachO.magic?(magic)
340
- raise FatBinaryError.new if MachO.fat_magic?(magic)
341
-
342
- magic
343
- end
344
-
345
- # The file's CPU type.
346
- # @return [Fixnum] the CPU type
347
- # @raise [MachO::CPUTypeError] if the CPU type is unknown
348
- # @private
349
- def get_cputype
350
- cputype = @raw_data[4..7].unpack("V").first
351
-
352
- raise CPUTypeError.new(cputype) unless CPU_TYPES.key?(cputype)
353
-
354
- cputype
355
- end
356
-
357
- # The file's CPU subtype.
358
- # @return [Fixnum] the CPU subtype
359
- # @raise [MachO::CPUSubtypeError] if the CPU subtype is unknown
360
- # @private
361
- def get_cpusubtype
362
- cpusubtype = @raw_data[8..11].unpack("V").first
363
- cpusubtype &= ~CPU_SUBTYPE_LIB64 # this mask isn't documented!
364
-
365
- raise CPUSubtypeError.new(cpusubtype) unless CPU_SUBTYPES.key?(cpusubtype)
366
-
367
- cpusubtype
368
- end
369
-
370
- # The file's type.
371
- # @return [Fixnum] the file type
372
- # @raise [MachO::FiletypeError] if the file type is unknown
373
- # @private
374
- def get_filetype
375
- filetype = @raw_data[12..15].unpack("V").first
376
-
377
- raise FiletypeError.new(filetype) unless MH_FILETYPES.key?(filetype)
378
-
379
- filetype
380
- end
381
-
382
- # The number of load commands in the file.
383
- # @return [Fixnum] the number of load commands
384
- # @private
385
- def get_ncmds
386
- @raw_data[16..19].unpack("V").first
387
- end
388
-
389
- # The size of all load commands, in bytes.
390
- # return [Fixnum] the size of all load commands
391
- # @private
392
- def get_sizeofcmds
393
- @raw_data[20..23].unpack("V").first
394
- end
395
-
396
- # The Mach-O header's flags.
397
- # @return [Fixnum] the flags
398
- # @private
399
- def get_flags
400
- @raw_data[24..27].unpack("V").first
401
- end
402
-
403
- # All load commands in the file.
404
- # @return [Array<MachO::LoadCommand>] an array of load commands
405
- # @raise [MachO::LoadCommandError] if an unknown load command is encountered
406
- # @private
407
- def get_load_commands
408
- offset = header.class.bytesize
409
- load_commands = []
410
-
411
- header.ncmds.times do
412
- cmd = @raw_data.slice(offset, 4).unpack("V").first
413
- cmd_sym = LOAD_COMMANDS[cmd]
414
-
415
- raise LoadCommandError.new(cmd) if cmd_sym.nil?
416
-
417
- # why do I do this? i don't like declaring constants below
418
- # classes, and i need them to resolve...
419
- klass = MachO.const_get "#{LC_STRUCTURES[cmd_sym]}"
420
- command = klass.new_from_bin(@raw_data, offset, @raw_data.slice(offset, klass.bytesize))
421
-
422
- load_commands << command
423
- offset += command.cmdsize
424
- end
425
-
426
- load_commands
427
- end
428
-
429
- # Updates the size of all load commands in the raw data.
430
- # @param size [Fixnum] the new size, in bytes
431
- # @return [void]
432
- # @private
433
- def set_sizeofcmds(size)
434
- new_size = [size].pack("V")
435
- @raw_data[20..23] = new_size
436
- end
437
-
438
- # Updates the `name` field in a DylibCommand.
439
- # @param dylib_cmd [MachO::DylibCommand] the dylib command
440
- # @param old_name [String] the old dylib name
441
- # @param new_name [String] the new dylib name
442
- # @return [void]
443
- # @private
444
- def set_name_in_dylib(dylib_cmd, old_name, new_name)
445
- set_lc_str_in_cmd(dylib_cmd, dylib_cmd.name, old_name, new_name)
446
- end
447
-
448
- # Updates the `path` field in an RpathCommand.
449
- # @param rpath_cmd [MachO::RpathCommand] the rpath command
450
- # @param old_path [String] the old runtime name
451
- # @param new_path [String] the new runtime name
452
- # @return [void]
453
- # @private
454
- def set_path_in_rpath(rpath_cmd, old_path, new_path)
455
- set_lc_str_in_cmd(rpath_cmd, rpath_cmd.path, old_path, new_path)
456
- end
457
-
458
- # Updates a generic LCStr field in any LoadCommand.
459
- # @param cmd [MachO::LoadCommand] the load command
460
- # @param lc_str [MachO::LoadCommand::LCStr] the load command string
461
- # @param old_str [String] the old string
462
- # @param new_str [String] the new string
463
- # @raise [MachO::HeaderPadError] if the new name exceeds the header pad buffer
464
- # @private
465
- def set_lc_str_in_cmd(cmd, lc_str, old_str, new_str)
466
- if magic32?
467
- cmd_round = 4
468
- else
469
- cmd_round = 8
470
- end
471
-
472
- new_sizeofcmds = header.sizeofcmds
473
- old_str = old_str.dup
474
- new_str = new_str.dup
475
-
476
- old_pad = MachO.round(old_str.size + 1, cmd_round) - old_str.size
477
- new_pad = MachO.round(new_str.size + 1, cmd_round) - new_str.size
478
-
479
- # pad the old and new IDs with null bytes to meet command bounds
480
- old_str << "\x00" * old_pad
481
- new_str << "\x00" * new_pad
482
-
483
- # calculate the new size of the cmd and sizeofcmds in MH
484
- new_size = cmd.class.bytesize + new_str.size
485
- new_sizeofcmds += new_size - cmd.cmdsize
486
-
487
- low_fileoff = 2**64 # ULLONGMAX
488
-
489
- # calculate the low file offset (offset to first section data)
490
- segments.each do |seg|
491
- sections(seg).each do |sect|
492
- if sect.size != 0 && !sect.flag?(:S_ZEROFILL) &&
493
- !sect.flag?(:S_THREAD_LOCAL_ZEROFILL) &&
494
- sect.offset < low_fileoff
495
-
496
- low_fileoff = sect.offset
497
- end
498
- end
499
- end
500
-
501
- if new_sizeofcmds + header.class.bytesize > low_fileoff
502
- raise HeaderPadError.new(@filename)
503
- end
504
-
505
- # update sizeofcmds in mach_header
506
- set_sizeofcmds(new_sizeofcmds)
507
-
508
- # update cmdsize in the cmd
509
- @raw_data[cmd.offset + 4, 4] = [new_size].pack("V")
510
-
511
- # delete the old str
512
- @raw_data.slice!(cmd.offset + lc_str.to_i...cmd.offset + cmd.class.bytesize + old_str.size)
513
-
514
- # insert the new str
515
- @raw_data.insert(cmd.offset + lc_str.to_i, new_str)
516
-
517
- # pad/unpad after new_sizeofcmds until offsets are corrected
518
- null_pad = old_str.size - new_str.size
519
-
520
- if null_pad < 0
521
- @raw_data.slice!(new_sizeofcmds + header.class.bytesize, null_pad.abs)
522
- else
523
- @raw_data.insert(new_sizeofcmds + header.class.bytesize, "\x00" * null_pad)
524
- end
525
-
526
- # synchronize fields with the raw data
527
- @header = get_mach_header
528
- @load_commands = get_load_commands
529
- end
530
- end
2
+ # Represents a Mach-O file, which contains a header and load commands
3
+ # as well as binary executable instructions. Mach-O binaries are
4
+ # architecture specific.
5
+ # @see https://en.wikipedia.org/wiki/Mach-O
6
+ # @see MachO::FatFile
7
+ class MachOFile
8
+ # @return [String] the filename loaded from, or nil if loaded from a binary string
9
+ attr_accessor :filename
10
+
11
+ # @return [Symbol] the endianness of the file, :big or :little
12
+ attr_reader :endianness
13
+
14
+ # @return [MachO::MachHeader] if the Mach-O is 32-bit
15
+ # @return [MachO::MachHeader64] if the Mach-O is 64-bit
16
+ attr_reader :header
17
+
18
+ # @return [Array<MachO::LoadCommand>] an array of the file's load commands
19
+ attr_reader :load_commands
20
+
21
+ # Creates a new MachOFile instance from a binary string.
22
+ # @param bin [String] a binary string containing raw Mach-O data
23
+ # @return [MachO::MachOFile] a new MachOFile
24
+ def self.new_from_bin(bin)
25
+ instance = allocate
26
+ instance.initialize_from_bin(bin)
27
+
28
+ instance
29
+ end
30
+
31
+ # Creates a new FatFile from the given filename.
32
+ # @param filename [String] the Mach-O file to load from
33
+ # @raise [ArgumentError] if the given file does not exist
34
+ def initialize(filename)
35
+ raise ArgumentError.new("#{filename}: no such file") unless File.file?(filename)
36
+
37
+ @filename = filename
38
+ @raw_data = File.open(@filename, "rb") { |f| f.read }
39
+ @header = get_mach_header
40
+ @load_commands = get_load_commands
41
+ end
42
+
43
+ # @api private
44
+ def initialize_from_bin(bin)
45
+ @filename = nil
46
+ @raw_data = bin
47
+ @header = get_mach_header
48
+ @load_commands = get_load_commands
49
+ end
50
+
51
+ # The file's raw Mach-O data.
52
+ # @return [String] the raw Mach-O data
53
+ def serialize
54
+ @raw_data
55
+ end
56
+
57
+ # @return [Boolean] true if the Mach-O has 32-bit magic, false otherwise
58
+ def magic32?
59
+ MachO.magic32?(header.magic)
60
+ end
61
+
62
+ # @return [Boolean] true if the Mach-O has 64-bit magic, false otherwise
63
+ def magic64?
64
+ MachO.magic64?(header.magic)
65
+ end
66
+
67
+ # @return [Boolean] true if the file is of type `MH_OBJECT`, false otherwise
68
+ def object?
69
+ header.filetype == MH_OBJECT
70
+ end
71
+
72
+ # @return [Boolean] true if the file is of type `MH_EXECUTE`, false otherwise
73
+ def executable?
74
+ header.filetype == MH_EXECUTE
75
+ end
76
+
77
+ # @return [Boolean] true if the file is of type `MH_FVMLIB`, false otherwise
78
+ def fvmlib?
79
+ header.filetype == MH_FVMLIB
80
+ end
81
+
82
+ # @return [Boolean] true if the file is of type `MH_CORE`, false otherwise
83
+ def core?
84
+ header.filetype == MH_CORE
85
+ end
86
+
87
+ # @return [Boolean] true if the file is of type `MH_PRELOAD`, false otherwise
88
+ def preload?
89
+ header.filetype == MH_PRELOAD
90
+ end
91
+
92
+ # @return [Boolean] true if the file is of type `MH_DYLIB`, false otherwise
93
+ def dylib?
94
+ header.filetype == MH_DYLIB
95
+ end
96
+
97
+ # @return [Boolean] true if the file is of type `MH_DYLINKER`, false otherwise
98
+ def dylinker?
99
+ header.filetype == MH_DYLINKER
100
+ end
101
+
102
+ # @return [Boolean] true if the file is of type `MH_BUNDLE`, false otherwise
103
+ def bundle?
104
+ header.filetype == MH_BUNDLE
105
+ end
106
+
107
+ # @return [Boolean] true if the file is of type `MH_DSYM`, false otherwise
108
+ def dsym?
109
+ header.filetype == MH_DSYM
110
+ end
111
+
112
+ # @return [Boolean] true if the file is of type `MH_KEXT_BUNDLE`, false otherwise
113
+ def kext?
114
+ header.filetype == MH_KEXT_BUNDLE
115
+ end
116
+
117
+ # @return [Fixnum] the file's magic number
118
+ def magic
119
+ header.magic
120
+ end
121
+
122
+ # @return [String] a string representation of the file's magic number
123
+ def magic_string
124
+ MH_MAGICS[magic]
125
+ end
126
+
127
+ # @return [String] a string representation of the Mach-O's filetype
128
+ def filetype
129
+ MH_FILETYPES[header.filetype]
130
+ end
131
+
132
+ # @return [Symbol] a symbol representation of the Mach-O's CPU type
133
+ def cputype
134
+ CPU_TYPES[header.cputype]
135
+ end
136
+
137
+ # @return [Symbol] a symbol representation of the Mach-O's CPU subtype
138
+ def cpusubtype
139
+ CPU_SUBTYPES[header.cputype][header.cpusubtype]
140
+ end
141
+
142
+ # @return [Fixnum] the number of load commands in the Mach-O's header
143
+ def ncmds
144
+ header.ncmds
145
+ end
146
+
147
+ # @return [Fixnum] the size of all load commands, in bytes
148
+ def sizeofcmds
149
+ header.sizeofcmds
150
+ end
151
+
152
+ # @return [Fixnum] execution flags set by the linker
153
+ def flags
154
+ header.flags
155
+ end
156
+
157
+ # All load commands of a given name.
158
+ # @example
159
+ # file.command("LC_LOAD_DYLIB")
160
+ # file[:LC_LOAD_DYLIB]
161
+ # @param [String, Symbol] name the load command ID
162
+ # @return [Array<MachO::LoadCommand>] an array of LoadCommands corresponding to `name`
163
+ def command(name)
164
+ load_commands.select { |lc| lc.type == name.to_sym }
165
+ end
166
+
167
+ alias :[] :command
168
+
169
+ # All load commands responsible for loading dylibs.
170
+ # @return [Array<MachO::DylibCommand>] an array of DylibCommands
171
+ def dylib_load_commands
172
+ load_commands.select { |lc| DYLIB_LOAD_COMMANDS.include?(lc.type) }
173
+ end
174
+
175
+ # All segment load commands in the Mach-O.
176
+ # @return [Array<MachO::SegmentCommand>] if the Mach-O is 32-bit
177
+ # @return [Array<MachO::SegmentCommand64>] if the Mach-O is 64-bit
178
+ def segments
179
+ if magic32?
180
+ command(:LC_SEGMENT)
181
+ else
182
+ command(:LC_SEGMENT_64)
183
+ end
184
+ end
185
+
186
+ # The Mach-O's dylib ID, or `nil` if not a dylib.
187
+ # @example
188
+ # file.dylib_id # => 'libBar.dylib'
189
+ # @return [String, nil] the Mach-O's dylib ID
190
+ def dylib_id
191
+ if !dylib?
192
+ return nil
193
+ end
194
+
195
+ dylib_id_cmd = command(:LC_ID_DYLIB).first
196
+
197
+ dylib_id_cmd.name.to_s
198
+ end
199
+
200
+ # Changes the Mach-O's dylib ID to `new_id`. Does nothing if not a dylib.
201
+ # @example
202
+ # file.dylib_id = "libFoo.dylib"
203
+ # @param new_id [String] the dylib's new ID
204
+ # @return [void]
205
+ # @raise [ArgumentError] if `new_id` is not a String
206
+ def dylib_id=(new_id)
207
+ if !new_id.is_a?(String)
208
+ raise ArgumentError.new("argument must be a String")
209
+ end
210
+
211
+ if !dylib?
212
+ return nil
213
+ end
214
+
215
+ dylib_cmd = command(:LC_ID_DYLIB).first
216
+ old_id = dylib_id
217
+
218
+ set_name_in_dylib(dylib_cmd, old_id, new_id)
219
+ end
220
+
221
+ # All shared libraries linked to the Mach-O.
222
+ # @return [Array<String>] an array of all shared libraries
223
+ def linked_dylibs
224
+ # Some linkers produce multiple `LC_LOAD_DYLIB` load commands for the same
225
+ # library, but at this point we're really only interested in a list of
226
+ # unique libraries this Mach-O file links to, thus: `uniq`. (This is also
227
+ # for consistency with `FatFile` that merges this list across all archs.)
228
+ dylib_load_commands.map(&:name).map(&:to_s).uniq
229
+ end
230
+
231
+ # Changes the shared library `old_name` to `new_name`
232
+ # @example
233
+ # file.change_install_name("/usr/lib/libWhatever.dylib", "/usr/local/lib/libWhatever2.dylib")
234
+ # @param old_name [String] the shared library's old name
235
+ # @param new_name [String] the shared library's new name
236
+ # @return [void]
237
+ # @raise [MachO::DylibUnknownError] if no shared library has the old name
238
+ def change_install_name(old_name, new_name)
239
+ dylib_cmd = dylib_load_commands.find { |d| d.name.to_s == old_name }
240
+ raise DylibUnknownError.new(old_name) if dylib_cmd.nil?
241
+
242
+ set_name_in_dylib(dylib_cmd, old_name, new_name)
243
+ end
244
+
245
+ alias :change_dylib :change_install_name
246
+
247
+ # All runtime paths searched by the dynamic linker for the Mach-O.
248
+ # @return [Array<String>] an array of all runtime paths
249
+ def rpaths
250
+ command(:LC_RPATH).map(&:path).map(&:to_s)
251
+ end
252
+
253
+ # Changes the runtime path `old_path` to `new_path`
254
+ # @example
255
+ # file.change_rpath("/usr/lib", "/usr/local/lib")
256
+ # @param old_path [String] the old runtime path
257
+ # @param new_path [String] the new runtime path
258
+ # @return [void]
259
+ # @raise [MachO::RpathUnknownError] if no such old runtime path exists
260
+ # @api private
261
+ def change_rpath(old_path, new_path)
262
+ rpath_cmd = command(:LC_RPATH).find { |r| r.path.to_s == old_path }
263
+ raise RpathUnknownError.new(old_path) if rpath_cmd.nil?
264
+
265
+ set_path_in_rpath(rpath_cmd, old_path, new_path)
266
+ end
267
+
268
+ # All sections of the segment `segment`.
269
+ # @param segment [MachO::SegmentCommand, MachO::SegmentCommand64] the segment being inspected
270
+ # @return [Array<MachO::Section>] if the Mach-O is 32-bit
271
+ # @return [Array<MachO::Section64>] if the Mach-O is 64-bit
272
+ def sections(segment)
273
+ sections = []
274
+
275
+ if !segment.is_a?(SegmentCommand) && !segment.is_a?(SegmentCommand64)
276
+ raise ArgumentError.new("not a valid segment")
277
+ end
278
+
279
+ if segment.nsects.zero?
280
+ return sections
281
+ end
282
+
283
+ offset = segment.offset + segment.class.bytesize
284
+
285
+ segment.nsects.times do
286
+ if segment.is_a? SegmentCommand
287
+ sections << Section.new_from_bin(endianness, @raw_data.slice(offset, Section.bytesize))
288
+ offset += Section.bytesize
289
+ else
290
+ sections << Section64.new_from_bin(endianness, @raw_data.slice(offset, Section64.bytesize))
291
+ offset += Section64.bytesize
292
+ end
293
+ end
294
+
295
+ sections
296
+ end
297
+
298
+ # Write all Mach-O data to the given filename.
299
+ # @param filename [String] the file to write to
300
+ # @return [void]
301
+ def write(filename)
302
+ File.open(filename, "wb") { |f| f.write(@raw_data) }
303
+ end
304
+
305
+ # Write all Mach-O data to the file used to initialize the instance.
306
+ # @return [void]
307
+ # @raise [MachO::MachOError] if the instance was initialized without a file
308
+ # @note Overwrites all data in the file!
309
+ def write!
310
+ if @filename.nil?
311
+ raise MachOError.new("cannot write to a default file when initialized from a binary string")
312
+ else
313
+ File.open(@filename, "wb") { |f| f.write(@raw_data) }
314
+ end
315
+ end
316
+
317
+ private
318
+
319
+ # The file's Mach-O header structure.
320
+ # @return [MachO::MachHeader] if the Mach-O is 32-bit
321
+ # @return [MachO::MachHeader64] if the Mach-O is 64-bit
322
+ # @raise [MachO::TruncatedFileError] if the file is too small to have a valid header
323
+ # @private
324
+ def get_mach_header
325
+ # the smallest Mach-O header is 28 bytes
326
+ raise TruncatedFileError.new if @raw_data.size < 28
327
+
328
+ magic = get_and_check_magic
329
+ mh_klass = MachO.magic32?(magic) ? MachHeader : MachHeader64
330
+ mh = mh_klass.new_from_bin(endianness, @raw_data[0, mh_klass.bytesize])
331
+
332
+ check_cputype(mh.cputype)
333
+ check_cpusubtype(mh.cputype, mh.cpusubtype)
334
+ check_filetype(mh.filetype)
335
+
336
+ mh
337
+ end
338
+
339
+ # Read just the file's magic number and check its validity.
340
+ # @return [Fixnum] the magic
341
+ # @raise [MachO::MagicError] if the magic is not valid Mach-O magic
342
+ # @raise [MachO::FatBinaryError] if the magic is for a Fat file
343
+ # @private
344
+ def get_and_check_magic
345
+ magic = @raw_data[0..3].unpack("N").first
346
+
347
+ raise MagicError.new(magic) unless MachO.magic?(magic)
348
+ raise FatBinaryError.new if MachO.fat_magic?(magic)
349
+
350
+ @endianness = MachO.little_magic?(magic) ? :little : :big
351
+
352
+ magic
353
+ end
354
+
355
+ # Check the file's CPU type.
356
+ # @param cputype [Fixnum] the CPU type
357
+ # @raise [MachO::CPUTypeError] if the CPU type is unknown
358
+ # @private
359
+ def check_cputype(cputype)
360
+ raise CPUTypeError.new(cputype) unless CPU_TYPES.key?(cputype)
361
+ end
362
+
363
+ # Check the file's CPU type/subtype pair.
364
+ # @param cpusubtype [Fixnum] the CPU subtype
365
+ # @raise [MachO::CPUSubtypeError] if the CPU sub-type is unknown
366
+ # @private
367
+ def check_cpusubtype(cputype, cpusubtype)
368
+ # Only check sub-type w/o capability bits (see `get_mach_header`).
369
+ raise CPUSubtypeError.new(cputype, cpusubtype) unless CPU_SUBTYPES[cputype].key?(cpusubtype)
370
+ end
371
+
372
+ # Check the file's type.
373
+ # @param filetype [Fixnum] the file type
374
+ # @raise [MachO::FiletypeError] if the file type is unknown
375
+ # @private
376
+ def check_filetype(filetype)
377
+ raise FiletypeError.new(filetype) unless MH_FILETYPES.key?(filetype)
378
+ end
379
+
380
+ # All load commands in the file.
381
+ # @return [Array<MachO::LoadCommand>] an array of load commands
382
+ # @raise [MachO::LoadCommandError] if an unknown load command is encountered
383
+ # @private
384
+ def get_load_commands
385
+ offset = header.class.bytesize
386
+ load_commands = []
387
+
388
+ header.ncmds.times do
389
+ fmt = (endianness == :little) ? "L<" : "L>"
390
+ cmd = @raw_data.slice(offset, 4).unpack(fmt).first
391
+ cmd_sym = LOAD_COMMANDS[cmd]
392
+
393
+ raise LoadCommandError.new(cmd) if cmd_sym.nil?
394
+
395
+ # why do I do this? i don't like declaring constants below
396
+ # classes, and i need them to resolve...
397
+ klass = MachO.const_get "#{LC_STRUCTURES[cmd_sym]}"
398
+ command = klass.new_from_bin(@raw_data, endianness, offset, @raw_data.slice(offset, klass.bytesize))
399
+
400
+ load_commands << command
401
+ offset += command.cmdsize
402
+ end
403
+
404
+ load_commands
405
+ end
406
+
407
+ # Updates the size of all load commands in the raw data.
408
+ # @param size [Fixnum] the new size, in bytes
409
+ # @return [void]
410
+ # @private
411
+ def set_sizeofcmds(size)
412
+ fmt = (endianness == :little) ? "L<" : "L>"
413
+ new_size = [size].pack(fmt)
414
+ @raw_data[20..23] = new_size
415
+ end
416
+
417
+ # Updates the `name` field in a DylibCommand.
418
+ # @param dylib_cmd [MachO::DylibCommand] the dylib command
419
+ # @param old_name [String] the old dylib name
420
+ # @param new_name [String] the new dylib name
421
+ # @return [void]
422
+ # @private
423
+ def set_name_in_dylib(dylib_cmd, old_name, new_name)
424
+ set_lc_str_in_cmd(dylib_cmd, dylib_cmd.name, old_name, new_name)
425
+ end
426
+
427
+ # Updates the `path` field in an RpathCommand.
428
+ # @param rpath_cmd [MachO::RpathCommand] the rpath command
429
+ # @param old_path [String] the old runtime name
430
+ # @param new_path [String] the new runtime name
431
+ # @return [void]
432
+ # @private
433
+ def set_path_in_rpath(rpath_cmd, old_path, new_path)
434
+ set_lc_str_in_cmd(rpath_cmd, rpath_cmd.path, old_path, new_path)
435
+ end
436
+
437
+ # Updates a generic LCStr field in any LoadCommand.
438
+ # @param cmd [MachO::LoadCommand] the load command
439
+ # @param lc_str [MachO::LoadCommand::LCStr] the load command string
440
+ # @param old_str [String] the old string
441
+ # @param new_str [String] the new string
442
+ # @raise [MachO::HeaderPadError] if the new name exceeds the header pad buffer
443
+ # @private
444
+ def set_lc_str_in_cmd(cmd, lc_str, old_str, new_str)
445
+ if magic32?
446
+ cmd_round = 4
447
+ else
448
+ cmd_round = 8
449
+ end
450
+
451
+ new_sizeofcmds = header.sizeofcmds
452
+ old_str = old_str.dup
453
+ new_str = new_str.dup
454
+
455
+ old_pad = MachO.round(old_str.size + 1, cmd_round) - old_str.size
456
+ new_pad = MachO.round(new_str.size + 1, cmd_round) - new_str.size
457
+
458
+ # pad the old and new IDs with null bytes to meet command bounds
459
+ old_str << "\x00" * old_pad
460
+ new_str << "\x00" * new_pad
461
+
462
+ # calculate the new size of the cmd and sizeofcmds in MH
463
+ new_size = cmd.class.bytesize + new_str.size
464
+ new_sizeofcmds += new_size - cmd.cmdsize
465
+
466
+ low_fileoff = @raw_data.size
467
+
468
+ # calculate the low file offset (offset to first section data)
469
+ segments.each do |seg|
470
+ sections(seg).each do |sect|
471
+ next if sect.size == 0
472
+ next if sect.flag?(:S_ZEROFILL)
473
+ next if sect.flag?(:S_THREAD_LOCAL_ZEROFILL)
474
+ next unless sect.offset < low_fileoff
475
+
476
+ low_fileoff = sect.offset
477
+ end
478
+ end
479
+
480
+ if new_sizeofcmds + header.class.bytesize > low_fileoff
481
+ raise HeaderPadError.new(@filename)
482
+ end
483
+
484
+ # update sizeofcmds in mach_header
485
+ set_sizeofcmds(new_sizeofcmds)
486
+
487
+ # update cmdsize in the cmd
488
+ fmt = (endianness == :little) ? "L<" : "L>"
489
+ @raw_data[cmd.offset + 4, 4] = [new_size].pack(fmt)
490
+
491
+ # delete the old str
492
+ @raw_data.slice!(cmd.offset + lc_str.to_i...cmd.offset + cmd.class.bytesize + old_str.size)
493
+
494
+ # insert the new str
495
+ @raw_data.insert(cmd.offset + lc_str.to_i, new_str)
496
+
497
+ # pad/unpad after new_sizeofcmds until offsets are corrected
498
+ null_pad = old_str.size - new_str.size
499
+
500
+ if null_pad < 0
501
+ @raw_data.slice!(new_sizeofcmds + header.class.bytesize, null_pad.abs)
502
+ else
503
+ @raw_data.insert(new_sizeofcmds + header.class.bytesize, "\x00" * null_pad)
504
+ end
505
+
506
+ # synchronize fields with the raw data
507
+ @header = get_mach_header
508
+ @load_commands = get_load_commands
509
+ end
510
+ end
531
511
  end