app-info 2.5.4 → 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 +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
|