app-info 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +40 -0
- data/.travis.yml +9 -0
- data/CHANGELOG.md +96 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.md +246 -0
- data/Rakefile +10 -0
- data/app_info.gemspec +33 -0
- data/lib/app-info.rb +3 -0
- data/lib/app_info.rb +88 -0
- data/lib/app_info/apk.rb +165 -0
- data/lib/app_info/core_ext/object/try.rb +114 -0
- data/lib/app_info/dsym.rb +150 -0
- data/lib/app_info/ipa.rb +173 -0
- data/lib/app_info/ipa/info_plist.rb +146 -0
- data/lib/app_info/ipa/mobile_provision.rb +260 -0
- data/lib/app_info/proguard.rb +90 -0
- data/lib/app_info/util.rb +58 -0
- data/lib/app_info/version.rb +5 -0
- metadata +231 -0
    
        data/Rakefile
    ADDED
    
    
    
        data/app_info.gemspec
    ADDED
    
    | @@ -0,0 +1,33 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            lib = File.expand_path('../lib', __FILE__)
         | 
| 4 | 
            +
            $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
         | 
| 5 | 
            +
            require 'app_info/version'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            Gem::Specification.new do |spec|
         | 
| 8 | 
            +
              spec.name          = 'app-info'
         | 
| 9 | 
            +
              spec.version       = AppInfo::VERSION
         | 
| 10 | 
            +
              spec.authors       = ['icyleaf']
         | 
| 11 | 
            +
              spec.email         = ['icyleaf.cn@gmail.com']
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              spec.summary       = 'Teardown tool for mobile app(ipa/apk) and dSYM file, analysis metedata like version, name, icon'
         | 
| 14 | 
            +
              spec.description   = 'Teardown tool for ipa/apk files and dSYM file, even support for info.plist and .mobileprovision files'
         | 
| 15 | 
            +
              spec.homepage      = 'http://github.com/icyleaf/app-info'
         | 
| 16 | 
            +
              spec.license       = 'MIT'
         | 
| 17 | 
            +
              spec.files         = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
         | 
| 18 | 
            +
              spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
         | 
| 19 | 
            +
              spec.require_paths = ['lib']
         | 
| 20 | 
            +
              spec.required_ruby_version = '>= 2.3'
         | 
| 21 | 
            +
             | 
| 22 | 
            +
              spec.add_dependency 'CFPropertyList', ['< 3.1.0', '>= 2.3.4']
         | 
| 23 | 
            +
              spec.add_dependency 'image_size', '>= 1.5', '< 2.1'
         | 
| 24 | 
            +
              spec.add_dependency 'pngdefry', '~> 0.1.2'
         | 
| 25 | 
            +
              spec.add_dependency 'ruby-macho', '~> 2.2.0'
         | 
| 26 | 
            +
              spec.add_dependency 'ruby_android', '~> 0.7.7'
         | 
| 27 | 
            +
              spec.add_dependency 'rubyzip', '>= 1.2', '< 3.0'
         | 
| 28 | 
            +
              spec.add_dependency 'uuidtools', '>= 2.1.5', '< 2.3.0'
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              spec.add_development_dependency 'bundler', '>= 1.12'
         | 
| 31 | 
            +
              spec.add_development_dependency 'rake', '>= 10.0'
         | 
| 32 | 
            +
              spec.add_development_dependency 'rspec', '~> 3.0'
         | 
| 33 | 
            +
            end
         | 
    
        data/lib/app-info.rb
    ADDED
    
    
    
        data/lib/app_info.rb
    ADDED
    
    | @@ -0,0 +1,88 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'app_info/core_ext/object/try'
         | 
| 4 | 
            +
            require 'app_info/version'
         | 
| 5 | 
            +
            require 'app_info/ipa'
         | 
| 6 | 
            +
            require 'app_info/ipa/info_plist'
         | 
| 7 | 
            +
            require 'app_info/ipa/mobile_provision'
         | 
| 8 | 
            +
            require 'app_info/apk'
         | 
| 9 | 
            +
            require 'app_info/dsym'
         | 
| 10 | 
            +
            require 'app_info/proguard'
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            # AppInfo Module
         | 
| 13 | 
            +
            module AppInfo
         | 
| 14 | 
            +
              class Error < StandardError; end
         | 
| 15 | 
            +
              class NotFoundError < Error; end
         | 
| 16 | 
            +
              class UnkownFileTypeError < Error; end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              # App Platform
         | 
| 19 | 
            +
              module Platform
         | 
| 20 | 
            +
                IOS = 'iOS'
         | 
| 21 | 
            +
                ANDROID = 'Android'
         | 
| 22 | 
            +
                DSYM = 'dSYM'
         | 
| 23 | 
            +
                PROGUARD = 'Proguard'
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              # Get a new parser for automatic
         | 
| 27 | 
            +
              def self.parse(file)
         | 
| 28 | 
            +
                raise NotFoundError, file unless File.exist?(file)
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                case file_type(file)
         | 
| 31 | 
            +
                when :ipa then IPA.new(file)
         | 
| 32 | 
            +
                when :apk then APK.new(file)
         | 
| 33 | 
            +
                when :mobileprovision then MobileProvision.new(file)
         | 
| 34 | 
            +
                when :dsym then DSYM.new(file)
         | 
| 35 | 
            +
                when :proguard then Proguard.new(file)
         | 
| 36 | 
            +
                else
         | 
| 37 | 
            +
                  raise UnkownFileTypeError, "Sorry, AppInfo can not detect file type: #{file}"
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
              singleton_class.send(:alias_method, :dump, :parse)
         | 
| 41 | 
            +
             | 
| 42 | 
            +
              # Detect file type by read file header
         | 
| 43 | 
            +
              #
         | 
| 44 | 
            +
              # TODO: This can be better way to solvt, if anyone knows, tell me please.
         | 
| 45 | 
            +
              def self.file_type(file)
         | 
| 46 | 
            +
                header_hex = IO.read(file, 100)
         | 
| 47 | 
            +
                type = if header_hex =~ /^\x50\x4b\x03\x04/
         | 
| 48 | 
            +
                         detect_zip_file(file)
         | 
| 49 | 
            +
                       else
         | 
| 50 | 
            +
                         detect_mobileprovision(header_hex)
         | 
| 51 | 
            +
                       end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                type || :unkown
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
              # :nodoc:
         | 
| 57 | 
            +
              def self.detect_zip_file(file)
         | 
| 58 | 
            +
                Zip.warn_invalid_date = false
         | 
| 59 | 
            +
                zip_file = Zip::File.open(file)
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                return :proguard unless zip_file.glob('*mapping*.txt').empty?
         | 
| 62 | 
            +
                return :apk unless zip_file.find_entry('AndroidManifest.xml').nil? &&
         | 
| 63 | 
            +
                                   zip_file.find_entry('classes.dex').nil?
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                zip_file.each do |f|
         | 
| 66 | 
            +
                  path = f.name
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  return :ipa if path.include?('Payload/') && path.end_with?('Info.plist')
         | 
| 69 | 
            +
                  return :dsym if path.include?('Contents/Resources/DWARF/')
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
              end
         | 
| 72 | 
            +
              private_class_method :detect_zip_file
         | 
| 73 | 
            +
             | 
| 74 | 
            +
              # :nodoc:
         | 
| 75 | 
            +
              def self.detect_mobileprovision(hex)
         | 
| 76 | 
            +
                if hex =~ /^\x3C\x3F\x78\x6D\x6C/
         | 
| 77 | 
            +
                  # plist
         | 
| 78 | 
            +
                  :mobileprovision
         | 
| 79 | 
            +
                elsif hex =~ /^\x62\x70\x6C\x69\x73\x74/
         | 
| 80 | 
            +
                  # bplist
         | 
| 81 | 
            +
                  :mobileprovision
         | 
| 82 | 
            +
                elsif hex =~ /\x3C\x3F\x78\x6D\x6C/
         | 
| 83 | 
            +
                  # signed plist
         | 
| 84 | 
            +
                  :mobileprovision
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
              end
         | 
| 87 | 
            +
              private_class_method :detect_mobileprovision
         | 
| 88 | 
            +
            end
         | 
    
        data/lib/app_info/apk.rb
    ADDED
    
    | @@ -0,0 +1,165 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'ruby_apk'
         | 
| 4 | 
            +
            require 'image_size'
         | 
| 5 | 
            +
            require 'forwardable'
         | 
| 6 | 
            +
            require 'app_info/util'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module AppInfo
         | 
| 9 | 
            +
              # Parse APK file
         | 
| 10 | 
            +
              class APK
         | 
| 11 | 
            +
                extend Forwardable
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                attr_reader :file, :apk
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                # APK Devices
         | 
| 16 | 
            +
                module Device
         | 
| 17 | 
            +
                  PHONE   = 'Phone'
         | 
| 18 | 
            +
                  TABLET  = 'Tablet'
         | 
| 19 | 
            +
                  WATCH   = 'Watch'
         | 
| 20 | 
            +
                  TV      = 'Television'
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def initialize(file)
         | 
| 24 | 
            +
                  @file = file
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  Zip.warn_invalid_date = false # fix invaild date format warnings
         | 
| 27 | 
            +
                  @apk = ::Android::Apk.new(file)
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def size(humanable = false)
         | 
| 31 | 
            +
                  AppInfo::Util.file_size(@file, humanable)
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def os
         | 
| 35 | 
            +
                  AppInfo::Platform::ANDROID
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
                alias file_type os
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def_delegators :@apk, :manifest, :resource, :dex
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def_delegators :manifest, :version_name, :package_name,
         | 
| 42 | 
            +
                               :use_permissions, :components
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                alias release_version version_name
         | 
| 45 | 
            +
                alias identifier package_name
         | 
| 46 | 
            +
                alias bundle_id package_name
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                def version_code
         | 
| 49 | 
            +
                  manifest.version_code.to_s
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
                alias build_version version_code
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                def name
         | 
| 54 | 
            +
                  resource.find('@string/app_name')
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def device_type
         | 
| 58 | 
            +
                  if wear?
         | 
| 59 | 
            +
                    Device::WATCH
         | 
| 60 | 
            +
                  elsif tv?
         | 
| 61 | 
            +
                    Device::TV
         | 
| 62 | 
            +
                  else
         | 
| 63 | 
            +
                    Device::PHONE
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                # TODO: find a way to detect
         | 
| 68 | 
            +
                # def tablet?
         | 
| 69 | 
            +
                #   resource
         | 
| 70 | 
            +
                # end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                def wear?
         | 
| 73 | 
            +
                  use_features.include?('android.hardware.type.watch')
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                def tv?
         | 
| 77 | 
            +
                  use_features.include?('android.software.leanback')
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                def min_sdk_version
         | 
| 81 | 
            +
                  manifest.min_sdk_ver
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                def target_sdk_version
         | 
| 85 | 
            +
                  manifest.doc
         | 
| 86 | 
            +
                          .elements['/manifest/uses-sdk']
         | 
| 87 | 
            +
                          .attributes['targetSdkVersion']
         | 
| 88 | 
            +
                          .to_i
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                def use_features
         | 
| 92 | 
            +
                  manifest_values('/manifest/uses-feature')
         | 
| 93 | 
            +
                end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                def signs
         | 
| 96 | 
            +
                  @apk.signs.each_with_object([]) do |(path, sign), obj|
         | 
| 97 | 
            +
                    obj << Sign.new(path, sign)
         | 
| 98 | 
            +
                  end
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                def certificates
         | 
| 102 | 
            +
                  @apk.certificates.each_with_object([]) do |(path, certificate), obj|
         | 
| 103 | 
            +
                    obj << Certificate.new(path, certificate)
         | 
| 104 | 
            +
                  end
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                def activities
         | 
| 108 | 
            +
                  components.select { |c| c.type == 'activity' }
         | 
| 109 | 
            +
                end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                def services
         | 
| 112 | 
            +
                  components.select { |c| c.type == 'service' }
         | 
| 113 | 
            +
                end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                def icons
         | 
| 116 | 
            +
                  unless @icons
         | 
| 117 | 
            +
                    tmp_path = File.join(Dir.mktmpdir, "AppInfo-android-#{SecureRandom.hex}")
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                    @icons = @apk.icon.each_with_object([]) do |(path, data), obj|
         | 
| 120 | 
            +
                      icon_name = File.basename(path)
         | 
| 121 | 
            +
                      icon_path = File.join(tmp_path, File.dirname(path))
         | 
| 122 | 
            +
                      icon_file = File.join(icon_path, icon_name)
         | 
| 123 | 
            +
                      FileUtils.mkdir_p icon_path
         | 
| 124 | 
            +
                      File.open(icon_file, 'wb') { |f| f.write(data) }
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                      obj << {
         | 
| 127 | 
            +
                        name: icon_name,
         | 
| 128 | 
            +
                        file: icon_file,
         | 
| 129 | 
            +
                        dimensions: ImageSize.path(icon_file).size
         | 
| 130 | 
            +
                      }
         | 
| 131 | 
            +
                    end
         | 
| 132 | 
            +
                  end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                  @icons
         | 
| 135 | 
            +
                end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                private
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                def manifest_values(path, key = 'name')
         | 
| 140 | 
            +
                  values = []
         | 
| 141 | 
            +
                  manifest.doc.each_element(path) do |elem|
         | 
| 142 | 
            +
                    values << elem.attributes[key]
         | 
| 143 | 
            +
                  end
         | 
| 144 | 
            +
                  values.uniq
         | 
| 145 | 
            +
                end
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                # Android Certificate
         | 
| 148 | 
            +
                class Certificate
         | 
| 149 | 
            +
                  attr_reader :path, :certificate
         | 
| 150 | 
            +
                  def initialize(path, certificate)
         | 
| 151 | 
            +
                    @path = path
         | 
| 152 | 
            +
                    @certificate = certificate
         | 
| 153 | 
            +
                  end
         | 
| 154 | 
            +
                end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                # Android Sign
         | 
| 157 | 
            +
                class Sign
         | 
| 158 | 
            +
                  attr_reader :path, :sign
         | 
| 159 | 
            +
                  def initialize(path, sign)
         | 
| 160 | 
            +
                    @path = path
         | 
| 161 | 
            +
                    @sign = sign
         | 
| 162 | 
            +
                  end
         | 
| 163 | 
            +
                end
         | 
| 164 | 
            +
              end
         | 
| 165 | 
            +
            end
         | 
| @@ -0,0 +1,114 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Copyrights rails
         | 
| 4 | 
            +
            # Copy from https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/object/try.rb
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module AppInfo
         | 
| 7 | 
            +
              module Tryable #:nodoc:
         | 
| 8 | 
            +
                def try(method_name = nil, *args, &block)
         | 
| 9 | 
            +
                  if method_name.nil? && block_given?
         | 
| 10 | 
            +
                    if block.arity.zero?
         | 
| 11 | 
            +
                      instance_eval(&b)
         | 
| 12 | 
            +
                    else
         | 
| 13 | 
            +
                      yield self
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
                  elsif respond_to?(method_name)
         | 
| 16 | 
            +
                    public_send(method_name, *args, &block)
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def try!(method_name = nil, *args, &block)
         | 
| 21 | 
            +
                  if method_name.nil? && block_given?
         | 
| 22 | 
            +
                    if block.arity.zero?
         | 
| 23 | 
            +
                      instance_eval(&block)
         | 
| 24 | 
            +
                    else
         | 
| 25 | 
            +
                      yield self
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                  else
         | 
| 28 | 
            +
                    public_send(method_name, *args, &block)
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
            end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            class Object
         | 
| 35 | 
            +
              include AppInfo::Tryable
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              ##
         | 
| 38 | 
            +
              # :method: try
         | 
| 39 | 
            +
              #
         | 
| 40 | 
            +
              # :call-seq:
         | 
| 41 | 
            +
              #   try(*a, &b)
         | 
| 42 | 
            +
              #
         | 
| 43 | 
            +
              # Invokes the public method whose name goes as first argument just like
         | 
| 44 | 
            +
              # +public_send+ does, except that if the receiver does not respond to it the
         | 
| 45 | 
            +
              # call returns +nil+ rather than raising an exception.
         | 
| 46 | 
            +
              #
         | 
| 47 | 
            +
              # This method is defined to be able to write
         | 
| 48 | 
            +
              #
         | 
| 49 | 
            +
              #   @person.try(:name)
         | 
| 50 | 
            +
              #
         | 
| 51 | 
            +
              # instead of
         | 
| 52 | 
            +
              #
         | 
| 53 | 
            +
              #   @person.name if @person
         | 
| 54 | 
            +
              #
         | 
| 55 | 
            +
              # +try+ calls can be chained:
         | 
| 56 | 
            +
              #
         | 
| 57 | 
            +
              #   @person.try(:spouse).try(:name)
         | 
| 58 | 
            +
              #
         | 
| 59 | 
            +
              # instead of
         | 
| 60 | 
            +
              #
         | 
| 61 | 
            +
              #   @person.spouse.name if @person && @person.spouse
         | 
| 62 | 
            +
              #
         | 
| 63 | 
            +
              # +try+ will also return +nil+ if the receiver does not respond to the method:
         | 
| 64 | 
            +
              #
         | 
| 65 | 
            +
              #   @person.try(:non_existing_method) # => nil
         | 
| 66 | 
            +
              #
         | 
| 67 | 
            +
              # instead of
         | 
| 68 | 
            +
              #
         | 
| 69 | 
            +
              #   @person.non_existing_method if @person.respond_to?(:non_existing_method) # => nil
         | 
| 70 | 
            +
              #
         | 
| 71 | 
            +
              # +try+ returns +nil+ when called on +nil+ regardless of whether it responds
         | 
| 72 | 
            +
              # to the method:
         | 
| 73 | 
            +
              #
         | 
| 74 | 
            +
              #   nil.try(:to_i) # => nil, rather than 0
         | 
| 75 | 
            +
              #
         | 
| 76 | 
            +
              # Arguments and blocks are forwarded to the method if invoked:
         | 
| 77 | 
            +
              #
         | 
| 78 | 
            +
              #   @posts.try(:each_slice, 2) do |a, b|
         | 
| 79 | 
            +
              #     ...
         | 
| 80 | 
            +
              #   end
         | 
| 81 | 
            +
              #
         | 
| 82 | 
            +
              # The number of arguments in the signature must match. If the object responds
         | 
| 83 | 
            +
              # to the method the call is attempted and +ArgumentError+ is still raised
         | 
| 84 | 
            +
              # in case of argument mismatch.
         | 
| 85 | 
            +
              #
         | 
| 86 | 
            +
              # If +try+ is called without arguments it yields the receiver to a given
         | 
| 87 | 
            +
              # block unless it is +nil+:
         | 
| 88 | 
            +
              #
         | 
| 89 | 
            +
              #   @person.try do |p|
         | 
| 90 | 
            +
              #     ...
         | 
| 91 | 
            +
              #   end
         | 
| 92 | 
            +
              #
         | 
| 93 | 
            +
              # You can also call try with a block without accepting an argument, and the block
         | 
| 94 | 
            +
              # will be instance_eval'ed instead:
         | 
| 95 | 
            +
              #
         | 
| 96 | 
            +
              #   @person.try { upcase.truncate(50) }
         | 
| 97 | 
            +
              #
         | 
| 98 | 
            +
              # Please also note that +try+ is defined on +Object+. Therefore, it won't work
         | 
| 99 | 
            +
              # with instances of classes that do not have +Object+ among their ancestors,
         | 
| 100 | 
            +
              # like direct subclasses of +BasicObject+.
         | 
| 101 | 
            +
             | 
| 102 | 
            +
              ##
         | 
| 103 | 
            +
              # :method: try!
         | 
| 104 | 
            +
              #
         | 
| 105 | 
            +
              # :call-seq:
         | 
| 106 | 
            +
              #   try!(*a, &b)
         | 
| 107 | 
            +
              #
         | 
| 108 | 
            +
              # Same as #try, but raises a +NoMethodError+ exception if the receiver is
         | 
| 109 | 
            +
              # not +nil+ and does not implement the tried method.
         | 
| 110 | 
            +
              #
         | 
| 111 | 
            +
              #   "a".try!(:upcase) # => "A"
         | 
| 112 | 
            +
              #   nil.try!(:upcase) # => nil
         | 
| 113 | 
            +
              #   123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Integer
         | 
| 114 | 
            +
            end
         | 
| @@ -0,0 +1,150 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'macho'
         | 
| 4 | 
            +
            require 'app_info/util'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module AppInfo
         | 
| 7 | 
            +
              # DSYM parser
         | 
| 8 | 
            +
              class DSYM
         | 
| 9 | 
            +
                attr_reader :file
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def initialize(file)
         | 
| 12 | 
            +
                  @file = file
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def file_type
         | 
| 16 | 
            +
                  AppInfo::Platform::DSYM
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def object
         | 
| 20 | 
            +
                  @object ||= File.basename(app_path)
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def macho_type
         | 
| 24 | 
            +
                  @macho_type ||= ::MachO.open(app_path)
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def machos
         | 
| 28 | 
            +
                  @machos ||= case macho_type
         | 
| 29 | 
            +
                              when ::MachO::MachOFile
         | 
| 30 | 
            +
                                [MachO.new(macho_type, File.size(app_path))]
         | 
| 31 | 
            +
                              else
         | 
| 32 | 
            +
                                size = macho_type.fat_archs.each_with_object([]) do |arch, obj|
         | 
| 33 | 
            +
                                  obj << arch.size
         | 
| 34 | 
            +
                                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                                machos = []
         | 
| 37 | 
            +
                                macho_type.machos.each_with_index do |file, i|
         | 
| 38 | 
            +
                                  machos << MachO.new(file, size[i])
         | 
| 39 | 
            +
                                end
         | 
| 40 | 
            +
                                machos
         | 
| 41 | 
            +
                              end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                def release_version
         | 
| 45 | 
            +
                  info.try(:[], 'CFBundleShortVersionString')
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                def build_version
         | 
| 49 | 
            +
                  info.try(:[], 'CFBundleVersion')
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def identifier
         | 
| 53 | 
            +
                  info.try(:[], 'CFBundleIdentifier').sub('com.apple.xcode.dsym.', '')
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
                alias bundle_id identifier
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def info
         | 
| 58 | 
            +
                  return nil unless File.exist?(info_path)
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  @info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: info_path).value)
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                def info_path
         | 
| 64 | 
            +
                  @info_path ||= File.join(contents, 'Contents', 'Info.plist')
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def app_path
         | 
| 68 | 
            +
                  unless @app_path
         | 
| 69 | 
            +
                    path = File.join(contents, 'Contents', 'Resources', 'DWARF')
         | 
| 70 | 
            +
                    name = Dir.entries(path).reject { |f| ['.', '..'].include?(f) }.first
         | 
| 71 | 
            +
                    @app_path = File.join(path, name)
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  @app_path
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                private
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                def contents
         | 
| 80 | 
            +
                  unless @contents
         | 
| 81 | 
            +
                    if File.directory?(@file)
         | 
| 82 | 
            +
                      @contents = @file
         | 
| 83 | 
            +
                    else
         | 
| 84 | 
            +
                      dsym_dir = nil
         | 
| 85 | 
            +
                      @contents = Util.unarchive(@file, path: 'dsym') do |path, zip_file|
         | 
| 86 | 
            +
                        zip_file.each do |f|
         | 
| 87 | 
            +
                          unless dsym_dir
         | 
| 88 | 
            +
                            dsym_dir = f.name
         | 
| 89 | 
            +
                            dsym_dir = dsym_dir.split('/')[0] # fix filename is xxx.app.dSYM/Contents
         | 
| 90 | 
            +
                          end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                          f_path = File.join(path, f.name)
         | 
| 93 | 
            +
                          zip_file.extract(f, f_path) unless File.exist?(f_path)
         | 
| 94 | 
            +
                        end
         | 
| 95 | 
            +
                      end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                      @contents = File.join(@contents, dsym_dir)
         | 
| 98 | 
            +
                    end
         | 
| 99 | 
            +
                  end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  @contents
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                # DSYM Mach-O
         | 
| 105 | 
            +
                class MachO
         | 
| 106 | 
            +
                  def initialize(file, size = 0)
         | 
| 107 | 
            +
                    @file = file
         | 
| 108 | 
            +
                    @size = size
         | 
| 109 | 
            +
                  end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                  def cpu_name
         | 
| 112 | 
            +
                    @file.cpusubtype
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                  def cpu_type
         | 
| 116 | 
            +
                    @file.cputype
         | 
| 117 | 
            +
                  end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                  def type
         | 
| 120 | 
            +
                    @file.filetype
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  def size(humanable = false)
         | 
| 124 | 
            +
                    return Util.size_to_humanable(@size) if humanable
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                    @size
         | 
| 127 | 
            +
                  end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                  def uuid
         | 
| 130 | 
            +
                    @file[:LC_UUID][0].uuid_string
         | 
| 131 | 
            +
                  end
         | 
| 132 | 
            +
                  alias debug_id uuid
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                  def header
         | 
| 135 | 
            +
                    @header ||= @file.header
         | 
| 136 | 
            +
                  end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                  def to_h
         | 
| 139 | 
            +
                    {
         | 
| 140 | 
            +
                      uuid: uuid,
         | 
| 141 | 
            +
                      type: type,
         | 
| 142 | 
            +
                      cpu_name: cpu_name,
         | 
| 143 | 
            +
                      cpu_type: cpu_type,
         | 
| 144 | 
            +
                      size: size,
         | 
| 145 | 
            +
                      humanable_size: size(true)
         | 
| 146 | 
            +
                    }
         | 
| 147 | 
            +
                  end
         | 
| 148 | 
            +
                end
         | 
| 149 | 
            +
              end
         | 
| 150 | 
            +
            end
         |