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.
@@ -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
@@ -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
- return false
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].unpack('N')[0]
98
+ length = @io[pos, 4].unpack1('N')
97
99
  data = @io[pos + 8, length]
98
- crc = @io[pos + 8 + length, 4].unpack('N')[0]
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].unpack("N")[0]
108
- height = data[4, 4].unpack("N")[0]
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.size > 0 && sections.last.first == 'IDAT'
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
- newPNG = String.new(@io.header)
136
+ new_png = String.new(@io.header)
135
137
  sections.map do |(type, length, data, crc, width, height)|
136
138
  if type == 'IDAT'
137
- bufSize = width * height * 4 + height
138
- data = inflate(data[0, bufSize])
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 |y|
145
+ (0...height).each do |_|
144
146
  newdata[pos] = data[pos, 1]
145
147
  pos += 1
146
- (0...width).each do |x|
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::crc32(type)
158
- crc = Zlib::crc32(data, crc)
159
+ crc = Zlib.crc32(type)
160
+ crc = Zlib.crc32(data, crc)
159
161
  crc = (crc + 0x100000000) % 0x100000000
160
162
  end
161
163
 
162
- 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')
163
165
  end
164
166
 
165
- newPNG
167
+ new_png
166
168
  end
167
169
 
168
170
  def inflate(data)
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'uuidtools'
4
4
  require 'rexml/document'
5
- require 'app_info/util'
6
5
 
7
6
  module AppInfo
8
7
  # Proguard parser
@@ -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, humanable)
50
+ def self.file_size(file, human_size)
20
51
  file_size = File.size(file)
21
- humanable ? size_to_humanable(file_size) : file_size
52
+ human_size ? size_to_human_size(file_size) : file_size
22
53
  end
23
54
 
24
- def self.size_to_humanable(number)
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