app-info 2.8.5 → 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 +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