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.
@@ -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