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>, Array<Headers::FatArch64] 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

options[R]

@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

new(filename, **opts) click to toggle source

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

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
new_from_machos(*machos, fat64: false) click to toggle source

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_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 260
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 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
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 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
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.

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

@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
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 167
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 291
def extract(cputype)
  machos.select { |macho| macho.cputype == cputype }.first
end
initialize_from_bin(bin, opts) click to toggle source

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

@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_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 159
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 230
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 115
def serialize
  @raw_data
end
to_h() click to toggle source

@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(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 298
def write(filename)
  File.binwrite(filename, @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 306
def write!
  raise MachOError, "no initial file to write to" if filename.nil?

  File.binwrite(@filename, @raw_data)
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 436
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 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
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 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
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 @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
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 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_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 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