app-info 2.5.2 → 2.6.1
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 +41 -1
- 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 +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 +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
@@ -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'
|
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)
|
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].freeze
|
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
|