class MachO::FatFile
Represents a “Fat” file, which contains a header, a listing of available architectures, and one or more Mach-O binaries. @see en.wikipedia.org/wiki/Mach-O#Multi-architecture_binaries @see MachOFile
Attributes
@return [Array<Headers::FatArch>, Array<Headers::FatArch64] an array of fat architectures
@return [String] the filename loaded from, or nil if loaded from a binary string
@return [Headers::FatHeader] the file’s header
@return [Array<MachOFile>] an array of Mach-O binaries
@return [Hash] any parser options that the instance was created with @note Options specified in a {FatFile} trickle down into the internal {MachOFile}s.
Public Class Methods
Creates a new FatFile
from the given filename. @param filename [String] the fat file to load from @param opts [Hash] options to control the parser with @note see {MachOFile#initialize} for currently valid options @raise [ArgumentError] if the given file does not exist
# File lib/macho/fat_file.rb, line 94 def initialize(filename, **opts) raise ArgumentError, "#{filename}: no such file" unless File.file?(filename) @filename = filename @options = opts @raw_data = File.binread(@filename) populate_fields end
Creates a new FatFile
instance from a binary string. @param bin [String] a binary string containing raw Mach-O data @param opts [Hash] options to control the parser with @note see {MachOFile#initialize} for currently valid options @return [FatFile] a new FatFile
# File lib/macho/fat_file.rb, line 82 def self.new_from_bin(bin, **opts) instance = allocate instance.initialize_from_bin(bin, opts) instance end
Creates a new FatFile
from the given (single-arch) Mach-Os @param machos [Array<MachOFile>] the machos to combine @param fat64 [Boolean] whether to use {Headers::FatArch64}s to represent each slice @return [FatFile] a new FatFile
containing the give machos @raise [ArgumentError] if less than one Mach-O is given @raise [FatArchOffsetOverflowError] if the Mach-Os are too big to be represented
in a 32-bit {Headers::FatArch} and `fat64` is `false`.
# File lib/macho/fat_file.rb, line 36 def self.new_from_machos(*machos, fat64: false) raise ArgumentError, "expected at least one Mach-O" if machos.empty? fa_klass, magic = if fat64 [Headers::FatArch64, Headers::FAT_MAGIC_64] else [Headers::FatArch, Headers::FAT_MAGIC] end # put the smaller alignments further forwards in fat macho, so that we do less padding machos = machos.sort_by(&:segment_alignment) bin = +"" bin << Headers::FatHeader.new(magic, machos.size).serialize offset = Headers::FatHeader.bytesize + (machos.size * fa_klass.bytesize) macho_pads = {} machos.each do |macho| macho_offset = Utils.round(offset, 2**macho.segment_alignment) raise FatArchOffsetOverflowError, macho_offset if !fat64 && macho_offset > ((2**32) - 1) macho_pads[macho] = Utils.padding_for(offset, 2**macho.segment_alignment) bin << fa_klass.new(macho.header.cputype, macho.header.cpusubtype, macho_offset, macho.serialize.bytesize, macho.segment_alignment).serialize offset += (macho.serialize.bytesize + macho_pads[macho]) end machos.each do |macho| # rubocop:disable Style/CombinableLoops bin << Utils.nullpad(macho_pads[macho]) bin << macho.serialize end new_from_bin(bin) end
Public Instance Methods
Add the given runtime path to the file’s Mach-Os. @param path [String] the new runtime path @param options [Hash] @option options [Boolean] :strict (true) if true, fail if one slice fails.
if false, fail only if all slices fail.
@return [void] @see MachOFile#add_rpath
# File lib/macho/fat_file.rb, line 260 def add_rpath(path, options = {}) each_macho(options) do |macho| macho.add_rpath(path, options) end repopulate_raw_machos end
Changes the file’s dylib ID to ‘new_id`. If the file is not a dylib,
does nothing.
@example
file.change_dylib_id('libFoo.dylib')
@param new_id [String] the new dylib ID @param options [Hash] @option options [Boolean] :strict (true) if true, fail if one slice fails.
if false, fail only if all slices fail.
@return [void] @raise [ArgumentError] if ‘new_id` is not a String @see MachOFile#linked_dylibs
# File lib/macho/fat_file.rb, line 182 def change_dylib_id(new_id, options = {}) raise ArgumentError, "argument must be a String" unless new_id.is_a?(String) return unless machos.all?(&:dylib?) each_macho(options) do |macho| macho.change_dylib_id(new_id, options) end repopulate_raw_machos end
Changes all dependent shared library install names from ‘old_name` to `new_name`. In a fat file, this changes install names in all internal Mach-Os. @example
file.change_install_name('/usr/lib/libFoo.dylib', '/usr/lib/libBar.dylib')
@param old_name [String] the shared library name being changed @param new_name [String] the new name @param options [Hash] @option options [Boolean] :strict (true) if true, fail if one slice fails.
if false, fail only if all slices fail.
@return [void] @see MachOFile#change_install_name
# File lib/macho/fat_file.rb, line 217 def change_install_name(old_name, new_name, options = {}) each_macho(options) do |macho| macho.change_install_name(old_name, new_name, options) end repopulate_raw_machos end
Change the runtime path ‘old_path` to `new_path` in the file’s Mach-Os. @param old_path [String] the old runtime path @param new_path [String] the new runtime path @param options [Hash] @option options [Boolean] :strict (true) if true, fail if one slice fails.
if false, fail only if all slices fail.
@option options [Boolean] :uniq (false) for each slice: if true, change
each rpath simultaneously.
@return [void] @see MachOFile#change_rpath
# File lib/macho/fat_file.rb, line 245 def change_rpath(old_path, new_path, options = {}) each_macho(options) do |macho| macho.change_rpath(old_path, new_path, options) end repopulate_raw_machos end
Delete the given runtime path from the file’s Mach-Os. @param path [String] the runtime path to delete @param options [Hash] @option options [Boolean] :strict (true) if true, fail if one slice fails.
if false, fail only if all slices fail.
@option options [Boolean] :uniq (false) for each slice: if true, delete
only the first runtime path that matches. if false, delete all duplicate paths that match.
@return void @see MachOFile#delete_rpath
# File lib/macho/fat_file.rb, line 278 def delete_rpath(path, options = {}) each_macho(options) do |macho| macho.delete_rpath(path, options) end repopulate_raw_machos end
All load commands responsible for loading dylibs in the file’s Mach-O’s. @return [Array<LoadCommands::DylibCommand>] an array of DylibCommands
# File lib/macho/fat_file.rb, line 167 def dylib_load_commands machos.map(&:dylib_load_commands).flatten end
Extract a Mach-O with the given CPU type from the file. @example
file.extract(:i386) # => MachO::MachOFile
@param cputype [Symbol] the CPU type of the Mach-O being extracted @return [MachOFile, nil] the extracted Mach-O or nil if no Mach-O has the given CPU type
# File lib/macho/fat_file.rb, line 291 def extract(cputype) machos.select { |macho| macho.cputype == cputype }.first end
Initializes a new FatFile
instance from a binary string with the given options. @see new_from_bin
@api private
# File lib/macho/fat_file.rb, line 106 def initialize_from_bin(bin, opts) @filename = nil @options = opts @raw_data = bin populate_fields end
All shared libraries linked to the file’s Mach-Os. @return [Array<String>] an array of all shared libraries @see MachOFile#linked_dylibs
# File lib/macho/fat_file.rb, line 198 def linked_dylibs # Individual architectures in a fat binary can link to different subsets # of libraries, but at this point we want to have the full picture, i.e. # the union of all libraries used by all architectures. machos.map(&:linked_dylibs).flatten.uniq end
@return [String] a string representation of the file’s magic number
# File lib/macho/fat_file.rb, line 152 def magic_string Headers::MH_MAGICS[magic] end
Populate the instance’s fields with the raw Fat Mach-O data. @return [void] @note This method is public, but should (almost) never need to be called.
# File lib/macho/fat_file.rb, line 159 def populate_fields @header = populate_fat_header @fat_archs = populate_fat_archs @machos = populate_machos end
All runtime paths associated with the file’s Mach-Os. @return [Array<String>] an array of all runtime paths @see MachOFile#rpaths
# File lib/macho/fat_file.rb, line 230 def rpaths # Can individual architectures have different runtime paths? machos.map(&:rpaths).flatten.uniq end
The file’s raw fat data. @return [String] the raw fat data
# File lib/macho/fat_file.rb, line 115 def serialize @raw_data end
@return [Hash] a hash representation of this {FatFile}
# File lib/macho/fat_file.rb, line 313 def to_h { "header" => header.to_h, "fat_archs" => fat_archs.map(&:to_h), "machos" => machos.map(&:to_h), } end
Write all (fat) data to the given filename. @param filename [String] the file to write to @return [void]
# File lib/macho/fat_file.rb, line 298 def write(filename) File.binwrite(filename, @raw_data) end
Write all (fat) data to the file used to initialize the instance. @return [void] @raise [MachOError] if the instance was initialized without a file @note Overwrites all data in the file!
# File lib/macho/fat_file.rb, line 306 def write! raise MachOError, "no initial file to write to" if filename.nil? File.binwrite(@filename, @raw_data) end
Private Instance Methods
Return a single-arch Mach-O that represents this fat Mach-O for purposes
of delegation.
@return [MachOFile] the Mach-O file @api private
# File lib/macho/fat_file.rb, line 436 def canonical_macho machos.first end
Yield each Mach-O object in the file, rescuing and accumulating errors. @param options [Hash] @option options [Boolean] :strict (true) whether or not to fail loudly
with an exception if at least one Mach-O raises an exception. If false, only raises an exception if *all* Mach-Os raise exceptions.
@raise [RecoverableModificationError] under the conditions of
the `:strict` option above.
@api private
# File lib/macho/fat_file.rb, line 413 def each_macho(options = {}) strict = options.fetch(:strict, true) errors = [] machos.each_with_index do |macho, index| yield macho rescue RecoverableModificationError => e e.macho_slice = index # Strict mode: Immediately re-raise. Otherwise: Retain, check later. raise e if strict errors << e end # Non-strict mode: Raise first error if *all* Mach-O slices failed. raise errors.first if errors.size == machos.size end
Obtain an array of fat architectures from raw file data. @return [Array<Headers::FatArch>] an array of fat architectures @api private
# File lib/macho/fat_file.rb, line 360 def populate_fat_archs archs = [] fa_klass = Utils.fat_magic32?(header.magic) ? Headers::FatArch : Headers::FatArch64 fa_off = Headers::FatHeader.bytesize fa_len = fa_klass.bytesize header.nfat_arch.times do |i| archs << fa_klass.new_from_bin(:big, @raw_data[fa_off + (fa_len * i), fa_len]) end archs end
Obtain the fat header from raw file data. @return [Headers::FatHeader] the fat header @raise [TruncatedFileError] if the file is too small to have a
valid header
@raise [MagicError] if the magic is not valid Mach-O magic @raise [MachOBinaryError] if the magic is for a non-fat Mach-O file @raise [JavaClassFileError] if the file is a Java classfile @raise [ZeroArchitectureError] if the file has no internal slices
(i.e., nfat_arch == 0) and the permissive option is not set
@api private
# File lib/macho/fat_file.rb, line 333 def populate_fat_header # the smallest fat Mach-O header is 8 bytes raise TruncatedFileError if @raw_data.size < 8 fh = Headers::FatHeader.new_from_bin(:big, @raw_data[0, Headers::FatHeader.bytesize]) raise MagicError, fh.magic unless Utils.magic?(fh.magic) raise MachOBinaryError unless Utils.fat_magic?(fh.magic) # Rationale: Java classfiles have the same magic as big-endian fat # Mach-Os. Classfiles encode their version at the same offset as # `nfat_arch` and the lowest version number is 43, so we error out # if a file claims to have over 30 internal architectures. It's # technically possible for a fat Mach-O to have over 30 architectures, # but this is extremely unlikely and in practice distinguishes the two # formats. raise JavaClassFileError if fh.nfat_arch > 30 # Rationale: return an error if the file has no internal slices. raise ZeroArchitectureError if fh.nfat_arch.zero? fh end
Obtain an array of Mach-O blobs from raw file data. @return [Array<MachOFile>] an array of Mach-Os @api private
# File lib/macho/fat_file.rb, line 377 def populate_machos machos = [] fat_archs.each do |arch| machos << MachOFile.new_from_bin(@raw_data[arch.offset, arch.size], **options) # Make sure that each fat_arch and internal slice. # contain matching cputypes and cpusubtypes next if machos.last.header.cputype == arch.cputype && machos.last.header.cpusubtype == arch.cpusubtype raise CPUTypeMismatchError.new(arch.cputype, arch.cpusubtype, machos.last.header.cputype, machos.last.header.cpusubtype) end machos end
Repopulate the raw Mach-O data with each internal Mach-O object. @return [void] @api private
# File lib/macho/fat_file.rb, line 397 def repopulate_raw_machos machos.each_with_index do |macho, i| arch = fat_archs[i] @raw_data[arch.offset, arch.size] = macho.serialize end end