app-info 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path('lib', __dir__)
4
+ require 'app_info'
5
+ require 'bundler/gem_tasks'
6
+ require 'rspec/core/rake_task'
7
+
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ task default: :spec
@@ -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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.dirname(__FILE__) + '/app_info'
@@ -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
@@ -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