app-info 2.8.4 → 3.0.0.beta1

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +12 -7
  3. data/.github/workflows/ci.yml +3 -1
  4. data/.rubocop.yml +31 -11
  5. data/CHANGELOG.md +30 -2
  6. data/Gemfile +7 -1
  7. data/README.md +74 -9
  8. data/Rakefile +11 -0
  9. data/app_info.gemspec +12 -3
  10. data/lib/app_info/aab.rb +58 -39
  11. data/lib/app_info/android/signature.rb +114 -0
  12. data/lib/app_info/android/signatures/base.rb +49 -0
  13. data/lib/app_info/android/signatures/info.rb +152 -0
  14. data/lib/app_info/android/signatures/v1.rb +59 -0
  15. data/lib/app_info/android/signatures/v2.rb +117 -0
  16. data/lib/app_info/android/signatures/v3.rb +127 -0
  17. data/lib/app_info/android/signatures/v4.rb +14 -0
  18. data/lib/app_info/apk.rb +43 -46
  19. data/lib/app_info/certificate.rb +181 -0
  20. data/lib/app_info/const.rb +41 -0
  21. data/lib/app_info/core_ext/object/try.rb +3 -1
  22. data/lib/app_info/core_ext/string/inflector.rb +2 -0
  23. data/lib/app_info/dsym/debug_info.rb +72 -0
  24. data/lib/app_info/dsym/macho.rb +55 -0
  25. data/lib/app_info/dsym.rb +27 -134
  26. data/lib/app_info/error.rb +8 -0
  27. data/lib/app_info/file.rb +23 -0
  28. data/lib/app_info/helper/archive.rb +37 -0
  29. data/lib/app_info/helper/file_size.rb +25 -0
  30. data/lib/app_info/helper/generate_class.rb +29 -0
  31. data/lib/app_info/helper/protobuf.rb +12 -0
  32. data/lib/app_info/helper/signatures.rb +229 -0
  33. data/lib/app_info/helper.rb +5 -126
  34. data/lib/app_info/info_plist.rb +14 -6
  35. data/lib/app_info/ipa/framework.rb +4 -4
  36. data/lib/app_info/ipa.rb +41 -36
  37. data/lib/app_info/macos.rb +34 -26
  38. data/lib/app_info/mobile_provision.rb +19 -30
  39. data/lib/app_info/pe.rb +226 -0
  40. data/lib/app_info/png_uncrush.rb +5 -4
  41. data/lib/app_info/proguard.rb +11 -17
  42. data/lib/app_info/protobuf/manifest.rb +16 -9
  43. data/lib/app_info/protobuf/models/Configuration_pb.rb +1 -0
  44. data/lib/app_info/protobuf/models/README.md +7 -0
  45. data/lib/app_info/protobuf/models/Resources_pb.rb +2 -0
  46. data/lib/app_info/protobuf/resources.rb +5 -5
  47. data/lib/app_info/version.rb +1 -1
  48. data/lib/app_info.rb +91 -42
  49. metadata +46 -35
data/lib/app_info/dsym.rb CHANGED
@@ -1,78 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'macho'
3
+ require 'app_info/dsym/debug_info'
4
4
 
5
5
  module AppInfo
6
6
  # DSYM parser
7
- class DSYM
7
+ class DSYM < File
8
8
  include Helper::Archive
9
9
 
10
- attr_reader :file
11
-
12
- def initialize(file)
13
- @file = file
14
- end
15
-
16
10
  def file_type
17
- Platform::DSYM
18
- end
19
-
20
- def object
21
- @object ||= File.basename(app_path)
22
- end
23
-
24
- def macho_type
25
- @macho_type ||= ::MachO.open(app_path)
26
- end
27
-
28
- def machos
29
- @machos ||= case macho_type
30
- when ::MachO::MachOFile
31
- [MachO.new(macho_type, File.size(app_path))]
32
- else
33
- size = macho_type.fat_archs.each_with_object([]) do |arch, obj|
34
- obj << arch.size
35
- end
36
-
37
- machos = []
38
- macho_type.machos.each_with_index do |file, i|
39
- machos << MachO.new(file, size[i])
40
- end
41
- machos
42
- end
43
- end
44
-
45
- def release_version
46
- info.try(:[], 'CFBundleShortVersionString')
11
+ Format::DSYM
47
12
  end
48
13
 
49
- def build_version
50
- info.try(:[], 'CFBundleVersion')
14
+ def each_file(&block)
15
+ files.each { |file| block.call(file) }
51
16
  end
52
17
 
53
- def identifier
54
- info.try(:[], 'CFBundleIdentifier').sub('com.apple.xcode.dsym.', '')
55
- end
56
- alias bundle_id identifier
57
-
58
- def info
59
- return nil unless File.exist?(info_path)
60
-
61
- @info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: info_path).value)
62
- end
63
-
64
- def info_path
65
- @info_path ||= File.join(contents, 'Contents', 'Info.plist')
66
- end
67
-
68
- def app_path
69
- unless @app_path
70
- path = File.join(contents, 'Contents', 'Resources', 'DWARF')
71
- name = Dir.entries(path).reject { |f| ['.', '..'].include?(f) }.first
72
- @app_path = File.join(path, name)
18
+ def files
19
+ @files ||= Dir.children(contents).each_with_object([]) do |file, obj|
20
+ obj << DebugInfo.new(::File.join(contents, file))
73
21
  end
74
-
75
- @app_path
76
22
  end
77
23
 
78
24
  def clear!
@@ -81,85 +27,32 @@ module AppInfo
81
27
  FileUtils.rm_rf(@contents)
82
28
 
83
29
  @contents = nil
84
- @app_path = nil
85
- @info = nil
86
- @object = nil
87
- @macho_type = nil
30
+ @files = nil
88
31
  end
89
32
 
90
33
  def contents
91
- unless @contents
92
- if File.directory?(@file)
93
- @contents = @file
94
- else
95
- dsym_dir = nil
96
- @contents = unarchive(@file, path: 'dsym') do |path, zip_file|
97
- zip_file.each do |f|
98
- unless dsym_dir
99
- dsym_dir = f.name
100
- # fix filename is xxx.app.dSYM/Contents
101
- dsym_dir = dsym_dir.split('/')[0] if dsym_dir.include?('/')
102
- end
34
+ @contents ||= lambda {
35
+ return @file if ::File.directory?(@file)
103
36
 
104
- f_path = File.join(path, f.name)
105
- FileUtils.mkdir_p(File.dirname(f_path))
106
- f.extract(f_path) unless File.exist?(f_path)
107
- end
108
- end
37
+ dsym_filenames = []
38
+ unarchive(@file, prefix: 'dsym') do |base_path, zip_file|
39
+ zip_file.each do |entry|
40
+ file_path = entry.name
41
+ next unless file_path.downcase.include?('.dsym/contents/')
42
+ next if ::File.basename(file_path).start_with?('.')
109
43
 
110
- @contents = File.join(@contents, dsym_dir)
111
- end
112
- end
113
-
114
- @contents
115
- end
116
-
117
- # DSYM Mach-O
118
- class MachO
119
- include Helper::HumanFileSize
120
-
121
- def initialize(file, size = 0)
122
- @file = file
123
- @size = size
124
- end
125
-
126
- def cpu_name
127
- @file.cpusubtype
128
- end
129
-
130
- def cpu_type
131
- @file.cputype
132
- end
133
-
134
- def type
135
- @file.filetype
136
- end
137
-
138
- def size(human_size: false)
139
- return number_to_human_size(@size) if human_size
140
-
141
- @size
142
- end
143
-
144
- def uuid
145
- @file[:LC_UUID][0].uuid_string
146
- end
147
- alias debug_id uuid
44
+ dsym_filename = file_path.split('/').select { |f| f.downcase.end_with?('.dsym') }.last
45
+ dsym_filenames << dsym_filename unless dsym_filenames.include?(dsym_filename)
148
46
 
149
- def header
150
- @header ||= @file.header
151
- end
47
+ unless file_path.start_with?(dsym_filename)
48
+ file_path = file_path.split('/')[1..-1].join('/')
49
+ end
152
50
 
153
- def to_h
154
- {
155
- uuid: uuid,
156
- type: type,
157
- cpu_name: cpu_name,
158
- cpu_type: cpu_type,
159
- size: size,
160
- human_size: size(human_size: true)
161
- }
162
- end
51
+ dest_path = ::File.join(base_path, file_path)
52
+ entry.extract(dest_path) unless ::File.exist?(dest_path)
53
+ end
54
+ end
55
+ }.call
163
56
  end
164
57
  end
165
58
  end
@@ -9,5 +9,13 @@ module AppInfo
9
9
 
10
10
  class NotFoundError < Error; end
11
11
 
12
+ class NotFoundWinBinraryError < NotFoundError; end
13
+
14
+ class ProtobufParseError < Error; end
15
+
16
+ class UnknownFileTypeError < Error; end
17
+
18
+ # @deprecated Correct to the new {UnknownFileTypeError} class because typo.
19
+ # It will remove since 2.7.0.
12
20
  class UnkownFileTypeError < Error; end
13
21
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # AppInfo base file
4
+ module AppInfo
5
+ class File
6
+ attr_reader :file, :logger
7
+
8
+ def initialize(file, logger: AppInfo.logger)
9
+ @file = file
10
+ @logger = logger
11
+ end
12
+
13
+ # @abstract Subclass and override {#file_type} to implement
14
+ def file_type
15
+ Platform::UNKNOWN
16
+ end
17
+
18
+ # @abstract Subclass and override {#size} to implement
19
+ def size(human_size: false)
20
+ raise NotImplementedError, ".#{__method__} method implantation required in #{self.class}"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zip'
4
+ require 'tmpdir'
5
+ require 'fileutils'
6
+ require 'securerandom'
7
+
8
+ module AppInfo::Helper
9
+ module Archive
10
+ # Unarchive zip file
11
+ #
12
+ # source: https://github.com/soffes/lagunitas/blob/master/lib/lagunitas/ipa.rb
13
+ def unarchive(file, prefix:, dest_path: '/tmp')
14
+ base_path = Dir.mktmpdir("appinfo-#{prefix}", dest_path)
15
+ Zip::File.open(file) do |zip_file|
16
+ if block_given?
17
+ yield base_path, zip_file
18
+ else
19
+ zip_file.each do |f|
20
+ f_path = ::File.join(base_path, f.name)
21
+ FileUtils.mkdir_p(::File.dirname(f_path))
22
+ zip_file.extract(f, f_path) unless ::File.exist?(f_path)
23
+ end
24
+ end
25
+ end
26
+
27
+ base_path
28
+ end
29
+
30
+ def tempdir(file, prefix:, system: false)
31
+ base_path = system ? '/tmp' : ::File.dirname(file)
32
+ full_prefix = "appinfo-#{prefix}-#{::File.basename(file, '.*')}"
33
+ dest_path = Dir.mktmpdir(full_prefix, base_path)
34
+ ::File.join(dest_path, ::File.basename(file))
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo::Helper
4
+ module HumanFileSize
5
+ def file_to_human_size(file, human_size:)
6
+ number = ::File.size(file)
7
+ human_size ? number_to_human_size(number) : number
8
+ end
9
+
10
+ FILE_SIZE_UNITS = %w[B KB MB GB TB].freeze
11
+
12
+ def number_to_human_size(number)
13
+ if number.to_i < 1024
14
+ exponent = 0
15
+ else
16
+ max_exp = FILE_SIZE_UNITS.size - 1
17
+ exponent = (Math.log(number) / Math.log(1024)).to_i
18
+ exponent = max_exp if exponent > max_exp
19
+ number = format('%<number>.2f', number: (number / (1024**exponent.to_f)))
20
+ end
21
+
22
+ "#{number} #{FILE_SIZE_UNITS[exponent]}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo::Helper
4
+ module GenerateClass
5
+ def create_class(klass_name, parent_class, namespace:)
6
+ klass = Class.new(parent_class) do
7
+ yield if block_given?
8
+ end
9
+
10
+ name = namespace.to_s.empty? ? klass_name : "#{namespace}::#{klass_name}"
11
+ if Object.const_get(namespace).const_defined?(klass_name)
12
+ Object.const_get(namespace).const_get(klass_name)
13
+ elsif Object.const_defined?(name)
14
+ Object.const_get(name)
15
+ else
16
+ Object.const_get(namespace).const_set(klass_name, klass)
17
+ end
18
+ end
19
+
20
+ def define_instance_method(key, value)
21
+ instance_variable_set("@#{key}", value)
22
+ self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
23
+ def #{key}
24
+ @#{key}
25
+ end
26
+ RUBY
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo::Helper
4
+ module Protobuf
5
+ def reference_segments(value)
6
+ new_value = value.is_a?(Aapt::Pb::Reference) ? value.name : value
7
+ return new_value.split('/', 2) if new_value.include?('/')
8
+
9
+ [nil, new_value]
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo::Helper
4
+ # Binary IO Block Helper
5
+ module IOBlock
6
+ def length_prefix_block(
7
+ io, size: AppInfo::Android::Signature::UINT32_SIZE,
8
+ raw: false, ignore_left_size_precheck: false
9
+ )
10
+ offset = io.size - io.pos
11
+ if offset < AppInfo::Android::Signature::UINT32_SIZE
12
+ raise SecurityError,
13
+ 'Remaining buffer too short to contain length of length-prefixed field.'
14
+ end
15
+
16
+ size = io.read(size).unpack1('I')
17
+ raise SecurityError, 'Negative length' if size.negative?
18
+
19
+ if !ignore_left_size_precheck && size > io.size
20
+ message = "Underflow while reading length-prefixed value. #{size} > #{io.size}"
21
+ raise SecurityError, message
22
+ end
23
+
24
+ raw_data = io.read(size)
25
+ raw ? raw_data : StringIO.new(raw_data)
26
+ end
27
+
28
+ # Only use for uint32 length-prefixed block
29
+ def loop_length_prefix_io(
30
+ io, name:, max_bytes: nil, logger: nil, raw: false,
31
+ ignore_left_size_precheck: false, &block
32
+ )
33
+ index = 0
34
+ until io.eof?
35
+ logger&.debug "#{name} count ##{index}"
36
+ buffer = length_prefix_block(
37
+ io,
38
+ raw: raw,
39
+ ignore_left_size_precheck: ignore_left_size_precheck
40
+ )
41
+
42
+ left_bytes_check(buffer, max_bytes, SecurityError) do |left_bytes|
43
+ "#{name} too short: #{left_bytes} < #{max_bytes}"
44
+ end
45
+
46
+ block.call(buffer)
47
+ index += 1
48
+ end
49
+ end
50
+
51
+ def left_bytes_check(io, max_bytes, exception, message = nil, &block)
52
+ return if max_bytes.nil?
53
+
54
+ left_bytes = io.size - io.pos
55
+ return left_bytes if left_bytes.zero?
56
+
57
+ message ||= if block_given?
58
+ block.call(left_bytes)
59
+ else
60
+ "IO too short: #{offset} < #{max_bytes}"
61
+ end
62
+
63
+ raise exception, message if left_bytes < max_bytes
64
+
65
+ left_bytes
66
+ end
67
+ end
68
+
69
+ # Signature Block helper
70
+ module Signatures
71
+ def singers_block(block_id)
72
+ info = AppInfo::Android::Signature::Info.new(@version, @parser, logger)
73
+ raise SecurityError, 'ZIP64 APK not supported' if info.zip64?
74
+
75
+ info.signers(block_id)
76
+ end
77
+
78
+ def signed_data_certs(io)
79
+ certificates = []
80
+ loop_length_prefix_io(io, name: 'Certificates', raw: true) do |cert_data|
81
+ certificates << AppInfo::Certificate.parse(cert_data)
82
+ end
83
+ certificates
84
+ end
85
+
86
+ def signed_data_digests(io)
87
+ content_digests = {}
88
+ loop_length_prefix_io(
89
+ io,
90
+ name: 'Digests',
91
+ max_bytes: AppInfo::Android::Signature::UINT64_SIZE
92
+ ) do |digest|
93
+ algorithm = digest.read(AppInfo::Android::Signature::UINT32_SIZE).unpack('C*')
94
+ digest_name = algorithm_match(algorithm)
95
+ next unless digest_name
96
+
97
+ content = length_prefix_block(digest)
98
+ content_digests[digest_name] = {
99
+ id: algorithm,
100
+ content: content
101
+ }
102
+ end
103
+
104
+ content_digests
105
+ end
106
+
107
+ # FIXME: this code not work, need fix.
108
+ def verify_additional_attrs(attrs, _certs)
109
+ loop_length_prefix_io(
110
+ attrs, name: 'Additional Attributes', ignore_left_size_precheck: true
111
+ ) do |attr|
112
+ id = attr.read(AppInfo::Android::Signature::UINT32_SIZE)
113
+ logger.debug "ID #{id} / #{id.size} / #{id.unpack('H*')} / #{id.unpack('C*')}"
114
+ if id.unpack('C*') == AppInfo::Helper::Algorithm::SIG_STRIPPING_PROTECTION_ATTR_ID
115
+ offset = attr.size - attr.pos
116
+ if offset < AppInfo::Android::Signature::UINT32_SIZE
117
+ raise SecurityError,
118
+ "V2 Signature Scheme Stripping Protection Attribute value too small. Expected #{UINT32_SIZE} bytes, but found #{offset}"
119
+ end
120
+
121
+ # value = attr.read(UINT32_SIZE).unpack1('I')
122
+ if @version == AppInfo::Android::Signature::Version::V3
123
+ raise SecurityError,
124
+ 'V2 signature indicates APK is signed using APK Signature Scheme v3, but none was found. Signature stripped?'
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ def signature_algorithms(signatures)
131
+ algorithems = []
132
+ loop_length_prefix_io(
133
+ signatures,
134
+ name: 'Signature Algorithms',
135
+ max_bytes: AppInfo::Android::Signature::UINT64_SIZE,
136
+ logger: logger
137
+ ) do |signature|
138
+ algorithm = signature.read(AppInfo::Android::Signature::UINT32_SIZE).unpack('C*')
139
+ digest = algorithm_match(algorithm)
140
+ next unless digest
141
+
142
+ signature = length_prefix_block(signature, raw: true)
143
+ algorithems << {
144
+ id: algorithm,
145
+ digest: digest,
146
+ signature: signature
147
+ }
148
+ end
149
+
150
+ algorithems
151
+ end
152
+ end
153
+
154
+ # Signature Algorithm helper
155
+ module Algorithm
156
+ # Signature certificate identifiers
157
+ SIG_RSA_PSS_WITH_SHA256 = [0x01, 0x01, 0x00, 0x00].freeze # 0x0101
158
+ SIG_RSA_PSS_WITH_SHA512 = [0x02, 0x01, 0x00, 0x00].freeze # 0x0102
159
+ SIG_RSA_PKCS1_V1_5_WITH_SHA256 = [0x03, 0x01, 0x00, 0x00].freeze # 0x0103
160
+ SIG_RSA_PKCS1_V1_5_WITH_SHA512 = [0x04, 0x01, 0x00, 0x00].freeze # 0x0104
161
+ SIG_ECDSA_WITH_SHA256 = [0x01, 0x02, 0x00, 0x00].freeze # 0x0201
162
+ SIG_ECDSA_WITH_SHA512 = [0x02, 0x02, 0x00, 0x00].freeze # 0x0202
163
+ SIG_DSA_WITH_SHA256 = [0x01, 0x03, 0x00, 0x00].freeze # 0x0301
164
+ SIG_VERITY_RSA_PKCS1_V1_5_WITH_SHA256 = [0x21, 0x04, 0x00, 0x00].freeze # 0x0421
165
+ SIG_VERITY_ECDSA_WITH_SHA256 = [0x23, 0x04, 0x00, 0x00].freeze # 0x0423
166
+ SIG_VERITY_DSA_WITH_SHA256 = [0x25, 0x04, 0x00, 0x00].freeze # 0x0425
167
+
168
+ SIG_STRIPPING_PROTECTION_ATTR_ID = [0x0d, 0xf0, 0xef, 0xbe].freeze # 0xbeeff00d
169
+
170
+ def best_algorithem(algorithems)
171
+ methods = algorithems.map { |algorithem| algorithem[:method] }
172
+ best_method = methods.max { |a, b| algorithem_priority(a) <=> algorithem_priority(b) }
173
+ best_method_index = methods.index(best_method)
174
+ algorithems[best_method_index]
175
+ end
176
+
177
+ def compare_algorithem(source, target)
178
+ case algorithem_priority(source) <=> algorithem_priority(target)
179
+ when -1
180
+ target
181
+ else
182
+ source
183
+ end
184
+ end
185
+
186
+ def algorithem_priority(algorithm)
187
+ case algorithm
188
+ when SIG_RSA_PSS_WITH_SHA256,
189
+ SIG_RSA_PKCS1_V1_5_WITH_SHA256,
190
+ SIG_ECDSA_WITH_SHA256,
191
+ SIG_DSA_WITH_SHA256
192
+ 1
193
+ when SIG_RSA_PSS_WITH_SHA512,
194
+ SIG_RSA_PKCS1_V1_5_WITH_SHA512,
195
+ SIG_ECDSA_WITH_SHA512
196
+ 2
197
+ when SIG_VERITY_RSA_PKCS1_V1_5_WITH_SHA256,
198
+ SIG_VERITY_ECDSA_WITH_SHA256,
199
+ SIG_VERITY_DSA_WITH_SHA256
200
+ 3
201
+ end
202
+ end
203
+
204
+ def algorithm_method(algorithm)
205
+ case algorithm
206
+ when SIG_RSA_PSS_WITH_SHA256, SIG_RSA_PSS_WITH_SHA512,
207
+ SIG_RSA_PKCS1_V1_5_WITH_SHA256, SIG_RSA_PKCS1_V1_5_WITH_SHA512,
208
+ SIG_VERITY_RSA_PKCS1_V1_5_WITH_SHA256
209
+ :rsa
210
+ when SIG_ECDSA_WITH_SHA256, SIG_ECDSA_WITH_SHA512, SIG_VERITY_ECDSA_WITH_SHA256
211
+ :ec
212
+ when SIG_DSA_WITH_SHA256, SIG_VERITY_DSA_WITH_SHA256
213
+ :dsa
214
+ end
215
+ end
216
+
217
+ def algorithm_match(algorithm)
218
+ case algorithm
219
+ when SIG_RSA_PSS_WITH_SHA256, SIG_RSA_PKCS1_V1_5_WITH_SHA256,
220
+ SIG_ECDSA_WITH_SHA256, SIG_DSA_WITH_SHA256,
221
+ SIG_VERITY_RSA_PKCS1_V1_5_WITH_SHA256, SIG_VERITY_ECDSA_WITH_SHA256,
222
+ SIG_VERITY_DSA_WITH_SHA256
223
+ 'SHA256'
224
+ when SIG_RSA_PSS_WITH_SHA512, SIG_RSA_PKCS1_V1_5_WITH_SHA512, SIG_ECDSA_WITH_SHA512
225
+ 'SHA512'
226
+ end
227
+ end
228
+ end
229
+ end
@@ -1,128 +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
- FileUtils.mkdir_p(dest_path, mode: 0_700)
89
- dest_file
90
- end
91
- end
92
-
93
- module Defines
94
- def create_class(klass_name, parent_class, namespace:)
95
- klass = Class.new(parent_class) do
96
- yield if block_given?
97
- end
98
-
99
- name = namespace.to_s.empty? ? klass_name : "#{namespace}::#{klass_name}"
100
- if Object.const_get(namespace).const_defined?(klass_name)
101
- Object.const_get(namespace).const_get(klass_name)
102
- elsif Object.const_defined?(name)
103
- Object.const_get(name)
104
- else
105
- Object.const_get(namespace).const_set(klass_name, klass)
106
- end
107
- end
108
-
109
- def define_instance_method(key, value)
110
- instance_variable_set("@#{key}", value)
111
- self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
112
- def #{key}
113
- @#{key}
114
- end
115
- RUBY
116
- end
117
- end
118
-
119
- module ReferenceParser
120
- def reference_segments(value)
121
- new_value = value.is_a?(Aapt::Pb::Reference) ? value.name : value
122
- return new_value.split('/', 2) if new_value.include?('/')
123
-
124
- [nil, new_value]
125
- end
126
- end
127
- end
128
- 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'