ruby-macho 0.2.2 → 0.2.3

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.
@@ -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