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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +12 -7
- data/.github/workflows/ci.yml +3 -1
- data/.rubocop.yml +33 -11
- data/CHANGELOG.md +34 -1
- data/Gemfile +7 -1
- data/README.md +68 -13
- data/Rakefile +11 -0
- data/app_info.gemspec +12 -3
- data/lib/app_info/aab.rb +48 -108
- data/lib/app_info/android/signature.rb +114 -0
- data/lib/app_info/android/signatures/base.rb +53 -0
- data/lib/app_info/android/signatures/info.rb +158 -0
- data/lib/app_info/android/signatures/v1.rb +63 -0
- data/lib/app_info/android/signatures/v2.rb +121 -0
- data/lib/app_info/android/signatures/v3.rb +131 -0
- data/lib/app_info/android/signatures/v4.rb +18 -0
- data/lib/app_info/android.rb +162 -0
- data/lib/app_info/apk.rb +54 -111
- data/lib/app_info/apple.rb +192 -0
- data/lib/app_info/certificate.rb +175 -0
- data/lib/app_info/const.rb +75 -0
- data/lib/app_info/core_ext/object/try.rb +3 -1
- data/lib/app_info/core_ext/string/inflector.rb +2 -0
- data/lib/app_info/dsym/debug_info.rb +72 -0
- data/lib/app_info/dsym/macho.rb +55 -0
- data/lib/app_info/dsym.rb +31 -135
- data/lib/app_info/error.rb +2 -2
- data/lib/app_info/file.rb +49 -0
- data/lib/app_info/helper/archive.rb +37 -0
- data/lib/app_info/helper/file_size.rb +25 -0
- data/lib/app_info/helper/generate_class.rb +29 -0
- data/lib/app_info/helper/protobuf.rb +12 -0
- data/lib/app_info/helper/signatures.rb +229 -0
- data/lib/app_info/helper.rb +5 -126
- data/lib/app_info/info_plist.rb +66 -29
- data/lib/app_info/ipa/framework.rb +4 -4
- data/lib/app_info/ipa.rb +61 -135
- data/lib/app_info/macos.rb +54 -102
- data/lib/app_info/mobile_provision.rb +67 -49
- data/lib/app_info/pe.rb +260 -0
- data/lib/app_info/png_uncrush.rb +24 -4
- data/lib/app_info/proguard.rb +29 -16
- data/lib/app_info/protobuf/manifest.rb +6 -3
- data/lib/app_info/protobuf/models/Configuration_pb.rb +1 -0
- data/lib/app_info/protobuf/models/README.md +7 -0
- data/lib/app_info/protobuf/models/Resources_pb.rb +2 -0
- data/lib/app_info/protobuf/resources.rb +5 -5
- data/lib/app_info/version.rb +1 -1
- data/lib/app_info.rb +90 -46
- 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
|
data/lib/app_info/helper.rb
CHANGED
@@ -1,128 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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'
|
data/lib/app_info/info_plist.rb
CHANGED
@@ -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
|
-
|
13
|
-
|
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[
|
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
|
-
|
110
|
+
device == Device::IPHONE
|
82
111
|
end
|
83
112
|
|
113
|
+
# @return [Boolean]
|
84
114
|
def ipad?
|
85
|
-
|
115
|
+
device == Device::IPAD
|
86
116
|
end
|
87
117
|
|
118
|
+
# @return [Boolean]
|
88
119
|
def universal?
|
89
|
-
|
120
|
+
device == Device::UNIVERSAL
|
90
121
|
end
|
91
122
|
|
123
|
+
# @return [Boolean]
|
92
124
|
def macos?
|
93
|
-
|
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
|
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
|