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

fat_archs[R]

@return [Array<Headers::FatArch>] an array of fat architectures

filename[RW]

@return [String] the filename loaded from, or nil if loaded from a binary string

header[R]

@return [Headers::FatHeader] the file's header

machos[R]

@return [Array<MachOFile>] an array of Mach-O binaries

Public Class Methods

new(filename) click to toggle source

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
new_from_bin(bin) click to toggle source

Creates a new FatFile instance from a binary string. @param bin [String] a binary string containing raw Mach-O data @return [FatFile] a new FatFile

# File lib/macho/fat_file.rb, line 48
def self.new_from_bin(bin)
  instance = allocate
  instance.initialize_from_bin(bin)

  instance
end
new_from_machos(*machos) click to toggle source

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_rpath(path, options = {}) click to toggle source

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
change_dylib(old_name, new_name, options = {})
Alias for: change_install_name
change_dylib_id(new_id, options = {}) click to toggle source

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
Also aliased as: dylib_id=
change_install_name(old_name, new_name, options = {}) click to toggle source

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
Also aliased as: change_dylib
change_rpath(old_path, new_path, options = {}) click to toggle source

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_rpath(path, options = {}) click to toggle source

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
dylib_id=(new_id, options = {})
Alias for: change_dylib_id
dylib_load_commands() click to toggle source

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(cputype) click to toggle source

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
initialize_from_bin(bin) click to toggle source

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
linked_dylibs() click to toggle source

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
magic_string() click to toggle source

@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_fields() click to toggle source

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
rpaths() click to toggle source

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
serialize() click to toggle source

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(filename) click to toggle source

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!() click to toggle source

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

canonical_macho() click to toggle source

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
each_macho(options = {}) { |macho| ... } click to toggle source

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
populate_fat_archs() click to toggle source

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
populate_fat_header() click to toggle source

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
populate_machos() click to toggle source

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_raw_machos() click to toggle source

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