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,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo
4
+ class Android < File
5
+ module Signature
6
+ # Android v2 Signature
7
+ #
8
+ # FULL FORMAT:
9
+ # OFFSET DATA TYPE DESCRIPTION
10
+ # * @+0 bytes uint32: signer size in bytes
11
+ # * @+4 bytes payload signer block
12
+ # * @+0 bytes unit32: signed data size in bytes
13
+ # * @+4 bytes payload signed data block
14
+ # * @+0 bytes unit32: digests with size in bytes
15
+ # * @+0 bytes unit32: digests with size in bytes
16
+ # * @+X bytes unit32: signatures with size in bytes
17
+ # * @+X+4 bytes payload signed data block
18
+ # * @+Y bytes unit32: public key with size in bytes
19
+ # * @+Y+4 bytes payload signed data block
20
+ class V2 < Base
21
+ include AppInfo::Helper::IOBlock
22
+ include AppInfo::Helper::Signatures
23
+ include AppInfo::Helper::Algorithm
24
+
25
+ # V2 Signature ID 0x7109871a
26
+ BLOCK_ID = [0x1a, 0x87, 0x09, 0x71].freeze
27
+
28
+ attr_reader :certificates, :digests
29
+
30
+ def version
31
+ Version::V2
32
+ end
33
+
34
+ # Verify
35
+ # @todo verified signatures
36
+ def verify
37
+ signers_block = singers_block(BLOCK_ID)
38
+ @certificates, @digests = verified_certs(signers_block, verify: true)
39
+ # @verified = true
40
+ end
41
+
42
+ private
43
+
44
+ def verified_certs(signers_block, verify:)
45
+ unless (signers = length_prefix_block(signers_block))
46
+ raise SecurityError, 'Not found signers'
47
+ end
48
+
49
+ certificates = []
50
+ content_digests = {}
51
+ loop_length_prefix_io(signers, name: 'Singer', logger: logger) do |signer|
52
+ signer_certs, signer_digests = extract_signer_data(signer, verify: verify)
53
+ certificates.concat(signer_certs)
54
+ content_digests.merge!(signer_digests)
55
+ end
56
+ raise SecurityError, 'No signers found' if certificates.empty?
57
+
58
+ [certificates, content_digests]
59
+ end
60
+
61
+ def extract_signer_data(signer, verify:)
62
+ # raw data
63
+ signed_data = length_prefix_block(signer)
64
+ signatures = length_prefix_block(signer)
65
+ public_key = length_prefix_block(signer, raw: true)
66
+
67
+ # FIXME: extract code below and re-organize
68
+
69
+ algorithems = signature_algorithms(signatures)
70
+ raise SecurityError, 'No signatures found' if verify && algorithems.empty?
71
+
72
+ # find best algorithem to verify signed data with public key and signature
73
+ unless best_algorithem = best_algorithem(algorithems)
74
+ raise SecurityError, 'No supported signatures found'
75
+ end
76
+
77
+ algorithems_digest = best_algorithem[:digest]
78
+ signature = best_algorithem[:signature]
79
+
80
+ pkey = OpenSSL::PKey.read(public_key)
81
+ digest = OpenSSL::Digest.new(algorithems_digest)
82
+ verified = pkey.verify(digest, signature, signed_data.string)
83
+ raise SecurityError, "#{algorithems_digest} signature did not verify" unless verified
84
+
85
+ # verify algorithm ID full equal (and sort) between digests and signature
86
+ digests = length_prefix_block(signed_data)
87
+ content_digests = signed_data_digests(digests)
88
+ content_digest = content_digests[algorithems_digest]&.fetch(:content)
89
+
90
+ unless content_digest
91
+ raise SecurityError,
92
+ 'Signature algorithms don\'t match between digests and signatures records'
93
+ end
94
+
95
+ previous_digest = content_digests.fetch(algorithems_digest)
96
+ content_digests[algorithems_digest] = content_digest
97
+ if previous_digest && previous_digest[:content] != content_digest
98
+ raise SecurityError,
99
+ 'Signature algorithms don\'t match between digests and signatures records'
100
+ end
101
+
102
+ certificates = length_prefix_block(signed_data)
103
+ certs = signed_data_certs(certificates)
104
+ raise SecurityError, 'No certificates listed' if certs.empty?
105
+
106
+ main_cert = certs[0]
107
+ if main_cert.public_key.to_der != pkey.to_der
108
+ raise SecurityError, 'Public key mismatch between certificate and signature record'
109
+ end
110
+
111
+ additional_attrs = length_prefix_block(signed_data)
112
+ verify_additional_attrs(additional_attrs, certs)
113
+
114
+ [certs, content_digests]
115
+ end
116
+ end
117
+
118
+ register(Version::V2, V2)
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo
4
+ class Android < File
5
+ module Signature
6
+ # Android v3 Signature
7
+ #
8
+ # FULL FORMAT:
9
+ # OFFSET DATA TYPE DESCRIPTION
10
+ # * @+0 bytes uint32: signer size in bytes
11
+ # * @+4 bytes payload signer block
12
+ # * @+0 bytes unit32: signed data size in bytes
13
+ # * @+4 bytes payload signed data block
14
+ # * @+0 bytes unit32: digests with size in bytes
15
+ # * @+0 bytes unit32: digests with size in bytes
16
+ # * @+W bytes unit32: minSDK
17
+ # * @+X+4 bytes unit32: maxSDK
18
+ # * @+Y+4 bytes unit32: signatures with size in bytes
19
+ # * @+Y+4 bytes payload signed data block
20
+ # * @+Z bytes unit32: public key with size in bytes
21
+ # * @+Z+4 bytes payload signed data block
22
+ class V3 < Base
23
+ include AppInfo::Helper::IOBlock
24
+ include AppInfo::Helper::Signatures
25
+ include AppInfo::Helper::Algorithm
26
+
27
+ # V3 Signature ID 0xf05368c0
28
+ V3_BLOCK_ID = [0xc0, 0x68, 0x53, 0xf0].freeze
29
+
30
+ # V3.1 Signature ID 0x1b93ad61
31
+ V3_1_BLOCK_ID = [0x61, 0xad, 0x93, 0x1b].freeze
32
+
33
+ attr_reader :certificates, :digests
34
+
35
+ def version
36
+ Version::V3
37
+ end
38
+
39
+ def verify
40
+ begin
41
+ signers_block = singers_block(V3_1_BLOCK_ID)
42
+ rescue NotFoundError
43
+ signers_block = singers_block(V3_BLOCK_ID)
44
+ end
45
+
46
+ @certificates, @digests = verified_certs(signers_block)
47
+ end
48
+
49
+ private
50
+
51
+ def verified_certs(signers_block)
52
+ unless (signers = length_prefix_block(signers_block))
53
+ raise SecurityError, 'Not found signers'
54
+ end
55
+
56
+ certificates = []
57
+ content_digests = {}
58
+ loop_length_prefix_io(signers, name: 'Singer', logger: logger) do |signer|
59
+ signer_certs, signer_digests = extract_signer_data(signer)
60
+ certificates.concat(signer_certs)
61
+ content_digests.merge!(signer_digests)
62
+ end
63
+ raise SecurityError, 'No signers found' if certificates.empty?
64
+
65
+ [certificates, content_digests]
66
+ end
67
+
68
+ def extract_signer_data(signer)
69
+ # raw data
70
+ signed_data = length_prefix_block(signer)
71
+
72
+ # TODO: verify min_sdk and max_sdk
73
+ min_sdk = signer.read(UINT32_SIZE)
74
+ max_sdk = signer.read(UINT32_SIZE)
75
+
76
+ signatures = length_prefix_block(signer)
77
+ public_key = length_prefix_block(signer, raw: true)
78
+
79
+ algorithems = signature_algorithms(signatures)
80
+ raise SecurityError, 'No signatures found' if algorithems.empty?
81
+
82
+ # find best algorithem to verify signed data with public key and signature
83
+ unless best_algorithem = best_algorithem(algorithems)
84
+ raise SecurityError, 'No supported signatures found'
85
+ end
86
+
87
+ algorithems_digest = best_algorithem[:digest]
88
+ signature = best_algorithem[:signature]
89
+
90
+ pkey = OpenSSL::PKey.read(public_key)
91
+ digest = OpenSSL::Digest.new(algorithems_digest)
92
+ verified = pkey.verify(digest, signature, signed_data.string)
93
+ raise SecurityError, "#{algorithems_digest} signature did not verify" unless verified
94
+
95
+ # verify algorithm ID full equal (and sort) between digests and signature
96
+ digests = length_prefix_block(signed_data)
97
+ content_digests = signed_data_digests(digests)
98
+ content_digest = content_digests[algorithems_digest]&.fetch(:content)
99
+
100
+ unless content_digest
101
+ raise SecurityError,
102
+ 'Signature algorithms don\'t match between digests and signatures records'
103
+ end
104
+
105
+ previous_digest = content_digests.fetch(algorithems_digest)
106
+ content_digests[algorithems_digest] = content_digest
107
+ if previous_digest && previous_digest[:content] != content_digest
108
+ raise SecurityError,
109
+ 'Signature algorithms don\'t match between digests and signatures records'
110
+ end
111
+
112
+ certificates = length_prefix_block(signed_data)
113
+ certs = signed_data_certs(certificates)
114
+ raise SecurityError, 'No certificates listed' if certs.empty?
115
+
116
+ main_cert = certs[0]
117
+ if main_cert.public_key.to_der != pkey.to_der
118
+ raise SecurityError, 'Public key mismatch between certificate and signature record'
119
+ end
120
+
121
+ additional_attrs = length_prefix_block(signed_data)
122
+ verify_additional_attrs(additional_attrs, certs)
123
+
124
+ [certs, content_digests]
125
+ end
126
+ end
127
+
128
+ register(Version::V3, V3)
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo
4
+ class Android < File
5
+ module Signature
6
+ # Android v4 Signature
7
+ #
8
+ # TODO: ApkSignatureSchemeV4Verifier.java
9
+ class V4 < Base
10
+ def version
11
+ Version::V4
12
+ end
13
+ end
14
+
15
+ # register(Version::V4, V4)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'app_info/android/signature'
4
+ require 'image_size'
5
+ require 'forwardable'
6
+
7
+ module AppInfo
8
+ # Android base parser for apk and aab file
9
+ class Android < File
10
+ extend Forwardable
11
+ include Helper::HumanFileSize
12
+
13
+ # return file size
14
+ # @example Read file size in integer
15
+ # aab.size # => 3618865
16
+ #
17
+ # @example Read file size in human readabale
18
+ # aab.size(human_size: true) # => '3.45 MB'
19
+ #
20
+ # @param [Boolean] human_size Convert integer value to human readable.
21
+ # @return [Integer, String]
22
+ def size(human_size: false)
23
+ file_to_human_size(@file, human_size: human_size)
24
+ end
25
+
26
+ # @return [Symbol] {Manufacturer}
27
+ def manufacturer
28
+ Manufacturer::GOOGLE
29
+ end
30
+
31
+ # @return [Symbol] {Platform}
32
+ def platform
33
+ Platform::ANDROID
34
+ end
35
+
36
+ # @return [Symbol] {Device}
37
+ def device
38
+ if watch?
39
+ Device::WATCH
40
+ elsif television?
41
+ Device::TELEVISION
42
+ elsif automotive?
43
+ Device::AUTOMOTIVE
44
+ elsif tablet?
45
+ Device::TABLET
46
+ else
47
+ Device::PHONE
48
+ end
49
+ end
50
+
51
+ # @abstract Subclass and override {#name} to implement.
52
+ def name
53
+ not_implemented_error!(__method__)
54
+ end
55
+
56
+ # @todo find a way to detect, no way!
57
+ # @see https://stackoverflow.com/questions/9279111/determine-if-the-device-is-a-smartphone-or-tablet
58
+ # @return [Boolean] false always false
59
+ def tablet?
60
+ # Not works!
61
+ # resource.first_package
62
+ # .entries('bool')
63
+ # .select{|e| e.name == 'isTablet' }
64
+ # .size >= 1
65
+ false
66
+ end
67
+
68
+ # @return [Boolean]
69
+ def watch?
70
+ !!use_features&.include?('android.hardware.type.watch')
71
+ end
72
+
73
+ # @return [Boolean]
74
+ def television?
75
+ !!use_features&.include?('android.software.leanback')
76
+ end
77
+
78
+ # @return [Boolean]
79
+ def automotive?
80
+ !!use_features&.include?('android.hardware.type.automotive')
81
+ end
82
+
83
+ # @abstract Subclass and override {#use_features} to implement.
84
+ def use_features
85
+ not_implemented_error!(__method__)
86
+ end
87
+
88
+ # @abstract Subclass and override {#use_permissions} to implement.
89
+ def use_permissions
90
+ not_implemented_error!(__method__)
91
+ end
92
+
93
+ # @abstract Subclass and override {#use_permissions} to implement.
94
+ def activities
95
+ not_implemented_error!(__method__)
96
+ end
97
+
98
+ # @abstract Subclass and override {#use_permissions} to implement.
99
+ def services
100
+ not_implemented_error!(__method__)
101
+ end
102
+
103
+ # @abstract Subclass and override {#use_permissions} to implement.
104
+ def components
105
+ not_implemented_error!(__method__)
106
+ end
107
+
108
+ # Return multi version certifiates of signatures
109
+ # @return [Array<Hash>] signatures
110
+ # @see AppInfo::Android::Signature.verify
111
+ def signatures
112
+ @signatures ||= Android::Signature.verify(self)
113
+ end
114
+
115
+ # Legacy v1 scheme signatures, it will remove soon.
116
+ # @deprecated Use {#signatures}
117
+ # @return [Array<OpenSSL::PKCS7, nil>] signatures
118
+ def signs
119
+ @signs ||= v1sign&.signatures || []
120
+ end
121
+
122
+ # Legacy v1 scheme certificates, it will remove soon.
123
+ # @deprecated Use {#signatures}
124
+ # @return [Array<OpenSSL::PKCS7, nil>] certificates
125
+ def certificates
126
+ @certificates ||= v1sign&.certificates || []
127
+ end
128
+
129
+ # @abstract Subclass and override {#manifest} to implement.
130
+ def manifest
131
+ not_implemented_error!(__method__)
132
+ end
133
+
134
+ # @abstract Subclass and override {#resource} to implement.
135
+ def resource
136
+ not_implemented_error!(__method__)
137
+ end
138
+
139
+ # @abstract Subclass and override {#zip} to implement.
140
+ def zip
141
+ not_implemented_error!(__method__)
142
+ end
143
+
144
+ # @abstract Subclass and override {#clear!} to implement.
145
+ def clear!
146
+ not_implemented_error!(__method__)
147
+ end
148
+
149
+ # @return [String] contents path of contents
150
+ def contents
151
+ @contents ||= ::File.join(Dir.mktmpdir, "AppInfo-android-#{SecureRandom.hex}")
152
+ end
153
+
154
+ protected
155
+
156
+ def extract_icon(icons, exclude: nil)
157
+ excludes = exclude_icon_exts(exclude: exclude)
158
+ icons.reject { |icon| icon_ext_match?(icon[:name], excludes) }
159
+ end
160
+
161
+ def exclude_icon_exts(exclude:)
162
+ case exclude
163
+ when String then [exclude]
164
+ when Array then exclude.map(&:to_s)
165
+ when Symbol then [exclude.to_s]
166
+ end
167
+ end
168
+
169
+ def icon_ext_match?(file, excludes)
170
+ return false if file.nil? || excludes.nil?
171
+
172
+ excludes.include?(::File.extname(file)[1..-1])
173
+ end
174
+
175
+ def v1sign
176
+ @v1sign ||= Android::Signature::V1.verify(self)
177
+ rescue Android::Signature::NotFoundError
178
+ nil
179
+ end
180
+ end
181
+ end
data/lib/app_info/apk.rb CHANGED
@@ -1,41 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'ruby_apk'
4
- require 'image_size'
5
- require 'forwardable'
6
4
 
7
5
  module AppInfo
8
- # Parse APK file
9
- class APK
10
- include Helper::HumanFileSize
11
- extend Forwardable
12
-
13
- attr_reader :file
14
-
15
- # APK Devices
16
- module Device
17
- PHONE = 'Phone'
18
- TABLET = 'Tablet'
19
- WATCH = 'Watch'
20
- TV = 'Television'
21
- AUTOMOTIVE = 'Automotive'
22
- end
23
-
24
- def initialize(file)
25
- @file = file
26
- end
27
-
28
- def size(human_size: false)
29
- file_to_human_size(@file, human_size: human_size)
30
- end
31
-
32
- def os
33
- Platform::ANDROID
34
- end
35
- alias file_type os
36
-
6
+ # Parse APK file parser, wrapper for {https://github.com/icyleaf/android_parser android_parser}.
7
+ class APK < Android
8
+ # @!method manifest
9
+ # @see https://rubydoc.info/gems/android_parser/Android/Apk#manifest-instance_method ::Android::Apk#manifest
10
+ # @!method resource
11
+ # @see https://rubydoc.info/gems/android_parser/Android/Apk#resource-instance_method ::Android::Apk#resource
12
+ # @!method dex
13
+ # @see https://rubydoc.info/gems/android_parser/Android/Apk#dex-instance_method ::Android::Apk#dex
37
14
  def_delegators :apk, :manifest, :resource, :dex
38
15
 
16
+ # @!method version_name
17
+ # @see https://rubydoc.info/gems/android_parser/Android/Manifest#version_name-instance_method ::Android::Manifest#version_name
18
+ # @!method package_name
19
+ # @see https://rubydoc.info/gems/android_parser/Android/Manifest#package_name-instance_method ::Android::Manifest#package_name
20
+ # @!method target_sdk_versionx
21
+ # @see https://rubydoc.info/gems/android_parser/Android/Manifest#target_sdk_versionx-instance_method ::Android::Manifest#target_sdk_version
22
+ # @!method components
23
+ # @see https://rubydoc.info/gems/android_parser/Android/Manifest#components-instance_method ::Android::Manifest#components
24
+ # @!method services
25
+ # @see https://rubydoc.info/gems/android_parser/Android/Manifest#services-instance_method ::Android::Manifest#services
26
+ # @!method use_permissions
27
+ # @see https://rubydoc.info/gems/android_parser/Android/Manifest#use_permissions-instance_method ::Android::Manifest#use_permissions
28
+ # @!method use_features
29
+ # @see https://rubydoc.info/gems/android_parser/Android/Manifest#use_features-instance_method ::Android::Manifest#use_features
30
+ # @!method deep_links
31
+ # @see https://rubydoc.info/gems/android_parser/Android/Manifest#deep_links-instance_method ::Android::Manifest#deep_links
32
+ # @!method schemes
33
+ # @see https://rubydoc.info/gems/android_parser/Android/Manifest#schemes-instance_method ::Android::Manifest#schemes
39
34
  def_delegators :manifest, :version_name, :package_name, :target_sdk_version,
40
35
  :components, :services, :use_permissions, :use_features,
41
36
  :deep_links, :schemes
@@ -53,78 +48,70 @@ module AppInfo
53
48
  manifest.label || resource.find('@string/app_name')
54
49
  end
55
50
 
56
- def device_type
57
- if wear?
58
- Device::WATCH
59
- elsif tv?
60
- Device::TV
61
- elsif automotive?
62
- Device::AUTOMOTIVE
63
- else
64
- Device::PHONE
65
- end
66
- end
67
-
68
- # TODO: find a way to detect, no way!
69
- # def tablet?
70
- # end
71
-
72
- def wear?
73
- use_features.include?('android.hardware.type.watch')
74
- end
75
-
76
- def tv?
77
- use_features.include?('android.software.leanback')
78
- end
79
-
80
- def automotive?
81
- use_features.include?('android.hardware.type.automotive')
82
- end
83
-
51
+ # @return [String]
84
52
  def min_sdk_version
85
53
  manifest.min_sdk_ver
86
54
  end
87
55
  alias min_os_version min_sdk_version
88
56
 
89
- def sign_version
90
- return 'v1' unless signs.empty?
91
-
92
- # when ?
93
- # https://source.android.com/security/apksigning/v2?hl=zh-cn
94
- # 'v2'
95
- # when ?
96
- # https://source.android.com/security/apksigning/v3?hl=zh-cn
97
- # 'v3'
98
- 'unknown'
99
- end
100
-
101
- def signs
102
- apk.signs.each_with_object([]) do |(path, sign), obj|
103
- obj << Sign.new(path, sign)
104
- end
105
- end
106
-
107
- def certificates
108
- apk.certificates.each_with_object([]) do |(path, certificate), obj|
109
- obj << Certificate.new(path, certificate)
110
- end
111
- end
112
-
57
+ # @return [String]
113
58
  def activities
114
59
  components.select { |c| c.type == 'activity' }
115
60
  end
116
61
 
62
+ # @return [::Android::Apk]
117
63
  def apk
118
64
  @apk ||= ::Android::Apk.new(@file)
119
65
  end
120
66
 
121
- def icons
67
+ # @return [Zip::File]
68
+ def zip
69
+ @zip ||= apk.instance_variable_get(:@zip)
70
+ end
71
+
72
+ # Full icons metadata
73
+ # @example full icons
74
+ # apk.icons
75
+ # # => [
76
+ # # {
77
+ # # name: 'ic_launcher.png',
78
+ # # file: '/path/to/ic_launcher.png',
79
+ # # dimensions: [29, 29]
80
+ # # },
81
+ # # {
82
+ # # name: 'ic_launcher.png',
83
+ # # file: '/path/to/ic_launcher.png',
84
+ # # dimensions: [120, 120]
85
+ # # },
86
+ # # {
87
+ # # name: 'ic_launcher.xml',
88
+ # # file: '/path/to/ic_launcher.xml',
89
+ # # dimensions: [nil, nil]
90
+ # # },
91
+ # # ]
92
+ # @example exclude xml icons
93
+ # apk.icons(exclude: :xml)
94
+ # # => [
95
+ # # {
96
+ # # name: 'ic_launcher.png',
97
+ # # file: '/path/to/ic_launcher.png',
98
+ # # dimensions: [29, 29]
99
+ # # },
100
+ # # {
101
+ # # name: 'ic_launcher.png',
102
+ # # file: '/path/to/ic_launcher.png',
103
+ # # dimensions: [120, 120]
104
+ # # }
105
+ # # ]
106
+ # @param [Boolean] xml return xml icons
107
+ # @return [Array<Hash{Symbol => String, Array<Integer>}>] icons paths of icons
108
+ def icons(exclude: nil)
122
109
  @icons ||= apk.icon.each_with_object([]) do |(path, data), obj|
123
- icon_name = File.basename(path)
124
- icon_path = File.join(contents, File.dirname(path))
125
- icon_file = File.join(icon_path, icon_name)
110
+ icon_name = ::File.basename(path)
111
+ icon_path = ::File.join(contents, ::File.dirname(path))
112
+ icon_file = ::File.join(icon_path, icon_name)
126
113
  FileUtils.mkdir_p icon_path
127
- File.write(icon_file, data, encoding: Encoding::BINARY)
114
+ ::File.write(icon_file, data, encoding: Encoding::BINARY)
128
115
 
129
116
  obj << {
130
117
  name: icon_name,
@@ -132,6 +119,8 @@ module AppInfo
132
119
  dimensions: ImageSize.path(icon_file).size
133
120
  }
134
121
  end
122
+
123
+ extract_icon(@icons, exclude: exclude)
135
124
  end
136
125
 
137
126
  def clear!
@@ -145,29 +134,5 @@ module AppInfo
145
134
  @app_path = nil
146
135
  @info = nil
147
136
  end
148
-
149
- def contents
150
- @contents ||= File.join(Dir.mktmpdir, "AppInfo-android-#{SecureRandom.hex}")
151
- end
152
-
153
- # Android Certificate
154
- class Certificate
155
- attr_reader :path, :certificate
156
-
157
- def initialize(path, certificate)
158
- @path = path
159
- @certificate = certificate
160
- end
161
- end
162
-
163
- # Android Sign
164
- class Sign
165
- attr_reader :path, :sign
166
-
167
- def initialize(path, sign)
168
- @path = path
169
- @sign = sign
170
- end
171
- end
172
137
  end
173
138
  end