emonti/pliney

View on GitHub
lib/pliney/ipa.rb

Summary

Maintainability
B
5 hrs
Test Coverage
require 'rubygems'
require 'zip'
require_relative 'util'
require_relative 'macho'
require_relative 'apple_code_signature'

module Pliney
    class IPA
        class ZipExtractError < StandardError
        end

        SYSTEM_HAS_UNZIP = system("which unzip > /dev/null")

        def self.from_path(path)
            ipa = new(Zip::File.open(path))
            if block_given?
                ret = yield(ipa)
                ipa.close
                return ret
            else
                return ipa
            end
        end

        # TODO - creating an ipa from scratch?
        # def self.create(path)
        #    new(Zip::File.open(path, Zip::File::CREATE))
        # end

        attr_reader :zipfile

        def initialize(zipfile)
            @zipfile = zipfile
        end

        def appdir
            @appdir ||= find_appdir
        end

        def parse_plist_entry(path)
            Pliney.parse_plist(read_path(path))
        end

        def find_appdir
            if e = @zipfile.find{|ent| ent.directory? and ent.name =~ /^Payload\/[^\/]*\.app\/$/ }
                return Pathname(e.name)
            end
        end

        def read_path(path, *args)
            return @zipfile.get_input_stream(path.to_s){|sio| sio.read(*args)}
        end

        def info_plist
            return parse_plist_entry(appdir.join("Info.plist"))
        end

        def bundle_identifier
            return info_plist["CFBundleIdentifier"]
        end

        def bundle_version
            return info_plist["CFBundleVersion"]
        end

        def bundle_short_version
            return info_plist["CFBundleShortVersionString"]
        end

        def executable_path
            return appdir.join(info_plist["CFBundleExecutable"])
        end

        def executable_entry
            return get_entry(executable_path)
        end

        def ls
            return @zipfile.entries.map{|e| e.name}
        end

        def close
            @zipfile.close
        end

        def get_entry(path)
            return @zipfile.find_entry(path.to_s)
        end

        def provisioning_profile
            begin
                profile_data = read_path(appdir.join("embedded.mobileprovision"))
            rescue Errno::ENOENT
                return nil
            end

            ProvisioningProfile.from_asn1(profile_data)
        end

        def with_macho_for_entry(file_entry)
            with_extracted_tmpfile(file_entry) do |tmpfile|
                yield ::Pliney::MachO.from_stream(tmpfile)
            end
        end

        def with_executable_macho(&block)
            with_macho_for_entry(self.executable_entry, &block)
        end

        def codesignature_for_entry(file_entry)
            _with_tmpdir do |tmp_path|
                tmpf = tmp_path.join("executable")
                file_entry.extract(tmpf.to_s)
                return ::Pliney::AppleCodeSignature.from_path(tmpf.to_s)
            end
        end

        def executable_codesignature
            return codesignature_for_entry(self.executable_entry)
        end

        def entitlements_data_for_entry(file_entry)
            cs = self.codesignature_for_entry(file_entry)
            if cs
                ents_blob = cs.contents.find{|c| c.is_a? ::Pliney::AppleCodeSignature::Entitlement}
                if ents_blob
                    return ents_blob.data
                end
            end
        end

        def entitlements_for_entry(file_entry)
            dat = entitlements_data_for_entry(file_entry)
            if dat
                return ::Pliney.parse_plist(dat)
            end
        end

        def executable_entitlements_data
            return entitlements_data_for_entry(self.executable_entry)
        end

        def executable_entitlements
            return entitlements_for_entry(self.executable_entry)
        end

        def team_identifier
            entitlements = executable_entitlements()
            if entitlements
                return entitlements["com.apple.developer.team-identifier"]
            end
        end

        def extract(path)
            if SYSTEM_HAS_UNZIP
                ret = system("unzip", "-qd", path.to_s, self.zipfile.name.to_s)
                unless ret
                    raise(ZipExtractError, "'unzip' command returned non-zero status: #{$?.inspect}")
                end
            else
                path = Pathname(path)
                zipfile.each do |ent|
                    extract_path = path.join(ent.name)
                    FileUtils.mkdir_p(extract_path.dirname)
                    ent.extract(extract_path.to_s)
                    extract_path.chmod(ent.unix_perms & 0777)
                end
            end
            return path
        end

        def with_extracted_tmpdir(&block)
            _with_tmpdir {|tmp_path| yield(extract(tmp_path)) }
        end

        def with_extracted_tmpfile(ent, &block)
            tmpf = Tempfile.new("ent")
            begin
                Zip::IOExtras.copy_stream(tmpf, ent.get_input_stream)
                tmpf.rewind
                yield(tmpf)
            ensure
                tmpf.unlink()
            end
        end

        def each_file_entry
            zipfile.entries.select do |ent|
                not ent.name_is_directory?
            end.each do |ent|
                yield(ent)
            end
            return nil
        end

        def each_executable_entry
            each_file_entry do |entry|
                next if (zipstream = entry.get_input_stream).nil?
                next if (magicbytes = zipstream.read(4)).nil?
                next if (magic = magicbytes.unpack("N").first).nil?
                if ::Pliney::MachO::is_macho_magic(magic) or ::Pliney::MachO::is_fat_magic(magic)
                    yield(entry)
                end
            end
        end

        def executable_entries
            a = []
            each_executable_entry{|e| a << e}
            return a
        end

        def canonical_name
            return "#{self.team_identifier}.#{self.bundle_identifier}.v#{self.bundle_short_version}"
        end

        private

        def _with_tmpdir
            Dir.mktmpdir {|tmpd| yield(Pathname(tmpd)) }
        end
    end
end