require 'ffi' module DsymUuidExtractor # Based on the details found in https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/MachORuntime/index.html class MachOReader # Constants ## CPU Types CPU_ARCH_ABI64 = 0x01000000 CPU_TYPE_I386 = 7 CPU_TYPE_X86_64 = CPU_TYPE_I386 | CPU_ARCH_ABI64 CPU_TYPE_ARM = 12 CPU_TYPE_ARM64 = CPU_TYPE_ARM | CPU_ARCH_ABI64 ## CPU Subtypes CPU_SUBTYPE_ARM_V7 = 9 CPU_SUBTYPE_ARM_V7S = 11 # Readers attr_reader :file # Methods def initialize(opts = {}) @file = File.open(opts[:file_path], mode: 'rb') @file.seek(opts[:seek_to_pos]) if opts[:seek_to_pos] end def read_struct(struct_class, opts = {}) struct_size = struct_class.size buffer = FFI::Buffer.new(:char, struct_size) buffer = buffer.order(opts[:byte_order]) if opts[:byte_order] buffer.put_bytes(0, @file.read(struct_size)) struct_class.new(buffer) end class EmptySymbolicationFile < StandardError; end class MachOReaderError < StandardError; end class UnkownCpuSubtype < MachOReaderError; end class UnkownCpuType < MachOReaderError; end class ArchIsNil < MachOReaderError; end end class MachOBinaryReader < MachOReader ## Magic Numbers FAT_MAGIC = 0xcafebabe FAT_CIGAM = 0xbebafeca # Struct Classes class FatHeader < FFI::Struct layout magic: :uint32, nfat_arch: :uint32 end class FatArch < FFI::Struct layout cputype: :int32, cpusubtype: :int32, offset: :uint32, size: :uint32, align: :uint32 end # Methods def self.is_binary_file?(file_path) fail EmptySymbolicationFile if File.zero?(file_path) File.read(file_path, 4).unpack('L>')[0] == FAT_MAGIC || File.read(file_path, 4).unpack('L<')[0] == FAT_CIGAM rescue EmptySymbolicationFile # To be logged in CloudWatch... false end def each_file arch_count = fat_header[:nfat_arch] seek_to_fat_arch arch_offsets = arch_count.times.map do fat_arch = read_struct(FatArch) fat_arch[:offset] end arch_offsets.each do |arch_offset| yield MachOFileReader.new(file_path: File.absolute_path(file.path), seek_to_pos: arch_offset) end end def read_struct(struct_class, opts = {}) super(struct_class, opts.merge(byte_order: :big)) end private def fat_header return @fat_header if @fat_header seek_to_file_beginning @fat_header = read_struct(FatHeader) end def seek_to_fat_arch seek_to_file_beginning file.seek(FatHeader.size, IO::SEEK_CUR) end def seek_to_file_beginning file.seek(0) end end class MachOFileReader < MachOReader # Constants ## Load Commands LC_UUID = 0x1b ## Magic Numbers MH_MAGIC = 0xfeedface MH_CIGAM = 0xcefaedfe MH_MAGIC_64 = 0xfeedfacf MH_CIGAM_64 = 0xcffaedfe ## Byte Order BYTE_ORDER = FFI::MemoryPointer.new(:char).order BYTE_ORDER_INVERSE = BYTE_ORDER == :little ? :big : :little # Struct Classes class MachHeader < FFI::Struct layout magic: :uint32, cputype: :int32, cpusubtype: :int32, filetype: :uint32, ncmds: :uint32, sizeofcmds: :uint32, flags: :uint32 end class MachHeader64 < FFI::Struct layout magic: :uint32, cputype: :int32, cpusubtype: :int32, filetype: :uint32, ncmds: :uint32, sizeofcmds: :uint32, flags: :uint32, reserved: :uint32 end class LoadCommand < FFI::Struct layout cmd: :uint32, cmdsize: :uint32 end class UuidCommand < FFI::Struct layout cmd: :uint32, cmdsize: :uint32, uuid: [:uint8, 16] end # Readers attr_reader :arch, :uuid, :is_64_arch # Methods def self.is_macho_file?(file_path) magic_number = new(file_path: file_path).magic_number [MH_MAGIC, MH_MAGIC_64, MH_CIGAM, MH_CIGAM_64].include?(magic_number) end def initialize(opts = {}) super(opts) @file_beginning_pos = file.pos end def magic_number mach_header[:magic] end def extract_info extract_arch extract_uuid end def each_file yield self end private def mach_header return @mach_header if @mach_header seek_to_file_beginning @mach_header = read_struct(MachHeader) end def extract_arch @arch = case mach_header[:cputype] when CPU_TYPE_I386 'i386' when CPU_TYPE_X86_64 'x86_64' when CPU_TYPE_ARM64 'arm64' when CPU_TYPE_ARM case mach_header[:cpusubtype] when CPU_SUBTYPE_ARM_V7 'armv7' when CPU_SUBTYPE_ARM_V7S 'armv7s' else fail MachOReader::UnkownCpuSubtype, "Extracting arch failed; unknown cpusubtype '#{mach_header[:cputype]}'" end else fail MachOReader::UnkownCpuType, "Extracting arch failed; unknown cputype '#{mach_header[:cputype]}'" end @is_64_arch = (mach_header[:cputype] & CPU_ARCH_ABI64 != 0) end def extract_uuid seek_to_load_commands mach_header[:ncmds].times do load_command = read_struct(LoadCommand, byte_order: byte_order) file.seek(-LoadCommand.size, IO::SEEK_CUR) if load_command[:cmd] == LC_UUID uuid_command = read_struct(UuidCommand, byte_order: byte_order) # Copied from securerandom stdlib uuid_unpacked = uuid_command[:uuid].to_a.pack('C' * 16).unpack('NnnnnN') @uuid = (format('%08x-%04x-%04x-%04x-%04x%08x', *uuid_unpacked)).upcase break else file.seek(load_command[:cmdsize], IO::SEEK_CUR) end end end def seek_to_file_beginning file.seek(@file_beginning_pos) end def seek_to_load_commands fail MachOReader::ArchIsNil, '@arch is nil; call #extract_arch first' if @arch.nil? header_struct = is_64_arch ? MachHeader64 : MachHeader seek_to_file_beginning file.seek(header_struct.size, IO::SEEK_CUR) true end def byte_order @byte_order ||= if is_64_arch mach_header[:magic] == MH_MAGIC_64 ? BYTE_ORDER : BYTE_ORDER_INVERSE else mach_header[:magic] == MH_MAGIC ? BYTE_ORDER : BYTE_ORDER_INVERSE end end end end