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>] 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
Public Class Methods
Creates a new FatFile
from the given filename. @param filename [String] the fat file to load from @raise [ArgumentError] if the given file does not exist
# File lib/macho/fat_file.rb, line 58 def initialize(filename) raise ArgumentError, "#{filename}: no such file" unless File.file?(filename) @filename = filename @raw_data = File.open(@filename, "rb", &:read) populate_fields end
Creates a new FatFile
from the given (single-arch) Mach-Os @param machos [Array<MachOFile>] the machos to combine @return [FatFile] a new FatFile
containing the give machos
# File lib/macho/fat_file.rb, line 26 def self.new_from_machos(*machos) header = Headers::FatHeader.new(Headers::FAT_MAGIC, machos.size) offset = Headers::FatHeader.bytesize + (machos.size * Headers::FatArch.bytesize) fat_archs = [] machos.each do |macho| fat_archs << Headers::FatArch.new(macho.header.cputype, macho.header.cpusubtype, offset, macho.serialize.bytesize, macho.alignment) offset += macho.serialize.bytesize end bin = header.serialize bin << fat_archs.map(&:serialize).join bin << machos.map(&:serialize).join 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 220 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 144 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 179 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.
@return [void] @see MachOFile#change_rpath
# File lib/macho/fat_file.rb, line 205 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.
@return void @see MachOFile#delete_rpath
# File lib/macho/fat_file.rb, line 235 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 129 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 248 def extract(cputype) machos.select { |macho| macho.cputype == cputype }.first end
Initializes a new FatFile
instance from a binary string. @see new_from_bin
@api private
# File lib/macho/fat_file.rb, line 69 def initialize_from_bin(bin) @filename = nil @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 160 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 114 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 121 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 192 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 77 def serialize @raw_data 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 255 def write(filename) File.open(filename, "wb") { |f| f.write(@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 263 def write! if filename.nil? raise MachOError, "cannot write to a default file when initialized from a binary string" else File.open(@filename, "wb") { |f| f.write(@raw_data) } end 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 373 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 349 def each_macho(options = {}) strict = options.fetch(:strict, true) errors = [] machos.each_with_index do |macho, index| begin yield macho rescue RecoverableModificationError => error error.macho_slice = index # Strict mode: Immediately re-raise. Otherwise: Retain, check later. raise error if strict errors << error end 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 305 def populate_fat_archs archs = [] fa_off = Headers::FatHeader.bytesize fa_len = Headers::FatArch.bytesize header.nfat_arch.times do |i| archs << Headers::FatArch.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 @api private
# File lib/macho/fat_file.rb, line 281 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 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 320 def populate_machos machos = [] fat_archs.each do |arch| machos << MachOFile.new_from_bin(@raw_data[arch.offset, arch.size]) 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 333 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