app-info 2.5.1 → 2.6.0
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 -2
- data/Gemfile +6 -0
- data/README.md +110 -50
- data/app_info.gemspec +6 -3
- data/exe/app-info +12 -0
- data/lib/app_info/apk.rb +17 -22
- 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} +8 -9
- data/lib/app_info/png_uncrush.rb +31 -26
- 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 +51 -12
- data/.travis.yml +0 -9
- data/lib/app_info/ipa/info_plist.rb +0 -161
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)
|
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'
|
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
|
@@ -80,6 +82,9 @@ module AppInfo
|
|
80
82
|
return false unless content
|
81
83
|
|
82
84
|
write_file(output, content)
|
85
|
+
rescue Zlib::DataError
|
86
|
+
# perhops thi is a normal png image file
|
87
|
+
false
|
83
88
|
end
|
84
89
|
|
85
90
|
private
|
@@ -90,9 +95,9 @@ module AppInfo
|
|
90
95
|
[].tap do |sections|
|
91
96
|
while pos < @io.size
|
92
97
|
type = @io[pos + 4, 4]
|
93
|
-
length = @io[pos, 4].
|
98
|
+
length = @io[pos, 4].unpack1('N')
|
94
99
|
data = @io[pos + 8, length]
|
95
|
-
crc = @io[pos + 8 + length, 4].
|
100
|
+
crc = @io[pos + 8 + length, 4].unpack1('N')
|
96
101
|
pos += length + 12
|
97
102
|
|
98
103
|
if type == 'CgBI'
|
@@ -101,14 +106,14 @@ module AppInfo
|
|
101
106
|
end
|
102
107
|
|
103
108
|
if type == 'IHDR'
|
104
|
-
width = data[0, 4].
|
105
|
-
height = data[4, 4].
|
109
|
+
width = data[0, 4].unpack1('N')
|
110
|
+
height = data[4, 4].unpack1('N')
|
106
111
|
return [width, height] if dimensions
|
107
112
|
end
|
108
113
|
|
109
114
|
break if type == 'IEND'
|
110
115
|
|
111
|
-
if type == 'IDAT' && sections
|
116
|
+
if type == 'IDAT' && sections&.last&.first == 'IDAT'
|
112
117
|
# Append to the previous IDAT
|
113
118
|
sections.last[1] += length
|
114
119
|
sections.last[2] += data
|
@@ -128,38 +133,38 @@ module AppInfo
|
|
128
133
|
end
|
129
134
|
|
130
135
|
def _remap(sections)
|
131
|
-
|
136
|
+
new_png = String.new(@io.header)
|
132
137
|
sections.map do |(type, length, data, crc, width, height)|
|
133
138
|
if type == 'IDAT'
|
134
|
-
|
135
|
-
data = inflate(data[0,
|
139
|
+
buff_size = width * height * 4 + height
|
140
|
+
data = inflate(data[0, buff_size])
|
136
141
|
# duplicate the content of old data at first to avoid creating too many string objects
|
137
142
|
newdata = String.new(data)
|
138
143
|
pos = 0
|
139
144
|
|
140
|
-
(0...height).each do |
|
145
|
+
(0...height).each do |_|
|
141
146
|
newdata[pos] = data[pos, 1]
|
142
147
|
pos += 1
|
143
|
-
(0...width).each do |
|
144
|
-
newdata[pos+0] = data[pos+2, 1]
|
145
|
-
newdata[pos+1] = data[pos+1, 1]
|
146
|
-
newdata[pos+2] = data[pos+0, 1]
|
147
|
-
newdata[pos+3] = data[pos+3, 1]
|
148
|
+
(0...width).each do |_|
|
149
|
+
newdata[pos + 0] = data[pos + 2, 1]
|
150
|
+
newdata[pos + 1] = data[pos + 1, 1]
|
151
|
+
newdata[pos + 2] = data[pos + 0, 1]
|
152
|
+
newdata[pos + 3] = data[pos + 3, 1]
|
148
153
|
pos += 4
|
149
154
|
end
|
150
155
|
end
|
151
156
|
|
152
157
|
data = deflate(newdata)
|
153
158
|
length = data.length
|
154
|
-
crc = Zlib
|
155
|
-
crc = Zlib
|
159
|
+
crc = Zlib.crc32(type)
|
160
|
+
crc = Zlib.crc32(data, crc)
|
156
161
|
crc = (crc + 0x100000000) % 0x100000000
|
157
162
|
end
|
158
163
|
|
159
|
-
|
164
|
+
new_png += [length].pack('N') + type + (data if length.positive?) + [crc].pack('N')
|
160
165
|
end
|
161
166
|
|
162
|
-
|
167
|
+
new_png
|
163
168
|
end
|
164
169
|
|
165
170
|
def inflate(data)
|