app-info 2.8.5 → 3.0.0.beta1

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 (49) 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 +31 -11
  5. data/CHANGELOG.md +22 -0
  6. data/Gemfile +7 -1
  7. data/README.md +64 -9
  8. data/Rakefile +11 -0
  9. data/app_info.gemspec +12 -3
  10. data/lib/app_info/aab.rb +58 -39
  11. data/lib/app_info/android/signature.rb +114 -0
  12. data/lib/app_info/android/signatures/base.rb +49 -0
  13. data/lib/app_info/android/signatures/info.rb +152 -0
  14. data/lib/app_info/android/signatures/v1.rb +59 -0
  15. data/lib/app_info/android/signatures/v2.rb +117 -0
  16. data/lib/app_info/android/signatures/v3.rb +127 -0
  17. data/lib/app_info/android/signatures/v4.rb +14 -0
  18. data/lib/app_info/apk.rb +43 -46
  19. data/lib/app_info/certificate.rb +181 -0
  20. data/lib/app_info/const.rb +41 -0
  21. data/lib/app_info/core_ext/object/try.rb +3 -1
  22. data/lib/app_info/core_ext/string/inflector.rb +2 -0
  23. data/lib/app_info/dsym/debug_info.rb +72 -0
  24. data/lib/app_info/dsym/macho.rb +55 -0
  25. data/lib/app_info/dsym.rb +27 -134
  26. data/lib/app_info/error.rb +7 -1
  27. data/lib/app_info/file.rb +23 -0
  28. data/lib/app_info/helper/archive.rb +37 -0
  29. data/lib/app_info/helper/file_size.rb +25 -0
  30. data/lib/app_info/helper/generate_class.rb +29 -0
  31. data/lib/app_info/helper/protobuf.rb +12 -0
  32. data/lib/app_info/helper/signatures.rb +229 -0
  33. data/lib/app_info/helper.rb +5 -126
  34. data/lib/app_info/info_plist.rb +14 -6
  35. data/lib/app_info/ipa/framework.rb +4 -4
  36. data/lib/app_info/ipa.rb +41 -36
  37. data/lib/app_info/macos.rb +34 -26
  38. data/lib/app_info/mobile_provision.rb +19 -30
  39. data/lib/app_info/pe.rb +226 -0
  40. data/lib/app_info/png_uncrush.rb +5 -4
  41. data/lib/app_info/proguard.rb +11 -17
  42. data/lib/app_info/protobuf/manifest.rb +1 -2
  43. data/lib/app_info/protobuf/models/Configuration_pb.rb +1 -0
  44. data/lib/app_info/protobuf/models/README.md +7 -0
  45. data/lib/app_info/protobuf/models/Resources_pb.rb +2 -0
  46. data/lib/app_info/protobuf/resources.rb +5 -5
  47. data/lib/app_info/version.rb +1 -1
  48. data/lib/app_info.rb +88 -45
  49. metadata +46 -35
@@ -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,11 +6,19 @@ 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
+ def file_type
21
+ Format::INFOPLIST
14
22
  end
15
23
 
16
24
  def version
@@ -126,7 +134,7 @@ module AppInfo
126
134
  private
127
135
 
128
136
  def info
129
- return unless File.file?(@file)
137
+ return unless ::File.file?(@file)
130
138
 
131
139
  @info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: @file).value)
132
140
  end
@@ -134,9 +142,9 @@ module AppInfo
134
142
  def app_path
135
143
  @app_path ||= case device_type
136
144
  when Device::MACOS
137
- File.dirname(@file)
145
+ ::File.dirname(@file)
138
146
  else
139
- File.expand_path('../', @file)
147
+ ::File.expand_path('../', @file)
140
148
  end
141
149
  end
142
150
  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
data/lib/app_info/ipa.rb CHANGED
@@ -7,7 +7,7 @@ require 'cfpropertylist'
7
7
 
8
8
  module AppInfo
9
9
  # IPA parser
10
- class IPA
10
+ class IPA < File
11
11
  include Helper::HumanFileSize
12
12
  include Helper::Archive
13
13
  extend Forwardable
@@ -25,18 +25,26 @@ module AppInfo
25
25
  INHOUSE = 'Enterprise' # Rename and Alias to enterprise
26
26
  end
27
27
 
28
- def initialize(file)
29
- @file = file
30
- end
31
-
28
+ # return file size
29
+ # @example Read file size in integer
30
+ # aab.size # => 3618865
31
+ #
32
+ # @example Read file size in human readabale
33
+ # aab.size(human_size: true) # => '3.45 MB'
34
+ #
35
+ # @param [Boolean] human_size Convert integer value to human readable.
36
+ # @return [Integer, String]
32
37
  def size(human_size: false)
33
38
  file_to_human_size(@file, human_size: human_size)
34
39
  end
35
40
 
36
- def os
41
+ def file_type
42
+ Format::IPA
43
+ end
44
+
45
+ def platform
37
46
  Platform::IOS
38
47
  end
39
- alias file_type os
40
48
 
41
49
  def_delegators :info, :iphone?, :ipad?, :universal?, :build_version, :name,
42
50
  :release_version, :identifier, :bundle_id, :display_name,
@@ -70,7 +78,7 @@ module AppInfo
70
78
  end
71
79
 
72
80
  def archs
73
- return unless File.exist?(bundle_path)
81
+ return unless ::File.exist?(bundle_path)
74
82
 
75
83
  file = MachO.open(bundle_path)
76
84
  case file
@@ -114,14 +122,14 @@ module AppInfo
114
122
  end
115
123
 
116
124
  def mobileprovision?
117
- File.exist?(mobileprovision_path)
125
+ ::File.exist?(mobileprovision_path)
118
126
  end
119
127
 
120
128
  def mobileprovision_path
121
129
  filename = 'embedded.mobileprovision'
122
- @mobileprovision_path ||= File.join(@file, filename)
123
- unless File.exist?(@mobileprovision_path)
124
- @mobileprovision_path = File.join(app_path, filename)
130
+ @mobileprovision_path ||= ::File.join(@file, filename)
131
+ unless ::File.exist?(@mobileprovision_path)
132
+ @mobileprovision_path = ::File.join(app_path, filename)
125
133
  end
126
134
 
127
135
  @mobileprovision_path
@@ -134,15 +142,15 @@ module AppInfo
134
142
  end
135
143
 
136
144
  def metadata?
137
- File.exist?(metadata_path)
145
+ ::File.exist?(metadata_path)
138
146
  end
139
147
 
140
148
  def metadata_path
141
- @metadata_path ||= File.join(contents, 'iTunesMetadata.plist')
149
+ @metadata_path ||= ::File.join(contents, 'iTunesMetadata.plist')
142
150
  end
143
151
 
144
152
  def bundle_path
145
- @bundle_path ||= File.join(app_path, info.bundle_name)
153
+ @bundle_path ||= ::File.join(app_path, info.bundle_name)
146
154
  end
147
155
 
148
156
  def info
@@ -150,35 +158,32 @@ module AppInfo
150
158
  end
151
159
 
152
160
  def info_path
153
- @info_path ||= File.join(app_path, 'Info.plist')
161
+ @info_path ||= ::File.join(app_path, 'Info.plist')
154
162
  end
155
163
 
156
164
  def app_path
157
- @app_path ||= Dir.glob(File.join(contents, 'Payload', '*.app')).first
165
+ @app_path ||= Dir.glob(::File.join(contents, 'Payload', '*.app')).first
158
166
  end
159
167
 
160
168
  IPHONE_KEY = 'CFBundleIcons'
161
169
  IPAD_KEY = 'CFBundleIcons~ipad'
162
170
 
163
171
  def icons_path
164
- return @icons_path if @icons_path
165
-
166
- @icons_path = []
167
- icon_keys.each do |name|
168
- filenames = info.try(:[], name)
169
- .try(:[], 'CFBundlePrimaryIcon')
170
- .try(:[], 'CFBundleIconFiles')
171
-
172
- next if filenames.nil? || filenames.empty?
173
-
174
- filenames.each do |filename|
175
- Dir.glob(File.join(app_path, "#{filename}*")).find_all.each do |file|
176
- @icons_path << file
172
+ @icons_path ||= lambda {
173
+ icon_keys.each_with_object([]) do |name, icons|
174
+ filenames = info.try(:[], name)
175
+ .try(:[], 'CFBundlePrimaryIcon')
176
+ .try(:[], 'CFBundleIconFiles')
177
+
178
+ next if filenames.nil? || filenames.empty?
179
+
180
+ filenames.each do |filename|
181
+ Dir.glob(::File.join(app_path, "#{filename}*")).find_all.each do |file|
182
+ icons << file
183
+ end
177
184
  end
178
185
  end
179
- end
180
-
181
- @icons_path
186
+ }.call
182
187
  end
183
188
 
184
189
  def clear!
@@ -197,7 +202,7 @@ module AppInfo
197
202
  end
198
203
 
199
204
  def contents
200
- @contents ||= unarchive(@file, path: 'ios')
205
+ @contents ||= unarchive(@file, prefix: 'ios')
201
206
  end
202
207
 
203
208
  private
@@ -206,7 +211,7 @@ module AppInfo
206
211
  uncrushed_file = uncrush ? uncrush_png(file) : nil
207
212
 
208
213
  {
209
- name: File.basename(file),
214
+ name: ::File.basename(file),
210
215
  file: file,
211
216
  uncrushed_file: uncrushed_file,
212
217
  dimensions: PngUncrush.dimensions(file)
@@ -217,7 +222,7 @@ module AppInfo
217
222
  def uncrush_png(src_file)
218
223
  dest_file = tempdir(src_file, prefix: 'uncrushed')
219
224
  PngUncrush.decompress(src_file, dest_file)
220
- File.exist?(dest_file) ? dest_file : nil
225
+ ::File.exist?(dest_file) ? dest_file : nil
221
226
  end
222
227
 
223
228
  def icon_keys