app-info 2.8.2 → 3.0.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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +12 -7
  3. data/.github/workflows/ci.yml +7 -5
  4. data/.github/workflows/create_release.yml +15 -0
  5. data/.rubocop.yml +33 -11
  6. data/CHANGELOG.md +107 -1
  7. data/Gemfile +10 -5
  8. data/README.md +82 -15
  9. data/Rakefile +11 -0
  10. data/app_info.gemspec +14 -5
  11. data/lib/app_info/aab.rb +76 -110
  12. data/lib/app_info/android/signature.rb +114 -0
  13. data/lib/app_info/android/signatures/base.rb +53 -0
  14. data/lib/app_info/android/signatures/info.rb +158 -0
  15. data/lib/app_info/android/signatures/v1.rb +63 -0
  16. data/lib/app_info/android/signatures/v2.rb +121 -0
  17. data/lib/app_info/android/signatures/v3.rb +131 -0
  18. data/lib/app_info/android/signatures/v4.rb +18 -0
  19. data/lib/app_info/android.rb +181 -0
  20. data/lib/app_info/apk.rb +77 -112
  21. data/lib/app_info/apple.rb +192 -0
  22. data/lib/app_info/certificate.rb +176 -0
  23. data/lib/app_info/const.rb +76 -0
  24. data/lib/app_info/core_ext/object/try.rb +3 -1
  25. data/lib/app_info/core_ext/string/inflector.rb +2 -0
  26. data/lib/app_info/dsym/debug_info.rb +81 -0
  27. data/lib/app_info/dsym/macho.rb +62 -0
  28. data/lib/app_info/dsym.rb +35 -135
  29. data/lib/app_info/error.rb +3 -1
  30. data/lib/app_info/file.rb +49 -0
  31. data/lib/app_info/helper/archive.rb +37 -0
  32. data/lib/app_info/helper/file_size.rb +25 -0
  33. data/lib/app_info/helper/generate_class.rb +29 -0
  34. data/lib/app_info/helper/protobuf.rb +12 -0
  35. data/lib/app_info/helper/signatures.rb +229 -0
  36. data/lib/app_info/helper.rb +5 -128
  37. data/lib/app_info/info_plist.rb +66 -29
  38. data/lib/app_info/ipa/framework.rb +4 -4
  39. data/lib/app_info/ipa.rb +61 -135
  40. data/lib/app_info/macos.rb +54 -102
  41. data/lib/app_info/mobile_provision.rb +66 -48
  42. data/lib/app_info/pe.rb +322 -0
  43. data/lib/app_info/png_uncrush.rb +25 -5
  44. data/lib/app_info/proguard.rb +39 -22
  45. data/lib/app_info/protobuf/manifest.rb +22 -11
  46. data/lib/app_info/protobuf/models/Configuration_pb.rb +1 -0
  47. data/lib/app_info/protobuf/models/README.md +8 -1
  48. data/lib/app_info/protobuf/models/Resources.proto +51 -0
  49. data/lib/app_info/protobuf/models/Resources_pb.rb +42 -0
  50. data/lib/app_info/protobuf/resources.rb +5 -5
  51. data/lib/app_info/version.rb +1 -1
  52. data/lib/app_info.rb +93 -43
  53. metadata +57 -37
@@ -1,130 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AppInfo
4
- # App Platform
5
- module Platform
6
- MACOS = 'macOS'
7
- IOS = 'iOS'
8
- ANDROID = 'Android'
9
- DSYM = 'dSYM'
10
- PROGUARD = 'Proguard'
11
- end
12
-
13
- # Device Type
14
- module Device
15
- MACOS = 'macOS'
16
- IPHONE = 'iPhone'
17
- IPAD = 'iPad'
18
- UNIVERSAL = 'Universal'
19
- end
20
-
21
- module AndroidDevice
22
- PHONE = 'Phone'
23
- TABLET = 'Tablet'
24
- WATCH = 'Watch'
25
- TV = 'Television'
26
- end
27
-
28
- # Icon Key
29
- ICON_KEYS = {
30
- Device::IPHONE => ['CFBundleIcons'],
31
- Device::IPAD => ['CFBundleIcons~ipad'],
32
- Device::UNIVERSAL => ['CFBundleIcons', 'CFBundleIcons~ipad'],
33
- Device::MACOS => %w[CFBundleIconFile CFBundleIconName]
34
- }.freeze
35
-
36
- module Helper
37
- module HumanFileSize
38
- def file_to_human_size(file, human_size:)
39
- number = File.size(file)
40
- human_size ? number_to_human_size(number) : number
41
- end
42
-
43
- FILE_SIZE_UNITS = %w[B KB MB GB TB].freeze
44
-
45
- def number_to_human_size(number)
46
- if number.to_i < 1024
47
- exponent = 0
48
- else
49
- max_exp = FILE_SIZE_UNITS.size - 1
50
- exponent = (Math.log(number) / Math.log(1024)).to_i
51
- exponent = max_exp if exponent > max_exp
52
- number = format('%<number>.2f', number: (number / (1024**exponent.to_f)))
53
- end
54
-
55
- "#{number} #{FILE_SIZE_UNITS[exponent]}"
56
- end
57
- end
58
-
59
- module Archive
60
- require 'zip'
61
- require 'fileutils'
62
- require 'securerandom'
63
-
64
- # Unarchive zip file
65
- #
66
- # source: https://github.com/soffes/lagunitas/blob/master/lib/lagunitas/ipa.rb
67
- def unarchive(file, path: nil)
68
- path = path ? "#{path}-" : ''
69
- root_path = "#{Dir.mktmpdir}/AppInfo-#{path}#{SecureRandom.hex}"
70
- Zip::File.open(file) do |zip_file|
71
- if block_given?
72
- yield root_path, zip_file
73
- else
74
- zip_file.each do |f|
75
- f_path = File.join(root_path, f.name)
76
- FileUtils.mkdir_p(File.dirname(f_path))
77
- zip_file.extract(f, f_path) unless File.exist?(f_path)
78
- end
79
- end
80
- end
81
-
82
- root_path
83
- end
84
-
85
- def tempdir(file, prefix:)
86
- dest_path ||= File.join(File.dirname(file), prefix)
87
- dest_file = File.join(dest_path, File.basename(file))
88
-
89
- Dir.mkdir(dest_path, 0_700) unless Dir.exist?(dest_path)
90
-
91
- dest_file
92
- end
93
- end
94
-
95
- module Defines
96
- def create_class(klass_name, parent_class, namespace:)
97
- klass = Class.new(parent_class) do
98
- yield if block_given?
99
- end
100
-
101
- name = namespace.to_s.empty? ? klass_name : "#{namespace}::#{klass_name}"
102
- if Object.const_get(namespace).const_defined?(klass_name)
103
- Object.const_get(namespace).const_get(klass_name)
104
- elsif Object.const_defined?(name)
105
- Object.const_get(name)
106
- else
107
- Object.const_get(namespace).const_set(klass_name, klass)
108
- end
109
- end
110
-
111
- def define_instance_method(key, value)
112
- instance_variable_set("@#{key}", value)
113
- self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
114
- def #{key}
115
- @#{key}
116
- end
117
- RUBY
118
- end
119
- end
120
-
121
- module ReferenceParser
122
- def reference_segments(value)
123
- new_value = value.is_a?(Aapt::Pb::Reference) ? value.name : value
124
- return new_value.split('/', 2) if new_value.include?('/')
125
-
126
- [nil, new_value]
127
- end
128
- end
129
- end
130
- end
3
+ require 'app_info/helper/archive'
4
+ require 'app_info/helper/file_size'
5
+ require 'app_info/helper/generate_class'
6
+ require 'app_info/helper/protobuf'
7
+ require 'app_info/helper/signatures'
@@ -6,97 +6,131 @@ require 'app_info/png_uncrush'
6
6
 
7
7
  module AppInfo
8
8
  # iOS Info.plist parser
9
- class InfoPlist
9
+ class InfoPlist < File
10
10
  extend Forwardable
11
11
 
12
- def initialize(file)
13
- @file = file
12
+ # Icon Key
13
+ ICON_KEYS = {
14
+ Device::IPHONE => ['CFBundleIcons'],
15
+ Device::IPAD => ['CFBundleIcons~ipad'],
16
+ Device::UNIVERSAL => ['CFBundleIcons', 'CFBundleIcons~ipad'],
17
+ Device::MACOS => %w[CFBundleIconFile CFBundleIconName]
18
+ }.freeze
19
+
20
+ # @return [Symbol] {Manufacturer}
21
+ def manufacturer
22
+ Manufacturer::APPLE
23
+ end
24
+
25
+ # @return [Symbol] {Platform}
26
+ def platform
27
+ case device
28
+ when Device::MACOS
29
+ Platform::MACOS
30
+ when Device::IPHONE, Device::IPAD, Device::UNIVERSAL
31
+ Platform::IOS
32
+ end
33
+ end
34
+
35
+ # @return [Symbol] {Device}
36
+ def device
37
+ if device_family == [1]
38
+ Device::IPHONE
39
+ elsif device_family == [2]
40
+ Device::IPAD
41
+ elsif device_family == [1, 2]
42
+ Device::UNIVERSAL
43
+ elsif !info.try(:[], 'DTSDKName').nil? || !info.try(:[], 'DTManufacturerName').nil?
44
+ Device::MACOS
45
+ else
46
+ raise NotImplementedError, "Unkonwn device: #{device_family}"
47
+ end
14
48
  end
15
49
 
50
+ # @return [String, nil]
16
51
  def version
17
52
  release_version || build_version
18
53
  end
19
54
 
55
+ # @return [String, nil]
20
56
  def build_version
21
57
  info.try(:[], 'CFBundleVersion')
22
58
  end
23
59
 
60
+ # @return [String, nil]
24
61
  def release_version
25
62
  info.try(:[], 'CFBundleShortVersionString')
26
63
  end
27
64
 
65
+ # @return [String, nil]
28
66
  def identifier
29
67
  info.try(:[], 'CFBundleIdentifier')
30
68
  end
31
69
  alias bundle_id identifier
32
70
 
71
+ # @return [String, nil]
33
72
  def name
34
73
  display_name || bundle_name
35
74
  end
36
75
 
76
+ # @return [String, nil]
37
77
  def display_name
38
78
  info.try(:[], 'CFBundleDisplayName')
39
79
  end
40
80
 
81
+ # @return [String, nil]
41
82
  def bundle_name
42
83
  info.try(:[], 'CFBundleName')
43
84
  end
44
85
 
86
+ # @return [String, nil]
45
87
  def min_os_version
46
88
  min_sdk_version || min_system_version
47
89
  end
48
90
 
49
- #
50
91
  # Extract the Minimum OS Version from the Info.plist (iOS Only)
51
- #
92
+ # @return [String, nil]
52
93
  def min_sdk_version
53
94
  info.try(:[], 'MinimumOSVersion')
54
95
  end
55
96
 
56
- #
57
97
  # Extract the Minimum OS Version from the Info.plist (macOS Only)
58
- #
98
+ # @return [String, nil]
59
99
  def min_system_version
60
100
  info.try(:[], 'LSMinimumSystemVersion')
61
101
  end
62
102
 
103
+ # @return [Array<String>]
63
104
  def icons
64
- @icons ||= ICON_KEYS[device_type]
65
- end
66
-
67
- def device_type
68
- device_family = info.try(:[], 'UIDeviceFamily')
69
- if device_family == [1]
70
- Device::IPHONE
71
- elsif device_family == [2]
72
- Device::IPAD
73
- elsif device_family == [1, 2]
74
- Device::UNIVERSAL
75
- elsif !info.try(:[], 'DTSDKName').nil? || !info.try(:[], 'DTPlatformName').nil?
76
- Device::MACOS
77
- end
105
+ @icons ||= ICON_KEYS[device]
78
106
  end
79
107
 
108
+ # @return [Boolean]
80
109
  def iphone?
81
- device_type == Device::IPHONE
110
+ device == Device::IPHONE
82
111
  end
83
112
 
113
+ # @return [Boolean]
84
114
  def ipad?
85
- device_type == Device::IPAD
115
+ device == Device::IPAD
86
116
  end
87
117
 
118
+ # @return [Boolean]
88
119
  def universal?
89
- device_type == Device::UNIVERSAL
120
+ device == Device::UNIVERSAL
90
121
  end
91
122
 
123
+ # @return [Boolean]
92
124
  def macos?
93
- device_type == Device::MACOS
125
+ device == Device::MACOS
94
126
  end
95
127
 
128
+ # @return [Array<String>]
96
129
  def device_family
97
130
  info.try(:[], 'UIDeviceFamily') || []
98
131
  end
99
132
 
133
+ # @return [String]
100
134
  def release_type
101
135
  if stored?
102
136
  'Store'
@@ -105,10 +139,13 @@ module AppInfo
105
139
  end
106
140
  end
107
141
 
142
+ # @return [String, nil]
108
143
  def [](key)
109
144
  info.try(:[], key.to_s)
110
145
  end
111
146
 
147
+ # @!method to_h
148
+ # @see CFPropertyList#to_h
112
149
  def_delegators :info, :to_h
113
150
 
114
151
  def method_missing(method_name, *args, &block)
@@ -126,17 +163,17 @@ module AppInfo
126
163
  private
127
164
 
128
165
  def info
129
- return unless File.file?(@file)
166
+ return unless ::File.file?(@file)
130
167
 
131
168
  @info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: @file).value)
132
169
  end
133
170
 
134
171
  def app_path
135
- @app_path ||= case device_type
172
+ @app_path ||= case device
136
173
  when Device::MACOS
137
- File.dirname(@file)
174
+ ::File.dirname(@file)
138
175
  else
139
- File.expand_path('../', @file)
176
+ ::File.expand_path('../', @file)
140
177
  end
141
178
  end
142
179
  end
@@ -8,7 +8,7 @@ module AppInfo
8
8
  extend Forwardable
9
9
 
10
10
  def self.parse(path, name = 'Frameworks')
11
- files = Dir.glob(File.join(path, name.to_s, '*'))
11
+ files = Dir.glob(::File.join(path, name.to_s, '*'))
12
12
  return [] if files.empty?
13
13
 
14
14
  files.sort.each_with_object([]) do |file, obj|
@@ -26,7 +26,7 @@ module AppInfo
26
26
  end
27
27
 
28
28
  def name
29
- File.basename(file)
29
+ ::File.basename(file)
30
30
  end
31
31
 
32
32
  def macho
@@ -37,11 +37,11 @@ module AppInfo
37
37
  end
38
38
 
39
39
  def lib?
40
- File.file?(file)
40
+ ::File.file?(file)
41
41
  end
42
42
 
43
43
  def info
44
- @info ||= InfoPlist.new(File.join(file, 'Info.plist'))
44
+ @info ||= InfoPlist.new(::File.join(file, 'Info.plist'))
45
45
  end
46
46
 
47
47
  def to_s
data/lib/app_info/ipa.rb CHANGED
@@ -6,179 +6,106 @@ require 'forwardable'
6
6
  require 'cfpropertylist'
7
7
 
8
8
  module AppInfo
9
- # IPA parser
10
- class IPA
11
- include Helper::HumanFileSize
12
- include Helper::Archive
13
- extend Forwardable
14
-
15
- attr_reader :file
16
-
17
- # iOS Export types
18
- module ExportType
19
- DEBUG = 'Debug'
20
- ADHOC = 'AdHoc'
21
- ENTERPRISE = 'Enterprise'
22
- RELEASE = 'Release'
23
- UNKOWN = nil
24
-
25
- INHOUSE = 'Enterprise' # Rename and Alias to enterprise
26
- end
27
-
28
- def initialize(file)
29
- @file = file
30
- end
31
-
32
- def size(human_size: false)
33
- file_to_human_size(@file, human_size: human_size)
34
- end
35
-
36
- def os
37
- Platform::IOS
38
- end
39
- alias file_type os
40
-
41
- def_delegators :info, :iphone?, :ipad?, :universal?, :build_version, :name,
42
- :release_version, :identifier, :bundle_id, :display_name,
43
- :bundle_name, :min_sdk_version, :min_os_version, :device_type
44
-
45
- def_delegators :mobileprovision, :devices, :team_name, :team_identifier,
46
- :profile_name, :expired_date
47
-
48
- def distribution_name
49
- "#{profile_name} - #{team_name}" if profile_name && team_name
50
- end
51
-
52
- def release_type
53
- if stored?
54
- ExportType::RELEASE
55
- else
56
- build_type
57
- end
58
- end
59
-
60
- def build_type
61
- if mobileprovision?
62
- if devices
63
- ExportType::ADHOC
64
- else
65
- ExportType::ENTERPRISE
66
- end
67
- else
68
- ExportType::DEBUG
69
- end
70
- end
71
-
72
- def archs
73
- return unless File.exist?(bundle_path)
74
-
75
- file = MachO.open(bundle_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
-
9
+ class IPA < Apple
10
+ # Full icons metadata
11
+ # @example
12
+ # aab.icons
13
+ # # => [
14
+ # # {
15
+ # # name: 'icon.png',
16
+ # # file: '/path/to/icon.png',
17
+ # # uncrushed_file: '/path/to/uncrushed_icon.png',
18
+ # # dimensions: [64, 64]
19
+ # # },
20
+ # # {
21
+ # # name: 'icon1.png',
22
+ # # file: '/path/to/icon1.png',
23
+ # # uncrushed_file: '/path/to/uncrushed_icon1.png',
24
+ # # dimensions: [120, 120]
25
+ # # }
26
+ # # ]
27
+ # @return [Array<Hash{Symbol => String, Array<Integer>}>] icons paths of icons
87
28
  def icons(uncrush: true)
88
29
  @icons ||= icons_path.each_with_object([]) do |file, obj|
89
30
  obj << build_icon_metadata(file, uncrush: uncrush)
90
31
  end
91
32
  end
92
33
 
34
+ # @return [Boolean]
93
35
  def stored?
94
36
  !!metadata?
95
37
  end
96
38
 
39
+ # @return [Array<Plugin>]
97
40
  def plugins
98
41
  @plugins ||= Plugin.parse(app_path)
99
42
  end
100
43
 
44
+ # @return [Array<Framework>]
101
45
  def frameworks
102
46
  @frameworks ||= Framework.parse(app_path)
103
47
  end
104
48
 
105
- def hide_developer_certificates
106
- mobileprovision.delete('DeveloperCertificates') if mobileprovision?
107
- end
108
-
109
- def mobileprovision
110
- return unless mobileprovision?
111
- return @mobileprovision if @mobileprovision
112
-
113
- @mobileprovision = MobileProvision.new(mobileprovision_path)
114
- end
115
-
116
- def mobileprovision?
117
- File.exist?(mobileprovision_path)
118
- end
119
-
49
+ # @return [String]
120
50
  def mobileprovision_path
121
51
  filename = 'embedded.mobileprovision'
122
- @mobileprovision_path ||= File.join(@file, filename)
123
- unless File.exist?(@mobileprovision_path)
124
- @mobileprovision_path = File.join(app_path, filename)
52
+ @mobileprovision_path ||= ::File.join(@file, filename)
53
+ unless ::File.exist?(@mobileprovision_path)
54
+ @mobileprovision_path = ::File.join(app_path, filename)
125
55
  end
126
56
 
127
57
  @mobileprovision_path
128
58
  end
129
59
 
60
+ # @return [CFPropertyList]
130
61
  def metadata
131
62
  return unless metadata?
132
63
 
133
64
  @metadata ||= CFPropertyList.native_types(CFPropertyList::List.new(file: metadata_path).value)
134
65
  end
135
66
 
67
+ # @return [Boolean]
136
68
  def metadata?
137
- File.exist?(metadata_path)
69
+ ::File.exist?(metadata_path)
138
70
  end
139
71
 
72
+ # @return [String]
140
73
  def metadata_path
141
- @metadata_path ||= File.join(contents, 'iTunesMetadata.plist')
74
+ @metadata_path ||= ::File.join(contents, 'iTunesMetadata.plist')
142
75
  end
143
76
 
144
- def bundle_path
145
- @bundle_path ||= File.join(app_path, info.bundle_name)
146
- end
147
-
148
- def info
149
- @info ||= InfoPlist.new(info_path)
77
+ # @return [String]
78
+ def binary_path
79
+ @binary_path ||= ::File.join(app_path, info.bundle_name)
150
80
  end
151
81
 
82
+ # @return [String]
152
83
  def info_path
153
- @info_path ||= File.join(app_path, 'Info.plist')
84
+ @info_path ||= ::File.join(app_path, 'Info.plist')
154
85
  end
155
86
 
87
+ # @return [String]
156
88
  def app_path
157
- @app_path ||= Dir.glob(File.join(contents, 'Payload', '*.app')).first
89
+ @app_path ||= Dir.glob(::File.join(contents, 'Payload', '*.app')).first
158
90
  end
159
91
 
160
- IPHONE_KEY = 'CFBundleIcons'
161
- IPAD_KEY = 'CFBundleIcons~ipad'
162
-
92
+ # @return [Array<String>]
163
93
  def icons_path
164
- return @icons_path if @icons_path
165
-
166
- @icons_path = []
167
- icon_keys.each do |name|
168
- filenames = info.try(:[], name)
169
- .try(:[], 'CFBundlePrimaryIcon')
170
- .try(:[], 'CFBundleIconFiles')
171
-
172
- next if filenames.nil? || filenames.empty?
173
-
174
- filenames.each do |filename|
175
- Dir.glob(File.join(app_path, "#{filename}*")).find_all.each do |file|
176
- @icons_path << file
94
+ @icons_path ||= lambda {
95
+ icon_keys.each_with_object([]) do |name, icons|
96
+ filenames = info.try(:[], name)
97
+ .try(:[], 'CFBundlePrimaryIcon')
98
+ .try(:[], 'CFBundleIconFiles')
99
+
100
+ next if filenames.nil? || filenames.empty?
101
+
102
+ filenames.each do |filename|
103
+ Dir.glob(::File.join(app_path, "#{filename}*")).find_all.each do |file|
104
+ icons << file
105
+ end
177
106
  end
178
107
  end
179
- end
180
-
181
- @icons_path
108
+ }.call
182
109
  end
183
110
 
184
111
  def clear!
@@ -196,17 +123,13 @@ module AppInfo
196
123
  @icons = nil
197
124
  end
198
125
 
199
- def contents
200
- @contents ||= unarchive(@file, path: 'ios')
201
- end
202
-
203
126
  private
204
127
 
205
128
  def build_icon_metadata(file, uncrush: true)
206
129
  uncrushed_file = uncrush ? uncrush_png(file) : nil
207
130
 
208
131
  {
209
- name: File.basename(file),
132
+ name: ::File.basename(file),
210
133
  file: file,
211
134
  uncrushed_file: uncrushed_file,
212
135
  dimensions: PngUncrush.dimensions(file)
@@ -217,16 +140,19 @@ module AppInfo
217
140
  def uncrush_png(src_file)
218
141
  dest_file = tempdir(src_file, prefix: 'uncrushed')
219
142
  PngUncrush.decompress(src_file, dest_file)
220
- File.exist?(dest_file) ? dest_file : nil
143
+ ::File.exist?(dest_file) ? dest_file : nil
221
144
  end
222
145
 
146
+ IPHONE_KEY = 'CFBundleIcons'
147
+ IPAD_KEY = 'CFBundleIcons~ipad'
148
+
223
149
  def icon_keys
224
- @icon_keys ||= case device_type
225
- when 'iPhone'
150
+ @icon_keys ||= case device
151
+ when Device::IPHONE
226
152
  [IPHONE_KEY]
227
- when 'iPad'
153
+ when Device::IPAD
228
154
  [IPAD_KEY]
229
- when 'Universal'
155
+ when Device::UNIVERSAL
230
156
  [IPHONE_KEY, IPAD_KEY]
231
157
  end
232
158
  end