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
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'macho'
4
+ require 'fileutils'
5
+ require 'forwardable'
6
+ require 'cfpropertylist'
7
+
8
+ module AppInfo
9
+ # Apple base parser for ipa and macos file
10
+ class Apple < File
11
+ extend Forwardable
12
+ include Helper::HumanFileSize
13
+ include Helper::Archive
14
+
15
+ # Export types
16
+ module ExportType
17
+ # debug configuration
18
+ DEBUG = :debug
19
+ # adhoc configuration (iOS only)
20
+ ADHOC = :adhoc
21
+ # enterprise configuration (iOS only)
22
+ ENTERPRISE = :enterprise
23
+ # release configuration
24
+ RELEASE = :release
25
+ # release configuration but download from App Store
26
+ APPSTORE = :appstore
27
+ end
28
+
29
+ # @return [Symbol] {Manufacturer}
30
+ def manufacturer
31
+ Manufacturer::APPLE
32
+ end
33
+
34
+ # return file size
35
+ # @example Read file size in integer
36
+ # aab.size # => 3618865
37
+ #
38
+ # @example Read file size in human readabale
39
+ # aab.size(human_size: true) # => '3.45 MB'
40
+ #
41
+ # @param [Boolean] human_size Convert integer value to human readable.
42
+ # @return [Integer, String]
43
+ def size(human_size: false)
44
+ file_to_human_size(@file, human_size: human_size)
45
+ end
46
+
47
+ # @!method device
48
+ # @see InfoPlist#device
49
+ # @!method platform
50
+ # @see InfoPlist#platform
51
+ # @!method iphone?
52
+ # @see InfoPlist#iphone?
53
+ # @!method ipad?
54
+ # @see InfoPlist#ipad?
55
+ # @!method universal?
56
+ # @see InfoPlist#universal?
57
+ # @!method build_version
58
+ # @see InfoPlist#build_version
59
+ # @!method name
60
+ # @see InfoPlist#name
61
+ # @!method release_version
62
+ # @see InfoPlist#release_version
63
+ # @!method identifier
64
+ # @see InfoPlist#identifier
65
+ # @!method bundle_id
66
+ # @see InfoPlist#bundle_id
67
+ # @!method display_name
68
+ # @see InfoPlist#display_name
69
+ # @!method bundle_name
70
+ # @see InfoPlist#bundle_name
71
+ # @!method min_sdk_version
72
+ # @see InfoPlist#min_sdk_version
73
+ # @!method min_os_version
74
+ # @see InfoPlist#min_os_version
75
+ def_delegators :info, :device, :platform, :iphone?, :ipad?, :universal?, :macos?,
76
+ :build_version, :name, :release_version, :identifier, :bundle_id,
77
+ :display_name, :bundle_name, :min_sdk_version, :min_os_version
78
+
79
+ # @!method devices
80
+ # @see MobileProvision#devices
81
+ # @!method team_name
82
+ # @see MobileProvision#team_name
83
+ # @!method team_identifier
84
+ # @see MobileProvision#team_identifier
85
+ # @!method profile_name
86
+ # @see MobileProvision#profile_name
87
+ # @!method expired_date
88
+ # @see MobileProvision#expired_date
89
+ def_delegators :mobileprovision, :devices, :team_name, :team_identifier,
90
+ :profile_name, :expired_date
91
+
92
+ # @return [String, nil]
93
+ def distribution_name
94
+ "#{profile_name} - #{team_name}" if profile_name && team_name
95
+ end
96
+
97
+ # @return [String]
98
+ def release_type
99
+ if stored?
100
+ ExportType::APPSTORE
101
+ else
102
+ build_type
103
+ end
104
+ end
105
+
106
+ # return iOS build configuration, not correct in macOS app.
107
+ # @return [String]
108
+ def build_type
109
+ if mobileprovision?
110
+ return ExportType::RELEASE if macos?
111
+
112
+ devices ? ExportType::ADHOC : ExportType::ENTERPRISE
113
+ else
114
+ ExportType::DEBUG
115
+ end
116
+ end
117
+
118
+ # @return [Array<MachO>, nil]
119
+ def archs
120
+ return unless ::File.exist?(binary_path)
121
+
122
+ file = MachO.open(binary_path)
123
+ case file
124
+ when MachO::MachOFile
125
+ [file.cpusubtype]
126
+ else
127
+ file.machos.each_with_object([]) do |arch, obj|
128
+ obj << arch.cpusubtype
129
+ end
130
+ end
131
+ end
132
+ alias architectures archs
133
+
134
+ # @abstract Subclass and override {#icons} to implement.
135
+ def icons(_uncrush: true)
136
+ not_implemented_error!(__method__)
137
+ end
138
+
139
+ # @abstract Subclass and override {#stored?} to implement.
140
+ def stored?
141
+ not_implemented_error!(__method__)
142
+ end
143
+
144
+ # force remove developer certificate data from {#mobileprovision} method
145
+ # @return [nil]
146
+ def hide_developer_certificates
147
+ mobileprovision.delete('DeveloperCertificates') if mobileprovision?
148
+ end
149
+
150
+ # @return [MobileProvision]
151
+ def mobileprovision
152
+ return unless mobileprovision?
153
+
154
+ @mobileprovision ||= MobileProvision.new(mobileprovision_path)
155
+ end
156
+
157
+ # @return [Boolean]
158
+ def mobileprovision?
159
+ ::File.exist?(mobileprovision_path)
160
+ end
161
+
162
+ # @abstract Subclass and override {#mobileprovision_path} to implement.
163
+ def mobileprovision_path
164
+ not_implemented_error!(__method__)
165
+ end
166
+
167
+ # @return [InfoPlist]
168
+ def info
169
+ @info ||= InfoPlist.new(info_path)
170
+ end
171
+
172
+ # @abstract Subclass and override {#info_path} to implement.
173
+ def info_path
174
+ not_implemented_error!(__method__)
175
+ end
176
+
177
+ # @abstract Subclass and override {#app_path} to implement.
178
+ def app_path
179
+ not_implemented_error!(__method__)
180
+ end
181
+
182
+ # @abstract Subclass and override {#clear!} to implement.
183
+ def clear!
184
+ not_implemented_error!(__method__)
185
+ end
186
+
187
+ # @return [String] contents path of contents
188
+ def contents
189
+ @contents ||= unarchive(@file, prefix: format.to_s)
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,176 @@
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
+ # @return [AppInfo::Certificate]
10
+ def self.parse(data)
11
+ cert = OpenSSL::X509::Certificate.new(data)
12
+ new(cert)
13
+ end
14
+
15
+ # @param [OpenSSL::X509::Certificate] certificate
16
+ def initialize(cert)
17
+ @cert = cert
18
+ end
19
+
20
+ # return version of certificate
21
+ # @param [String] prefix
22
+ # @param [Integer] base
23
+ # @return [String] version
24
+ def version(prefix: 'v', base: 1)
25
+ "#{prefix}#{raw.version + base}"
26
+ end
27
+
28
+ # return serial of certificate
29
+ # @param [Integer] base
30
+ # @param [Symbol] transform avaiables in :lower, :upper
31
+ # @param [String] prefix
32
+ # @return [String] serial
33
+ def serial(base = 10, transform: :lower, prefix: nil)
34
+ serial = raw.serial.to_s(base)
35
+ serial = transform == :lower ? serial.downcase : serial.upcase
36
+ return serial unless prefix
37
+
38
+ "#{prefix}#{serial}"
39
+ end
40
+
41
+ # return issuer from DN, similar to {#subject}.
42
+ #
43
+ # Example:
44
+ #
45
+ # @param [Symbol] format avaiables in `:to_a`, `:to_s` and `:raw`
46
+ # @return [Array, String, OpenSSL::X509::Name] the object converted into the expected format.
47
+ def issuer(format: :raw)
48
+ convert_cert_name(raw.issuer, format: format)
49
+ end
50
+
51
+ # return subject from DN, similar to {#issuer}.
52
+ # @param [Symbol] format avaiables in `:to_a`, `:to_s` and `:raw`
53
+ # @return [Array, String, OpenSSL::X509::Name] the object converted into the expected format.
54
+ def subject(format: :raw)
55
+ convert_cert_name(raw.subject, format: format)
56
+ end
57
+
58
+ # @return [Time]
59
+ def created_at
60
+ raw.not_before
61
+ end
62
+
63
+ # @return [Time]
64
+ def expired_at
65
+ raw.not_after
66
+ end
67
+
68
+ # @return [Boolean]
69
+ def expired?
70
+ expired_at < Time.now.utc
71
+ end
72
+
73
+ # @return [String] format always be :x509.
74
+ def format
75
+ :x509
76
+ end
77
+
78
+ # return algorithm digest
79
+ #
80
+ # OpenSSL supported digests:
81
+ #
82
+ # -blake2b512 -blake2s256 -md4
83
+ # -md5 -md5-sha1 -mdc2
84
+ # -ripemd -ripemd160 -rmd160
85
+ # -sha1 -sha224 -sha256
86
+ # -sha3-224 -sha3-256 -sha3-384
87
+ # -sha3-512 -sha384 -sha512
88
+ # -sha512-224 -sha512-256 -shake128
89
+ # -shake256 -sm3 -ssl3-md5
90
+ # -ssl3-sha1 -whirlpool
91
+ def digest
92
+ signature_algorithm = raw.signature_algorithm
93
+
94
+ case signature_algorithm
95
+ when /md5/
96
+ :md5
97
+ when /sha1/
98
+ :sha1
99
+ when /sha224/
100
+ :sha224
101
+ when /sha256/
102
+ :sha256
103
+ when /sha512/
104
+ :sha512
105
+ else
106
+ # Android signature no need the others
107
+ signature_algorithm.to_sym
108
+ end
109
+ end
110
+
111
+ # return algorithm name of public key
112
+ def algorithm
113
+ case public_key
114
+ when OpenSSL::PKey::RSA then :rsa
115
+ when OpenSSL::PKey::DSA then :dsa
116
+ when OpenSSL::PKey::DH then :dh
117
+ when OpenSSL::PKey::EC then :ec
118
+ end
119
+ end
120
+
121
+ # return length of public key
122
+ # @return [Integer]
123
+ # @raise NotImplementedError
124
+ def length
125
+ case public_key
126
+ when OpenSSL::PKey::RSA
127
+ public_key.n.num_bits
128
+ when OpenSSL::PKey::DSA, OpenSSL::PKey::DH
129
+ public_key.p.num_bits
130
+ when OpenSSL::PKey::EC
131
+ raise NotImplementedError, "key length for #{public_key.inspect} not implemented"
132
+ end
133
+ end
134
+ alias size length
135
+
136
+ # return fingerprint of certificate
137
+ # @return [String]
138
+ def fingerprint(name = :sha256, transform: :lower, delimiter: nil)
139
+ digest = OpenSSL::Digest.new(name.to_s.upcase)
140
+ digest.update(raw.to_der)
141
+ fingerprint = digest.to_s
142
+ fingerprint = fingerprint.upcase if transform.to_sym == :upper
143
+ return fingerprint unless delimiter
144
+
145
+ fingerprint.scan(/../).join(delimiter)
146
+ end
147
+
148
+ # Orginal OpenSSL X509 certificate
149
+ # @return [OpenSSL::X509::Certificate]
150
+ def raw
151
+ @cert
152
+ end
153
+
154
+ private
155
+
156
+ def convert_cert_name(name, format:)
157
+ data = name.to_a
158
+ case format
159
+ when :to_a
160
+ data.map { |k, v, _| [k, v] }
161
+ when :to_s
162
+ data.map { |k, v, _| "#{k}=#{v}" }.join(' ')
163
+ else
164
+ name
165
+ end
166
+ end
167
+
168
+ def method_missing(method, *args, &block)
169
+ @cert.send(method.to_sym, *args, &block) || super
170
+ end
171
+
172
+ def respond_to_missing?(method, *args)
173
+ @cert.respond_to?(method.to_sym) || super
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo
4
+ # Full Format
5
+ module Format
6
+ # Apple
7
+
8
+ INFOPLIST = :infoplist
9
+ MOBILEPROVISION = :mobileprovision
10
+ DSYM = :dsym
11
+
12
+ # macOS
13
+
14
+ MACOS = :macos
15
+
16
+ # iOS
17
+
18
+ IPA = :ipa
19
+
20
+ # Android
21
+
22
+ APK = :apk
23
+ AAB = :aab
24
+ PROGUARD = :proguard
25
+
26
+ # Windows
27
+
28
+ PE = :pe
29
+
30
+ UNKNOWN = :unknown
31
+ end
32
+
33
+ # Manufacturer
34
+ module Manufacturer
35
+ APPLE = :apple
36
+ GOOGLE = :google
37
+ MICROSOFT = :microsoft
38
+ end
39
+
40
+ # Platform
41
+ module Platform
42
+ MACOS = :macos
43
+ IOS = :ios
44
+ ANDROID = :android
45
+ WINDOWS = :windows
46
+ end
47
+
48
+ # Apple Device Type
49
+ module Device
50
+ # macOS
51
+ MACOS = :macos
52
+
53
+ # Apple iPhone
54
+ IPHONE = :iphone
55
+ # Apple iPad
56
+ IPAD = :ipad
57
+ # Apple Watch
58
+ IWATCH = :iwatch # not implemented yet
59
+ # Apple Universal (iPhone and iPad)
60
+ UNIVERSAL = :universal
61
+
62
+ # Android Phone
63
+ PHONE = :phone
64
+ # Android Tablet (not implemented yet)
65
+ TABLET = :tablet
66
+ # Android Watch
67
+ WATCH = :watch
68
+ # Android TV
69
+ TELEVISION = :television
70
+ # Android Car Automotive
71
+ AUTOMOTIVE = :automotive
72
+
73
+ # Windows
74
+ WINDOWS = :windows
75
+ end
76
+ 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,81 @@
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
+ # @return [String]
16
+ def object
17
+ @object ||= ::File.basename(bin_path)
18
+ end
19
+
20
+ # @return [::MachO::MachOFile, ::MachO::FatFile]
21
+ def macho_type
22
+ @macho_type ||= ::MachO.open(bin_path)
23
+ end
24
+
25
+ # @return [Array<AppInfo::DSYM::MachO>]
26
+ def machos
27
+ @machos ||= case macho_type
28
+ when ::MachO::MachOFile
29
+ [MachO.new(macho_type, ::File.size(bin_path))]
30
+ else
31
+ size = macho_type.fat_archs.each_with_object([]) do |arch, obj|
32
+ obj << arch.size
33
+ end
34
+
35
+ machos = []
36
+ macho_type.machos.each_with_index do |file, i|
37
+ machos << MachO.new(file, size[i])
38
+ end
39
+ machos
40
+ end
41
+ end
42
+
43
+ # @return [String, nil]
44
+ def release_version
45
+ info.try(:[], 'CFBundleShortVersionString')
46
+ end
47
+
48
+ # @return [String, nil]
49
+ def build_version
50
+ info.try(:[], 'CFBundleVersion')
51
+ end
52
+
53
+ # @return [String, nil]
54
+ def identifier
55
+ info.try(:[], 'CFBundleIdentifier').sub('com.apple.xcode.dsym.', '')
56
+ end
57
+ alias bundle_id identifier
58
+
59
+ # @return [CFPropertyList]
60
+ def info
61
+ return nil unless ::File.exist?(info_path)
62
+
63
+ @info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: info_path).value)
64
+ end
65
+
66
+ # @return [String]
67
+ def info_path
68
+ @info_path ||= ::File.join(path, 'Contents', 'Info.plist')
69
+ end
70
+
71
+ # @return [String]
72
+ def bin_path
73
+ @bin_path ||= lambda {
74
+ dwarf_path = ::File.join(path, 'Contents', 'Resources', 'DWARF')
75
+ name = Dir.children(dwarf_path)[0]
76
+ ::File.join(dwarf_path, name)
77
+ }.call
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,62 @@
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
+ # @return [String]
17
+ def cpu_name
18
+ @file.cpusubtype
19
+ end
20
+
21
+ # @return [String]
22
+ def cpu_type
23
+ @file.cputype
24
+ end
25
+
26
+ # @return [String]
27
+ def type
28
+ @file.filetype
29
+ end
30
+
31
+ # @return [String, Integer]
32
+ def size(human_size: false)
33
+ return number_to_human_size(@size) if human_size
34
+
35
+ @size
36
+ end
37
+
38
+ # @return [String]
39
+ def uuid
40
+ @file[:LC_UUID][0].uuid_string
41
+ end
42
+ alias debug_id uuid
43
+
44
+ # @return [::MachO::Headers]
45
+ def header
46
+ @header ||= @file.header
47
+ end
48
+
49
+ # @return [Hash{Symbol => String, Integer}]
50
+ def to_h
51
+ {
52
+ uuid: uuid,
53
+ type: type,
54
+ cpu_name: cpu_name,
55
+ cpu_type: cpu_type,
56
+ size: size,
57
+ human_size: size(human_size: true)
58
+ }
59
+ end
60
+ end
61
+ end
62
+ end