app-info 2.5.4 → 2.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +29 -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 +16 -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} +8 -9
- data/lib/app_info/png_uncrush.rb +29 -27
- 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
@@ -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
|
@@ -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
|
@@ -131,38 +133,38 @@ module AppInfo
|
|
131
133
|
end
|
132
134
|
|
133
135
|
def _remap(sections)
|
134
|
-
|
136
|
+
new_png = String.new(@io.header)
|
135
137
|
sections.map do |(type, length, data, crc, width, height)|
|
136
138
|
if type == 'IDAT'
|
137
|
-
|
138
|
-
data = inflate(data[0,
|
139
|
+
buff_size = width * height * 4 + height
|
140
|
+
data = inflate(data[0, buff_size])
|
139
141
|
# duplicate the content of old data at first to avoid creating too many string objects
|
140
142
|
newdata = String.new(data)
|
141
143
|
pos = 0
|
142
144
|
|
143
|
-
(0...height).each do |
|
145
|
+
(0...height).each do |_|
|
144
146
|
newdata[pos] = data[pos, 1]
|
145
147
|
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]
|
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]
|
151
153
|
pos += 4
|
152
154
|
end
|
153
155
|
end
|
154
156
|
|
155
157
|
data = deflate(newdata)
|
156
158
|
length = data.length
|
157
|
-
crc = Zlib
|
158
|
-
crc = Zlib
|
159
|
+
crc = Zlib.crc32(type)
|
160
|
+
crc = Zlib.crc32(data, crc)
|
159
161
|
crc = (crc + 0x100000000) % 0x100000000
|
160
162
|
end
|
161
163
|
|
162
|
-
|
164
|
+
new_png += [length].pack('N') + type + (data if length.positive?) + [crc].pack('N')
|
163
165
|
end
|
164
166
|
|
165
|
-
|
167
|
+
new_png
|
166
168
|
end
|
167
169
|
|
168
170
|
def inflate(data)
|
data/lib/app_info/proguard.rb
CHANGED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'irb'
|
4
|
+
|
5
|
+
module AppInfo
|
6
|
+
class Shell
|
7
|
+
PREFIX = "app-info (#{AppInfo::VERSION})"
|
8
|
+
|
9
|
+
PROMPT = {
|
10
|
+
PROMPT_I: "#{PREFIX}> ",
|
11
|
+
PROMPT_S: "#{PREFIX}> ",
|
12
|
+
PROMPT_C: "#{PREFIX}> ",
|
13
|
+
PROMPT_N: "#{PREFIX}> ",
|
14
|
+
RETURN: "=> %s\n"
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def run
|
19
|
+
setup
|
20
|
+
|
21
|
+
irb = IRB::Irb.new
|
22
|
+
irb.run
|
23
|
+
end
|
24
|
+
|
25
|
+
def setup
|
26
|
+
IRB.setup nil
|
27
|
+
|
28
|
+
IRB.conf[:PROMPT][:APPINFO] = PROMPT
|
29
|
+
IRB.conf[:PROMPT_MODE] = :APPINFO
|
30
|
+
IRB.conf[:AUTO_INDENT] = true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/app_info/util.rb
CHANGED
@@ -5,23 +5,54 @@ require 'fileutils'
|
|
5
5
|
require 'securerandom'
|
6
6
|
|
7
7
|
module AppInfo
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
class NotFoundError < Error; end
|
11
|
+
|
12
|
+
class UnkownFileTypeError < Error; end
|
13
|
+
|
14
|
+
# App Platform
|
15
|
+
module Platform
|
16
|
+
MACOS = 'macOS'
|
17
|
+
IOS = 'iOS'
|
18
|
+
ANDROID = 'Android'
|
19
|
+
DSYM = 'dSYM'
|
20
|
+
PROGUARD = 'Proguard'
|
21
|
+
end
|
22
|
+
|
23
|
+
# Device Type
|
24
|
+
module Device
|
25
|
+
MACOS = 'macOS'
|
26
|
+
IPHONE = 'iPhone'
|
27
|
+
IPAD = 'iPad'
|
28
|
+
UNIVERSAL = 'Universal'
|
29
|
+
end
|
30
|
+
|
31
|
+
# Icon Key
|
32
|
+
ICON_KEYS = {
|
33
|
+
AppInfo::Device::IPHONE => ['CFBundleIcons'],
|
34
|
+
AppInfo::Device::IPAD => ['CFBundleIcons~ipad'],
|
35
|
+
AppInfo::Device::UNIVERSAL => ['CFBundleIcons', 'CFBundleIcons~ipad'],
|
36
|
+
AppInfo::Device::MACOS => %w[CFBundleIconFile CFBundleIconName]
|
37
|
+
}.freeze
|
38
|
+
|
39
|
+
FILE_SIZE_UNITS = %w[B KB MB GB TB].freeze
|
40
|
+
|
8
41
|
# AppInfo Util
|
9
42
|
module Util
|
10
|
-
FILE_SIZE_UNITS = %w[B KB MB GB TB]
|
11
|
-
|
12
43
|
def self.format_key(key)
|
13
44
|
key = key.to_s
|
14
45
|
return key unless key.include?('_')
|
15
46
|
|
16
|
-
key.split('_').map(&:capitalize).join
|
47
|
+
key.split('_').map(&:capitalize).join
|
17
48
|
end
|
18
49
|
|
19
|
-
def self.file_size(file,
|
50
|
+
def self.file_size(file, human_size)
|
20
51
|
file_size = File.size(file)
|
21
|
-
|
52
|
+
human_size ? size_to_human_size(file_size) : file_size
|
22
53
|
end
|
23
54
|
|
24
|
-
def self.
|
55
|
+
def self.size_to_human_size(number)
|
25
56
|
if number.to_i < 1024
|
26
57
|
exponent = 0
|
27
58
|
else
|
@@ -54,5 +85,14 @@ module AppInfo
|
|
54
85
|
|
55
86
|
root_path
|
56
87
|
end
|
88
|
+
|
89
|
+
def self.tempdir(file, prefix:)
|
90
|
+
dest_path ||= File.join(File.dirname(file), prefix)
|
91
|
+
dest_file = File.join(dest_path, File.basename(file))
|
92
|
+
|
93
|
+
Dir.mkdir(dest_path, 0_700) unless Dir.exist?(dest_path)
|
94
|
+
|
95
|
+
dest_file
|
96
|
+
end
|
57
97
|
end
|
58
98
|
end
|