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.
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(humanable = false)
32
- AppInfo::Util.file_size(@file, humanable)
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, :icons, :min_sdk_version, :device_type
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? ? true : false
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(app_path)
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
- @metadata = nil
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 icons_root_path
169
- iphone = 'CFBundleIcons'
170
- ipad = 'CFBundleIcons~ipad'
171
-
172
- case device_type
173
- when 'iPhone'
174
- [iphone]
175
- when 'iPad'
176
- [ipad]
177
- when 'Universal'
178
- [iphone, ipad]
179
- end
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 { |v|
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, value|
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', 'com.apple.developer.healthkit.access'
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', 'com.apple.developer.icloud-container-identifiers'
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'
@@ -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
- StringIO.new(raw)
23
- elsif raw.respond_to?(:read) && raw.respond_to?(:eof?)
24
- raw
25
- else
26
- raise ArgumentError, "expected data as String or an object responding to read, got #{raw.class}"
27
- end
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(a)
37
- @io.unpack(a)
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].unpack('N')[0]
98
+ length = @io[pos, 4].unpack1('N')
94
99
  data = @io[pos + 8, length]
95
- crc = @io[pos + 8 + length, 4].unpack('N')[0]
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].unpack("N")[0]
105
- height = data[4, 4].unpack("N")[0]
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.size > 0 && sections.last.first == 'IDAT'
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
- newPNG = String.new(@io.header)
136
+ new_png = String.new(@io.header)
132
137
  sections.map do |(type, length, data, crc, width, height)|
133
138
  if type == 'IDAT'
134
- bufSize = width * height * 4 + height
135
- data = inflate(data[0, bufSize])
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 |y|
145
+ (0...height).each do |_|
141
146
  newdata[pos] = data[pos, 1]
142
147
  pos += 1
143
- (0...width).each do |x|
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::crc32(type)
155
- crc = Zlib::crc32(data, crc)
159
+ crc = Zlib.crc32(type)
160
+ crc = Zlib.crc32(data, crc)
156
161
  crc = (crc + 0x100000000) % 0x100000000
157
162
  end
158
163
 
159
- newPNG += [length].pack("N") + type + (data if length > 0) + [crc].pack("N")
164
+ new_png += [length].pack('N') + type + (data if length.positive?) + [crc].pack('N')
160
165
  end
161
166
 
162
- newPNG
167
+ new_png
163
168
  end
164
169
 
165
170
  def inflate(data)