app-info 2.5.4 → 2.6.3
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 +9 -0
- data/.github/workflows/ci.yml +41 -0
- data/.rubocop.yml +26 -7
- data/CHANGELOG.md +43 -1
- data/Gemfile +5 -3
- data/README.md +110 -50
- data/app_info.gemspec +4 -1
- data/exe/app-info +12 -0
- data/lib/app_info/apk.rb +17 -21
- data/lib/app_info/core_ext/object/try.rb +77 -79
- data/lib/app_info/dsym.rb +3 -4
- data/lib/app_info/info_plist.rb +143 -0
- data/lib/app_info/ipa/framework.rb +3 -3
- data/lib/app_info/ipa.rb +72 -22
- data/lib/app_info/macos.rb +188 -0
- data/lib/app_info/{ipa/mobile_provision.rb → mobile_provision.rb} +9 -10
- data/lib/app_info/png_uncrush.rb +30 -31
- data/lib/app_info/proguard.rb +0 -1
- data/lib/app_info/shell.rb +34 -0
- data/lib/app_info/util.rb +46 -6
- data/lib/app_info/version.rb +1 -1
- data/lib/app_info.rb +21 -25
- metadata +41 -8
- data/.travis.yml +0 -9
- data/lib/app_info/ipa/info_plist.rb +0 -162
data/lib/app_info/ipa.rb
CHANGED
@@ -4,7 +4,6 @@ require 'macho'
|
|
4
4
|
require 'fileutils'
|
5
5
|
require 'forwardable'
|
6
6
|
require 'cfpropertylist'
|
7
|
-
require 'app_info/util'
|
8
7
|
|
9
8
|
module AppInfo
|
10
9
|
# IPA parser
|
@@ -28,8 +27,8 @@ module AppInfo
|
|
28
27
|
@file = file
|
29
28
|
end
|
30
29
|
|
31
|
-
def size(
|
32
|
-
AppInfo::Util.file_size(@file,
|
30
|
+
def size(human_size: false)
|
31
|
+
AppInfo::Util.file_size(@file, human_size)
|
33
32
|
end
|
34
33
|
|
35
34
|
def os
|
@@ -39,7 +38,7 @@ module AppInfo
|
|
39
38
|
|
40
39
|
def_delegators :info, :iphone?, :ipad?, :universal?, :build_version, :name,
|
41
40
|
:release_version, :identifier, :bundle_id, :display_name,
|
42
|
-
:bundle_name, :
|
41
|
+
:bundle_name, :min_sdk_version, :min_os_version, :device_type
|
43
42
|
|
44
43
|
def_delegators :mobileprovision, :devices, :team_name, :team_identifier,
|
45
44
|
:profile_name, :expired_date
|
@@ -83,8 +82,14 @@ module AppInfo
|
|
83
82
|
end
|
84
83
|
alias architectures archs
|
85
84
|
|
85
|
+
def icons(uncrush: true)
|
86
|
+
@icons ||= icons_path.each_with_object([]) do |file, obj|
|
87
|
+
obj << build_icon_metadata(file, uncrush: uncrush)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
86
91
|
def stored?
|
87
|
-
metadata?
|
92
|
+
!!metadata?
|
88
93
|
end
|
89
94
|
|
90
95
|
def plugins
|
@@ -107,7 +112,7 @@ module AppInfo
|
|
107
112
|
end
|
108
113
|
|
109
114
|
def mobileprovision?
|
110
|
-
File.exist?mobileprovision_path
|
115
|
+
File.exist?(mobileprovision_path)
|
111
116
|
end
|
112
117
|
|
113
118
|
def mobileprovision_path
|
@@ -139,24 +144,54 @@ module AppInfo
|
|
139
144
|
end
|
140
145
|
|
141
146
|
def info
|
142
|
-
@info ||= InfoPlist.new(
|
147
|
+
@info ||= InfoPlist.new(info_path)
|
148
|
+
end
|
149
|
+
|
150
|
+
def info_path
|
151
|
+
@info_path ||= File.join(app_path, 'Info.plist')
|
143
152
|
end
|
144
153
|
|
145
154
|
def app_path
|
146
155
|
@app_path ||= Dir.glob(File.join(contents, 'Payload', '*.app')).first
|
147
156
|
end
|
148
157
|
|
158
|
+
IPHONE_KEY = 'CFBundleIcons'
|
159
|
+
IPAD_KEY = 'CFBundleIcons~ipad'
|
160
|
+
|
161
|
+
def icons_path
|
162
|
+
return @icons_path if @icons_path
|
163
|
+
|
164
|
+
@icons_path = []
|
165
|
+
icon_keys.each do |name|
|
166
|
+
filenames = info.try(:[], name)
|
167
|
+
.try(:[], 'CFBundlePrimaryIcon')
|
168
|
+
.try(:[], 'CFBundleIconFiles')
|
169
|
+
|
170
|
+
next if filenames.nil? || filenames.empty?
|
171
|
+
|
172
|
+
filenames.each do |filename|
|
173
|
+
Dir.glob(File.join(app_path, "#{filename}*")).find_all.each do |file|
|
174
|
+
@icons_path << file
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
@icons_path
|
180
|
+
end
|
181
|
+
|
149
182
|
def clear!
|
150
183
|
return unless @contents
|
151
184
|
|
152
185
|
FileUtils.rm_rf(@contents)
|
153
186
|
|
154
187
|
@contents = nil
|
155
|
-
@icons = nil
|
156
188
|
@app_path = nil
|
157
|
-
@
|
158
|
-
@metadata_path = nil
|
189
|
+
@info_path = nil
|
159
190
|
@info = nil
|
191
|
+
@metadata_path = nil
|
192
|
+
@metadata = nil
|
193
|
+
@icons_path = nil
|
194
|
+
@icons = nil
|
160
195
|
end
|
161
196
|
|
162
197
|
def contents
|
@@ -165,18 +200,33 @@ module AppInfo
|
|
165
200
|
|
166
201
|
private
|
167
202
|
|
168
|
-
def
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
203
|
+
def build_icon_metadata(file, uncrush: true)
|
204
|
+
uncrushed_file = uncrush ? uncrush_png(file) : nil
|
205
|
+
|
206
|
+
{
|
207
|
+
name: File.basename(file),
|
208
|
+
file: file,
|
209
|
+
uncrushed_file: uncrushed_file,
|
210
|
+
dimensions: PngUncrush.dimensions(file)
|
211
|
+
}
|
212
|
+
end
|
213
|
+
|
214
|
+
# Uncrush png to normal png file (iOS)
|
215
|
+
def uncrush_png(src_file)
|
216
|
+
dest_file = Util.tempdir(src_file, prefix: 'uncrushed')
|
217
|
+
PngUncrush.decompress(src_file, dest_file)
|
218
|
+
File.exist?(dest_file) ? dest_file : nil
|
219
|
+
end
|
220
|
+
|
221
|
+
def icon_keys
|
222
|
+
@icon_keys ||= case device_type
|
223
|
+
when 'iPhone'
|
224
|
+
[IPHONE_KEY]
|
225
|
+
when 'iPad'
|
226
|
+
[IPAD_KEY]
|
227
|
+
when 'Universal'
|
228
|
+
[IPHONE_KEY, IPAD_KEY]
|
229
|
+
end
|
180
230
|
end
|
181
231
|
end
|
182
232
|
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'macho'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'forwardable'
|
6
|
+
require 'cfpropertylist'
|
7
|
+
|
8
|
+
module AppInfo
|
9
|
+
# MacOS App parser
|
10
|
+
class Macos
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
attr_reader :file
|
14
|
+
|
15
|
+
# macOS Export types
|
16
|
+
module ExportType
|
17
|
+
DEBUG = 'Debug'
|
18
|
+
RELEASE = 'Release'
|
19
|
+
APPSTORE = 'AppStore'
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(file)
|
23
|
+
@file = file
|
24
|
+
end
|
25
|
+
|
26
|
+
def size(human_size: false)
|
27
|
+
AppInfo::Util.file_size(@file, human_size)
|
28
|
+
end
|
29
|
+
|
30
|
+
def os
|
31
|
+
AppInfo::Platform::MACOS
|
32
|
+
end
|
33
|
+
alias file_type os
|
34
|
+
|
35
|
+
def_delegators :info, :macos?, :iphone?, :ipad?, :universal?, :build_version, :name,
|
36
|
+
:release_version, :identifier, :bundle_id, :display_name,
|
37
|
+
:bundle_name, :min_system_version, :min_os_version, :device_type
|
38
|
+
|
39
|
+
def_delegators :mobileprovision, :team_name, :team_identifier,
|
40
|
+
:profile_name, :expired_date
|
41
|
+
|
42
|
+
def distribution_name
|
43
|
+
"#{profile_name} - #{team_name}" if profile_name && team_name
|
44
|
+
end
|
45
|
+
|
46
|
+
def release_type
|
47
|
+
if stored?
|
48
|
+
ExportType::APPSTORE
|
49
|
+
elsif mobileprovision?
|
50
|
+
ExportType::RELEASE
|
51
|
+
else
|
52
|
+
ExportType::DEBUG
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def stored?
|
57
|
+
File.exist?(store_path)
|
58
|
+
end
|
59
|
+
|
60
|
+
def icons(convert: true)
|
61
|
+
return unless icon_file
|
62
|
+
|
63
|
+
data = {
|
64
|
+
name: File.basename(icon_file),
|
65
|
+
file: icon_file
|
66
|
+
}
|
67
|
+
|
68
|
+
convert_icns_to_png(data) if convert
|
69
|
+
data
|
70
|
+
end
|
71
|
+
|
72
|
+
def archs
|
73
|
+
return unless File.exist?(binary_path)
|
74
|
+
|
75
|
+
file = MachO.open(binary_path)
|
76
|
+
case file
|
77
|
+
when MachO::MachOFile
|
78
|
+
[file.cpusubtype]
|
79
|
+
else
|
80
|
+
file.machos.each_with_object([]) do |arch, obj|
|
81
|
+
obj << arch.cpusubtype
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
alias architectures archs
|
86
|
+
|
87
|
+
def hide_developer_certificates
|
88
|
+
mobileprovision.delete('DeveloperCertificates') if mobileprovision?
|
89
|
+
end
|
90
|
+
|
91
|
+
def mobileprovision
|
92
|
+
return unless mobileprovision?
|
93
|
+
|
94
|
+
@mobileprovision ||= MobileProvision.new(mobileprovision_path)
|
95
|
+
end
|
96
|
+
|
97
|
+
def mobileprovision?
|
98
|
+
File.exist?(mobileprovision_path)
|
99
|
+
end
|
100
|
+
|
101
|
+
def mobileprovision_path
|
102
|
+
@mobileprovision_path ||= File.join(app_path, 'Contents', 'embedded.provisionprofile')
|
103
|
+
end
|
104
|
+
|
105
|
+
def store_path
|
106
|
+
@store_path ||= File.join(app_path, 'Contents', '_MASReceipt', 'receipt')
|
107
|
+
end
|
108
|
+
|
109
|
+
def binary_path
|
110
|
+
return @binary_path if @binary_path
|
111
|
+
|
112
|
+
base_path = File.join(app_path, 'Contents', 'MacOS')
|
113
|
+
binary = info['CFBundleExecutable']
|
114
|
+
return File.join(base_path, binary) if binary
|
115
|
+
|
116
|
+
@binary_path ||= Dir.glob(File.join(base_path, '*')).first
|
117
|
+
end
|
118
|
+
|
119
|
+
def info
|
120
|
+
@info ||= InfoPlist.new(info_path)
|
121
|
+
end
|
122
|
+
|
123
|
+
def info_path
|
124
|
+
@info_path ||= File.join(app_path, 'Contents', 'Info.plist')
|
125
|
+
end
|
126
|
+
|
127
|
+
def app_path
|
128
|
+
@app_path ||= Dir.glob(File.join(contents, '*.app')).first
|
129
|
+
end
|
130
|
+
|
131
|
+
def clear!
|
132
|
+
return unless @contents
|
133
|
+
|
134
|
+
FileUtils.rm_rf(@contents)
|
135
|
+
|
136
|
+
@contents = nil
|
137
|
+
@app_path = nil
|
138
|
+
@binrary_path = nil
|
139
|
+
@info_path = nil
|
140
|
+
@info = nil
|
141
|
+
@icons = nil
|
142
|
+
end
|
143
|
+
|
144
|
+
def contents
|
145
|
+
@contents ||= Util.unarchive(@file, path: 'macos')
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def icon_file
|
151
|
+
return @icon_file if @icon_file
|
152
|
+
|
153
|
+
info.icons.each do |key|
|
154
|
+
next unless value = info[key]
|
155
|
+
|
156
|
+
file = File.join(app_path, 'Contents', 'Resources', "#{value}.icns")
|
157
|
+
next unless File.file?(file)
|
158
|
+
|
159
|
+
return @icon_file = file
|
160
|
+
end
|
161
|
+
|
162
|
+
@icon_file = nil
|
163
|
+
end
|
164
|
+
|
165
|
+
# Convert iconv to png file (macOS)
|
166
|
+
def convert_icns_to_png(data)
|
167
|
+
require 'icns'
|
168
|
+
require 'image_size'
|
169
|
+
|
170
|
+
data[:sets] ||= []
|
171
|
+
file = data[:file]
|
172
|
+
reader = Icns::Reader.new(file)
|
173
|
+
Icns::SIZE_TO_TYPE.each do |size, _|
|
174
|
+
dest_filename = "#{File.basename(file, '.icns')}_#{size}x#{size}.png"
|
175
|
+
dest_file = Util.tempdir(File.join(File.dirname(file), dest_filename), prefix: 'converted')
|
176
|
+
next unless icon_data = reader.image(size: size)
|
177
|
+
|
178
|
+
File.write(dest_file, icon_data, encoding: Encoding::BINARY)
|
179
|
+
|
180
|
+
data[:sets] << {
|
181
|
+
name: File.basename(dest_filename),
|
182
|
+
file: dest_file,
|
183
|
+
dimensions: ImageSize.path(dest_file).size
|
184
|
+
}
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
require 'openssl'
|
4
4
|
require 'cfpropertylist'
|
5
|
-
require 'app_info/util'
|
6
5
|
|
7
6
|
module AppInfo
|
8
7
|
# .mobileprovision file parser
|
@@ -29,10 +28,10 @@ module AppInfo
|
|
29
28
|
def platforms
|
30
29
|
return unless platforms = mobileprovision.try(:[], 'Platform')
|
31
30
|
|
32
|
-
platforms.map
|
31
|
+
platforms.map do |v|
|
33
32
|
v = 'macOS' if v == 'OSX'
|
34
33
|
v.downcase.to_sym
|
35
|
-
|
34
|
+
end
|
36
35
|
end
|
37
36
|
|
38
37
|
def platform
|
@@ -122,11 +121,9 @@ module AppInfo
|
|
122
121
|
# Related link: https://developer.apple.com/support/app-capabilities/
|
123
122
|
def enabled_capabilities
|
124
123
|
capabilities = []
|
125
|
-
if adhoc? || appstore?
|
126
|
-
capabilities << 'In-App Purchase' << 'GameKit'
|
127
|
-
end
|
124
|
+
capabilities << 'In-App Purchase' << 'GameKit' if adhoc? || appstore?
|
128
125
|
|
129
|
-
entitlements.each do |key,
|
126
|
+
entitlements.each do |key, _|
|
130
127
|
case key
|
131
128
|
when 'aps-environment'
|
132
129
|
capabilities << 'Push Notifications'
|
@@ -144,9 +141,11 @@ module AppInfo
|
|
144
141
|
capabilities << 'Network Extensions'
|
145
142
|
when 'com.apple.developer.networking.vpn.api'
|
146
143
|
capabilities << 'Personal VPN'
|
147
|
-
when 'com.apple.developer.healthkit',
|
144
|
+
when 'com.apple.developer.healthkit',
|
145
|
+
'com.apple.developer.healthkit.access'
|
148
146
|
capabilities << 'HealthKit' unless capabilities.include?('HealthKit')
|
149
|
-
when 'com.apple.developer.icloud-services',
|
147
|
+
when 'com.apple.developer.icloud-services',
|
148
|
+
'com.apple.developer.icloud-container-identifiers'
|
150
149
|
capabilities << 'iCloud' unless capabilities.include?('iCloud')
|
151
150
|
when 'com.apple.developer.in-app-payments'
|
152
151
|
capabilities << 'Apple Pay'
|
@@ -246,7 +245,7 @@ module AppInfo
|
|
246
245
|
end
|
247
246
|
|
248
247
|
def name
|
249
|
-
@raw.subject.to_a.find { |name, _, _| name == 'CN' }[1]
|
248
|
+
@raw.subject.to_a.find { |name, _, _| name == 'CN' }[1].force_encoding('UTF-8')
|
250
249
|
end
|
251
250
|
|
252
251
|
def created_date
|
data/lib/app_info/png_uncrush.rb
CHANGED
@@ -9,6 +9,7 @@ require 'stringio'
|
|
9
9
|
module AppInfo
|
10
10
|
class PngUncrush
|
11
11
|
class Error < StandardError; end
|
12
|
+
|
12
13
|
class FormatError < Error; end
|
13
14
|
|
14
15
|
class PngReader # :nodoc:
|
@@ -19,12 +20,13 @@ module AppInfo
|
|
19
20
|
|
20
21
|
def initialize(raw)
|
21
22
|
@io = if raw.is_a?(String)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
StringIO.new(raw)
|
24
|
+
elsif raw.respond_to?(:read) && raw.respond_to?(:eof?)
|
25
|
+
raw
|
26
|
+
else
|
27
|
+
raise ArgumentError, "expected data as String or an object
|
28
|
+
responding to read, got #{raw.class}"
|
29
|
+
end
|
28
30
|
|
29
31
|
@data = String.new
|
30
32
|
end
|
@@ -33,8 +35,8 @@ module AppInfo
|
|
33
35
|
@io.size
|
34
36
|
end
|
35
37
|
|
36
|
-
def unpack(
|
37
|
-
@io.unpack(
|
38
|
+
def unpack(format)
|
39
|
+
@io.unpack(format)
|
38
40
|
end
|
39
41
|
|
40
42
|
def header
|
@@ -82,7 +84,7 @@ module AppInfo
|
|
82
84
|
write_file(output, content)
|
83
85
|
rescue Zlib::DataError
|
84
86
|
# perhops thi is a normal png image file
|
85
|
-
|
87
|
+
false
|
86
88
|
end
|
87
89
|
|
88
90
|
private
|
@@ -93,9 +95,9 @@ module AppInfo
|
|
93
95
|
[].tap do |sections|
|
94
96
|
while pos < @io.size
|
95
97
|
type = @io[pos + 4, 4]
|
96
|
-
length = @io[pos, 4].
|
98
|
+
length = @io[pos, 4].unpack1('N')
|
97
99
|
data = @io[pos + 8, length]
|
98
|
-
crc = @io[pos + 8 + length, 4].
|
100
|
+
crc = @io[pos + 8 + length, 4].unpack1('N')
|
99
101
|
pos += length + 12
|
100
102
|
|
101
103
|
if type == 'CgBI'
|
@@ -104,14 +106,14 @@ module AppInfo
|
|
104
106
|
end
|
105
107
|
|
106
108
|
if type == 'IHDR'
|
107
|
-
width = data[0, 4].
|
108
|
-
height = data[4, 4].
|
109
|
+
width = data[0, 4].unpack1('N')
|
110
|
+
height = data[4, 4].unpack1('N')
|
109
111
|
return [width, height] if dimensions
|
110
112
|
end
|
111
113
|
|
112
114
|
break if type == 'IEND'
|
113
115
|
|
114
|
-
if type == 'IDAT' && sections
|
116
|
+
if type == 'IDAT' && sections&.last&.first == 'IDAT'
|
115
117
|
# Append to the previous IDAT
|
116
118
|
sections.last[1] += length
|
117
119
|
sections.last[2] += data
|
@@ -123,46 +125,43 @@ module AppInfo
|
|
123
125
|
end
|
124
126
|
|
125
127
|
def write_file(path, content)
|
126
|
-
File.
|
127
|
-
file.puts content
|
128
|
-
end
|
129
|
-
|
128
|
+
File.write(path, content, encoding: Encoding::BINARY)
|
130
129
|
true
|
131
130
|
end
|
132
131
|
|
133
132
|
def _remap(sections)
|
134
|
-
|
133
|
+
new_png = String.new(@io.header)
|
135
134
|
sections.map do |(type, length, data, crc, width, height)|
|
136
135
|
if type == 'IDAT'
|
137
|
-
|
138
|
-
data = inflate(data[0,
|
136
|
+
buff_size = width * height * 4 + height
|
137
|
+
data = inflate(data[0, buff_size])
|
139
138
|
# duplicate the content of old data at first to avoid creating too many string objects
|
140
139
|
newdata = String.new(data)
|
141
140
|
pos = 0
|
142
141
|
|
143
|
-
(0...height).each do |
|
142
|
+
(0...height).each do |_|
|
144
143
|
newdata[pos] = data[pos, 1]
|
145
144
|
pos += 1
|
146
|
-
(0...width).each do |
|
147
|
-
newdata[pos+0] = data[pos+2, 1]
|
148
|
-
newdata[pos+1] = data[pos+1, 1]
|
149
|
-
newdata[pos+2] = data[pos+0, 1]
|
150
|
-
newdata[pos+3] = data[pos+3, 1]
|
145
|
+
(0...width).each do |_|
|
146
|
+
newdata[pos + 0] = data[pos + 2, 1]
|
147
|
+
newdata[pos + 1] = data[pos + 1, 1]
|
148
|
+
newdata[pos + 2] = data[pos + 0, 1]
|
149
|
+
newdata[pos + 3] = data[pos + 3, 1]
|
151
150
|
pos += 4
|
152
151
|
end
|
153
152
|
end
|
154
153
|
|
155
154
|
data = deflate(newdata)
|
156
155
|
length = data.length
|
157
|
-
crc = Zlib
|
158
|
-
crc = Zlib
|
156
|
+
crc = Zlib.crc32(type)
|
157
|
+
crc = Zlib.crc32(data, crc)
|
159
158
|
crc = (crc + 0x100000000) % 0x100000000
|
160
159
|
end
|
161
160
|
|
162
|
-
|
161
|
+
new_png += [length].pack('N') + type + (data if length.positive?) + [crc].pack('N')
|
163
162
|
end
|
164
163
|
|
165
|
-
|
164
|
+
new_png
|
166
165
|
end
|
167
166
|
|
168
167
|
def inflate(data)
|