cocoapods-entitlements-statistics 0.0.1
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 +3 -0
- data/Gemfile +18 -0
- data/LICENSE.txt +22 -0
- data/README.md +134 -0
- data/Rakefile +13 -0
- data/cocoapods-entitlements-statistics.gemspec +27 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/analyze.rb +218 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/core_ext/inflector.rb +35 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/core_ext/try.rb +112 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/entitlements.rb +185 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/extracter.rb +112 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/helper.rb +90 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/info_plist.rb +202 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/mobile_provision.rb +317 -0
- data/lib/cocoapods-entitlements-statistics/gem_version.rb +3 -0
- data/lib/cocoapods-entitlements-statistics/project_info.rb +82 -0
- data/lib/cocoapods-entitlements-statistics.rb +1 -0
- data/lib/cocoapods_plugin.rb +34 -0
- data/spec/command/statistics_spec.rb +12 -0
- data/spec/spec_helper.rb +50 -0
- metadata +161 -0
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'cfpropertylist'
|
5
|
+
|
6
|
+
module AppEntitlementsStatistics
|
7
|
+
|
8
|
+
# iOS app.entitlements parser
|
9
|
+
class EntitlementsPlist
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
def initialize(file)
|
13
|
+
@file = file
|
14
|
+
end
|
15
|
+
|
16
|
+
#
|
17
|
+
# Extract the capabilities
|
18
|
+
# https://developer.apple.com/help/account/reference/supported-capabilities-ios
|
19
|
+
# https://developer.apple.com/documentation/bundleresources/entitlements
|
20
|
+
# https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/AboutEntitlements.html
|
21
|
+
# a few entitlements are inherited from the iOS provisioning profile used to run the app.
|
22
|
+
#
|
23
|
+
def enabled_capabilities
|
24
|
+
capabilities = Hash.new
|
25
|
+
info.each do |key, value|
|
26
|
+
case key
|
27
|
+
when 'com.apple.developer.game-center'
|
28
|
+
capabilities['Game Center'] = {
|
29
|
+
key => value
|
30
|
+
}
|
31
|
+
when 'keychain-access-groups'
|
32
|
+
capabilities['Keychain sharing'] = {
|
33
|
+
key => value
|
34
|
+
}
|
35
|
+
# when 'aps-environment'
|
36
|
+
# capabilities['Push Notifications'] = {
|
37
|
+
# key => value
|
38
|
+
# }
|
39
|
+
when 'com.apple.developer.applesignin'
|
40
|
+
capabilities['Sign In with Apple'] = {
|
41
|
+
key => value
|
42
|
+
}
|
43
|
+
when 'com.apple.developer.siri'
|
44
|
+
capabilities['SiriKit'] = {
|
45
|
+
key => value
|
46
|
+
}
|
47
|
+
when 'com.apple.security.application-groups'
|
48
|
+
capabilities['App Groups'] = {
|
49
|
+
key => value
|
50
|
+
}
|
51
|
+
when 'com.apple.developer.associated-domains'
|
52
|
+
capabilities['Associated Domains'] = {
|
53
|
+
key => value
|
54
|
+
}
|
55
|
+
when 'com.apple.developer.default-data-protection'
|
56
|
+
capabilities['Data Protection'] = {
|
57
|
+
key => value
|
58
|
+
}
|
59
|
+
when 'com.apple.developer.networking.networkextension'
|
60
|
+
capabilities ['Network Extensions'] = {
|
61
|
+
key => value
|
62
|
+
}
|
63
|
+
when 'com.apple.developer.networking.vpn.api'
|
64
|
+
capabilities ['Personal VPN'] = {
|
65
|
+
key => value
|
66
|
+
}
|
67
|
+
when 'com.apple.developer.healthkit',
|
68
|
+
'com.apple.developer.healthkit.access'
|
69
|
+
capabilities['HealthKit'] = {
|
70
|
+
'com.apple.developer.healthkit' => info['com.apple.developer.healthkit'],
|
71
|
+
'com.apple.developer.healthkit.access' => info['com.apple.developer.healthkit.access'],
|
72
|
+
} unless capabilities.include?('HealthKit')
|
73
|
+
when 'com.apple.developer.icloud-services',
|
74
|
+
'com.apple.developer.icloud-container-identifiers'
|
75
|
+
capabilities['iCloud'] = {
|
76
|
+
'com.apple.developer.icloud-services' => info['com.apple.developer.icloud-services'],
|
77
|
+
'com.apple.developer.icloud-container-identifiers' => info['com.apple.developer.icloud-container-identifiers'],
|
78
|
+
} unless capabilities.include?('iCloud')
|
79
|
+
when 'com.apple.developer.in-app-payments'
|
80
|
+
capabilities['Apple Pay'] = {
|
81
|
+
key => value
|
82
|
+
}
|
83
|
+
when 'com.apple.developer.homekit'
|
84
|
+
capabilities['HomeKit'] = {
|
85
|
+
key => value
|
86
|
+
}
|
87
|
+
when 'com.apple.developer.user-fonts'
|
88
|
+
capabilities['Fonts'] = {
|
89
|
+
key => value
|
90
|
+
}
|
91
|
+
when 'com.apple.developer.pass-type-identifiers'
|
92
|
+
capabilities['Wallet'] = {
|
93
|
+
key => value
|
94
|
+
}
|
95
|
+
when 'inter-app-audio'
|
96
|
+
capabilities['Inter-App Audio'] = {
|
97
|
+
key => value
|
98
|
+
}
|
99
|
+
when 'com.apple.developer.networking.multipath'
|
100
|
+
capabilities['Multipath'] = {
|
101
|
+
key => value
|
102
|
+
}
|
103
|
+
when 'com.apple.developer.authentication-services.autofill-credential-provider'
|
104
|
+
capabilities['AutoFill Credential Provider'] = {
|
105
|
+
key => value
|
106
|
+
}
|
107
|
+
when 'com.apple.developer.networking.wifi-info'
|
108
|
+
capabilities['Access WiFi Information'] = {
|
109
|
+
key => value
|
110
|
+
}
|
111
|
+
when 'com.apple.external-accessory.wireless-configuration'
|
112
|
+
capabilities['Wireless Accessory Configuration'] = {
|
113
|
+
key => value
|
114
|
+
}
|
115
|
+
when 'com.apple.developer.kernel.extended-virtual-addressing'
|
116
|
+
capabilities['Extended Virtual Address Space'] = {
|
117
|
+
key => value
|
118
|
+
}
|
119
|
+
when 'com.apple.developer.nfc.readersession.formats'
|
120
|
+
capabilities['NFC Tag Reading'] = {
|
121
|
+
key => value
|
122
|
+
}
|
123
|
+
when 'com.apple.developer.ClassKit-environment'
|
124
|
+
capabilities['ClassKit'] = {
|
125
|
+
key => value
|
126
|
+
}
|
127
|
+
when 'com.apple.developer.networking.HotspotConfiguration'
|
128
|
+
capabilities['Hotspot'] = {
|
129
|
+
key => value
|
130
|
+
}
|
131
|
+
when 'com.apple.developer.devicecheck.appattest-environment'
|
132
|
+
capabilities['App Attest'] = {
|
133
|
+
key => value
|
134
|
+
}
|
135
|
+
when 'com.apple.developer.coremedia.hls.low-latency'
|
136
|
+
capabilities['Low Latency HLS'] = {
|
137
|
+
key => value
|
138
|
+
}
|
139
|
+
when 'com.apple.developer.associated-domains.mdm-managed'
|
140
|
+
capabilities['MDM Managed Associated Domains'] = {
|
141
|
+
key => value
|
142
|
+
}
|
143
|
+
end
|
144
|
+
end
|
145
|
+
capabilities
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
def game_center
|
150
|
+
info.try(:[], 'com.apple.developer.game-center').nil?
|
151
|
+
end
|
152
|
+
|
153
|
+
def keychain_access_groups
|
154
|
+
info.try(:[], 'keychain-access-groups').nil?
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
def [](key)
|
159
|
+
info.try(:[], key.to_s)
|
160
|
+
end
|
161
|
+
|
162
|
+
def_delegators :info, :to_h
|
163
|
+
|
164
|
+
def method_missing(method_name, *args, &block)
|
165
|
+
info.try(:[], method_name.to_s.ai_camelcase) ||
|
166
|
+
info.send(method_name) ||
|
167
|
+
super
|
168
|
+
end
|
169
|
+
|
170
|
+
def respond_to_missing?(method_name, *args)
|
171
|
+
info.key?(method_name.to_s.ai_camelcase) ||
|
172
|
+
info.respond_to?(method_name) ||
|
173
|
+
super
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
def info
|
179
|
+
return unless File.file?(@file)
|
180
|
+
|
181
|
+
@info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: @file).value)
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "mobile_provision"
|
4
|
+
require_relative "info_plist"
|
5
|
+
require_relative "entitlements"
|
6
|
+
require_relative "helper"
|
7
|
+
|
8
|
+
require 'fileutils'
|
9
|
+
require 'forwardable'
|
10
|
+
|
11
|
+
module AppEntitlementsStatistics
|
12
|
+
|
13
|
+
class Extracter
|
14
|
+
include Helper::Sigflat
|
15
|
+
include Helper::Utils
|
16
|
+
extend Forwardable
|
17
|
+
|
18
|
+
attr_reader :system_capabilities
|
19
|
+
attr_reader :info_plist_path
|
20
|
+
attr_reader :entitlements_path
|
21
|
+
attr_reader :identifier
|
22
|
+
attr_reader :cur_version
|
23
|
+
|
24
|
+
def initialize(args)
|
25
|
+
args.each do |key ,value|
|
26
|
+
case key
|
27
|
+
when "cur_version"
|
28
|
+
@cur_version = value
|
29
|
+
when "product_bundle_identifier"
|
30
|
+
@identifier = value
|
31
|
+
when "info_plist_path"
|
32
|
+
@info_plist_path = value
|
33
|
+
when "entitlements_path"
|
34
|
+
@entitlements_path = value
|
35
|
+
when "system_capabilities"
|
36
|
+
@system_capabilities = value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def extract_update
|
42
|
+
capabilities_summary = Hash.new
|
43
|
+
capabilities = extract_capabilities_info
|
44
|
+
capabilities.each do |key,value|
|
45
|
+
capabilities_summary[key] = createsig(value)
|
46
|
+
end
|
47
|
+
|
48
|
+
usage_desc_summary = Hash.new
|
49
|
+
usage_desc = plistInfo.permis_usagedescription
|
50
|
+
usage_desc.each do |key,value|
|
51
|
+
usage_desc_summary[key] = createsig(value)
|
52
|
+
end
|
53
|
+
|
54
|
+
yaml_content = {
|
55
|
+
'Capabilities_Summary' => capabilities_summary,
|
56
|
+
'Capabilities' => capabilities,
|
57
|
+
'PermissionsUsageDescription_Summary' => usage_desc_summary,
|
58
|
+
'PermissionsUsageDescription' => usage_desc
|
59
|
+
}
|
60
|
+
yaml_name = entitlements_yaml_name(@cur_version, @identifier , path: @store_path)
|
61
|
+
|
62
|
+
File.open(yaml_name, "w") { |file| file.write(yaml_content.to_yaml) }
|
63
|
+
update_versions
|
64
|
+
yaml_content
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
def update_versions
|
69
|
+
yaml_name = versions_yaml_name(@identifier , path: @store_path)
|
70
|
+
yaml_content = [ ]
|
71
|
+
if File.exist?(yaml_name)
|
72
|
+
yaml_content = YAML.load_file(yaml_name)
|
73
|
+
end
|
74
|
+
version = @cur_version
|
75
|
+
if !yaml_content.include?(version)
|
76
|
+
yaml_content.unshift(version)
|
77
|
+
File.open(yaml_name, "w") { |file| file.write(yaml_content.to_yaml) }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def extract_capabilities_info
|
82
|
+
#{"com.apple.BackgroundModes"=>{"enabled"=>"1"}, "com.apple.InAppPurchase"=>{"enabled"=>"1"}, "com.apple.Push"=>{"enabled"=>"1"}}
|
83
|
+
capabilities_info = {}
|
84
|
+
|
85
|
+
@system_capabilities.each do |key,value|
|
86
|
+
case key
|
87
|
+
when "com.apple.InAppPurchase"
|
88
|
+
capabilities_info["In-App Purchase"] = { key => value }
|
89
|
+
when "com.apple.BackgroundModes"
|
90
|
+
h1 = {key => value}
|
91
|
+
h2 = plistInfo.backgroundModes["Background Modes"]
|
92
|
+
capabilities_info["Background Modes"] = h1.merge(h2)
|
93
|
+
when "com.apple.Push"
|
94
|
+
capabilities_info["Push Notifications"] = { key => value }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
capabilities_info = capabilities_info.merge(entitlementsInfo.enabled_capabilities)
|
98
|
+
capabilities_info = capabilities_info.merge(plistInfo.enabled_capabilities)
|
99
|
+
capabilities_info
|
100
|
+
end
|
101
|
+
|
102
|
+
def plistInfo
|
103
|
+
@plistInfo ||= InfoPlist.new(@info_plist_path)
|
104
|
+
end
|
105
|
+
|
106
|
+
def entitlementsInfo
|
107
|
+
@entitlementsInfo ||= EntitlementsPlist.new(@entitlements_path)
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
require 'crimp'
|
3
|
+
require 'cocoapods'
|
4
|
+
|
5
|
+
|
6
|
+
module AppEntitlementsStatistics
|
7
|
+
|
8
|
+
module Helper
|
9
|
+
|
10
|
+
module Sigflat
|
11
|
+
def createsig(body)
|
12
|
+
return Crimp.signature(body)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module Utils
|
17
|
+
|
18
|
+
def defalut_store_path(app_identifier)
|
19
|
+
root_path = "#{Dir.home}/appInfo-#{app_identifier}"
|
20
|
+
FileUtils.mkdir_p(root_path) unless File.exists?(root_path)
|
21
|
+
root_path
|
22
|
+
end
|
23
|
+
|
24
|
+
def entitlements_yaml_name(v,app_identifier,path: nil)
|
25
|
+
dir_path = path ? "#{path}-#{app_identifier}" : defalut_store_path(app_identifier)
|
26
|
+
yaml_name = "#{dir_path}/entitlements_#{v}.yml";
|
27
|
+
yaml_name
|
28
|
+
end
|
29
|
+
|
30
|
+
def versions_yaml_name(app_identifier,path: nil)
|
31
|
+
dir_path = path ? "#{path}-#{app_identifier}" : defalut_store_path(app_identifier)
|
32
|
+
yaml_name = "#{dir_path}/entitlements_versions.yml";
|
33
|
+
yaml_name
|
34
|
+
end
|
35
|
+
|
36
|
+
def report_file_name(path: nil)
|
37
|
+
path = path ? "#{path}/entitlements_statistics" : "entitlements_statistics"
|
38
|
+
FileUtils.mkdir_p(path) unless File.exists?(path)
|
39
|
+
file_name = "#{path}/analyze_report"
|
40
|
+
file_name
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
module Archive
|
46
|
+
require 'zip'
|
47
|
+
require 'fileutils'
|
48
|
+
require 'securerandom'
|
49
|
+
|
50
|
+
# Unarchive zip file
|
51
|
+
#
|
52
|
+
# source: https://github.com/soffes/lagunitas/blob/master/lib/lagunitas/ipa.rb
|
53
|
+
def unarchive(file, path: nil)
|
54
|
+
path = path ? "#{path}-" : ''
|
55
|
+
root_path = "#{Dir.mktmpdir}/AppInfo-#{path}#{SecureRandom.hex}"
|
56
|
+
# puts root_path
|
57
|
+
Zip::File.open(file) do |zip_file|
|
58
|
+
if block_given?
|
59
|
+
yield root_path, zip_file
|
60
|
+
else
|
61
|
+
zip_file.each do |f|
|
62
|
+
f_path = File.join(root_path, f.name)
|
63
|
+
FileUtils.mkdir_p(File.dirname(f_path))
|
64
|
+
zip_file.extract(f, f_path) unless File.exist?(f_path)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
root_path
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def tempdir(file, prefix:, system: false)
|
74
|
+
dest_path = if system
|
75
|
+
Dir.mktmpdir("appinfo-#{prefix}-#{File.basename(file, '.*')}-", '/tmp')
|
76
|
+
else
|
77
|
+
File.join(File.dirname(file), prefix)
|
78
|
+
end
|
79
|
+
|
80
|
+
dest_file = File.join(dest_path, File.basename(file))
|
81
|
+
FileUtils.mkdir_p(dest_path, mode: 0_700) unless system
|
82
|
+
dest_file
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'cfpropertylist'
|
5
|
+
|
6
|
+
module AppEntitlementsStatistics
|
7
|
+
|
8
|
+
# Apple Device Type
|
9
|
+
module Device
|
10
|
+
MACOS = 'macOS'
|
11
|
+
IPHONE = 'iPhone'
|
12
|
+
IPAD = 'iPad'
|
13
|
+
UNIVERSAL = 'Universal'
|
14
|
+
end
|
15
|
+
|
16
|
+
# iOS Info.plist parser
|
17
|
+
class InfoPlist
|
18
|
+
extend Forwardable
|
19
|
+
|
20
|
+
def initialize(file)
|
21
|
+
@file = file
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Extract the permissions UsageDescription from the Info.plist
|
26
|
+
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html
|
27
|
+
#
|
28
|
+
def permis_usagedescription
|
29
|
+
desc = Hash.new
|
30
|
+
info.each do |key, value|
|
31
|
+
if key.include?("UsageDescription")
|
32
|
+
desc[key] = value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
desc
|
36
|
+
end
|
37
|
+
|
38
|
+
def backgroundModes
|
39
|
+
value = info.try(:[], 'UIBackgroundModes')
|
40
|
+
h = Hash.new
|
41
|
+
if !value.nil?
|
42
|
+
h["Background Modes"] = {
|
43
|
+
"UIBackgroundModes" => value
|
44
|
+
}
|
45
|
+
end
|
46
|
+
h
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# Extract the capabilities from the Info.plist
|
51
|
+
# https://mercury.tencent.com/introduction/show?item=ios
|
52
|
+
#
|
53
|
+
def enabled_capabilities
|
54
|
+
capabilities = Hash.new
|
55
|
+
info.each do |key, value|
|
56
|
+
case key
|
57
|
+
when 'UIRequiredDeviceCapabilities'
|
58
|
+
capabilities['RequiredDeviceCapabilities'] = {
|
59
|
+
key => value
|
60
|
+
}
|
61
|
+
when 'UIFileSharingEnabled'
|
62
|
+
capabilities['FileSharingEnabled' ] = {
|
63
|
+
key => value
|
64
|
+
}
|
65
|
+
when 'CADisableMinimumFrameDurationOnPhone'
|
66
|
+
capabilities['DisableMinimumFrameDurationOnPhone'] = {
|
67
|
+
key => value
|
68
|
+
}
|
69
|
+
when 'GCSupportedGameControllers'
|
70
|
+
capabilities['GameControllers'] = {
|
71
|
+
key => value
|
72
|
+
}
|
73
|
+
when 'GCSupportsControllerUserInteraction'
|
74
|
+
capabilities['SupportsControllerUserInteraction' ] = {
|
75
|
+
key => value
|
76
|
+
}
|
77
|
+
when 'MKDirectionsApplicationSupportedModes'
|
78
|
+
capabilities['Maps'] = {
|
79
|
+
key => value
|
80
|
+
}
|
81
|
+
when 'NSAppTransportSecurity'
|
82
|
+
capabilities['AppTransportSecurity'] = {
|
83
|
+
key => value
|
84
|
+
}
|
85
|
+
when 'CFBundleDocumentTypes'
|
86
|
+
capabilities['BundleDocumentTypes'] = {
|
87
|
+
key => value
|
88
|
+
}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
capabilities
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
def device_type
|
96
|
+
device_family = info.try(:[], 'UIDeviceFamily')
|
97
|
+
if device_family == [1]
|
98
|
+
Device::IPHONE
|
99
|
+
elsif device_family == [2]
|
100
|
+
Device::IPAD
|
101
|
+
elsif device_family == [1, 2]
|
102
|
+
Device::UNIVERSAL
|
103
|
+
elsif !info.try(:[], 'DTSDKName').nil? || !info.try(:[], 'DTPlatformName').nil?
|
104
|
+
Device::MACOS
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def version
|
109
|
+
release_version || build_version
|
110
|
+
end
|
111
|
+
|
112
|
+
def build_version
|
113
|
+
info.try(:[], 'CFBundleVersion')
|
114
|
+
end
|
115
|
+
|
116
|
+
def release_version
|
117
|
+
info.try(:[], 'CFBundleShortVersionString')
|
118
|
+
end
|
119
|
+
|
120
|
+
def identifier
|
121
|
+
info.try(:[], 'CFBundleIdentifier')
|
122
|
+
end
|
123
|
+
alias bundle_id identifier
|
124
|
+
|
125
|
+
def name
|
126
|
+
display_name || bundle_name
|
127
|
+
end
|
128
|
+
|
129
|
+
def display_name
|
130
|
+
info.try(:[], 'CFBundleDisplayName')
|
131
|
+
end
|
132
|
+
|
133
|
+
def bundle_name
|
134
|
+
info.try(:[], 'CFBundleName')
|
135
|
+
end
|
136
|
+
|
137
|
+
def min_os_version
|
138
|
+
min_sdk_version || min_system_version
|
139
|
+
end
|
140
|
+
|
141
|
+
#
|
142
|
+
# Extract the Minimum OS Version from the Info.plist (iOS Only)
|
143
|
+
#
|
144
|
+
def min_sdk_version
|
145
|
+
info.try(:[], 'MinimumOSVersion')
|
146
|
+
end
|
147
|
+
|
148
|
+
#
|
149
|
+
# Extract the Minimum OS Version from the Info.plist (macOS Only)
|
150
|
+
#
|
151
|
+
def min_system_version
|
152
|
+
info.try(:[], 'LSMinimumSystemVersion')
|
153
|
+
end
|
154
|
+
|
155
|
+
def iphone?
|
156
|
+
device_type == Device::IPHONE
|
157
|
+
end
|
158
|
+
|
159
|
+
def ipad?
|
160
|
+
device_type == Device::IPAD
|
161
|
+
end
|
162
|
+
|
163
|
+
def universal?
|
164
|
+
device_type == Device::UNIVERSAL
|
165
|
+
end
|
166
|
+
|
167
|
+
def macos?
|
168
|
+
device_type == Device::MACOS
|
169
|
+
end
|
170
|
+
|
171
|
+
def device_family
|
172
|
+
info.try(:[], 'UIDeviceFamily') || []
|
173
|
+
end
|
174
|
+
|
175
|
+
def [](key)
|
176
|
+
info.try(:[], key.to_s)
|
177
|
+
end
|
178
|
+
|
179
|
+
def_delegators :info, :to_h
|
180
|
+
|
181
|
+
def method_missing(method_name, *args, &block)
|
182
|
+
info.try(:[], method_name.to_s.ai_camelcase) ||
|
183
|
+
info.send(method_name) ||
|
184
|
+
super
|
185
|
+
end
|
186
|
+
|
187
|
+
def respond_to_missing?(method_name, *args)
|
188
|
+
info.key?(method_name.to_s.ai_camelcase) ||
|
189
|
+
info.respond_to?(method_name) ||
|
190
|
+
super
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
def info
|
196
|
+
return unless File.file?(@file)
|
197
|
+
|
198
|
+
@info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: @file).value)
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
end
|