vendor: Update vendored ruby-macho to 0.2.5.

This commit is contained in:
William Woodruff 2016-08-07 13:36:19 -04:00 committed by Martin Afanasjew
parent d7da1640ff
commit 5caa812e2c
13 changed files with 1296 additions and 505 deletions

View File

@ -3,7 +3,7 @@ Vendored Dependencies
* [okjson](https://github.com/kr/okjson), version 43. * [okjson](https://github.com/kr/okjson), version 43.
* [ruby-macho](https://github.com/Homebrew/ruby-macho), version 0.2.4 * [ruby-macho](https://github.com/Homebrew/ruby-macho), version 0.2.5
## Licenses: ## Licenses:

View File

@ -1,4 +1,5 @@
require "#{File.dirname(__FILE__)}/macho/structure" require "#{File.dirname(__FILE__)}/macho/structure"
require "#{File.dirname(__FILE__)}/macho/view"
require "#{File.dirname(__FILE__)}/macho/headers" require "#{File.dirname(__FILE__)}/macho/headers"
require "#{File.dirname(__FILE__)}/macho/load_commands" require "#{File.dirname(__FILE__)}/macho/load_commands"
require "#{File.dirname(__FILE__)}/macho/sections" require "#{File.dirname(__FILE__)}/macho/sections"
@ -12,5 +13,5 @@ require "#{File.dirname(__FILE__)}/macho/tools"
# The primary namespace for ruby-macho. # The primary namespace for ruby-macho.
module MachO module MachO
# release version # release version
VERSION = "0.2.4".freeze VERSION = "0.2.5".freeze
end end

View File

@ -3,6 +3,26 @@ module MachO
class MachOError < RuntimeError class MachOError < RuntimeError
end end
# Raised when a Mach-O file modification fails.
class ModificationError < MachOError
end
# Raised when a Mach-O file modification fails but can be recovered when
# operating on multiple Mach-O slices of a fat binary in non-strict mode.
class RecoverableModificationError < ModificationError
# @return [Fixnum, nil] The index of the Mach-O slice of a fat binary for
# which modification failed or `nil` if not a fat binary. This is used to
# make the error message more useful.
attr_accessor :macho_slice
# @return [String] The exception message.
def to_s
s = super.to_s
s = "While modifying Mach-O slice #{@macho_slice}: #{s}" if @macho_slice
s
end
end
# Raised when a file is not a Mach-O. # Raised when a file is not a Mach-O.
class NotAMachOError < MachOError class NotAMachOError < MachOError
# @param error [String] the error in question # @param error [String] the error in question
@ -80,32 +100,89 @@ module MachO
end end
end end
# Raised when a load command can't be created manually.
class LoadCommandNotCreatableError < MachOError
# @param cmd_sym [Symbol] the uncreatable load command's symbol
def initialize(cmd_sym)
super "Load commands of type #{cmd_sym} cannot be created manually"
end
end
# Raised when the number of arguments used to create a load command manually is wrong.
class LoadCommandCreationArityError < MachOError
# @param cmd_sym [Symbol] the load command's symbol
# @param expected_arity [Fixnum] the number of arguments expected
# @param actual_arity [Fixnum] the number of arguments received
def initialize(cmd_sym, expected_arity, actual_arity)
super "Expected #{expected_arity} arguments for #{cmd_sym} creation, got #{actual_arity}"
end
end
# Raised when a load command can't be serialized.
class LoadCommandNotSerializableError < MachOError
# @param cmd_sym [Symbol] the load command's symbol
def initialize(cmd_sym)
super "Load commands of type #{cmd_sym} cannot be serialized"
end
end
# Raised when a load command string is malformed in some way.
class LCStrMalformedError < MachOError
# @param lc [MachO::LoadCommand] the load command containing the string
def initialize(lc)
super "Load command #{lc.type} at offset #{lc.view.offset} contains a malformed string"
end
end
# Raised when a change at an offset is not valid.
class OffsetInsertionError < ModificationError
# @param offset [Fixnum] the invalid offset
def initialize(offset)
super "Insertion at offset #{offset} is not valid"
end
end
# Raised when load commands are too large to fit in the current file. # Raised when load commands are too large to fit in the current file.
class HeaderPadError < MachOError class HeaderPadError < ModificationError
# @param filename [String] the filename # @param filename [String] the filename
def initialize(filename) def initialize(filename)
super "Updated load commands do not fit in the header of " + super "Updated load commands do not fit in the header of " \
"#{filename}. #{filename} needs to be relinked, possibly with " + "#{filename}. #{filename} needs to be relinked, possibly with " \
"-headerpad or -headerpad_max_install_names" "-headerpad or -headerpad_max_install_names"
end end
end end
# Raised when attempting to change a dylib name that doesn't exist. # Raised when attempting to change a dylib name that doesn't exist.
class DylibUnknownError < MachOError class DylibUnknownError < RecoverableModificationError
# @param dylib [String] the unknown shared library name # @param dylib [String] the unknown shared library name
def initialize(dylib) def initialize(dylib)
super "No such dylib name: #{dylib}" super "No such dylib name: #{dylib}"
end end
end end
# Raised when a dylib is missing an ID
class DylibIdMissingError < RecoverableModificationError
def initialize
super "Dylib is missing a dylib ID"
end
end
# Raised when attempting to change an rpath that doesn't exist. # Raised when attempting to change an rpath that doesn't exist.
class RpathUnknownError < MachOError class RpathUnknownError < RecoverableModificationError
# @param path [String] the unknown runtime path # @param path [String] the unknown runtime path
def initialize(path) def initialize(path)
super "No such runtime path: #{path}" super "No such runtime path: #{path}"
end end
end end
# Raised when attempting to add an rpath that already exists.
class RpathExistsError < RecoverableModificationError
# @param path [String] the extant path
def initialize(path)
super "#{path} already exists"
end
end
# Raised whenever unfinished code is called. # Raised whenever unfinished code is called.
class UnimplementedError < MachOError class UnimplementedError < MachOError
# @param thing [String] the thing that is unimplemented # @param thing [String] the thing that is unimplemented

View File

@ -30,22 +30,24 @@ module MachO
# @param filename [String] the fat file to load from # @param filename [String] the fat file to load from
# @raise [ArgumentError] if the given file does not exist # @raise [ArgumentError] if the given file does not exist
def initialize(filename) def initialize(filename)
raise ArgumentError.new("#{filename}: no such file") unless File.file?(filename) raise ArgumentError, "#{filename}: no such file" unless File.file?(filename)
@filename = filename @filename = filename
@raw_data = File.open(@filename, "rb") { |f| f.read } @raw_data = File.open(@filename, "rb", &:read)
@header = get_fat_header @header = populate_fat_header
@fat_archs = get_fat_archs @fat_archs = populate_fat_archs
@machos = get_machos @machos = populate_machos
end end
# Initializes a new FatFile instance from a binary string.
# @see MachO::FatFile.new_from_bin
# @api private # @api private
def initialize_from_bin(bin) def initialize_from_bin(bin)
@filename = nil @filename = nil
@raw_data = bin @raw_data = bin
@header = get_fat_header @header = populate_fat_header
@fat_archs = get_fat_archs @fat_archs = populate_fat_archs
@machos = get_machos @machos = populate_machos
end end
# The file's raw fat data. # The file's raw fat data.
@ -115,7 +117,7 @@ module MachO
end end
# The file's type. Assumed to be the same for every Mach-O within. # The file's type. Assumed to be the same for every Mach-O within.
# @return [String] the filetype # @return [Symbol] the filetype
def filetype def filetype
machos.first.filetype machos.first.filetype
end end
@ -124,34 +126,37 @@ module MachO
# @example # @example
# file.dylib_id # => 'libBar.dylib' # file.dylib_id # => 'libBar.dylib'
# @return [String, nil] the file's dylib ID # @return [String, nil] the file's dylib ID
# @see MachO::MachOFile#linked_dylibs
def dylib_id def dylib_id
machos.first.dylib_id machos.first.dylib_id
end end
# Changes the file's dylib ID to `new_id`. If the file is not a dylib, does nothing. # Changes the file's dylib ID to `new_id`. If the file is not a dylib, does nothing.
# @example # @example
# file.dylib_id = 'libFoo.dylib' # file.change_dylib_id('libFoo.dylib')
# @param new_id [String] the new dylib ID # @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] # @return [void]
# @raise [ArgumentError] if `new_id` is not a String # @raise [ArgumentError] if `new_id` is not a String
def dylib_id=(new_id) # @see MachO::MachOFile#linked_dylibs
if !new_id.is_a?(String) def change_dylib_id(new_id, options = {})
raise ArgumentError.new("argument must be a String") raise ArgumentError, "argument must be a String" unless new_id.is_a?(String)
end return unless machos.all?(&:dylib?)
if !machos.all?(&:dylib?) each_macho(options) do |macho|
return nil macho.change_dylib_id(new_id, options)
end
machos.each do |macho|
macho.dylib_id = new_id
end end
synchronize_raw_data synchronize_raw_data
end end
alias dylib_id= change_dylib_id
# All shared libraries linked to the file's Mach-Os. # All shared libraries linked to the file's Mach-Os.
# @return [Array<String>] an array of all shared libraries # @return [Array<String>] an array of all shared libraries
# @see MachO::MachOFile#linked_dylibs
def linked_dylibs def linked_dylibs
# Individual architectures in a fat binary can link to different subsets # 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. # of libraries, but at this point we want to have the full picture, i.e.
@ -165,16 +170,74 @@ module MachO
# file.change_install_name('/usr/lib/libFoo.dylib', '/usr/lib/libBar.dylib') # file.change_install_name('/usr/lib/libFoo.dylib', '/usr/lib/libBar.dylib')
# @param old_name [String] the shared library name being changed # @param old_name [String] the shared library name being changed
# @param new_name [String] the new name # @param new_name [String] the new name
# @todo incomplete # @param options [Hash]
def change_install_name(old_name, new_name) # @option options [Boolean] :strict (true) if true, fail if one slice fails.
machos.each do |macho| # if false, fail only if all slices fail.
macho.change_install_name(old_name, new_name) # @return [void]
# @see MachO::MachOFile#change_install_name
def change_install_name(old_name, new_name, options = {})
each_macho(options) do |macho|
macho.change_install_name(old_name, new_name, options)
end end
synchronize_raw_data synchronize_raw_data
end end
alias :change_dylib :change_install_name alias change_dylib change_install_name
# All runtime paths associated with the file's Mach-Os.
# @return [Array<String>] an array of all runtime paths
# @see MachO::MachOFile#rpaths
def rpaths
# Can individual architectures have different runtime paths?
machos.map(&:rpaths).flatten.uniq
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 MachO::MachOFile#change_rpath
def change_rpath(old_path, new_path, options = {})
each_macho(options) do |macho|
macho.change_rpath(old_path, new_path, options)
end
synchronize_raw_data
end
# 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 MachO::MachOFile#add_rpath
def add_rpath(path, options = {})
each_macho(options) do |macho|
macho.add_rpath(path, options)
end
synchronize_raw_data
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 MachO::MachOFile#delete_rpath
def delete_rpath(path, options = {})
each_macho(options) do |macho|
macho.delete_rpath(path, options)
end
synchronize_raw_data
end
# Extract a Mach-O with the given CPU type from the file. # Extract a Mach-O with the given CPU type from the file.
# @example # @example
@ -197,7 +260,7 @@ module MachO
# @note Overwrites all data in the file! # @note Overwrites all data in the file!
def write! def write!
if filename.nil? if filename.nil?
raise MachOError.new("cannot write to a default file when initialized from a binary string") raise MachOError, "cannot write to a default file when initialized from a binary string"
else else
File.open(@filename, "wb") { |f| f.write(@raw_data) } File.open(@filename, "wb") { |f| f.write(@raw_data) }
end end
@ -211,15 +274,15 @@ module MachO
# @raise [MachO::MagicError] if the magic is not valid Mach-O magic # @raise [MachO::MagicError] if the magic is not valid Mach-O magic
# @raise [MachO::MachOBinaryError] if the magic is for a non-fat Mach-O file # @raise [MachO::MachOBinaryError] if the magic is for a non-fat Mach-O file
# @raise [MachO::JavaClassFileError] if the file is a Java classfile # @raise [MachO::JavaClassFileError] if the file is a Java classfile
# @private # @api private
def get_fat_header def populate_fat_header
# the smallest fat Mach-O header is 8 bytes # the smallest fat Mach-O header is 8 bytes
raise TruncatedFileError.new if @raw_data.size < 8 raise TruncatedFileError if @raw_data.size < 8
fh = FatHeader.new_from_bin(:big, @raw_data[0, FatHeader.bytesize]) fh = FatHeader.new_from_bin(:big, @raw_data[0, FatHeader.bytesize])
raise MagicError.new(fh.magic) unless MachO.magic?(fh.magic) raise MagicError, fh.magic unless Utils.magic?(fh.magic)
raise MachOBinaryError.new unless MachO.fat_magic?(fh.magic) raise MachOBinaryError unless Utils.fat_magic?(fh.magic)
# Rationale: Java classfiles have the same magic as big-endian fat # Rationale: Java classfiles have the same magic as big-endian fat
# Mach-Os. Classfiles encode their version at the same offset as # Mach-Os. Classfiles encode their version at the same offset as
@ -228,15 +291,15 @@ module MachO
# technically possible for a fat Mach-O to have over 30 architectures, # technically possible for a fat Mach-O to have over 30 architectures,
# but this is extremely unlikely and in practice distinguishes the two # but this is extremely unlikely and in practice distinguishes the two
# formats. # formats.
raise JavaClassFileError.new if fh.nfat_arch > 30 raise JavaClassFileError if fh.nfat_arch > 30
fh fh
end end
# Obtain an array of fat architectures from raw file data. # Obtain an array of fat architectures from raw file data.
# @return [Array<MachO::FatArch>] an array of fat architectures # @return [Array<MachO::FatArch>] an array of fat architectures
# @private # @api private
def get_fat_archs def populate_fat_archs
archs = [] archs = []
fa_off = FatHeader.bytesize fa_off = FatHeader.bytesize
@ -250,8 +313,8 @@ module MachO
# Obtain an array of Mach-O blobs from raw file data. # Obtain an array of Mach-O blobs from raw file data.
# @return [Array<MachO::MachOFile>] an array of Mach-Os # @return [Array<MachO::MachOFile>] an array of Mach-Os
# @private # @api private
def get_machos def populate_machos
machos = [] machos = []
fat_archs.each do |arch| fat_archs.each do |arch|
@ -261,9 +324,37 @@ module MachO
machos machos
end end
# @todo this needs to be redesigned. arch[:offset] and arch[:size] are # Yield each Mach-O object in the file, rescuing and accumulating errors.
# already out-of-date, and the header needs to be synchronized as well. # @param options [Hash]
# @private # @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 [MachO::RecoverableModificationError] under the conditions of
# the `:strict` option above.
# @api private
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
# Synchronize the raw file data with each internal Mach-O object.
# @return [void]
# @api private
def synchronize_raw_data def synchronize_raw_data
machos.each_with_index do |macho, i| machos.each_with_index do |macho, i|
arch = fat_archs[i] arch = fat_archs[i]

View File

@ -1,64 +1,82 @@
module MachO module MachO
# big-endian fat magic # big-endian fat magic
# @api private
FAT_MAGIC = 0xcafebabe FAT_MAGIC = 0xcafebabe
# little-endian fat magic # little-endian fat magic
# this is defined, but should never appear in ruby-macho code because # this is defined, but should never appear in ruby-macho code because
# fat headers are always big-endian and therefore always unpacked as such. # fat headers are always big-endian and therefore always unpacked as such.
# @api private
FAT_CIGAM = 0xbebafeca FAT_CIGAM = 0xbebafeca
# 32-bit big-endian magic # 32-bit big-endian magic
# @api private
MH_MAGIC = 0xfeedface MH_MAGIC = 0xfeedface
# 32-bit little-endian magic # 32-bit little-endian magic
# @api private
MH_CIGAM = 0xcefaedfe MH_CIGAM = 0xcefaedfe
# 64-bit big-endian magic # 64-bit big-endian magic
# @api private
MH_MAGIC_64 = 0xfeedfacf MH_MAGIC_64 = 0xfeedfacf
# 64-bit little-endian magic # 64-bit little-endian magic
# @api private
MH_CIGAM_64 = 0xcffaedfe MH_CIGAM_64 = 0xcffaedfe
# association of magic numbers to string representations # association of magic numbers to string representations
# @api private
MH_MAGICS = { MH_MAGICS = {
FAT_MAGIC => "FAT_MAGIC", FAT_MAGIC => "FAT_MAGIC",
MH_MAGIC => "MH_MAGIC", MH_MAGIC => "MH_MAGIC",
MH_CIGAM => "MH_CIGAM", MH_CIGAM => "MH_CIGAM",
MH_MAGIC_64 => "MH_MAGIC_64", MH_MAGIC_64 => "MH_MAGIC_64",
MH_CIGAM_64 => "MH_CIGAM_64" MH_CIGAM_64 => "MH_CIGAM_64",
} }.freeze
# mask for CPUs with 64-bit architectures (when running a 64-bit ABI?) # mask for CPUs with 64-bit architectures (when running a 64-bit ABI?)
# @api private
CPU_ARCH_ABI64 = 0x01000000 CPU_ARCH_ABI64 = 0x01000000
# any CPU (unused?) # any CPU (unused?)
# @api private
CPU_TYPE_ANY = -1 CPU_TYPE_ANY = -1
# m68k compatible CPUs # m68k compatible CPUs
# @api private
CPU_TYPE_MC680X0 = 0x06 CPU_TYPE_MC680X0 = 0x06
# i386 and later compatible CPUs # i386 and later compatible CPUs
# @api private
CPU_TYPE_I386 = 0x07 CPU_TYPE_I386 = 0x07
# x86_64 (AMD64) compatible CPUs # x86_64 (AMD64) compatible CPUs
# @api private
CPU_TYPE_X86_64 = (CPU_TYPE_I386 | CPU_ARCH_ABI64) CPU_TYPE_X86_64 = (CPU_TYPE_I386 | CPU_ARCH_ABI64)
# 32-bit ARM compatible CPUs # 32-bit ARM compatible CPUs
# @api private
CPU_TYPE_ARM = 0x0c CPU_TYPE_ARM = 0x0c
# m88k compatible CPUs # m88k compatible CPUs
# @api private
CPU_TYPE_MC88000 = 0xd CPU_TYPE_MC88000 = 0xd
# 64-bit ARM compatible CPUs # 64-bit ARM compatible CPUs
# @api private
CPU_TYPE_ARM64 = (CPU_TYPE_ARM | CPU_ARCH_ABI64) CPU_TYPE_ARM64 = (CPU_TYPE_ARM | CPU_ARCH_ABI64)
# PowerPC compatible CPUs # PowerPC compatible CPUs
# @api private
CPU_TYPE_POWERPC = 0x12 CPU_TYPE_POWERPC = 0x12
# PowerPC64 compatible CPUs # PowerPC64 compatible CPUs
# @api private
CPU_TYPE_POWERPC64 = (CPU_TYPE_POWERPC | CPU_ARCH_ABI64) CPU_TYPE_POWERPC64 = (CPU_TYPE_POWERPC | CPU_ARCH_ABI64)
# association of cpu types to symbol representations # association of cpu types to symbol representations
# @api private
CPU_TYPES = { CPU_TYPES = {
CPU_TYPE_ANY => :any, CPU_TYPE_ANY => :any,
CPU_TYPE_I386 => :i386, CPU_TYPE_I386 => :i386,
@ -67,156 +85,213 @@ module MachO
CPU_TYPE_ARM64 => :arm64, CPU_TYPE_ARM64 => :arm64,
CPU_TYPE_POWERPC => :ppc, CPU_TYPE_POWERPC => :ppc,
CPU_TYPE_POWERPC64 => :ppc64, CPU_TYPE_POWERPC64 => :ppc64,
} }.freeze
# mask for CPU subtype capabilities # mask for CPU subtype capabilities
# @api private
CPU_SUBTYPE_MASK = 0xff000000 CPU_SUBTYPE_MASK = 0xff000000
# 64-bit libraries (undocumented!) # 64-bit libraries (undocumented!)
# @see http://llvm.org/docs/doxygen/html/Support_2MachO_8h_source.html # @see http://llvm.org/docs/doxygen/html/Support_2MachO_8h_source.html
# @api private
CPU_SUBTYPE_LIB64 = 0x80000000 CPU_SUBTYPE_LIB64 = 0x80000000
# the lowest common sub-type for `CPU_TYPE_I386` # the lowest common sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_I386 = 3 CPU_SUBTYPE_I386 = 3
# the i486 sub-type for `CPU_TYPE_I386` # the i486 sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_486 = 4 CPU_SUBTYPE_486 = 4
# the i486SX sub-type for `CPU_TYPE_I386` # the i486SX sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_486SX = 132 CPU_SUBTYPE_486SX = 132
# the i586 (P5, Pentium) sub-type for `CPU_TYPE_I386` # the i586 (P5, Pentium) sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_586 = 5 CPU_SUBTYPE_586 = 5
CPU_SUBTYPE_PENT = CPU_SUBTYPE_586
# @see CPU_SUBTYPE_586
# @api private
CPU_SUBTYPE_PENT = CPU_SUBTYPE_586
# the Pentium Pro (P6) sub-type for `CPU_TYPE_I386` # the Pentium Pro (P6) sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_PENTPRO = 22 CPU_SUBTYPE_PENTPRO = 22
# the Pentium II (P6, M3?) sub-type for `CPU_TYPE_I386` # the Pentium II (P6, M3?) sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_PENTII_M3 = 54 CPU_SUBTYPE_PENTII_M3 = 54
# the Pentium II (P6, M5?) sub-type for `CPU_TYPE_I386` # the Pentium II (P6, M5?) sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_PENTII_M5 = 86 CPU_SUBTYPE_PENTII_M5 = 86
# the Pentium 4 (Netburst) sub-type for `CPU_TYPE_I386` # the Pentium 4 (Netburst) sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_PENTIUM_4 = 10 CPU_SUBTYPE_PENTIUM_4 = 10
# the lowest common sub-type for `CPU_TYPE_MC680X0` # the lowest common sub-type for `CPU_TYPE_MC680X0`
# @api private
CPU_SUBTYPE_MC680X0_ALL = 1 CPU_SUBTYPE_MC680X0_ALL = 1
# @see CPU_SUBTYPE_MC680X0_ALL
# @api private
CPU_SUBTYPE_MC68030 = CPU_SUBTYPE_MC680X0_ALL CPU_SUBTYPE_MC68030 = CPU_SUBTYPE_MC680X0_ALL
# the 040 subtype for `CPU_TYPE_MC680X0` # the 040 subtype for `CPU_TYPE_MC680X0`
# @api private
CPU_SUBTYPE_MC68040 = 2 CPU_SUBTYPE_MC68040 = 2
# the 030 subtype for `CPU_TYPE_MC680X0` # the 030 subtype for `CPU_TYPE_MC680X0`
# @api private
CPU_SUBTYPE_MC68030_ONLY = 3 CPU_SUBTYPE_MC68030_ONLY = 3
# the lowest common sub-type for `CPU_TYPE_X86_64` # the lowest common sub-type for `CPU_TYPE_X86_64`
# @api private
CPU_SUBTYPE_X86_64_ALL = CPU_SUBTYPE_I386 CPU_SUBTYPE_X86_64_ALL = CPU_SUBTYPE_I386
# the Haskell sub-type for `CPU_TYPE_X86_64` # the Haskell sub-type for `CPU_TYPE_X86_64`
# @api private
CPU_SUBTYPE_X86_64_H = 8 CPU_SUBTYPE_X86_64_H = 8
# the lowest common sub-type for `CPU_TYPE_ARM` # the lowest common sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_ALL = 0 CPU_SUBTYPE_ARM_ALL = 0
# the v4t sub-type for `CPU_TYPE_ARM` # the v4t sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V4T = 5 CPU_SUBTYPE_ARM_V4T = 5
# the v6 sub-type for `CPU_TYPE_ARM` # the v6 sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V6 = 6 CPU_SUBTYPE_ARM_V6 = 6
# the v5 sub-type for `CPU_TYPE_ARM` # the v5 sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V5TEJ = 7 CPU_SUBTYPE_ARM_V5TEJ = 7
# the xscale (v5 family) sub-type for `CPU_TYPE_ARM` # the xscale (v5 family) sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_XSCALE = 8 CPU_SUBTYPE_ARM_XSCALE = 8
# the v7 sub-type for `CPU_TYPE_ARM` # the v7 sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V7 = 9 CPU_SUBTYPE_ARM_V7 = 9
# the v7f (Cortex A9) sub-type for `CPU_TYPE_ARM` # the v7f (Cortex A9) sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V7F = 10 CPU_SUBTYPE_ARM_V7F = 10
# the v7s ("Swift") sub-type for `CPU_TYPE_ARM` # the v7s ("Swift") sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V7S = 11 CPU_SUBTYPE_ARM_V7S = 11
# the v7k ("Kirkwood40") sub-type for `CPU_TYPE_ARM` # the v7k ("Kirkwood40") sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V7K = 12 CPU_SUBTYPE_ARM_V7K = 12
# the v6m sub-type for `CPU_TYPE_ARM` # the v6m sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V6M = 14 CPU_SUBTYPE_ARM_V6M = 14
# the v7m sub-type for `CPU_TYPE_ARM` # the v7m sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V7M = 15 CPU_SUBTYPE_ARM_V7M = 15
# the v7em sub-type for `CPU_TYPE_ARM` # the v7em sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V7EM = 16 CPU_SUBTYPE_ARM_V7EM = 16
# the v8 sub-type for `CPU_TYPE_ARM` # the v8 sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V8 = 13 CPU_SUBTYPE_ARM_V8 = 13
# the lowest common sub-type for `CPU_TYPE_ARM64` # the lowest common sub-type for `CPU_TYPE_ARM64`
# @api private
CPU_SUBTYPE_ARM64_ALL = 0 CPU_SUBTYPE_ARM64_ALL = 0
# the v8 sub-type for `CPU_TYPE_ARM64` # the v8 sub-type for `CPU_TYPE_ARM64`
# @api private
CPU_SUBTYPE_ARM64_V8 = 1 CPU_SUBTYPE_ARM64_V8 = 1
# the lowest common sub-type for `CPU_TYPE_MC88000` # the lowest common sub-type for `CPU_TYPE_MC88000`
# @api private
CPU_SUBTYPE_MC88000_ALL = 0 CPU_SUBTYPE_MC88000_ALL = 0
# @see CPU_SUBTYPE_MC88000_ALL
# @api private
CPU_SUBTYPE_MMAX_JPC = CPU_SUBTYPE_MC88000_ALL CPU_SUBTYPE_MMAX_JPC = CPU_SUBTYPE_MC88000_ALL
# the 100 sub-type for `CPU_TYPE_MC88000` # the 100 sub-type for `CPU_TYPE_MC88000`
# @api private
CPU_SUBTYPE_MC88100 = 1 CPU_SUBTYPE_MC88100 = 1
# the 110 sub-type for `CPU_TYPE_MC88000` # the 110 sub-type for `CPU_TYPE_MC88000`
# @api private
CPU_SUBTYPE_MC88110 = 2 CPU_SUBTYPE_MC88110 = 2
# the lowest common sub-type for `CPU_TYPE_POWERPC` # the lowest common sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_ALL = 0 CPU_SUBTYPE_POWERPC_ALL = 0
# the 601 sub-type for `CPU_TYPE_POWERPC` # the 601 sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_601 = 1 CPU_SUBTYPE_POWERPC_601 = 1
# the 602 sub-type for `CPU_TYPE_POWERPC` # the 602 sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_602 = 2 CPU_SUBTYPE_POWERPC_602 = 2
# the 603 sub-type for `CPU_TYPE_POWERPC` # the 603 sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_603 = 3 CPU_SUBTYPE_POWERPC_603 = 3
# the 603e (G2) sub-type for `CPU_TYPE_POWERPC` # the 603e (G2) sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_603E = 4 CPU_SUBTYPE_POWERPC_603E = 4
# the 603ev sub-type for `CPU_TYPE_POWERPC` # the 603ev sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_603EV = 5 CPU_SUBTYPE_POWERPC_603EV = 5
# the 604 sub-type for `CPU_TYPE_POWERPC` # the 604 sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_604 = 6 CPU_SUBTYPE_POWERPC_604 = 6
# the 604e sub-type for `CPU_TYPE_POWERPC` # the 604e sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_604E = 7 CPU_SUBTYPE_POWERPC_604E = 7
# the 620 sub-type for `CPU_TYPE_POWERPC` # the 620 sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_620 = 8 CPU_SUBTYPE_POWERPC_620 = 8
# the 750 (G3) sub-type for `CPU_TYPE_POWERPC` # the 750 (G3) sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_750 = 9 CPU_SUBTYPE_POWERPC_750 = 9
# the 7400 (G4) sub-type for `CPU_TYPE_POWERPC` # the 7400 (G4) sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_7400 = 10 CPU_SUBTYPE_POWERPC_7400 = 10
# the 7450 (G4 "Voyager") sub-type for `CPU_TYPE_POWERPC` # the 7450 (G4 "Voyager") sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_7450 = 11 CPU_SUBTYPE_POWERPC_7450 = 11
# the 970 (G5) sub-type for `CPU_TYPE_POWERPC` # the 970 (G5) sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_970 = 100 CPU_SUBTYPE_POWERPC_970 = 100
# any CPU sub-type for CPU type `CPU_TYPE_POWERPC64` # any CPU sub-type for CPU type `CPU_TYPE_POWERPC64`
# @api private
CPU_SUBTYPE_POWERPC64_ALL = CPU_SUBTYPE_POWERPC_ALL CPU_SUBTYPE_POWERPC64_ALL = CPU_SUBTYPE_POWERPC_ALL
# association of CPU types/subtype pairs to symbol representations in # association of CPU types/subtype pairs to symbol representations in
# (very) roughly descending order of commonness # (very) roughly descending order of commonness
# @see https://opensource.apple.com/source/cctools/cctools-877.8/libstuff/arch.c # @see https://opensource.apple.com/source/cctools/cctools-877.8/libstuff/arch.c
# @api private
CPU_SUBTYPES = { CPU_SUBTYPES = {
CPU_TYPE_I386 => { CPU_TYPE_I386 => {
CPU_SUBTYPE_I386 => :i386, CPU_SUBTYPE_I386 => :i386,
@ -280,53 +355,64 @@ module MachO
}.freeze }.freeze
# relocatable object file # relocatable object file
# @api private
MH_OBJECT = 0x1 MH_OBJECT = 0x1
# demand paged executable file # demand paged executable file
# @api private
MH_EXECUTE = 0x2 MH_EXECUTE = 0x2
# fixed VM shared library file # fixed VM shared library file
# @api private
MH_FVMLIB = 0x3 MH_FVMLIB = 0x3
# core dump file # core dump file
# @api private
MH_CORE = 0x4 MH_CORE = 0x4
# preloaded executable file # preloaded executable file
# @api private
MH_PRELOAD = 0x5 MH_PRELOAD = 0x5
# dynamically bound shared library # dynamically bound shared library
# @api private
MH_DYLIB = 0x6 MH_DYLIB = 0x6
# dynamic link editor # dynamic link editor
# @api private
MH_DYLINKER = 0x7 MH_DYLINKER = 0x7
# dynamically bound bundle file # dynamically bound bundle file
# @api private
MH_BUNDLE = 0x8 MH_BUNDLE = 0x8
# shared library stub for static linking only, no section contents # shared library stub for static linking only, no section contents
# @api private
MH_DYLIB_STUB = 0x9 MH_DYLIB_STUB = 0x9
# companion file with only debug sections # companion file with only debug sections
# @api private
MH_DSYM = 0xa MH_DSYM = 0xa
# x86_64 kexts # x86_64 kexts
# @api private
MH_KEXT_BUNDLE = 0xb MH_KEXT_BUNDLE = 0xb
# association of filetypes to string representations # association of filetypes to Symbol representations
# @api private # @api private
MH_FILETYPES = { MH_FILETYPES = {
MH_OBJECT => "MH_OBJECT", MH_OBJECT => :object,
MH_EXECUTE => "MH_EXECUTE", MH_EXECUTE => :execute,
MH_FVMLIB => "MH_FVMLIB", MH_FVMLIB => :fvmlib,
MH_CORE => "MH_CORE", MH_CORE => :core,
MH_PRELOAD => "MH_PRELOAD", MH_PRELOAD => :preload,
MH_DYLIB => "MH_DYLIB", MH_DYLIB => :dylib,
MH_DYLINKER => "MH_DYLINKER", MH_DYLINKER => :dylinker,
MH_BUNDLE => "MH_BUNDLE", MH_BUNDLE => :bundle,
MH_DYLIB_STUB => "MH_DYLIB_STUB", MH_DYLIB_STUB => :dylib_stub,
MH_DSYM => "MH_DSYM", MH_DSYM => :dsym,
MH_KEXT_BUNDLE => "MH_KEXT_BUNDLE" MH_KEXT_BUNDLE => :kext_bundle,
} }.freeze
# association of mach header flag symbols to values # association of mach header flag symbols to values
# @api private # @api private
@ -356,8 +442,8 @@ module MachO
:MH_DEAD_STRIPPABLE_DYLIB => 0x400000, :MH_DEAD_STRIPPABLE_DYLIB => 0x400000,
:MH_HAS_TLV_DESCRIPTORS => 0x800000, :MH_HAS_TLV_DESCRIPTORS => 0x800000,
:MH_NO_HEAP_EXECUTION => 0x1000000, :MH_NO_HEAP_EXECUTION => 0x1000000,
:MH_APP_EXTENSION_SAFE => 0x02000000 :MH_APP_EXTENSION_SAFE => 0x02000000,
} }.freeze
# Fat binary header structure # Fat binary header structure
# @see MachO::FatArch # @see MachO::FatArch
@ -369,7 +455,12 @@ module MachO
attr_reader :nfat_arch attr_reader :nfat_arch
# always big-endian # always big-endian
FORMAT = "N2" # @see MachOStructure::FORMAT
# @api private
FORMAT = "N2".freeze
# @see MachOStructure::SIZEOF
# @api private
SIZEOF = 8 SIZEOF = 8
# @api private # @api private
@ -399,7 +490,12 @@ module MachO
attr_reader :align attr_reader :align
# always big-endian # always big-endian
FORMAT = "N5" # @see MachOStructure::FORMAT
# @api private
FORMAT = "N5".freeze
# @see MachOStructure::SIZEOF
# @api private
SIZEOF = 20 SIZEOF = 20
# @api private # @api private
@ -435,12 +531,17 @@ module MachO
# @return [Fixnum] the header flags associated with the Mach-O # @return [Fixnum] the header flags associated with the Mach-O
attr_reader :flags attr_reader :flags
FORMAT = "L=7" # @see MachOStructure::FORMAT
# @api private
FORMAT = "L=7".freeze
# @see MachOStructure::SIZEOF
# @api private
SIZEOF = 28 SIZEOF = 28
# @api private # @api private
def initialize(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, def initialize(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds,
flags) flags)
@magic = magic @magic = magic
@cputype = cputype @cputype = cputype
# For now we're not interested in additional capability bits also to be # For now we're not interested in additional capability bits also to be
@ -468,12 +569,17 @@ module MachO
# @return [void] # @return [void]
attr_reader :reserved attr_reader :reserved
FORMAT = "L=8" # @see MachOStructure::FORMAT
# @api private
FORMAT = "L=8".freeze
# @see MachOStructure::SIZEOF
# @api private
SIZEOF = 32 SIZEOF = 32
# @api private # @api private
def initialize(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, def initialize(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds,
flags, reserved) flags, reserved)
super(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags) super(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags)
@reserved = reserved @reserved = reserved
end end

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@ module MachO
attr_reader :header attr_reader :header
# @return [Array<MachO::LoadCommand>] an array of the file's load commands # @return [Array<MachO::LoadCommand>] an array of the file's load commands
# @note load commands are provided in order of ascending offset.
attr_reader :load_commands attr_reader :load_commands
# Creates a new MachOFile instance from a binary string. # Creates a new MachOFile instance from a binary string.
@ -32,20 +33,20 @@ module MachO
# @param filename [String] the Mach-O file to load from # @param filename [String] the Mach-O file to load from
# @raise [ArgumentError] if the given file does not exist # @raise [ArgumentError] if the given file does not exist
def initialize(filename) def initialize(filename)
raise ArgumentError.new("#{filename}: no such file") unless File.file?(filename) raise ArgumentError, "#{filename}: no such file" unless File.file?(filename)
@filename = filename @filename = filename
@raw_data = File.open(@filename, "rb") { |f| f.read } @raw_data = File.open(@filename, "rb", &:read)
@header = get_mach_header populate_fields
@load_commands = get_load_commands
end end
# Initializes a new MachOFile instance from a binary string.
# @see MachO::MachOFile.new_from_bin
# @api private # @api private
def initialize_from_bin(bin) def initialize_from_bin(bin)
@filename = nil @filename = nil
@raw_data = bin @raw_data = bin
@header = get_mach_header populate_fields
@load_commands = get_load_commands
end end
# The file's raw Mach-O data. # The file's raw Mach-O data.
@ -56,12 +57,17 @@ module MachO
# @return [Boolean] true if the Mach-O has 32-bit magic, false otherwise # @return [Boolean] true if the Mach-O has 32-bit magic, false otherwise
def magic32? def magic32?
MachO.magic32?(header.magic) Utils.magic32?(header.magic)
end end
# @return [Boolean] true if the Mach-O has 64-bit magic, false otherwise # @return [Boolean] true if the Mach-O has 64-bit magic, false otherwise
def magic64? def magic64?
MachO.magic64?(header.magic) Utils.magic64?(header.magic)
end
# @return [Fixnum] the file's internal alignment
def alignment
magic32? ? 4 : 8
end end
# @return [Boolean] true if the file is of type `MH_OBJECT`, false otherwise # @return [Boolean] true if the file is of type `MH_OBJECT`, false otherwise
@ -124,7 +130,7 @@ module MachO
MH_MAGICS[magic] MH_MAGICS[magic]
end end
# @return [String] a string representation of the Mach-O's filetype # @return [Symbol] a string representation of the Mach-O's filetype
def filetype def filetype
MH_FILETYPES[header.filetype] MH_FILETYPES[header.filetype]
end end
@ -164,7 +170,109 @@ module MachO
load_commands.select { |lc| lc.type == name.to_sym } load_commands.select { |lc| lc.type == name.to_sym }
end end
alias :[] :command alias [] command
# Inserts a load command at the given offset.
# @param offset [Fixnum] the offset to insert at
# @param lc [MachO::LoadCommand] the load command to insert
# @param options [Hash]
# @option options [Boolean] :repopulate (true) whether or not to repopulate
# the instance fields
# @raise [MachO::OffsetInsertionError] if the offset is not in the load command region
# @raise [MachO::HeaderPadError] if the new command exceeds the header pad buffer
# @note Calling this method with an arbitrary offset in the load command
# region **will leave the object in an inconsistent state**.
def insert_command(offset, lc, options = {})
context = LoadCommand::SerializationContext.context_for(self)
cmd_raw = lc.serialize(context)
if offset < header.class.bytesize || offset + cmd_raw.bytesize > low_fileoff
raise OffsetInsertionError, offset
end
new_sizeofcmds = sizeofcmds + cmd_raw.bytesize
if header.class.bytesize + new_sizeofcmds > low_fileoff
raise HeaderPadError, @filename
end
# update Mach-O header fields to account for inserted load command
update_ncmds(ncmds + 1)
update_sizeofcmds(new_sizeofcmds)
@raw_data.insert(offset, cmd_raw)
@raw_data.slice!(header.class.bytesize + new_sizeofcmds, cmd_raw.bytesize)
populate_fields if options.fetch(:repopulate, true)
end
# Replace a load command with another command in the Mach-O, preserving location.
# @param old_lc [MachO::LoadCommand] the load command being replaced
# @param new_lc [MachO::LoadCommand] the load command being added
# @return [void]
# @raise [MachO::HeaderPadError] if the new command exceeds the header pad buffer
# @see {#insert_command}
# @note This is public, but methods like {#dylib_id=} should be preferred.
def replace_command(old_lc, new_lc)
context = LoadCommand::SerializationContext.context_for(self)
cmd_raw = new_lc.serialize(context)
new_sizeofcmds = sizeofcmds + cmd_raw.bytesize - old_lc.cmdsize
if header.class.bytesize + new_sizeofcmds > low_fileoff
raise HeaderPadError, @filename
end
delete_command(old_lc)
insert_command(old_lc.view.offset, new_lc)
end
# Appends a new load command to the Mach-O.
# @param lc [MachO::LoadCommand] the load command being added
# @param options [Hash]
# @option options [Boolean] :repopulate (true) whether or not to repopulate
# the instance fields
# @return [void]
# @see {#insert_command}
# @note This is public, but methods like {#add_rpath} should be preferred.
# Setting `repopulate` to false **will leave the instance in an
# inconsistent state** unless {#populate_fields} is called **immediately**
# afterwards.
def add_command(lc, options = {})
insert_command(header.class.bytesize + sizeofcmds, lc, options)
end
# Delete a load command from the Mach-O.
# @param lc [MachO::LoadCommand] the load command being deleted
# @param options [Hash]
# @option options [Boolean] :repopulate (true) whether or not to repopulate
# the instance fields
# @return [void]
# @note This is public, but methods like {#delete_rpath} should be preferred.
# Setting `repopulate` to false **will leave the instance in an
# inconsistent state** unless {#populate_fields} is called **immediately**
# afterwards.
def delete_command(lc, options = {})
@raw_data.slice!(lc.view.offset, lc.cmdsize)
# update Mach-O header fields to account for deleted load command
update_ncmds(ncmds - 1)
update_sizeofcmds(sizeofcmds - lc.cmdsize)
# pad the space after the load commands to preserve offsets
null_pad = "\x00" * lc.cmdsize
@raw_data.insert(header.class.bytesize + sizeofcmds - lc.cmdsize, null_pad)
populate_fields if options.fetch(:repopulate, true)
end
# Populate the instance's fields with the raw Mach-O data.
# @return [void]
# @note This method is public, but should (almost) never need to be called.
# The exception to this rule is when methods like {#add_command} and
# {#delete_command} have been called with `repopulate = false`.
def populate_fields
@header = populate_mach_header
@load_commands = populate_load_commands
end
# All load commands responsible for loading dylibs. # All load commands responsible for loading dylibs.
# @return [Array<MachO::DylibCommand>] an array of DylibCommands # @return [Array<MachO::DylibCommand>] an array of DylibCommands
@ -188,9 +296,7 @@ module MachO
# file.dylib_id # => 'libBar.dylib' # file.dylib_id # => 'libBar.dylib'
# @return [String, nil] the Mach-O's dylib ID # @return [String, nil] the Mach-O's dylib ID
def dylib_id def dylib_id
if !dylib? return unless dylib?
return nil
end
dylib_id_cmd = command(:LC_ID_DYLIB).first dylib_id_cmd = command(:LC_ID_DYLIB).first
@ -199,25 +305,30 @@ module MachO
# Changes the Mach-O's dylib ID to `new_id`. Does nothing if not a dylib. # Changes the Mach-O's dylib ID to `new_id`. Does nothing if not a dylib.
# @example # @example
# file.dylib_id = "libFoo.dylib" # file.change_dylib_id("libFoo.dylib")
# @param new_id [String] the dylib's new ID # @param new_id [String] the dylib's new ID
# @param _options [Hash]
# @return [void] # @return [void]
# @raise [ArgumentError] if `new_id` is not a String # @raise [ArgumentError] if `new_id` is not a String
def dylib_id=(new_id) # @note `_options` is currently unused and is provided for signature
if !new_id.is_a?(String) # compatibility with {MachO::FatFile#change_dylib_id}
raise ArgumentError.new("argument must be a String") def change_dylib_id(new_id, _options = {})
end raise ArgumentError, "new ID must be a String" unless new_id.is_a?(String)
return unless dylib?
if !dylib? old_lc = command(:LC_ID_DYLIB).first
return nil raise DylibIdMissingError unless old_lc
end
dylib_cmd = command(:LC_ID_DYLIB).first new_lc = LoadCommand.create(:LC_ID_DYLIB, new_id,
old_id = dylib_id old_lc.timestamp,
old_lc.current_version,
old_lc.compatibility_version)
set_name_in_dylib(dylib_cmd, old_id, new_id) replace_command(old_lc, new_lc)
end end
alias dylib_id= change_dylib_id
# All shared libraries linked to the Mach-O. # All shared libraries linked to the Mach-O.
# @return [Array<String>] an array of all shared libraries # @return [Array<String>] an array of all shared libraries
def linked_dylibs def linked_dylibs
@ -233,16 +344,24 @@ module MachO
# file.change_install_name("/usr/lib/libWhatever.dylib", "/usr/local/lib/libWhatever2.dylib") # file.change_install_name("/usr/lib/libWhatever.dylib", "/usr/local/lib/libWhatever2.dylib")
# @param old_name [String] the shared library's old name # @param old_name [String] the shared library's old name
# @param new_name [String] the shared library's new name # @param new_name [String] the shared library's new name
# @param _options [Hash]
# @return [void] # @return [void]
# @raise [MachO::DylibUnknownError] if no shared library has the old name # @raise [MachO::DylibUnknownError] if no shared library has the old name
def change_install_name(old_name, new_name) # @note `_options` is currently unused and is provided for signature
dylib_cmd = dylib_load_commands.find { |d| d.name.to_s == old_name } # compatibility with {MachO::FatFile#change_install_name}
raise DylibUnknownError.new(old_name) if dylib_cmd.nil? def change_install_name(old_name, new_name, _options = {})
old_lc = dylib_load_commands.find { |d| d.name.to_s == old_name }
raise DylibUnknownError, old_name if old_lc.nil?
set_name_in_dylib(dylib_cmd, old_name, new_name) new_lc = LoadCommand.create(old_lc.type, new_name,
old_lc.timestamp,
old_lc.current_version,
old_lc.compatibility_version)
replace_command(old_lc, new_lc)
end end
alias :change_dylib :change_install_name alias change_dylib change_install_name
# All runtime paths searched by the dynamic linker for the Mach-O. # All runtime paths searched by the dynamic linker for the Mach-O.
# @return [Array<String>] an array of all runtime paths # @return [Array<String>] an array of all runtime paths
@ -255,44 +374,70 @@ module MachO
# file.change_rpath("/usr/lib", "/usr/local/lib") # file.change_rpath("/usr/lib", "/usr/local/lib")
# @param old_path [String] the old runtime path # @param old_path [String] the old runtime path
# @param new_path [String] the new runtime path # @param new_path [String] the new runtime path
# @param _options [Hash]
# @return [void] # @return [void]
# @raise [MachO::RpathUnknownError] if no such old runtime path exists # @raise [MachO::RpathUnknownError] if no such old runtime path exists
# @api private # @raise [MachO::RpathExistsError] if the new runtime path already exists
def change_rpath(old_path, new_path) # @note `_options` is currently unused and is provided for signature
rpath_cmd = command(:LC_RPATH).find { |r| r.path.to_s == old_path } # compatibility with {MachO::FatFile#change_rpath}
raise RpathUnknownError.new(old_path) if rpath_cmd.nil? def change_rpath(old_path, new_path, _options = {})
old_lc = command(:LC_RPATH).find { |r| r.path.to_s == old_path }
raise RpathUnknownError, old_path if old_lc.nil?
raise RpathExistsError, new_path if rpaths.include?(new_path)
set_path_in_rpath(rpath_cmd, old_path, new_path) new_lc = LoadCommand.create(:LC_RPATH, new_path)
delete_rpath(old_path)
insert_command(old_lc.view.offset, new_lc)
end
# Add the given runtime path to the Mach-O.
# @example
# file.rpaths # => ["/lib"]
# file.add_rpath("/usr/lib")
# file.rpaths # => ["/lib", "/usr/lib"]
# @param path [String] the new runtime path
# @param _options [Hash]
# @return [void]
# @raise [MachO::RpathExistsError] if the runtime path already exists
# @note `_options` is currently unused and is provided for signature
# compatibility with {MachO::FatFile#add_rpath}
def add_rpath(path, _options = {})
raise RpathExistsError, path if rpaths.include?(path)
rpath_cmd = LoadCommand.create(:LC_RPATH, path)
add_command(rpath_cmd)
end
# Delete the given runtime path from the Mach-O.
# @example
# file.rpaths # => ["/lib"]
# file.delete_rpath("/lib")
# file.rpaths # => []
# @param path [String] the runtime path to delete
# @param _options [Hash]
# @return void
# @raise [MachO::RpathUnknownError] if no such runtime path exists
# @note `_options` is currently unused and is provided for signature
# compatibility with {MachO::FatFile#delete_rpath}
def delete_rpath(path, _options = {})
rpath_cmds = command(:LC_RPATH).select { |r| r.path.to_s == path }
raise RpathUnknownError, path if rpath_cmds.empty?
# delete the commands in reverse order, offset descending. this
# allows us to defer (expensive) field population until the very end
rpath_cmds.reverse_each { |cmd| delete_command(cmd, :repopulate => false) }
populate_fields
end end
# All sections of the segment `segment`. # All sections of the segment `segment`.
# @param segment [MachO::SegmentCommand, MachO::SegmentCommand64] the segment being inspected # @param segment [MachO::SegmentCommand, MachO::SegmentCommand64] the segment being inspected
# @return [Array<MachO::Section>] if the Mach-O is 32-bit # @return [Array<MachO::Section>] if the Mach-O is 32-bit
# @return [Array<MachO::Section64>] if the Mach-O is 64-bit # @return [Array<MachO::Section64>] if the Mach-O is 64-bit
# @deprecated use {MachO::SegmentCommand#sections} instead
def sections(segment) def sections(segment)
sections = [] segment.sections
if !segment.is_a?(SegmentCommand) && !segment.is_a?(SegmentCommand64)
raise ArgumentError.new("not a valid segment")
end
if segment.nsects.zero?
return sections
end
offset = segment.offset + segment.class.bytesize
segment.nsects.times do
if segment.is_a? SegmentCommand
sections << Section.new_from_bin(endianness, @raw_data.slice(offset, Section.bytesize))
offset += Section.bytesize
else
sections << Section64.new_from_bin(endianness, @raw_data.slice(offset, Section64.bytesize))
offset += Section64.bytesize
end
end
sections
end end
# Write all Mach-O data to the given filename. # Write all Mach-O data to the given filename.
@ -308,7 +453,7 @@ module MachO
# @note Overwrites all data in the file! # @note Overwrites all data in the file!
def write! def write!
if @filename.nil? if @filename.nil?
raise MachOError.new("cannot write to a default file when initialized from a binary string") raise MachOError, "cannot write to a default file when initialized from a binary string"
else else
File.open(@filename, "wb") { |f| f.write(@raw_data) } File.open(@filename, "wb") { |f| f.write(@raw_data) }
end end
@ -320,13 +465,13 @@ module MachO
# @return [MachO::MachHeader] if the Mach-O is 32-bit # @return [MachO::MachHeader] if the Mach-O is 32-bit
# @return [MachO::MachHeader64] if the Mach-O is 64-bit # @return [MachO::MachHeader64] if the Mach-O is 64-bit
# @raise [MachO::TruncatedFileError] if the file is too small to have a valid header # @raise [MachO::TruncatedFileError] if the file is too small to have a valid header
# @private # @api private
def get_mach_header def populate_mach_header
# the smallest Mach-O header is 28 bytes # the smallest Mach-O header is 28 bytes
raise TruncatedFileError.new if @raw_data.size < 28 raise TruncatedFileError if @raw_data.size < 28
magic = get_and_check_magic magic = populate_and_check_magic
mh_klass = MachO.magic32?(magic) ? MachHeader : MachHeader64 mh_klass = Utils.magic32?(magic) ? MachHeader : MachHeader64
mh = mh_klass.new_from_bin(endianness, @raw_data[0, mh_klass.bytesize]) mh = mh_klass.new_from_bin(endianness, @raw_data[0, mh_klass.bytesize])
check_cputype(mh.cputype) check_cputype(mh.cputype)
@ -340,14 +485,14 @@ module MachO
# @return [Fixnum] the magic # @return [Fixnum] the magic
# @raise [MachO::MagicError] if the magic is not valid Mach-O magic # @raise [MachO::MagicError] if the magic is not valid Mach-O magic
# @raise [MachO::FatBinaryError] if the magic is for a Fat file # @raise [MachO::FatBinaryError] if the magic is for a Fat file
# @private # @api private
def get_and_check_magic def populate_and_check_magic
magic = @raw_data[0..3].unpack("N").first magic = @raw_data[0..3].unpack("N").first
raise MagicError.new(magic) unless MachO.magic?(magic) raise MagicError, magic unless Utils.magic?(magic)
raise FatBinaryError.new if MachO.fat_magic?(magic) raise FatBinaryError if Utils.fat_magic?(magic)
@endianness = MachO.little_magic?(magic) ? :little : :big @endianness = Utils.little_magic?(magic) ? :little : :big
magic magic
end end
@ -355,47 +500,48 @@ module MachO
# Check the file's CPU type. # Check the file's CPU type.
# @param cputype [Fixnum] the CPU type # @param cputype [Fixnum] the CPU type
# @raise [MachO::CPUTypeError] if the CPU type is unknown # @raise [MachO::CPUTypeError] if the CPU type is unknown
# @private # @api private
def check_cputype(cputype) def check_cputype(cputype)
raise CPUTypeError.new(cputype) unless CPU_TYPES.key?(cputype) raise CPUTypeError, cputype unless CPU_TYPES.key?(cputype)
end end
# Check the file's CPU type/subtype pair. # Check the file's CPU type/subtype pair.
# @param cpusubtype [Fixnum] the CPU subtype # @param cpusubtype [Fixnum] the CPU subtype
# @raise [MachO::CPUSubtypeError] if the CPU sub-type is unknown # @raise [MachO::CPUSubtypeError] if the CPU sub-type is unknown
# @private # @api private
def check_cpusubtype(cputype, cpusubtype) def check_cpusubtype(cputype, cpusubtype)
# Only check sub-type w/o capability bits (see `get_mach_header`). # Only check sub-type w/o capability bits (see `populate_mach_header`).
raise CPUSubtypeError.new(cputype, cpusubtype) unless CPU_SUBTYPES[cputype].key?(cpusubtype) raise CPUSubtypeError.new(cputype, cpusubtype) unless CPU_SUBTYPES[cputype].key?(cpusubtype)
end end
# Check the file's type. # Check the file's type.
# @param filetype [Fixnum] the file type # @param filetype [Fixnum] the file type
# @raise [MachO::FiletypeError] if the file type is unknown # @raise [MachO::FiletypeError] if the file type is unknown
# @private # @api private
def check_filetype(filetype) def check_filetype(filetype)
raise FiletypeError.new(filetype) unless MH_FILETYPES.key?(filetype) raise FiletypeError, filetype unless MH_FILETYPES.key?(filetype)
end end
# All load commands in the file. # All load commands in the file.
# @return [Array<MachO::LoadCommand>] an array of load commands # @return [Array<MachO::LoadCommand>] an array of load commands
# @raise [MachO::LoadCommandError] if an unknown load command is encountered # @raise [MachO::LoadCommandError] if an unknown load command is encountered
# @private # @api private
def get_load_commands def populate_load_commands
offset = header.class.bytesize offset = header.class.bytesize
load_commands = [] load_commands = []
header.ncmds.times do header.ncmds.times do
fmt = (endianness == :little) ? "L<" : "L>" fmt = Utils.specialize_format("L=", endianness)
cmd = @raw_data.slice(offset, 4).unpack(fmt).first cmd = @raw_data.slice(offset, 4).unpack(fmt).first
cmd_sym = LOAD_COMMANDS[cmd] cmd_sym = LOAD_COMMANDS[cmd]
raise LoadCommandError.new(cmd) if cmd_sym.nil? raise LoadCommandError, cmd if cmd_sym.nil?
# why do I do this? i don't like declaring constants below # why do I do this? i don't like declaring constants below
# classes, and i need them to resolve... # classes, and i need them to resolve...
klass = MachO.const_get "#{LC_STRUCTURES[cmd_sym]}" klass = MachO.const_get LC_STRUCTURES[cmd_sym]
command = klass.new_from_bin(@raw_data, endianness, offset, @raw_data.slice(offset, klass.bytesize)) view = MachOView.new(@raw_data, endianness, offset)
command = klass.new_from_bin(view)
load_commands << command load_commands << command
offset += command.cmdsize offset += command.cmdsize
@ -404,108 +550,44 @@ module MachO
load_commands load_commands
end end
# Updates the size of all load commands in the raw data. # The low file offset (offset to first section data).
# @param size [Fixnum] the new size, in bytes # @return [Fixnum] the offset
# @return [void] # @api private
# @private def low_fileoff
def set_sizeofcmds(size) offset = @raw_data.size
fmt = (endianness == :little) ? "L<" : "L>"
new_size = [size].pack(fmt)
@raw_data[20..23] = new_size
end
# Updates the `name` field in a DylibCommand.
# @param dylib_cmd [MachO::DylibCommand] the dylib command
# @param old_name [String] the old dylib name
# @param new_name [String] the new dylib name
# @return [void]
# @private
def set_name_in_dylib(dylib_cmd, old_name, new_name)
set_lc_str_in_cmd(dylib_cmd, dylib_cmd.name, old_name, new_name)
end
# Updates the `path` field in an RpathCommand.
# @param rpath_cmd [MachO::RpathCommand] the rpath command
# @param old_path [String] the old runtime name
# @param new_path [String] the new runtime name
# @return [void]
# @private
def set_path_in_rpath(rpath_cmd, old_path, new_path)
set_lc_str_in_cmd(rpath_cmd, rpath_cmd.path, old_path, new_path)
end
# Updates a generic LCStr field in any LoadCommand.
# @param cmd [MachO::LoadCommand] the load command
# @param lc_str [MachO::LoadCommand::LCStr] the load command string
# @param old_str [String] the old string
# @param new_str [String] the new string
# @raise [MachO::HeaderPadError] if the new name exceeds the header pad buffer
# @private
def set_lc_str_in_cmd(cmd, lc_str, old_str, new_str)
if magic32?
cmd_round = 4
else
cmd_round = 8
end
new_sizeofcmds = header.sizeofcmds
old_str = old_str.dup
new_str = new_str.dup
old_pad = MachO.round(old_str.size + 1, cmd_round) - old_str.size
new_pad = MachO.round(new_str.size + 1, cmd_round) - new_str.size
# pad the old and new IDs with null bytes to meet command bounds
old_str << "\x00" * old_pad
new_str << "\x00" * new_pad
# calculate the new size of the cmd and sizeofcmds in MH
new_size = cmd.class.bytesize + new_str.size
new_sizeofcmds += new_size - cmd.cmdsize
low_fileoff = @raw_data.size
# calculate the low file offset (offset to first section data)
segments.each do |seg| segments.each do |seg|
sections(seg).each do |sect| seg.sections.each do |sect|
next if sect.size == 0 next if sect.empty?
next if sect.flag?(:S_ZEROFILL) next if sect.flag?(:S_ZEROFILL)
next if sect.flag?(:S_THREAD_LOCAL_ZEROFILL) next if sect.flag?(:S_THREAD_LOCAL_ZEROFILL)
next unless sect.offset < low_fileoff next unless sect.offset < offset
low_fileoff = sect.offset offset = sect.offset
end end
end end
if new_sizeofcmds + header.class.bytesize > low_fileoff offset
raise HeaderPadError.new(@filename) end
end
# update sizeofcmds in mach_header # Updates the number of load commands in the raw data.
set_sizeofcmds(new_sizeofcmds) # @param ncmds [Fixnum] the new number of commands
# @return [void]
# @api private
def update_ncmds(ncmds)
fmt = Utils.specialize_format("L=", endianness)
ncmds_raw = [ncmds].pack(fmt)
@raw_data[16..19] = ncmds_raw
end
# update cmdsize in the cmd # Updates the size of all load commands in the raw data.
fmt = (endianness == :little) ? "L<" : "L>" # @param size [Fixnum] the new size, in bytes
@raw_data[cmd.offset + 4, 4] = [new_size].pack(fmt) # @return [void]
# @api private
# delete the old str def update_sizeofcmds(size)
@raw_data.slice!(cmd.offset + lc_str.to_i...cmd.offset + cmd.class.bytesize + old_str.size) fmt = Utils.specialize_format("L=", endianness)
size_raw = [size].pack(fmt)
# insert the new str @raw_data[20..23] = size_raw
@raw_data.insert(cmd.offset + lc_str.to_i, new_str)
# pad/unpad after new_sizeofcmds until offsets are corrected
null_pad = old_str.size - new_str.size
if null_pad < 0
@raw_data.slice!(new_sizeofcmds + header.class.bytesize, null_pad.abs)
else
@raw_data.insert(new_sizeofcmds + header.class.bytesize, "\x00" * null_pad)
end
# synchronize fields with the raw data
@header = get_mach_header
@load_commands = get_load_commands
end end
end end
end end

View File

@ -7,17 +7,17 @@ module MachO
# @raise [MachO::TruncatedFileError] if the file is too small to have a valid header # @raise [MachO::TruncatedFileError] if the file is too small to have a valid header
# @raise [MachO::MagicError] if the file's magic is not valid Mach-O magic # @raise [MachO::MagicError] if the file's magic is not valid Mach-O magic
def self.open(filename) def self.open(filename)
raise ArgumentError.new("#{filename}: no such file") unless File.file?(filename) raise ArgumentError, "#{filename}: no such file" unless File.file?(filename)
raise TruncatedFileError.new unless File.stat(filename).size >= 4 raise TruncatedFileError unless File.stat(filename).size >= 4
magic = File.open(filename, "rb") { |f| f.read(4) }.unpack("N").first magic = File.open(filename, "rb") { |f| f.read(4) }.unpack("N").first
if MachO.fat_magic?(magic) if Utils.fat_magic?(magic)
file = FatFile.new(filename) file = FatFile.new(filename)
elsif MachO.magic?(magic) elsif Utils.magic?(magic)
file = MachOFile.new(filename) file = MachOFile.new(filename)
else else
raise MagicError.new(magic) raise MagicError, magic
end end
file file

View File

@ -45,7 +45,7 @@ module MachO
:S_ATTR_DEBUG => 0x02000000, :S_ATTR_DEBUG => 0x02000000,
:S_ATTR_SOME_INSTRUCTIONS => 0x00000400, :S_ATTR_SOME_INSTRUCTIONS => 0x00000400,
:S_ATTR_EXT_RELOC => 0x00000200, :S_ATTR_EXT_RELOC => 0x00000200,
:S_ATTR_LOC_RELOC => 0x00000100 :S_ATTR_LOC_RELOC => 0x00000100,
}.freeze }.freeze
# association of section name symbols to names # association of section name symbols to names
@ -62,7 +62,7 @@ module MachO
:SECT_OBJC_STRINGS => "__selector_strs", :SECT_OBJC_STRINGS => "__selector_strs",
:SECT_OBJC_REFS => "__selector_refs", :SECT_OBJC_REFS => "__selector_refs",
:SECT_ICON_HEADER => "__header", :SECT_ICON_HEADER => "__header",
:SECT_ICON_TIFF => "__tiff" :SECT_ICON_TIFF => "__tiff",
}.freeze }.freeze
# Represents a section of a segment for 32-bit architectures. # Represents a section of a segment for 32-bit architectures.
@ -91,7 +91,7 @@ module MachO
# @return [Fixnum] the number of relocation entries # @return [Fixnum] the number of relocation entries
attr_reader :nreloc attr_reader :nreloc
# @return [Fixnum] flags for type and addrributes of the section # @return [Fixnum] flags for type and attributes of the section
attr_reader :flags attr_reader :flags
# @return [void] reserved (for offset or index) # @return [void] reserved (for offset or index)
@ -100,12 +100,15 @@ module MachO
# @return [void] reserved (for count or sizeof) # @return [void] reserved (for count or sizeof)
attr_reader :reserved2 attr_reader :reserved2
FORMAT = "a16a16L=9" # @see MachOStructure::FORMAT
FORMAT = "a16a16L=9".freeze
# @see MachOStructure::SIZEOF
SIZEOF = 68 SIZEOF = 68
# @api private # @api private
def initialize(sectname, segname, addr, size, offset, align, reloff, def initialize(sectname, segname, addr, size, offset, align, reloff,
nreloc, flags, reserved1, reserved2) nreloc, flags, reserved1, reserved2)
@sectname = sectname @sectname = sectname
@segname = segname @segname = segname
@addr = addr @addr = addr
@ -121,12 +124,17 @@ module MachO
# @return [String] the section's name, with any trailing NULL characters removed # @return [String] the section's name, with any trailing NULL characters removed
def section_name def section_name
@sectname.delete("\x00") sectname.delete("\x00")
end end
# @return [String] the parent segment's name, with any trailing NULL characters removed # @return [String] the parent segment's name, with any trailing NULL characters removed
def segment_name def segment_name
@segname.delete("\x00") segname.delete("\x00")
end
# @return [Boolean] true if the section has no contents (i.e, `size` is 0)
def empty?
size.zero?
end end
# @example # @example
@ -145,12 +153,15 @@ module MachO
# @return [void] reserved # @return [void] reserved
attr_reader :reserved3 attr_reader :reserved3
FORMAT = "a16a16Q=2L=8" # @see MachOStructure::FORMAT
FORMAT = "a16a16Q=2L=8".freeze
# @see MachOStructure::SIZEOF
SIZEOF = 80 SIZEOF = 80
# @api private # @api private
def initialize(sectname, segname, addr, size, offset, align, reloff, def initialize(sectname, segname, addr, size, offset, align, reloff,
nreloc, flags, reserved1, reserved2, reserved3) nreloc, flags, reserved1, reserved2, reserved3)
super(sectname, segname, addr, size, offset, align, reloff, super(sectname, segname, addr, size, offset, align, reloff,
nreloc, flags, reserved1, reserved2) nreloc, flags, reserved1, reserved2)
@reserved3 = reserved3 @reserved3 = reserved3

View File

@ -3,9 +3,13 @@ module MachO
# @abstract # @abstract
class MachOStructure class MachOStructure
# The String#unpack format of the data structure. # The String#unpack format of the data structure.
FORMAT = "" # @return [String] the unpacking format
# @api private
FORMAT = "".freeze
# The size of the data structure, in bytes. # The size of the data structure, in bytes.
# @return [Fixnum] the size, in bytes
# @api private
SIZEOF = 0 SIZEOF = 0
# @return [Fixnum] the size, in bytes, of the represented structure. # @return [Fixnum] the size, in bytes, of the represented structure.
@ -13,26 +17,14 @@ module MachO
self::SIZEOF self::SIZEOF
end end
# @param endianness [Symbol] either :big or :little # @param endianness [Symbol] either `:big` or `:little`
# @param bin [String] the string to be unpacked into the new structure # @param bin [String] the string to be unpacked into the new structure
# @return [MachO::MachOStructure] a new MachOStructure initialized with `bin` # @return [MachO::MachOStructure] a new MachOStructure initialized with `bin`
# @api private # @api private
def self.new_from_bin(endianness, bin) def self.new_from_bin(endianness, bin)
format = specialize_format(self::FORMAT, endianness) format = Utils.specialize_format(self::FORMAT, endianness)
self.new(*bin.unpack(format)) new(*bin.unpack(format))
end
private
# Convert an abstract (native-endian) String#unpack format to big or little.
# @param format [String] the format string being converted
# @param endianness [Symbol] either :big or :little
# @return [String] the converted string
# @api private
def self.specialize_format(format, endianness)
modifier = (endianness == :big) ? ">" : "<"
format.tr("=", modifier)
end end
end end
end end

View File

@ -12,12 +12,14 @@ module MachO
# Changes the dylib ID of a Mach-O or Fat binary, overwriting the source file. # Changes the dylib ID of a Mach-O or Fat binary, overwriting the source file.
# @param filename [String] the Mach-O or Fat binary being modified # @param filename [String] the Mach-O or Fat binary being modified
# @param new_id [String] the new dylib ID for the binary # @param new_id [String] the new dylib ID for the binary
# @param options [Hash]
# @option options [Boolean] :strict (true) whether or not to fail loudly
# with an exception if the change cannot be performed
# @return [void] # @return [void]
# @todo unstub for fat files def self.change_dylib_id(filename, new_id, options = {})
def self.change_dylib_id(filename, new_id)
file = MachO.open(filename) file = MachO.open(filename)
file.dylib_id = new_id file.change_dylib_id(new_id, options)
file.write! file.write!
end end
@ -25,12 +27,14 @@ module MachO
# @param filename [String] the Mach-O or Fat binary being modified # @param filename [String] the Mach-O or Fat binary being modified
# @param old_name [String] the old shared library name # @param old_name [String] the old shared library name
# @param new_name [String] the new shared library name # @param new_name [String] the new shared library name
# @param options [Hash]
# @option options [Boolean] :strict (true) whether or not to fail loudly
# with an exception if the change cannot be performed
# @return [void] # @return [void]
# @todo unstub for fat files def self.change_install_name(filename, old_name, new_name, options = {})
def self.change_install_name(filename, old_name, new_name)
file = MachO.open(filename) file = MachO.open(filename)
file.change_install_name(old_name, new_name) file.change_install_name(old_name, new_name, options)
file.write! file.write!
end end
@ -38,28 +42,43 @@ module MachO
# @param filename [String] the Mach-O or Fat binary being modified # @param filename [String] the Mach-O or Fat binary being modified
# @param old_path [String] the old runtime path # @param old_path [String] the old runtime path
# @param new_path [String] the new runtime path # @param new_path [String] the new runtime path
# @param options [Hash]
# @option options [Boolean] :strict (true) whether or not to fail loudly
# with an exception if the change cannot be performed
# @return [void] # @return [void]
# @todo unstub def self.change_rpath(filename, old_path, new_path, options = {})
def self.change_rpath(filename, old_path, new_path) file = MachO.open(filename)
raise UnimplementedError.new("changing rpaths in a Mach-O")
file.change_rpath(old_path, new_path, options)
file.write!
end end
# Add a runtime path to a Mach-O or Fat binary, overwriting the source file. # Add a runtime path to a Mach-O or Fat binary, overwriting the source file.
# @param filename [String] the Mach-O or Fat binary being modified # @param filename [String] the Mach-O or Fat binary being modified
# @param new_path [String] the new runtime path # @param new_path [String] the new runtime path
# @param options [Hash]
# @option options [Boolean] :strict (true) whether or not to fail loudly
# with an exception if the change cannot be performed
# @return [void] # @return [void]
# @todo unstub def self.add_rpath(filename, new_path, options = {})
def self.add_rpath(filename, new_path) file = MachO.open(filename)
raise UnimplementedError.new("adding rpaths to a Mach-O")
file.add_rpath(new_path, options)
file.write!
end end
# Delete a runtime path from a Mach-O or Fat binary, overwriting the source file. # Delete a runtime path from a Mach-O or Fat binary, overwriting the source file.
# @param filename [String] the Mach-O or Fat binary being modified # @param filename [String] the Mach-O or Fat binary being modified
# @param old_path [String] the old runtime path # @param old_path [String] the old runtime path
# @param options [Hash]
# @option options [Boolean] :strict (true) whether or not to fail loudly
# with an exception if the change cannot be performed
# @return [void] # @return [void]
# @todo unstub def self.delete_rpath(filename, old_path, options = {})
def self.delete_rpath(filename, old_path) file = MachO.open(filename)
raise UnimplementedError.new("removing rpaths from a Mach-O")
file.delete_rpath(old_path, options)
file.write!
end end
end end
end end

View File

@ -1,48 +1,96 @@
module MachO module MachO
# @param value [Fixnum] the number being rounded # A collection of utility functions used throughout ruby-macho.
# @param round [Fixnum] the number being rounded with module Utils
# @return [Fixnum] the next number >= `value` such that `round` is its divisor # Rounds a value to the next multiple of the given round.
# @see http://www.opensource.apple.com/source/cctools/cctools-870/libstuff/rnd.c # @param value [Fixnum] the number being rounded
def self.round(value, round) # @param round [Fixnum] the number being rounded with
round -= 1 # @return [Fixnum] the rounded value
value += round # @see http://www.opensource.apple.com/source/cctools/cctools-870/libstuff/rnd.c
value &= ~round def self.round(value, round)
value round -= 1
end value += round
value &= ~round
value
end
# @param num [Fixnum] the number being checked # Returns the number of bytes needed to pad the given size to the given alignment.
# @return [Boolean] true if `num` is a valid Mach-O magic number, false otherwise # @param size [Fixnum] the unpadded size
def self.magic?(num) # @param alignment [Fixnum] the number to alignment the size with
MH_MAGICS.has_key?(num) # @return [Fixnum] the number of pad bytes required
end def self.padding_for(size, alignment)
round(size, alignment) - size
end
# @param num [Fixnum] the number being checked # Converts an abstract (native-endian) String#unpack format to big or little.
# @return [Boolean] true if `num` is a valid Fat magic number, false otherwise # @param format [String] the format string being converted
def self.fat_magic?(num) # @param endianness [Symbol] either `:big` or `:little`
num == FAT_MAGIC # @return [String] the converted string
end def self.specialize_format(format, endianness)
modifier = endianness == :big ? ">" : "<"
format.tr("=", modifier)
end
# @param num [Fixnum] the number being checked # Packs tagged strings into an aligned payload.
# @return [Boolean] true if `num` is a valid 32-bit magic number, false otherwise # @param fixed_offset [Fixnum] the baseline offset for the first packed string
def self.magic32?(num) # @param alignment [Fixnum] the alignment value to use for packing
num == MH_MAGIC || num == MH_CIGAM # @param strings [Hash] the labeled strings to pack
end # @return [Array<String, Hash>] the packed string and labeled offsets
def self.pack_strings(fixed_offset, alignment, strings = {})
offsets = {}
next_offset = fixed_offset
payload = ""
# @param num [Fixnum] the number being checked strings.each do |key, string|
# @return [Boolean] true if `num` is a valid 64-bit magic number, false otherwise offsets[key] = next_offset
def self.magic64?(num) payload << string
num == MH_MAGIC_64 || num == MH_CIGAM_64 payload << "\x00"
end next_offset += string.bytesize + 1
end
# @param num [Fixnum] the number being checked payload << "\x00" * padding_for(fixed_offset + payload.bytesize, alignment)
# @return [Boolean] true if `num` is a valid little-endian magic number, false otherwise [payload, offsets]
def self.little_magic?(num) end
num == MH_CIGAM || num == MH_CIGAM_64
end
# @param num [Fixnum] the number being checked # Compares the given number to valid Mach-O magic numbers.
# @return [Boolean] true if `num` is a valid big-endian magic number, false otherwise # @param num [Fixnum] the number being checked
def self.big_magic?(num) # @return [Boolean] true if `num` is a valid Mach-O magic number, false otherwise
num == MH_CIGAM || num == MH_CIGAM_64 def self.magic?(num)
MH_MAGICS.key?(num)
end
# Compares the given number to valid Fat magic numbers.
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid Fat magic number, false otherwise
def self.fat_magic?(num)
num == FAT_MAGIC
end
# Compares the given number to valid 32-bit Mach-O magic numbers.
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid 32-bit magic number, false otherwise
def self.magic32?(num)
num == MH_MAGIC || num == MH_CIGAM
end
# Compares the given number to valid 64-bit Mach-O magic numbers.
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid 64-bit magic number, false otherwise
def self.magic64?(num)
num == MH_MAGIC_64 || num == MH_CIGAM_64
end
# Compares the given number to valid little-endian magic numbers.
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid little-endian magic number, false otherwise
def self.little_magic?(num)
num == MH_CIGAM || num == MH_CIGAM_64
end
# Compares the given number to valid big-endian magic numbers.
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid big-endian magic number, false otherwise
def self.big_magic?(num)
num == MH_CIGAM || num == MH_CIGAM_64
end
end end
end end

View File

@ -0,0 +1,23 @@
module MachO
# A representation of some unspecified Mach-O data.
class MachOView
# @return [String] the raw Mach-O data
attr_reader :raw_data
# @return [Symbol] the endianness of the data (`:big` or `:little`)
attr_reader :endianness
# @return [Fixnum] the offset of the relevant data (in {#raw_data})
attr_reader :offset
# Creates a new MachOView.
# @param raw_data [String] the raw Mach-O data
# @param endianness [Symbol] the endianness of the data
# @param offset [Fixnum] the offset of the relevant data
def initialize(raw_data, endianness, offset)
@raw_data = raw_data
@endianness = endianness
@offset = offset
end
end
end