app-info 2.8.5 → 3.0.0.beta2

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +12 -7
  3. data/.github/workflows/ci.yml +3 -1
  4. data/.rubocop.yml +33 -11
  5. data/CHANGELOG.md +34 -1
  6. data/Gemfile +7 -1
  7. data/README.md +68 -13
  8. data/Rakefile +11 -0
  9. data/app_info.gemspec +12 -3
  10. data/lib/app_info/aab.rb +48 -108
  11. data/lib/app_info/android/signature.rb +114 -0
  12. data/lib/app_info/android/signatures/base.rb +53 -0
  13. data/lib/app_info/android/signatures/info.rb +158 -0
  14. data/lib/app_info/android/signatures/v1.rb +63 -0
  15. data/lib/app_info/android/signatures/v2.rb +121 -0
  16. data/lib/app_info/android/signatures/v3.rb +131 -0
  17. data/lib/app_info/android/signatures/v4.rb +18 -0
  18. data/lib/app_info/android.rb +162 -0
  19. data/lib/app_info/apk.rb +54 -111
  20. data/lib/app_info/apple.rb +192 -0
  21. data/lib/app_info/certificate.rb +175 -0
  22. data/lib/app_info/const.rb +75 -0
  23. data/lib/app_info/core_ext/object/try.rb +3 -1
  24. data/lib/app_info/core_ext/string/inflector.rb +2 -0
  25. data/lib/app_info/dsym/debug_info.rb +72 -0
  26. data/lib/app_info/dsym/macho.rb +55 -0
  27. data/lib/app_info/dsym.rb +31 -135
  28. data/lib/app_info/error.rb +2 -2
  29. data/lib/app_info/file.rb +49 -0
  30. data/lib/app_info/helper/archive.rb +37 -0
  31. data/lib/app_info/helper/file_size.rb +25 -0
  32. data/lib/app_info/helper/generate_class.rb +29 -0
  33. data/lib/app_info/helper/protobuf.rb +12 -0
  34. data/lib/app_info/helper/signatures.rb +229 -0
  35. data/lib/app_info/helper.rb +5 -126
  36. data/lib/app_info/info_plist.rb +66 -29
  37. data/lib/app_info/ipa/framework.rb +4 -4
  38. data/lib/app_info/ipa.rb +61 -135
  39. data/lib/app_info/macos.rb +54 -102
  40. data/lib/app_info/mobile_provision.rb +67 -49
  41. data/lib/app_info/pe.rb +260 -0
  42. data/lib/app_info/png_uncrush.rb +24 -4
  43. data/lib/app_info/proguard.rb +29 -16
  44. data/lib/app_info/protobuf/manifest.rb +6 -3
  45. data/lib/app_info/protobuf/models/Configuration_pb.rb +1 -0
  46. data/lib/app_info/protobuf/models/README.md +7 -0
  47. data/lib/app_info/protobuf/models/Resources_pb.rb +2 -0
  48. data/lib/app_info/protobuf/resources.rb +5 -5
  49. data/lib/app_info/version.rb +1 -1
  50. data/lib/app_info.rb +90 -46
  51. metadata +48 -35
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo::Helper
4
+ module HumanFileSize
5
+ def file_to_human_size(file, human_size:)
6
+ number = ::File.size(file)
7
+ human_size ? number_to_human_size(number) : number
8
+ end
9
+
10
+ FILE_SIZE_UNITS = %w[B KB MB GB TB].freeze
11
+
12
+ def number_to_human_size(number)
13
+ if number.to_i < 1024
14
+ exponent = 0
15
+ else
16
+ max_exp = FILE_SIZE_UNITS.size - 1
17
+ exponent = (Math.log(number) / Math.log(1024)).to_i
18
+ exponent = max_exp if exponent > max_exp
19
+ number = Kernel.format('%<number>.2f', number: (number / (1024**exponent.to_f)))
20
+ end
21
+
22
+ "#{number} #{FILE_SIZE_UNITS[exponent]}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo::Helper
4
+ module GenerateClass
5
+ def create_class(klass_name, parent_class, namespace:)
6
+ klass = Class.new(parent_class) do
7
+ yield if block_given?
8
+ end
9
+
10
+ name = namespace.to_s.empty? ? klass_name : "#{namespace}::#{klass_name}"
11
+ if Object.const_get(namespace).const_defined?(klass_name)
12
+ Object.const_get(namespace).const_get(klass_name)
13
+ elsif Object.const_defined?(name)
14
+ Object.const_get(name)
15
+ else
16
+ Object.const_get(namespace).const_set(klass_name, klass)
17
+ end
18
+ end
19
+
20
+ def define_instance_method(key, value)
21
+ instance_variable_set("@#{key}", value)
22
+ self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
23
+ def #{key}
24
+ @#{key}
25
+ end
26
+ RUBY
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo::Helper
4
+ module Protobuf
5
+ def reference_segments(value)
6
+ new_value = value.is_a?(Aapt::Pb::Reference) ? value.name : value
7
+ return new_value.split('/', 2) if new_value.include?('/')
8
+
9
+ [nil, new_value]
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo::Helper
4
+ # Binary IO Block Helper
5
+ module IOBlock
6
+ def length_prefix_block(
7
+ io, size: AppInfo::Android::Signature::UINT32_SIZE,
8
+ raw: false, ignore_left_size_precheck: false
9
+ )
10
+ offset = io.size - io.pos
11
+ if offset < AppInfo::Android::Signature::UINT32_SIZE
12
+ raise SecurityError,
13
+ 'Remaining buffer too short to contain length of length-prefixed field.'
14
+ end
15
+
16
+ size = io.read(size).unpack1('I')
17
+ raise SecurityError, 'Negative length' if size.negative?
18
+
19
+ if !ignore_left_size_precheck && size > io.size
20
+ message = "Underflow while reading length-prefixed value. #{size} > #{io.size}"
21
+ raise SecurityError, message
22
+ end
23
+
24
+ raw_data = io.read(size)
25
+ raw ? raw_data : StringIO.new(raw_data)
26
+ end
27
+
28
+ # Only use for uint32 length-prefixed block
29
+ def loop_length_prefix_io(
30
+ io, name:, max_bytes: nil, logger: nil, raw: false,
31
+ ignore_left_size_precheck: false, &block
32
+ )
33
+ index = 0
34
+ until io.eof?
35
+ logger&.debug "#{name} count ##{index}"
36
+ buffer = length_prefix_block(
37
+ io,
38
+ raw: raw,
39
+ ignore_left_size_precheck: ignore_left_size_precheck
40
+ )
41
+
42
+ left_bytes_check(buffer, max_bytes, SecurityError) do |left_bytes|
43
+ "#{name} too short: #{left_bytes} < #{max_bytes}"
44
+ end
45
+
46
+ block.call(buffer)
47
+ index += 1
48
+ end
49
+ end
50
+
51
+ def left_bytes_check(io, max_bytes, exception, message = nil, &block)
52
+ return if max_bytes.nil?
53
+
54
+ left_bytes = io.size - io.pos
55
+ return left_bytes if left_bytes.zero?
56
+
57
+ message ||= if block_given?
58
+ block.call(left_bytes)
59
+ else
60
+ "IO too short: #{offset} < #{max_bytes}"
61
+ end
62
+
63
+ raise exception, message if left_bytes < max_bytes
64
+
65
+ left_bytes
66
+ end
67
+ end
68
+
69
+ # Signature Block helper
70
+ module Signatures
71
+ def singers_block(block_id)
72
+ info = AppInfo::Android::Signature::Info.new(@version, @parser, logger)
73
+ raise SecurityError, 'ZIP64 APK not supported' if info.zip64?
74
+
75
+ info.signers(block_id)
76
+ end
77
+
78
+ def signed_data_certs(io)
79
+ certificates = []
80
+ loop_length_prefix_io(io, name: 'Certificates', raw: true) do |cert_data|
81
+ certificates << AppInfo::Certificate.parse(cert_data)
82
+ end
83
+ certificates
84
+ end
85
+
86
+ def signed_data_digests(io)
87
+ content_digests = {}
88
+ loop_length_prefix_io(
89
+ io,
90
+ name: 'Digests',
91
+ max_bytes: AppInfo::Android::Signature::UINT64_SIZE
92
+ ) do |digest|
93
+ algorithm = digest.read(AppInfo::Android::Signature::UINT32_SIZE).unpack('C*')
94
+ digest_name = algorithm_match(algorithm)
95
+ next unless digest_name
96
+
97
+ content = length_prefix_block(digest)
98
+ content_digests[digest_name] = {
99
+ id: algorithm,
100
+ content: content
101
+ }
102
+ end
103
+
104
+ content_digests
105
+ end
106
+
107
+ # FIXME: this code not work, need fix.
108
+ def verify_additional_attrs(attrs, _certs)
109
+ loop_length_prefix_io(
110
+ attrs, name: 'Additional Attributes', ignore_left_size_precheck: true
111
+ ) do |attr|
112
+ id = attr.read(AppInfo::Android::Signature::UINT32_SIZE)
113
+ logger.debug "ID #{id} / #{id.size} / #{id.unpack('H*')} / #{id.unpack('C*')}"
114
+ if id.unpack('C*') == AppInfo::Helper::Algorithm::SIG_STRIPPING_PROTECTION_ATTR_ID
115
+ offset = attr.size - attr.pos
116
+ if offset < AppInfo::Android::Signature::UINT32_SIZE
117
+ raise SecurityError,
118
+ "V2 Signature Scheme Stripping Protection Attribute value too small. Expected #{UINT32_SIZE} bytes, but found #{offset}"
119
+ end
120
+
121
+ # value = attr.read(UINT32_SIZE).unpack1('I')
122
+ if @version == AppInfo::Android::Signature::Version::V3
123
+ raise SecurityError,
124
+ 'V2 signature indicates APK is signed using APK Signature Scheme v3, but none was found. Signature stripped?'
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ def signature_algorithms(signatures)
131
+ algorithems = []
132
+ loop_length_prefix_io(
133
+ signatures,
134
+ name: 'Signature Algorithms',
135
+ max_bytes: AppInfo::Android::Signature::UINT64_SIZE,
136
+ logger: logger
137
+ ) do |signature|
138
+ algorithm = signature.read(AppInfo::Android::Signature::UINT32_SIZE).unpack('C*')
139
+ digest = algorithm_match(algorithm)
140
+ next unless digest
141
+
142
+ signature = length_prefix_block(signature, raw: true)
143
+ algorithems << {
144
+ id: algorithm,
145
+ digest: digest,
146
+ signature: signature
147
+ }
148
+ end
149
+
150
+ algorithems
151
+ end
152
+ end
153
+
154
+ # Signature Algorithm helper
155
+ module Algorithm
156
+ # Signature certificate identifiers
157
+ SIG_RSA_PSS_WITH_SHA256 = [0x01, 0x01, 0x00, 0x00].freeze # 0x0101
158
+ SIG_RSA_PSS_WITH_SHA512 = [0x02, 0x01, 0x00, 0x00].freeze # 0x0102
159
+ SIG_RSA_PKCS1_V1_5_WITH_SHA256 = [0x03, 0x01, 0x00, 0x00].freeze # 0x0103
160
+ SIG_RSA_PKCS1_V1_5_WITH_SHA512 = [0x04, 0x01, 0x00, 0x00].freeze # 0x0104
161
+ SIG_ECDSA_WITH_SHA256 = [0x01, 0x02, 0x00, 0x00].freeze # 0x0201
162
+ SIG_ECDSA_WITH_SHA512 = [0x02, 0x02, 0x00, 0x00].freeze # 0x0202
163
+ SIG_DSA_WITH_SHA256 = [0x01, 0x03, 0x00, 0x00].freeze # 0x0301
164
+ SIG_VERITY_RSA_PKCS1_V1_5_WITH_SHA256 = [0x21, 0x04, 0x00, 0x00].freeze # 0x0421
165
+ SIG_VERITY_ECDSA_WITH_SHA256 = [0x23, 0x04, 0x00, 0x00].freeze # 0x0423
166
+ SIG_VERITY_DSA_WITH_SHA256 = [0x25, 0x04, 0x00, 0x00].freeze # 0x0425
167
+
168
+ SIG_STRIPPING_PROTECTION_ATTR_ID = [0x0d, 0xf0, 0xef, 0xbe].freeze # 0xbeeff00d
169
+
170
+ def best_algorithem(algorithems)
171
+ methods = algorithems.map { |algorithem| algorithem[:method] }
172
+ best_method = methods.max { |a, b| algorithem_priority(a) <=> algorithem_priority(b) }
173
+ best_method_index = methods.index(best_method)
174
+ algorithems[best_method_index]
175
+ end
176
+
177
+ def compare_algorithem(source, target)
178
+ case algorithem_priority(source) <=> algorithem_priority(target)
179
+ when -1
180
+ target
181
+ else
182
+ source
183
+ end
184
+ end
185
+
186
+ def algorithem_priority(algorithm)
187
+ case algorithm
188
+ when SIG_RSA_PSS_WITH_SHA256,
189
+ SIG_RSA_PKCS1_V1_5_WITH_SHA256,
190
+ SIG_ECDSA_WITH_SHA256,
191
+ SIG_DSA_WITH_SHA256
192
+ 1
193
+ when SIG_RSA_PSS_WITH_SHA512,
194
+ SIG_RSA_PKCS1_V1_5_WITH_SHA512,
195
+ SIG_ECDSA_WITH_SHA512
196
+ 2
197
+ when SIG_VERITY_RSA_PKCS1_V1_5_WITH_SHA256,
198
+ SIG_VERITY_ECDSA_WITH_SHA256,
199
+ SIG_VERITY_DSA_WITH_SHA256
200
+ 3
201
+ end
202
+ end
203
+
204
+ def algorithm_method(algorithm)
205
+ case algorithm
206
+ when SIG_RSA_PSS_WITH_SHA256, SIG_RSA_PSS_WITH_SHA512,
207
+ SIG_RSA_PKCS1_V1_5_WITH_SHA256, SIG_RSA_PKCS1_V1_5_WITH_SHA512,
208
+ SIG_VERITY_RSA_PKCS1_V1_5_WITH_SHA256
209
+ :rsa
210
+ when SIG_ECDSA_WITH_SHA256, SIG_ECDSA_WITH_SHA512, SIG_VERITY_ECDSA_WITH_SHA256
211
+ :ec
212
+ when SIG_DSA_WITH_SHA256, SIG_VERITY_DSA_WITH_SHA256
213
+ :dsa
214
+ end
215
+ end
216
+
217
+ def algorithm_match(algorithm)
218
+ case algorithm
219
+ when SIG_RSA_PSS_WITH_SHA256, SIG_RSA_PKCS1_V1_5_WITH_SHA256,
220
+ SIG_ECDSA_WITH_SHA256, SIG_DSA_WITH_SHA256,
221
+ SIG_VERITY_RSA_PKCS1_V1_5_WITH_SHA256, SIG_VERITY_ECDSA_WITH_SHA256,
222
+ SIG_VERITY_DSA_WITH_SHA256
223
+ 'SHA256'
224
+ when SIG_RSA_PSS_WITH_SHA512, SIG_RSA_PKCS1_V1_5_WITH_SHA512, SIG_ECDSA_WITH_SHA512
225
+ 'SHA512'
226
+ end
227
+ end
228
+ end
229
+ end
@@ -1,128 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AppInfo
4
- # App Platform
5
- module Platform
6
- MACOS = 'macOS'
7
- IOS = 'iOS'
8
- ANDROID = 'Android'
9
- DSYM = 'dSYM'
10
- PROGUARD = 'Proguard'
11
- end
12
-
13
- # Device Type
14
- module Device
15
- MACOS = 'macOS'
16
- IPHONE = 'iPhone'
17
- IPAD = 'iPad'
18
- UNIVERSAL = 'Universal'
19
- end
20
-
21
- module AndroidDevice
22
- PHONE = 'Phone'
23
- TABLET = 'Tablet'
24
- WATCH = 'Watch'
25
- TV = 'Television'
26
- end
27
-
28
- # Icon Key
29
- ICON_KEYS = {
30
- Device::IPHONE => ['CFBundleIcons'],
31
- Device::IPAD => ['CFBundleIcons~ipad'],
32
- Device::UNIVERSAL => ['CFBundleIcons', 'CFBundleIcons~ipad'],
33
- Device::MACOS => %w[CFBundleIconFile CFBundleIconName]
34
- }.freeze
35
-
36
- module Helper
37
- module HumanFileSize
38
- def file_to_human_size(file, human_size:)
39
- number = File.size(file)
40
- human_size ? number_to_human_size(number) : number
41
- end
42
-
43
- FILE_SIZE_UNITS = %w[B KB MB GB TB].freeze
44
-
45
- def number_to_human_size(number)
46
- if number.to_i < 1024
47
- exponent = 0
48
- else
49
- max_exp = FILE_SIZE_UNITS.size - 1
50
- exponent = (Math.log(number) / Math.log(1024)).to_i
51
- exponent = max_exp if exponent > max_exp
52
- number = format('%<number>.2f', number: (number / (1024**exponent.to_f)))
53
- end
54
-
55
- "#{number} #{FILE_SIZE_UNITS[exponent]}"
56
- end
57
- end
58
-
59
- module Archive
60
- require 'zip'
61
- require 'fileutils'
62
- require 'securerandom'
63
-
64
- # Unarchive zip file
65
- #
66
- # source: https://github.com/soffes/lagunitas/blob/master/lib/lagunitas/ipa.rb
67
- def unarchive(file, path: nil)
68
- path = path ? "#{path}-" : ''
69
- root_path = "#{Dir.mktmpdir}/AppInfo-#{path}#{SecureRandom.hex}"
70
- Zip::File.open(file) do |zip_file|
71
- if block_given?
72
- yield root_path, zip_file
73
- else
74
- zip_file.each do |f|
75
- f_path = File.join(root_path, f.name)
76
- FileUtils.mkdir_p(File.dirname(f_path))
77
- zip_file.extract(f, f_path) unless File.exist?(f_path)
78
- end
79
- end
80
- end
81
-
82
- root_path
83
- end
84
-
85
- def tempdir(file, prefix:)
86
- dest_path ||= File.join(File.dirname(file), prefix)
87
- dest_file = File.join(dest_path, File.basename(file))
88
- FileUtils.mkdir_p(dest_path, mode: 0_700)
89
- dest_file
90
- end
91
- end
92
-
93
- module Defines
94
- def create_class(klass_name, parent_class, namespace:)
95
- klass = Class.new(parent_class) do
96
- yield if block_given?
97
- end
98
-
99
- name = namespace.to_s.empty? ? klass_name : "#{namespace}::#{klass_name}"
100
- if Object.const_get(namespace).const_defined?(klass_name)
101
- Object.const_get(namespace).const_get(klass_name)
102
- elsif Object.const_defined?(name)
103
- Object.const_get(name)
104
- else
105
- Object.const_get(namespace).const_set(klass_name, klass)
106
- end
107
- end
108
-
109
- def define_instance_method(key, value)
110
- instance_variable_set("@#{key}", value)
111
- self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
112
- def #{key}
113
- @#{key}
114
- end
115
- RUBY
116
- end
117
- end
118
-
119
- module ReferenceParser
120
- def reference_segments(value)
121
- new_value = value.is_a?(Aapt::Pb::Reference) ? value.name : value
122
- return new_value.split('/', 2) if new_value.include?('/')
123
-
124
- [nil, new_value]
125
- end
126
- end
127
- end
128
- end
3
+ require 'app_info/helper/archive'
4
+ require 'app_info/helper/file_size'
5
+ require 'app_info/helper/generate_class'
6
+ require 'app_info/helper/protobuf'
7
+ require 'app_info/helper/signatures'
@@ -6,97 +6,131 @@ require 'app_info/png_uncrush'
6
6
 
7
7
  module AppInfo
8
8
  # iOS Info.plist parser
9
- class InfoPlist
9
+ class InfoPlist < File
10
10
  extend Forwardable
11
11
 
12
- def initialize(file)
13
- @file = file
12
+ # Icon Key
13
+ ICON_KEYS = {
14
+ Device::IPHONE => ['CFBundleIcons'],
15
+ Device::IPAD => ['CFBundleIcons~ipad'],
16
+ Device::UNIVERSAL => ['CFBundleIcons', 'CFBundleIcons~ipad'],
17
+ Device::MACOS => %w[CFBundleIconFile CFBundleIconName]
18
+ }.freeze
19
+
20
+ # @return [Symbol] {Platform}
21
+ def platform
22
+ Platform::APPLE
23
+ end
24
+
25
+ # @return [Symbol] {OperaSystem}
26
+ def opera_system
27
+ case device
28
+ when Device::MACOS
29
+ OperaSystem::MACOS
30
+ when Device::IPHONE, Device::IPAD, Device::UNIVERSAL
31
+ OperaSystem::IOS
32
+ end
33
+ end
34
+
35
+ # @return [Symbol] {Device}
36
+ def device
37
+ if device_family == [1]
38
+ Device::IPHONE
39
+ elsif device_family == [2]
40
+ Device::IPAD
41
+ elsif device_family == [1, 2]
42
+ Device::UNIVERSAL
43
+ elsif !info.try(:[], 'DTSDKName').nil? || !info.try(:[], 'DTPlatformName').nil?
44
+ Device::MACOS
45
+ else
46
+ raise NotImplementedError, "Unkonwn device: #{device_family}"
47
+ end
14
48
  end
15
49
 
50
+ # @return [String, nil]
16
51
  def version
17
52
  release_version || build_version
18
53
  end
19
54
 
55
+ # @return [String, nil]
20
56
  def build_version
21
57
  info.try(:[], 'CFBundleVersion')
22
58
  end
23
59
 
60
+ # @return [String, nil]
24
61
  def release_version
25
62
  info.try(:[], 'CFBundleShortVersionString')
26
63
  end
27
64
 
65
+ # @return [String, nil]
28
66
  def identifier
29
67
  info.try(:[], 'CFBundleIdentifier')
30
68
  end
31
69
  alias bundle_id identifier
32
70
 
71
+ # @return [String, nil]
33
72
  def name
34
73
  display_name || bundle_name
35
74
  end
36
75
 
76
+ # @return [String, nil]
37
77
  def display_name
38
78
  info.try(:[], 'CFBundleDisplayName')
39
79
  end
40
80
 
81
+ # @return [String, nil]
41
82
  def bundle_name
42
83
  info.try(:[], 'CFBundleName')
43
84
  end
44
85
 
86
+ # @return [String, nil]
45
87
  def min_os_version
46
88
  min_sdk_version || min_system_version
47
89
  end
48
90
 
49
- #
50
91
  # Extract the Minimum OS Version from the Info.plist (iOS Only)
51
- #
92
+ # @return [String, nil]
52
93
  def min_sdk_version
53
94
  info.try(:[], 'MinimumOSVersion')
54
95
  end
55
96
 
56
- #
57
97
  # Extract the Minimum OS Version from the Info.plist (macOS Only)
58
- #
98
+ # @return [String, nil]
59
99
  def min_system_version
60
100
  info.try(:[], 'LSMinimumSystemVersion')
61
101
  end
62
102
 
103
+ # @return [Array<String>]
63
104
  def icons
64
- @icons ||= ICON_KEYS[device_type]
65
- end
66
-
67
- def device_type
68
- device_family = info.try(:[], 'UIDeviceFamily')
69
- if device_family == [1]
70
- Device::IPHONE
71
- elsif device_family == [2]
72
- Device::IPAD
73
- elsif device_family == [1, 2]
74
- Device::UNIVERSAL
75
- elsif !info.try(:[], 'DTSDKName').nil? || !info.try(:[], 'DTPlatformName').nil?
76
- Device::MACOS
77
- end
105
+ @icons ||= ICON_KEYS[device]
78
106
  end
79
107
 
108
+ # @return [Boolean]
80
109
  def iphone?
81
- device_type == Device::IPHONE
110
+ device == Device::IPHONE
82
111
  end
83
112
 
113
+ # @return [Boolean]
84
114
  def ipad?
85
- device_type == Device::IPAD
115
+ device == Device::IPAD
86
116
  end
87
117
 
118
+ # @return [Boolean]
88
119
  def universal?
89
- device_type == Device::UNIVERSAL
120
+ device == Device::UNIVERSAL
90
121
  end
91
122
 
123
+ # @return [Boolean]
92
124
  def macos?
93
- device_type == Device::MACOS
125
+ device == Device::MACOS
94
126
  end
95
127
 
128
+ # @return [Array<String>]
96
129
  def device_family
97
130
  info.try(:[], 'UIDeviceFamily') || []
98
131
  end
99
132
 
133
+ # @return [String]
100
134
  def release_type
101
135
  if stored?
102
136
  'Store'
@@ -105,10 +139,13 @@ module AppInfo
105
139
  end
106
140
  end
107
141
 
142
+ # @return [String, nil]
108
143
  def [](key)
109
144
  info.try(:[], key.to_s)
110
145
  end
111
146
 
147
+ # @!method to_h
148
+ # @see CFPropertyList#to_h
112
149
  def_delegators :info, :to_h
113
150
 
114
151
  def method_missing(method_name, *args, &block)
@@ -126,17 +163,17 @@ module AppInfo
126
163
  private
127
164
 
128
165
  def info
129
- return unless File.file?(@file)
166
+ return unless ::File.file?(@file)
130
167
 
131
168
  @info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: @file).value)
132
169
  end
133
170
 
134
171
  def app_path
135
- @app_path ||= case device_type
172
+ @app_path ||= case device
136
173
  when Device::MACOS
137
- File.dirname(@file)
174
+ ::File.dirname(@file)
138
175
  else
139
- File.expand_path('../', @file)
176
+ ::File.expand_path('../', @file)
140
177
  end
141
178
  end
142
179
  end
@@ -8,7 +8,7 @@ module AppInfo
8
8
  extend Forwardable
9
9
 
10
10
  def self.parse(path, name = 'Frameworks')
11
- files = Dir.glob(File.join(path, name.to_s, '*'))
11
+ files = Dir.glob(::File.join(path, name.to_s, '*'))
12
12
  return [] if files.empty?
13
13
 
14
14
  files.sort.each_with_object([]) do |file, obj|
@@ -26,7 +26,7 @@ module AppInfo
26
26
  end
27
27
 
28
28
  def name
29
- File.basename(file)
29
+ ::File.basename(file)
30
30
  end
31
31
 
32
32
  def macho
@@ -37,11 +37,11 @@ module AppInfo
37
37
  end
38
38
 
39
39
  def lib?
40
- File.file?(file)
40
+ ::File.file?(file)
41
41
  end
42
42
 
43
43
  def info
44
- @info ||= InfoPlist.new(File.join(file, 'Info.plist'))
44
+ @info ||= InfoPlist.new(::File.join(file, 'Info.plist'))
45
45
  end
46
46
 
47
47
  def to_s