app-info 2.8.5 → 3.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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 +22 -0
  6. data/Gemfile +7 -1
  7. data/README.md +64 -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 +7 -1
  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 +1 -2
  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 +88 -45
  49. metadata +46 -35
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo
4
+ # Certificate wrapper for
5
+ # {https://docs.ruby-lang.org/en/3.0/OpenSSL/X509/Certificate.html OpenSSL::X509::Certifiate}.
6
+ class Certificate
7
+ # Parse Raw data into X509 cerificate wrapper
8
+ # @param [String] certificate raw data
9
+ def self.parse(data)
10
+ cert = OpenSSL::X509::Certificate.new(data)
11
+ new(cert)
12
+ end
13
+
14
+ # @param [OpenSSL::X509::Certificate] certificate
15
+ def initialize(cert)
16
+ @cert = cert
17
+ end
18
+
19
+ # return version of certificate
20
+ # @param [String] prefix
21
+ # @param [Integer] base
22
+ # @return [String] version
23
+ def version(prefix: 'v', base: 1)
24
+ "#{prefix}#{raw.version + base}"
25
+ end
26
+
27
+ # return serial of certificate
28
+ # @param [Integer] base
29
+ # @param [Symbol] transform avaiables in :lower, :upper
30
+ # @param [String] prefix
31
+ # @return [String] serial
32
+ def serial(base = 10, transform: :lower, prefix: nil)
33
+ serial = raw.serial.to_s(base)
34
+ serial = transform == :lower ? serial.downcase : serial.upcase
35
+ return serial unless prefix
36
+
37
+ "#{prefix}#{serial}"
38
+ end
39
+
40
+ # return issuer from DN, similar to {#subject}.
41
+ #
42
+ # Example:
43
+ #
44
+ # @param [Symbol] format avaiables in `:to_a`, `:to_s` and `:raw`
45
+ # @return [Array, String, OpenSSL::X509::Name] the object converted into the expected format.
46
+ def issuer(format: :raw)
47
+ convert_cert_name(raw.issuer, format: format)
48
+ end
49
+
50
+ # return subject from DN, similar to {#issuer}.
51
+ # @param [Symbol] format avaiables in `:to_a`, `:to_s` and `:raw`
52
+ # @return [Array, String, OpenSSL::X509::Name] the object converted into the expected format.
53
+ def subject(format: :raw)
54
+ convert_cert_name(raw.subject, format: format)
55
+ end
56
+
57
+ def created_at
58
+ raw.not_before
59
+ end
60
+
61
+ def expired_at
62
+ raw.not_after
63
+ end
64
+
65
+ def expired?
66
+ expired_at < Time.now.utc
67
+ end
68
+
69
+ def format
70
+ :x509
71
+ end
72
+
73
+ # return algorithm digest
74
+ #
75
+ # OpenSSL supported digests:
76
+ #
77
+ # -blake2b512 -blake2s256 -md4
78
+ # -md5 -md5-sha1 -mdc2
79
+ # -ripemd -ripemd160 -rmd160
80
+ # -sha1 -sha224 -sha256
81
+ # -sha3-224 -sha3-256 -sha3-384
82
+ # -sha3-512 -sha384 -sha512
83
+ # -sha512-224 -sha512-256 -shake128
84
+ # -shake256 -sm3 -ssl3-md5
85
+ # -ssl3-sha1 -whirlpool
86
+ def digest
87
+ signature_algorithm = raw.signature_algorithm
88
+
89
+ case signature_algorithm
90
+ when /md5/
91
+ :md5
92
+ when /sha1/
93
+ :sha1
94
+ when /sha224/
95
+ :sha224
96
+ when /sha256/
97
+ :sha256
98
+ when /sha512/
99
+ :sha512
100
+ else
101
+ # Android signature no need the others
102
+ signature_algorithm.to_sym
103
+ end
104
+ end
105
+
106
+ # return algorithm name of public key
107
+ def algorithm
108
+ case public_key
109
+ when OpenSSL::PKey::RSA then :rsa
110
+ when OpenSSL::PKey::DSA then :dsa
111
+ when OpenSSL::PKey::DH then :dh
112
+ when OpenSSL::PKey::EC then :ec
113
+ end
114
+ end
115
+
116
+ # return size of public key
117
+ def size
118
+ case public_key
119
+ when OpenSSL::PKey::RSA
120
+ public_key.n.num_bits
121
+ when OpenSSL::PKey::DSA, OpenSSL::PKey::DH
122
+ public_key.p.num_bits
123
+ when OpenSSL::PKey::EC
124
+ raise NotImplementedError, "key size for #{public_key.inspect} not implemented"
125
+ end
126
+ end
127
+
128
+ # return fingerprint of certificate
129
+ def fingerprint(name = :sha256, transform: :lower, delimiter: nil)
130
+ digest = OpenSSL::Digest.new(name.to_s.upcase)
131
+ # digest = case name.to_sym
132
+ # when :sha1
133
+ # OpenSSL::Digest::SHA1.new
134
+ # when :sha224
135
+ # OpenSSL::Digest::SHA224.new
136
+ # when :sha384
137
+ # OpenSSL::Digest::SHA384.new
138
+ # when :sha512
139
+ # OpenSSL::Digest::SHA512.new
140
+ # when :md5
141
+ # OpenSSL::Digest::MD5.new
142
+ # else
143
+ # OpenSSL::Digest::SHA256.new
144
+ # end
145
+
146
+ digest.update(raw.to_der)
147
+ fingerprint = digest.to_s
148
+ fingerprint = fingerprint.upcase if transform.to_sym == :upper
149
+ return fingerprint unless delimiter
150
+
151
+ fingerprint.scan(/../).join(delimiter)
152
+ end
153
+
154
+ # Orginal OpenSSL X509 certificate
155
+ def raw
156
+ @cert
157
+ end
158
+
159
+ private
160
+
161
+ def convert_cert_name(name, format:)
162
+ data = name.to_a
163
+ case format
164
+ when :to_a
165
+ data.map { |k, v, _| [k, v] }
166
+ when :to_s
167
+ data.map { |k, v, _| "#{k}=#{v}" }.join(' ')
168
+ else
169
+ name
170
+ end
171
+ end
172
+
173
+ def method_missing(method, *args, &block)
174
+ @cert.send(method.to_sym, *args, &block) || super
175
+ end
176
+
177
+ def respond_to_missing?(method, *args)
178
+ @cert.include?(method.to_sym) || super
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo
4
+ # Full Format
5
+ module Format
6
+ # iOS
7
+ IPA = :ipa
8
+ INFOPLIST = :infoplist
9
+ MOBILEPROVISION = :mobileprovision
10
+ DSYM = :dsym
11
+
12
+ # Android
13
+ APK = :apk
14
+ AAB = :aab
15
+ PROGUARD = :proguard
16
+
17
+ # macOS
18
+ MACOS = :macos
19
+
20
+ # Windows
21
+ PE = :pe
22
+
23
+ UNKNOWN = :unknown
24
+ end
25
+
26
+ # Platform
27
+ module Platform
28
+ WINDOWS = 'Windows'
29
+ MACOS = 'macOS'
30
+ IOS = 'iOS'
31
+ ANDROID = 'Android'
32
+ end
33
+
34
+ # Apple Device Type
35
+ module Device
36
+ MACOS = 'macOS'
37
+ IPHONE = 'iPhone'
38
+ IPAD = 'iPad'
39
+ UNIVERSAL = 'Universal'
40
+ end
41
+ end
@@ -4,7 +4,8 @@
4
4
  # Copy from https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/object/try.rb
5
5
 
6
6
  module AppInfo
7
- module Tryable # :nodoc:
7
+ # @!visibility private
8
+ module Tryable
8
9
  ##
9
10
  # :method: try
10
11
  #
@@ -107,6 +108,7 @@ module AppInfo
107
108
  end
108
109
  end
109
110
 
111
+ # @!visibility private
110
112
  class Object
111
113
  include AppInfo::Tryable
112
114
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppInfo
4
+ # @!visibility private
4
5
  module Inflector
5
6
  def ai_snakecase
6
7
  gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
@@ -30,6 +31,7 @@ module AppInfo
30
31
  end
31
32
  end
32
33
 
34
+ # @!visibility private
33
35
  class String
34
36
  include AppInfo::Inflector
35
37
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'app_info/dsym/macho'
4
+
5
+ module AppInfo
6
+ class DSYM < File
7
+ # DSYM Debug Information Format Struct
8
+ class DebugInfo
9
+ attr_reader :path
10
+
11
+ def initialize(path)
12
+ @path = path
13
+ end
14
+
15
+ def object
16
+ @object ||= ::File.basename(bin_path)
17
+ end
18
+
19
+ def macho_type
20
+ @macho_type ||= ::MachO.open(bin_path)
21
+ end
22
+
23
+ def machos
24
+ @machos ||= case macho_type
25
+ when ::MachO::MachOFile
26
+ [MachO.new(macho_type, ::File.size(bin_path))]
27
+ else
28
+ size = macho_type.fat_archs.each_with_object([]) do |arch, obj|
29
+ obj << arch.size
30
+ end
31
+
32
+ machos = []
33
+ macho_type.machos.each_with_index do |file, i|
34
+ machos << MachO.new(file, size[i])
35
+ end
36
+ machos
37
+ end
38
+ end
39
+
40
+ def release_version
41
+ info.try(:[], 'CFBundleShortVersionString')
42
+ end
43
+
44
+ def build_version
45
+ info.try(:[], 'CFBundleVersion')
46
+ end
47
+
48
+ def identifier
49
+ info.try(:[], 'CFBundleIdentifier').sub('com.apple.xcode.dsym.', '')
50
+ end
51
+ alias bundle_id identifier
52
+
53
+ def info
54
+ return nil unless ::File.exist?(info_path)
55
+
56
+ @info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: info_path).value)
57
+ end
58
+
59
+ def info_path
60
+ @info_path ||= ::File.join(path, 'Contents', 'Info.plist')
61
+ end
62
+
63
+ def bin_path
64
+ @bin_path ||= lambda {
65
+ dwarf_path = ::File.join(path, 'Contents', 'Resources', 'DWARF')
66
+ name = Dir.children(dwarf_path)[0]
67
+ ::File.join(dwarf_path, name)
68
+ }.call
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'macho'
4
+
5
+ module AppInfo
6
+ class DSYM < File
7
+ # Mach-O Struct
8
+ class MachO
9
+ include Helper::HumanFileSize
10
+
11
+ def initialize(file, size = 0)
12
+ @file = file
13
+ @size = size
14
+ end
15
+
16
+ def cpu_name
17
+ @file.cpusubtype
18
+ end
19
+
20
+ def cpu_type
21
+ @file.cputype
22
+ end
23
+
24
+ def type
25
+ @file.filetype
26
+ end
27
+
28
+ def size(human_size: false)
29
+ return number_to_human_size(@size) if human_size
30
+
31
+ @size
32
+ end
33
+
34
+ def uuid
35
+ @file[:LC_UUID][0].uuid_string
36
+ end
37
+ alias debug_id uuid
38
+
39
+ def header
40
+ @header ||= @file.header
41
+ end
42
+
43
+ def to_h
44
+ {
45
+ uuid: uuid,
46
+ type: type,
47
+ cpu_name: cpu_name,
48
+ cpu_type: cpu_type,
49
+ size: size,
50
+ human_size: size(human_size: true)
51
+ }
52
+ end
53
+ end
54
+ end
55
+ end
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,7 +9,13 @@ module AppInfo
9
9
 
10
10
  class NotFoundError < Error; end
11
11
 
12
- class UnkownFileTypeError < Error; end
12
+ class NotFoundWinBinraryError < NotFoundError; end
13
13
 
14
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.
20
+ class UnkownFileTypeError < Error; end
15
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