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.
@@ -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 { |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)
@@ -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].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, 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