cocoapods-entitlements-statistics 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b18c67b431404139e32390800cee36de2c9443a053716f29832d32d22424954a
4
+ data.tar.gz: be771fb8171db38589b66f32155b946958d5d34b79b4ace92f8e8e6ea146bbd5
5
+ SHA512:
6
+ metadata.gz: e3596496a3b67f9ae0cb268fe08118f415ca5e9073cea82017bb8176cef932158b44768cfbcebb9b7185b74ff293420895f2221c8aa02d069ebae6dc04305c70
7
+ data.tar.gz: 9c740e1c280ab955be62a9f51a7678bc07a50c5a28d16049cedf1964283f17ec0c12d87a6cab6710c0540821f6da7a888130264b3e7ca0d72b63a2288951f1a1
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .DS_Store
2
+ pkg
3
+ .idea/
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cocoapods-entitlements-statistics.gemspec
4
+ gemspec
5
+ gem 'crimp'
6
+ gem 'zip'
7
+
8
+ group :development do
9
+ gem 'cocoapods'
10
+
11
+ gem 'mocha'
12
+ gem 'bacon'
13
+ gem 'mocha-on-bacon'
14
+ gem 'prettybacon'
15
+ gem 'crimp'
16
+ gem 'zip'
17
+
18
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2023 bin <tang.bin@olaola.chat>
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # cocoapods-entitlements-statistics
2
+
3
+ iOS app 权限统计, 使用cocoapods plugin 触发
4
+
5
+
6
+ ## 安装
7
+
8
+ ```shell
9
+ # 下载Releases v0.0.1 gem文件,然后进入下载目录执行下面命令
10
+ sudo gem install cocoapods-entitlements-statistics-0.0.1.gem
11
+ ```
12
+
13
+ ## 使用
14
+
15
+ 在 Podfile 文件添加插件引用
16
+
17
+ ```ruby
18
+ #引入插件, 可以自定义 report_path, 默认在(iOS)项目根目录
19
+ plugin 'cocoapods-entitlements-statistics', :report_path => '/Users/rd01/Desktop'
20
+ #plugin 'cocoapods-entitlements-statistics'
21
+
22
+ target 'xxx' do
23
+ ....
24
+
25
+ ```
26
+
27
+ 执行 pod install 会输出统计报告路径
28
+
29
+ ```shell
30
+ $ pod install
31
+ .....
32
+ [!] Entitlements Statistics Report : path/to/entitlements_statistics/analyze_report
33
+ Pod installation complete! There are 75 dependencies from the Podfile and 116 total pods installed.
34
+
35
+ ```
36
+
37
+ ## 说明
38
+
39
+ ##### 版本权限统计
40
+
41
+ 默认会在 ~/appInfo-#{app_bundle_id}/ 下为每个版本建立权限统计文件
42
+
43
+ ```yaml
44
+
45
+ ├── appInfo-com.ola.chat
46
+ │   ├── entitlements_5.2.0.yml
47
+ │   ├── entitlements_5.3.0.yml
48
+ │   └── entitlements_versions.yml
49
+ ```
50
+
51
+ ##### 报告内容如下:
52
+
53
+ * 权限变化对比分析(增、删、改)
54
+ * (对比的各)版本权限列表详情
55
+
56
+ ```yaml
57
+
58
+ compared 5.3.0 5.2.0
59
+
60
+ modify capabilitys :
61
+ 5.3.0
62
+ - items ..
63
+ 5.2.0
64
+ - items ..
65
+ ------------------------------
66
+ add capabilitys :
67
+ - items ..
68
+ ------------------------------
69
+ remove capabilitys :
70
+ - items ..
71
+ ------------------------------
72
+
73
+ 5.3.0 entitlements list:
74
+ ...
75
+
76
+ 5.2.0 entitlements list:
77
+ ...
78
+
79
+ ```
80
+
81
+ ## iOS 项目权限
82
+
83
+ #### 大致分类 Capabilitys、info.plist(Cocoa Keys)
84
+
85
+ * Capabilitys
86
+
87
+ > Xcode->Target->Signing&Capabilitys->Capabilitys 下添加和删除 Capabilitys , 这里不是随便添加的,需要首先在苹果开发者后台注册或者声明对应Capabilitys. 更新的Provisioning Profile文件,其中包含Entitlements 字段包含(几乎所有的)已注册Capabilitys, Xcode中添加Capabilitys后会同Profile文件校验。不匹配则无法通过编译签名。
88
+
89
+ Supported capabilities (iOS):
90
+ https://developer.apple.com/help/account/reference/supported-capabilities-ios
91
+
92
+ * info.plist (Cocoa Key-Values)
93
+ * NS{Permissions}UsageDescription
94
+ > info.plist 中声明使用相机、麦克风、摄像头..等权限的说明
95
+
96
+ * 其它权限声明Keys
97
+ > info.plist 中也会包含一些权限的详细设置、声明,比如屏幕高刷、代理、后台模式...
98
+
99
+ info.plist 涉及的所有 Cocoa Keys
100
+ https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW1
101
+
102
+ #### 涉及的文件
103
+
104
+ * info.plist
105
+ * Runner.entitlements Capabilitys被添加后的,Xcode自动创建plist格式文件,里面包含相关Capability的声明、设置
106
+ * {embedded/xxx-xx-xx}.mobileprovision
107
+ 包括里面的Entitlements字段, 保存一些 Capabilitys被添加后的 声明、设置
108
+ * Runner.xcodeproj
109
+ 工程文件SystemCapabilities字段包含了一些Capabilitys声明
110
+
111
+ 相关文档:
112
+
113
+ xx.mobileprovision
114
+ https://developer.apple.com/forums/thread/685723
115
+
116
+ Runner.entitlements
117
+ https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/AboutEntitlements.html#//apple_ref/doc/uid/TP40011195-CH1-SW1
118
+
119
+ #### 问题
120
+
121
+ * IPA包中找不到 In-App Purchase 功能标记
122
+
123
+ > 一般Capabilitys添加后会在Runner.entitlements、xx.mobileprovision找到对应key:value声明、设置
124
+
125
+ 添加 In-App Purchase后, 仅在Runner.xcodeproj中有 "com.apple.InAppPurchase"=>{"enabled"=>"1"} 声明
126
+
127
+ * Background Modes 属于 Capabilitys 中的异类
128
+
129
+ > 一般Capabilitys添加后会在Runner.entitlements、xx.mobileprovision找到对应key:value声明、设置, 而Background Modes在这里没有任何记录
130
+
131
+ Background Modes的声明位置:
132
+ * Runner.xcodeproj : "com.apple.BackgroundModes"=>{"enabled"=>"1"}
133
+ * info.plist : "UIBackgroundModes"=>{"audio"、"remote-notification" ...}
134
+
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ def specs(dir)
4
+ FileList["spec/#{dir}/*_spec.rb"].shuffle.join(' ')
5
+ end
6
+
7
+ desc 'Runs all the specs'
8
+ task :specs do
9
+ sh "bundle exec bacon #{specs('**')}"
10
+ end
11
+
12
+ task :default => :specs
13
+
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cocoapods-entitlements-statistics/gem_version.rb'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'cocoapods-entitlements-statistics'
8
+ spec.version = CocoapodsEntitlementsStatistics::VERSION
9
+ spec.authors = ['bin']
10
+ spec.email = ['tang.bin@olaola.chat']
11
+ spec.description = %q{A short description of cocoapods-entitlements-statistics.}
12
+ spec.summary = %q{A longer description of cocoapods-entitlements-statistics.}
13
+ spec.homepage = 'https://github.com/EXAMPLE/cocoapods-entitlements-statistics'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_runtime_dependency "yaml"
22
+ spec.add_runtime_dependency "crimp"
23
+ spec.add_dependency 'CFPropertyList', '< 3.1.0', '>= 2.3.4'
24
+ spec.add_development_dependency 'bundler', '>= 1.12'
25
+ spec.add_dependency 'rubyzip', '>= 1.2', '< 3.0'
26
+ spec.add_development_dependency 'rake'
27
+ end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helper"
4
+ require 'fileutils'
5
+ require 'yaml'
6
+
7
+ module AppEntitlementsStatistics
8
+
9
+ class Analyzer
10
+ include Helper::Utils
11
+
12
+ attr_reader :store_path
13
+ attr_reader :report_path
14
+ attr_reader :identifier
15
+
16
+ def initialize(identifier,report_path: nil, store_path: nil)
17
+ @identifier = identifier
18
+ @report_path = report_path
19
+ @store_path = store_path
20
+ end
21
+
22
+ def analyze
23
+ load_version
24
+ return singleGenerate unless @versions.length > 1
25
+ pre_version = @versions[1]
26
+ cur_version = @versions[0]
27
+ pre_entitlements = load_entitlements_yaml(pre_version)
28
+ cur_entitlements = load_entitlements_yaml(cur_version)
29
+ return "" unless pre_entitlements.any?
30
+ return "" unless cur_entitlements.any?
31
+
32
+ capabilities_diff = analyze_capabilities_diff(pre_entitlements,cur_entitlements)
33
+ usage_descs_diff = analyze_usage_descs_diff(pre_entitlements,cur_entitlements);
34
+ report_path = generate(capabilities_diff,usage_descs_diff)
35
+ report_path
36
+ end
37
+
38
+ def singleGenerate
39
+ return "" unless @versions.length > 0
40
+ output = "no more versions to compare !! "
41
+ output += "\n\n\n#{@versions.first} entitlements list: "
42
+ output += "\n" + read_each_line(@versions.first)
43
+ output += "\n" + "----------------------------------------"
44
+ report_path = report_file_name(path: @report_path)
45
+ File.open(report_path, 'w') { |file|
46
+ file.write(output)
47
+ }
48
+ report_path
49
+ end
50
+
51
+ def generate(capabilities_diff,usage_descs_diff)
52
+ return unless @versions.length > 1
53
+ cur_version = @versions[0]
54
+ pre_version = @versions[1]
55
+
56
+ output = "\n" + "compared #{cur_version} #{pre_version}"
57
+ output += "\n" + capabilities_diff unless capabilities_diff.empty?
58
+ output += "\n" + usage_descs_diff unless usage_descs_diff.empty?
59
+ output += "\n\nThere is no difference between the two versions \n\n" unless (!capabilities_diff.empty? && !usage_descs_diff.empty?)
60
+ # puts output
61
+
62
+ output += "\n\n\n#{cur_version} entitlements list: "
63
+ output += "\n" + read_each_line(cur_version)
64
+ output += "\n" + "----------------------------------------"
65
+ output += "\n\n#{pre_version} entitlements list: "
66
+ output += "\n" + read_each_line(pre_version)
67
+ output += "\n" + "----------------------------------------"
68
+
69
+ report_path = report_file_name(path: @report_path)
70
+ File.open(report_path, 'w') { |file|
71
+ file.write(output)
72
+ }
73
+ report_path
74
+ end
75
+
76
+ def read_each_line(version)
77
+ file_name = entitlements_yaml_name(version,@identifier, path: @store_path)
78
+ file_content = ""
79
+ File.open(file_name,"r").each_line do |line|
80
+ file_content += "\n" + line
81
+ end
82
+ file_content
83
+ end
84
+
85
+ def load_version
86
+ yaml_name = versions_yaml_name(@identifier, path: @store_path)
87
+ if File.exist?(yaml_name)
88
+ @versions = YAML.load_file(yaml_name)
89
+ end
90
+ end
91
+
92
+ def load_entitlements_yaml(version)
93
+ yaml_name = entitlements_yaml_name(version,@identifier, path: @store_path)
94
+ yaml_content = [ ]
95
+ if File.exist?(yaml_name)
96
+ yaml_content = YAML.load_file(yaml_name)
97
+ end
98
+ puts "#{yaml_name} not found!!!" unless !yaml_content.empty?
99
+ yaml_content
100
+ end
101
+
102
+ def analyze_capabilities_diff(pre,cur)
103
+ output = ""
104
+ cur_capabilities_summary = cur['Capabilities_Summary']
105
+ pre_capabilities_summary = pre['Capabilities_Summary']
106
+ cur_capabilities = cur['Capabilities']
107
+ pre_capabilities = pre['Capabilities']
108
+ output = ""
109
+ # 修改
110
+ comm_capabilities = cur_capabilities_summary.keys & pre_capabilities_summary.keys
111
+ modifys = modifys_cur = modifys_pre = ""
112
+ comm_capabilities.each do |key|
113
+ if cur_capabilities_summary[key] != pre_capabilities_summary[key]
114
+ modifys_cur += "\n" + '- ' + key + ': ' + cur_capabilities[key].to_s
115
+ modifys_pre += "\n" + '- ' + key + ': ' + pre_capabilities[key].to_s
116
+ end
117
+ end
118
+
119
+ modifys += "\n" + @versions[0] unless modifys_cur.empty?
120
+ modifys += modifys_cur unless modifys_cur.empty?
121
+ modifys += "\n" + @versions[1] unless modifys_pre.empty?
122
+ modifys += modifys_pre unless modifys_pre.empty?
123
+
124
+ #新增
125
+ add_capabilities = cur_capabilities_summary.keys - pre_capabilities_summary.keys
126
+ adds = ""
127
+ add_capabilities.each do |key|
128
+ adds += "\n" + '- ' + key + ': ' + cur_capabilities[key].to_s
129
+ end
130
+
131
+ #移除
132
+ remove_capabilities = pre_capabilities_summary.keys - cur_capabilities_summary.keys
133
+ removes = ""
134
+ remove_capabilities.each do |key|
135
+ removes += "\n" + '- ' + key + ': ' + pre_capabilities[key].to_s
136
+ end
137
+
138
+ if !modifys.empty?
139
+ output += "\n" + "modify capabilitys : "
140
+ output += modifys
141
+ output += "\n------------------------------"
142
+ end
143
+
144
+ if !adds.empty?
145
+ output += "\n" + "add capabilitys : "
146
+ output += adds
147
+ output += "\n------------------------------"
148
+ end
149
+
150
+ if !removes.empty?
151
+ output += "\n" + "remove capabilitys : "
152
+ output += removes
153
+ output += "\n------------------------------"
154
+ end
155
+
156
+ output
157
+ end
158
+
159
+ def analyze_usage_descs_diff(pre,cur)
160
+ output = ""
161
+ cur_usage_descs_summary = cur['PermissionsUsageDescription_Summary']
162
+ pre_usage_descs_summary = pre['PermissionsUsageDescription_Summary']
163
+
164
+ cur_usage_descs = cur['PermissionsUsageDescription']
165
+ pre_usage_descs = pre['PermissionsUsageDescription']
166
+
167
+ # 修改
168
+ comm_usage_descs = cur_usage_descs_summary.keys & pre_usage_descs_summary.keys
169
+ modifys = modifys_cur = modifys_pre = ""
170
+ comm_usage_descs.each do |key|
171
+ if cur_usage_descs_summary[key] != pre_usage_descs_summary[key]
172
+ modifys_cur += "\n" + '- ' + key + ': ' + cur_usage_descs[key].to_s
173
+ modifys_pre += "\n" + '- ' + key + ': ' + pre_usage_descs[key].to_s
174
+ end
175
+ end
176
+
177
+ modifys += "\n" + @versions[0] unless modifys_cur.empty?
178
+ modifys += modifys_cur unless modifys_cur.empty?
179
+ modifys += "\n" + @versions[1] unless modifys_pre.empty?
180
+ modifys += modifys_pre unless modifys_pre.empty?
181
+
182
+ #新增
183
+ add_usage_descs = cur_usage_descs_summary.keys - pre_usage_descs_summary.keys
184
+ adds = ""
185
+ add_usage_descs.each do |key|
186
+ adds += "\n" + '- ' + key + ': ' + cur_usage_descs[key].to_s
187
+ end
188
+
189
+ #移除
190
+ remove_usage_descs = pre_usage_descs_summary.keys - cur_usage_descs_summary.keys
191
+ removes = ""
192
+ remove_usage_descs.each do |key|
193
+ removes += "\n" + '- ' + key + ': ' + pre_usage_descs[key].to_s
194
+ end
195
+
196
+ if !modifys.empty?
197
+ output += "\n" + "modify permissions usage description : "
198
+ output += modifys
199
+ output += "\n------------------------------"
200
+ end
201
+
202
+ if !adds.empty?
203
+ output += "\n" + "add permissions usage description : "
204
+ output += adds
205
+ output += "\n------------------------------"
206
+ end
207
+
208
+ if !removes.empty?
209
+ output += "\n" + "remove permissions usage description : "
210
+ output += removes
211
+ output += "\n------------------------------"
212
+ end
213
+
214
+ output
215
+ end
216
+ end
217
+
218
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppEntitlementsStatistics
4
+ module Inflector
5
+ def ai_snakecase
6
+ gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
7
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
8
+ .tr('-', '_')
9
+ .gsub(/\s/, '_')
10
+ .gsub(/__+/, '_')
11
+ .downcase
12
+ end
13
+
14
+ def ai_camelcase(first_letter: :upper, separators: ['-', '_', '\s'])
15
+ str = dup
16
+
17
+ separators.each do |s|
18
+ str = str.gsub(/(?:#{s}+)([a-z])/) { $1.upcase }
19
+ end
20
+
21
+ case first_letter
22
+ when :upper, true
23
+ str = str.gsub(/(\A|\s)([a-z])/) { $1 + $2.upcase }
24
+ when :lower, false
25
+ str = str.gsub(/(\A|\s)([A-Z])/) { $1 + $2.downcase }
26
+ end
27
+
28
+ str
29
+ end
30
+ end
31
+ end
32
+
33
+ class String
34
+ include AppEntitlementsStatistics::Inflector
35
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyrights rails
4
+ # Copy from https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/object/try.rb
5
+
6
+ module AppEntitlementsStatistics
7
+ module Tryable # :nodoc:
8
+ ##
9
+ # :method: try
10
+ #
11
+ # :call-seq:
12
+ # try(*a, &b)
13
+ #
14
+ # Invokes the public method whose name goes as first argument just like
15
+ # +public_send+ does, except that if the receiver does not respond to it the
16
+ # call returns +nil+ rather than raising an exception.
17
+ #
18
+ # This method is defined to be able to write
19
+ #
20
+ # @person.try(:name)
21
+ #
22
+ # instead of
23
+ #
24
+ # @person.name if @person
25
+ #
26
+ # +try+ calls can be chained:
27
+ #
28
+ # @person.try(:spouse).try(:name)
29
+ #
30
+ # instead of
31
+ #
32
+ # @person.spouse.name if @person && @person.spouse
33
+ #
34
+ # +try+ will also return +nil+ if the receiver does not respond to the method:
35
+ #
36
+ # @person.try(:non_existing_method) # => nil
37
+ #
38
+ # instead of
39
+ #
40
+ # @person.non_existing_method if @person.respond_to?(:non_existing_method) # => nil
41
+ #
42
+ # +try+ returns +nil+ when called on +nil+ regardless of whether it responds
43
+ # to the method:
44
+ #
45
+ # nil.try(:to_i) # => nil, rather than 0
46
+ #
47
+ # Arguments and blocks are forwarded to the method if invoked:
48
+ #
49
+ # @posts.try(:each_slice, 2) do |a, b|
50
+ # ...
51
+ # end
52
+ #
53
+ # The number of arguments in the signature must match. If the object responds
54
+ # to the method the call is attempted and +ArgumentError+ is still raised
55
+ # in case of argument mismatch.
56
+ #
57
+ # If +try+ is called without arguments it yields the receiver to a given
58
+ # block unless it is +nil+:
59
+ #
60
+ # @person.try do |p|
61
+ # ...
62
+ # end
63
+ #
64
+ # You can also call try with a block without accepting an argument, and the block
65
+ # will be instance_eval'ed instead:
66
+ #
67
+ # @person.try { upcase.truncate(50) }
68
+ #
69
+ # Please also note that +try+ is defined on +Object+. Therefore, it won't work
70
+ # with instances of classes that do not have +Object+ among their ancestors,
71
+ # like direct subclasses of +BasicObject+.
72
+ def try(method_name = nil, *args, &block)
73
+ if method_name.nil? && block_given?
74
+ if block.arity.zero?
75
+ instance_eval(&b)
76
+ else
77
+ yield self
78
+ end
79
+ elsif respond_to?(method_name)
80
+ public_send(method_name, *args, &block)
81
+ end
82
+ end
83
+
84
+ ##
85
+ # :method: try!
86
+ #
87
+ # :call-seq:
88
+ # try!(*a, &b)
89
+ #
90
+ # Same as #try, but raises a +NoMethodError+ exception if the receiver is
91
+ # not +nil+ and does not implement the tried method.
92
+ #
93
+ # "a".try!(:upcase) # => "A"
94
+ # nil.try!(:upcase) # => nil
95
+ # 123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Integer
96
+ def try!(method_name = nil, *args, &block)
97
+ if method_name.nil? && block_given?
98
+ if block.arity.zero?
99
+ instance_eval(&block)
100
+ else
101
+ yield self
102
+ end
103
+ else
104
+ public_send(method_name, *args, &block)
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ class Object
111
+ include AppEntitlementsStatistics::Tryable
112
+ end