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,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'macho'
4
+ require 'pngdefry'
5
+ require 'fileutils'
6
+ require 'forwardable'
7
+ require 'cfpropertylist'
8
+ require 'app_info/util'
9
+
10
+ module AppInfo
11
+ # IPA parser
12
+ class IPA
13
+ extend Forwardable
14
+
15
+ attr_reader :file
16
+
17
+ # iOS Export types
18
+ module ExportType
19
+ DEBUG = 'Debug'
20
+ ADHOC = 'AdHoc'
21
+ INHOUSE = 'inHouse'
22
+ RELEASE = 'Release'
23
+ UNKOWN = nil
24
+ end
25
+
26
+ def initialize(file)
27
+ @file = 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::IOS
36
+ end
37
+ alias file_type os
38
+
39
+ def_delegators :info, :iphone?, :ipad?, :universal?, :build_version, :name,
40
+ :release_version, :identifier, :bundle_id, :display_name,
41
+ :bundle_name, :icons, :min_sdk_version, :device_type
42
+
43
+ def_delegators :mobileprovision, :devices, :team_name, :team_identifier,
44
+ :profile_name, :expired_date
45
+
46
+ def distribution_name
47
+ "#{profile_name} - #{team_name}" if profile_name && team_name
48
+ end
49
+
50
+ def release_type
51
+ if stored?
52
+ ExportType::RELEASE
53
+ else
54
+ build_type
55
+ end
56
+ end
57
+
58
+ def build_type
59
+ if mobileprovision?
60
+ if devices
61
+ ExportType::ADHOC
62
+ else
63
+ ExportType::INHOUSE
64
+ end
65
+ else
66
+ ExportType::DEBUG
67
+ end
68
+ end
69
+
70
+ def archs
71
+ return unless File.exist?(bundle_path)
72
+
73
+ file = MachO.open(bundle_path)
74
+ case file
75
+ when MachO::MachOFile
76
+ [file.cpusubtype]
77
+ else
78
+ file.machos.each_with_object([]) do |arch, obj|
79
+ obj << arch.cpusubtype
80
+ end
81
+ end
82
+ end
83
+ alias architectures archs
84
+
85
+ def stored?
86
+ metadata? ? true : false
87
+ end
88
+
89
+ def hide_developer_certificates
90
+ mobileprovision.delete('DeveloperCertificates') if mobileprovision?
91
+ end
92
+
93
+ def mobileprovision
94
+ return unless mobileprovision?
95
+ return @mobileprovision if @mobileprovision
96
+
97
+ @mobileprovision = MobileProvision.new(mobileprovision_path)
98
+ end
99
+
100
+ def mobileprovision?
101
+ File.exist?mobileprovision_path
102
+ end
103
+
104
+ def mobileprovision_path
105
+ filename = 'embedded.mobileprovision'
106
+ @mobileprovision_path ||= File.join(@file, filename)
107
+ unless File.exist?(@mobileprovision_path)
108
+ @mobileprovision_path = File.join(app_path, filename)
109
+ end
110
+
111
+ @mobileprovision_path
112
+ end
113
+
114
+ def metadata
115
+ return unless metadata?
116
+
117
+ @metadata ||= CFPropertyList.native_types(CFPropertyList::List.new(file: metadata_path).value)
118
+ end
119
+
120
+ def metadata?
121
+ File.exist?(metadata_path)
122
+ end
123
+
124
+ def metadata_path
125
+ @metadata_path ||= File.join(contents, 'iTunesMetadata.plist')
126
+ end
127
+
128
+ def bundle_path
129
+ @bundle_path ||= File.join(app_path, info.bundle_name)
130
+ end
131
+
132
+ def info
133
+ @info ||= InfoPlist.new(app_path)
134
+ end
135
+
136
+ def app_path
137
+ @app_path ||= Dir.glob(File.join(contents, 'Payload', '*.app')).first
138
+ end
139
+
140
+ def cleanup!
141
+ return unless @contents
142
+
143
+ FileUtils.rm_rf(@contents)
144
+
145
+ @contents = nil
146
+ @icons = nil
147
+ @app_path = nil
148
+ @metadata = nil
149
+ @metadata_path = nil
150
+ @info = nil
151
+ end
152
+
153
+ private
154
+
155
+ def contents
156
+ @contents ||= Util.unarchive(@file, path: 'ios')
157
+ end
158
+
159
+ def icons_root_path
160
+ iphone = 'CFBundleIcons'
161
+ ipad = 'CFBundleIcons~ipad'
162
+
163
+ case device_type
164
+ when 'iPhone'
165
+ [iphone]
166
+ when 'iPad'
167
+ [ipad]
168
+ when 'Universal'
169
+ [iphone, ipad]
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cfpropertylist'
4
+ require 'app_info/util'
5
+
6
+ module AppInfo
7
+ # iOS Info.plist parser
8
+ class InfoPlist
9
+ def initialize(app_path)
10
+ @app_path = app_path
11
+ end
12
+
13
+ def build_version
14
+ info.try(:[], 'CFBundleVersion')
15
+ end
16
+
17
+ def release_version
18
+ info.try(:[], 'CFBundleShortVersionString')
19
+ end
20
+
21
+ def identifier
22
+ info.try(:[], 'CFBundleIdentifier')
23
+ end
24
+ alias bundle_id identifier
25
+
26
+ def name
27
+ display_name || bundle_name
28
+ end
29
+
30
+ def display_name
31
+ info.try(:[], 'CFBundleDisplayName')
32
+ end
33
+
34
+ def bundle_name
35
+ info.try(:[], 'CFBundleName')
36
+ end
37
+
38
+ #
39
+ # Extract the Minimum OS Version from the Info.plist
40
+ #
41
+ def min_sdk_version
42
+ info.try(:[], 'MinimumOSVersion')
43
+ end
44
+
45
+ def icons
46
+ return @icons if @icons
47
+
48
+ @icons = []
49
+ icons_root_path.each do |name|
50
+ icon_array = info.try(:[], name)
51
+ .try(:[], 'CFBundlePrimaryIcon')
52
+ .try(:[], 'CFBundleIconFiles')
53
+
54
+ next if icon_array.nil? || icon_array.empty?
55
+
56
+ icon_array.each do |items|
57
+ Dir.glob(File.join(@app_path, "#{items}*")).find_all.each do |file|
58
+ dict = {
59
+ name: File.basename(file),
60
+ file: file,
61
+ dimensions: Pngdefry.dimensions(file)
62
+ }
63
+
64
+ @icons.push(dict)
65
+ end
66
+ end
67
+ end
68
+
69
+ @icons
70
+ end
71
+
72
+ def device_type
73
+ device_family = info.try(:[], 'UIDeviceFamily')
74
+ if device_family.length == 1
75
+ case device_family
76
+ when [1]
77
+ 'iPhone'
78
+ when [2]
79
+ 'iPad'
80
+ end
81
+ elsif device_family.length == 2 && device_family == [1, 2]
82
+ 'Universal'
83
+ end
84
+ end
85
+
86
+ def iphone?
87
+ device_type == 'iPhone'
88
+ end
89
+
90
+ def ipad?
91
+ device_type == 'iPad'
92
+ end
93
+
94
+ def universal?
95
+ device_type == 'Universal'
96
+ end
97
+
98
+ def release_type
99
+ if stored?
100
+ 'Store'
101
+ else
102
+ build_type
103
+ end
104
+ end
105
+
106
+ def [](key)
107
+ info.try(:[], key.to_s)
108
+ end
109
+
110
+ def method_missing(method_name, *args, &block)
111
+ info.try(:[], Util.format_key(method_name)) ||
112
+ info.send(method_name) ||
113
+ super
114
+ end
115
+
116
+ def respond_to_missing?(method_name, *args)
117
+ info.key?(Util.format_key(method_name)) ||
118
+ info.respond_to?(method_name) ||
119
+ super
120
+ end
121
+
122
+ private
123
+
124
+ def info
125
+ @info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: info_path).value)
126
+ end
127
+
128
+ def info_path
129
+ File.join(@app_path, 'Info.plist')
130
+ end
131
+
132
+ IPHONE_KEY = 'CFBundleIcons'
133
+ IPAD_KEY = 'CFBundleIcons~ipad'
134
+
135
+ def icons_root_path
136
+ case device_type
137
+ when 'iPhone'
138
+ [IPHONE_KEY]
139
+ when 'iPad'
140
+ [IPAD_KEY]
141
+ when 'Universal'
142
+ [IPHONE_KEY, IPAD_KEY]
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'cfpropertylist'
5
+ require 'app_info/util'
6
+
7
+ module AppInfo
8
+ # .mobileprovision file parser
9
+ class MobileProvision
10
+ def initialize(path)
11
+ @path = path
12
+ end
13
+
14
+ def name
15
+ mobileprovision.try(:[], 'Name')
16
+ end
17
+
18
+ def app_name
19
+ mobileprovision.try(:[], 'AppIDName')
20
+ end
21
+
22
+ def type
23
+ return :development if development?
24
+ return :adhoc if adhoc?
25
+ return :appstore if appstore?
26
+ return :inhouse if inhouse?
27
+ end
28
+
29
+ def platforms
30
+ return unless platforms = mobileprovision.try(:[], 'Platform')
31
+
32
+ platforms.map { |v|
33
+ v = 'macOS' if v == 'OSX'
34
+ v.downcase.to_sym
35
+ }
36
+ end
37
+
38
+ def platform
39
+ platforms[0]
40
+ end
41
+
42
+ def devices
43
+ mobileprovision.try(:[], 'ProvisionedDevices')
44
+ end
45
+
46
+ def team_identifier
47
+ mobileprovision.try(:[], 'TeamIdentifier')
48
+ end
49
+
50
+ def team_name
51
+ mobileprovision.try(:[], 'TeamName')
52
+ end
53
+
54
+ def profile_name
55
+ mobileprovision.try(:[], 'Name')
56
+ end
57
+
58
+ def created_date
59
+ mobileprovision.try(:[], 'CreationDate')
60
+ end
61
+
62
+ def expired_date
63
+ mobileprovision.try(:[], 'ExpirationDate')
64
+ end
65
+
66
+ def entitlements
67
+ mobileprovision.try(:[], 'Entitlements')
68
+ end
69
+
70
+ def developer_certs
71
+ certs = mobileprovision.try(:[], 'DeveloperCertificates')
72
+ return if certs.empty?
73
+
74
+ certs.each_with_object([]) do |cert, obj|
75
+ obj << DeveloperCertificate.new(cert)
76
+ end
77
+ end
78
+
79
+ # Detect is development type of mobileprovision
80
+ #
81
+ # related link: https://stackoverflow.com/questions/1003066/what-does-get-task-allow-do-in-xcode
82
+ def development?
83
+ case platform.downcase.to_sym
84
+ when :ios
85
+ entitlements['get-task-allow'] == true
86
+ when :macos
87
+ !devices.nil?
88
+ else
89
+ raise Error, "Not implement with platform: #{platform}"
90
+ end
91
+ end
92
+
93
+ # Detect app store type
94
+ #
95
+ # related link: https://developer.apple.com/library/archive/qa/qa1830/_index.html
96
+ def appstore?
97
+ case platform.downcase.to_sym
98
+ when :ios
99
+ !development? && entitlements.key?('beta-reports-active')
100
+ when :macos
101
+ !development?
102
+ else
103
+ raise Error, "Not implement with platform: #{platform}"
104
+ end
105
+ end
106
+
107
+ def adhoc?
108
+ return false if platform == :macos # macOS no need adhoc
109
+
110
+ !development? && !devices.nil?
111
+ end
112
+
113
+ def inhouse?
114
+ return false if platform == :macos # macOS no need adhoc
115
+
116
+ !development? && !adhoc? && !appstore?
117
+ end
118
+
119
+ # Enabled Capabilites
120
+ #
121
+ # Related link: https://developer.apple.com/support/app-capabilities/
122
+ def enabled_capabilities
123
+ capabilities = []
124
+ if adhoc? || appstore?
125
+ capabilities << 'In-App Purchase' << 'GameKit'
126
+ end
127
+
128
+ entitlements.each do |key, value|
129
+ case key
130
+ when 'aps-environment'
131
+ capabilities << 'Push Notifications'
132
+ when 'com.apple.developer.applesignin'
133
+ capabilities << 'Sign In with Apple'
134
+ when 'com.apple.developer.siri'
135
+ capabilities << 'SiriKit'
136
+ when 'com.apple.security.application-groups'
137
+ capabilities << 'App Groups'
138
+ when 'com.apple.developer.associated-domains'
139
+ capabilities << 'Associated Domains'
140
+ when 'com.apple.developer.default-data-protection'
141
+ capabilities << 'Data Protection'
142
+ when 'com.apple.developer.networking.networkextension'
143
+ capabilities << 'Network Extensions'
144
+ when 'com.apple.developer.networking.vpn.api'
145
+ capabilities << 'Personal VPN'
146
+ when 'com.apple.developer.healthkit', 'com.apple.developer.healthkit.access'
147
+ capabilities << 'HealthKit' unless capabilities.include?('HealthKit')
148
+ when 'com.apple.developer.icloud-services', 'com.apple.developer.icloud-container-identifiers'
149
+ capabilities << 'iCloud' unless capabilities.include?('iCloud')
150
+ when 'com.apple.developer.in-app-payments'
151
+ capabilities << 'Apple Pay'
152
+ when 'com.apple.developer.homekit'
153
+ capabilities << 'HomeKit'
154
+ when 'com.apple.developer.user-fonts'
155
+ capabilities << 'Fonts'
156
+ when 'com.apple.developer.pass-type-identifiers'
157
+ capabilities << 'Wallet'
158
+ when 'inter-app-audio'
159
+ capabilities << 'Inter-App Audio'
160
+ when 'com.apple.developer.networking.multipath'
161
+ capabilities << 'Multipath'
162
+ when 'com.apple.developer.authentication-services.autofill-credential-provider'
163
+ capabilities << 'AutoFill Credential Provider'
164
+ when 'com.apple.developer.networking.wifi-info'
165
+ capabilities << 'Access WiFi Information'
166
+ when 'com.apple.external-accessory.wireless-configuration'
167
+ capabilities << 'Wireless Accessory Configuration'
168
+ when 'com.apple.developer.kernel.extended-virtual-addressing'
169
+ capabilities << 'Extended Virtual Address Space'
170
+ when 'com.apple.developer.nfc.readersession.formats'
171
+ capabilities << 'NFC Tag Reading'
172
+ when 'com.apple.developer.ClassKit-environment'
173
+ capabilities << 'ClassKit'
174
+ when 'com.apple.developer.networking.HotspotConfiguration'
175
+ capabilities << 'Hotspot'
176
+ when 'com.apple.developer.devicecheck.appattest-environment'
177
+ capabilities << 'App Attest'
178
+ when 'com.apple.developer.coremedia.hls.low-latency'
179
+ capabilities << 'Low Latency HLS'
180
+ when 'com.apple.developer.associated-domains.mdm-managed'
181
+ capabilities << 'MDM Managed Associated Domains'
182
+ # macOS Only
183
+ when 'com.apple.developer.maps'
184
+ capabilities << 'Maps'
185
+ when 'com.apple.developer.system-extension.install'
186
+ capabilities << 'System Extension'
187
+ when 'com.apple.developer.networking.custom-protocol'
188
+ capabilities << 'Custom Network Protocol'
189
+ end
190
+ end
191
+
192
+ capabilities
193
+ end
194
+
195
+ def [](key)
196
+ mobileprovision.try(:[], key.to_s)
197
+ end
198
+
199
+ def empty?
200
+ mobileprovision.nil?
201
+ end
202
+
203
+ def mobileprovision
204
+ return @mobileprovision = nil unless File.exist?(@path)
205
+
206
+ data = File.read(@path)
207
+ data = strip_plist_wrapper(data) unless bplist?(data)
208
+ list = CFPropertyList::List.new(data: data).value
209
+ @mobileprovision = CFPropertyList.native_types(list)
210
+ rescue CFFormatError
211
+ @mobileprovision = nil
212
+ end
213
+
214
+ def method_missing(method_name, *args, &block)
215
+ mobileprovision.try(:[], Util.format_key(method_name)) ||
216
+ mobileprovision.send(method_name) ||
217
+ super
218
+ end
219
+
220
+ def respond_to_missing?(method_name, *args)
221
+ mobileprovision.key?(Util.format_key(method_name)) ||
222
+ mobileprovision.respond_to?(method_name) ||
223
+ super
224
+ end
225
+
226
+ private
227
+
228
+ def bplist?(raw)
229
+ raw[0..5] == 'bplist'
230
+ end
231
+
232
+ def strip_plist_wrapper(raw)
233
+ end_tag = '</plist>'
234
+ start_point = raw.index('<?xml version=')
235
+ end_point = raw.index(end_tag) + end_tag.size - 1
236
+ raw[start_point..end_point]
237
+ end
238
+
239
+ # Developer Certificate
240
+ class DeveloperCertificate
241
+ attr_reader :raw
242
+
243
+ def initialize(data)
244
+ @raw = OpenSSL::X509::Certificate.new(data)
245
+ end
246
+
247
+ def name
248
+ @raw.subject.to_a.find { |name, _, _| name == 'CN' }[1]
249
+ end
250
+
251
+ def created_date
252
+ @raw.not_after
253
+ end
254
+
255
+ def expired_date
256
+ @raw.not_before
257
+ end
258
+ end
259
+ end
260
+ end