dependabot-cargo 0.81.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.
- checksums.yaml +7 -0
- data/lib/dependabot/cargo.rb +11 -0
- data/lib/dependabot/cargo/file_fetcher.rb +241 -0
- data/lib/dependabot/cargo/file_parser.rb +214 -0
- data/lib/dependabot/cargo/file_updater.rb +83 -0
- data/lib/dependabot/cargo/file_updater/lockfile_updater.rb +247 -0
- data/lib/dependabot/cargo/file_updater/manifest_updater.rb +158 -0
- data/lib/dependabot/cargo/metadata_finder.rb +62 -0
- data/lib/dependabot/cargo/requirement.rb +108 -0
- data/lib/dependabot/cargo/update_checker.rb +283 -0
- data/lib/dependabot/cargo/update_checker/file_preparer.rb +200 -0
- data/lib/dependabot/cargo/update_checker/requirements_updater.rb +173 -0
- data/lib/dependabot/cargo/update_checker/version_resolver.rb +239 -0
- data/lib/dependabot/cargo/version.rb +34 -0
- metadata +183 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "toml-rb"
|
|
4
|
+
require "dependabot/git_commit_checker"
|
|
5
|
+
require "dependabot/file_updaters"
|
|
6
|
+
require "dependabot/file_updaters/base"
|
|
7
|
+
require "dependabot/shared_helpers"
|
|
8
|
+
|
|
9
|
+
module Dependabot
|
|
10
|
+
module Cargo
|
|
11
|
+
class FileUpdater < Dependabot::FileUpdaters::Base
|
|
12
|
+
require_relative "file_updater/manifest_updater"
|
|
13
|
+
require_relative "file_updater/lockfile_updater"
|
|
14
|
+
|
|
15
|
+
def self.updated_files_regex
|
|
16
|
+
[
|
|
17
|
+
/^Cargo\.toml$/,
|
|
18
|
+
/^Cargo\.lock$/
|
|
19
|
+
]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def updated_dependency_files
|
|
23
|
+
# Returns an array of updated files. Only files that have been updated
|
|
24
|
+
# should be returned.
|
|
25
|
+
updated_files = []
|
|
26
|
+
|
|
27
|
+
manifest_files.each do |file|
|
|
28
|
+
next unless file_changed?(file)
|
|
29
|
+
|
|
30
|
+
updated_files <<
|
|
31
|
+
updated_file(
|
|
32
|
+
file: file,
|
|
33
|
+
content: updated_manifest_content(file)
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if lockfile && updated_lockfile_content != lockfile.content
|
|
38
|
+
updated_files <<
|
|
39
|
+
updated_file(file: lockfile, content: updated_lockfile_content)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
raise "No files changed!" if updated_files.empty?
|
|
43
|
+
|
|
44
|
+
updated_files
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def check_required_files
|
|
50
|
+
raise "No Cargo.toml!" unless get_original_file("Cargo.toml")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def updated_manifest_content(file)
|
|
54
|
+
ManifestUpdater.new(
|
|
55
|
+
dependencies: dependencies,
|
|
56
|
+
manifest: file
|
|
57
|
+
).updated_manifest_content
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def updated_lockfile_content
|
|
61
|
+
@updated_lockfile_content ||=
|
|
62
|
+
LockfileUpdater.new(
|
|
63
|
+
dependencies: dependencies,
|
|
64
|
+
dependency_files: dependency_files,
|
|
65
|
+
credentials: credentials
|
|
66
|
+
).updated_lockfile_content
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def manifest_files
|
|
70
|
+
@manifest_files ||=
|
|
71
|
+
dependency_files.
|
|
72
|
+
select { |f| f.name.end_with?("Cargo.toml") }.
|
|
73
|
+
reject { |f| f.type == "path_dependency" }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def lockfile
|
|
77
|
+
@lockfile ||= get_original_file("Cargo.lock")
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
Dependabot::FileUpdaters.register("cargo", Dependabot::Cargo::FileUpdater)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "toml-rb"
|
|
4
|
+
require "dependabot/git_commit_checker"
|
|
5
|
+
require "dependabot/cargo/file_updater"
|
|
6
|
+
require "dependabot/cargo/file_updater/manifest_updater"
|
|
7
|
+
require "dependabot/cargo/file_parser"
|
|
8
|
+
require "dependabot/shared_helpers"
|
|
9
|
+
|
|
10
|
+
module Dependabot
|
|
11
|
+
module Cargo
|
|
12
|
+
class FileUpdater
|
|
13
|
+
class LockfileUpdater
|
|
14
|
+
def initialize(dependencies:, dependency_files:, credentials:)
|
|
15
|
+
@dependencies = dependencies
|
|
16
|
+
@dependency_files = dependency_files
|
|
17
|
+
@credentials = credentials
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def updated_lockfile_content
|
|
21
|
+
base_directory = dependency_files.first.directory
|
|
22
|
+
SharedHelpers.in_a_temporary_directory(base_directory) do
|
|
23
|
+
write_temporary_dependency_files
|
|
24
|
+
|
|
25
|
+
SharedHelpers.with_git_configured(credentials: credentials) do
|
|
26
|
+
# Shell out to Cargo, which handles everything for us, and does
|
|
27
|
+
# so without doing an install (so it's fast).
|
|
28
|
+
command = "cargo update -p #{dependency_spec}"
|
|
29
|
+
run_shell_command(command)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
updated_lockfile = File.read("Cargo.lock")
|
|
33
|
+
updated_lockfile = post_process_lockfile(updated_lockfile)
|
|
34
|
+
|
|
35
|
+
if updated_lockfile.include?(desired_lockfile_content)
|
|
36
|
+
next updated_lockfile
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
raise "Failed to update #{dependency.name}!"
|
|
40
|
+
end
|
|
41
|
+
rescue Dependabot::SharedHelpers::HelperSubprocessFailed => error
|
|
42
|
+
handle_cargo_error(error)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
attr_reader :dependencies, :dependency_files, :credentials
|
|
48
|
+
|
|
49
|
+
# Currently, there will only be a single updated dependency
|
|
50
|
+
def dependency
|
|
51
|
+
dependencies.first
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def handle_cargo_error(error)
|
|
55
|
+
raise unless error.message.include?("failed to select a version")
|
|
56
|
+
raise if error.message.include?("`#{dependency.name} ")
|
|
57
|
+
|
|
58
|
+
raise Dependabot::DependencyFileNotResolvable, error.message
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def dependency_spec
|
|
62
|
+
spec = dependency.name
|
|
63
|
+
|
|
64
|
+
if git_dependency?
|
|
65
|
+
spec += ":#{git_previous_version}" if git_previous_version
|
|
66
|
+
elsif dependency.previous_version
|
|
67
|
+
spec += ":#{dependency.previous_version}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
spec
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def git_previous_version
|
|
74
|
+
TomlRB.parse(lockfile.content).
|
|
75
|
+
fetch("package", []).
|
|
76
|
+
select { |p| p["name"] == dependency.name }.
|
|
77
|
+
find { |p| p["source"].end_with?(dependency.previous_version) }.
|
|
78
|
+
fetch("version")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def desired_lockfile_content
|
|
82
|
+
return dependency.version if git_dependency?
|
|
83
|
+
|
|
84
|
+
%(name = "#{dependency.name}"\nversion = "#{dependency.version}")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def run_shell_command(command)
|
|
88
|
+
raw_response = nil
|
|
89
|
+
IO.popen(command, err: %i(child out)) do |process|
|
|
90
|
+
raw_response = process.read
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Raise an error with the output from the shell session if Cargo
|
|
94
|
+
# returns a non-zero status
|
|
95
|
+
return if $CHILD_STATUS.success?
|
|
96
|
+
|
|
97
|
+
raise SharedHelpers::HelperSubprocessFailed.new(
|
|
98
|
+
raw_response,
|
|
99
|
+
command
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def write_temporary_dependency_files
|
|
104
|
+
write_temporary_manifest_files
|
|
105
|
+
write_temporary_path_dependency_files
|
|
106
|
+
|
|
107
|
+
File.write(lockfile.name, lockfile.content)
|
|
108
|
+
File.write(toolchain.name, toolchain.content) if toolchain
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def write_temporary_manifest_files
|
|
112
|
+
manifest_files.each do |file|
|
|
113
|
+
path = file.name
|
|
114
|
+
dir = Pathname.new(path).dirname
|
|
115
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
|
116
|
+
File.write(file.name, prepared_manifest_content(file))
|
|
117
|
+
|
|
118
|
+
FileUtils.mkdir_p(File.join(dir, "src"))
|
|
119
|
+
File.write(File.join(dir, "src/lib.rs"), dummy_app_content)
|
|
120
|
+
File.write(File.join(dir, "src/main.rs"), dummy_app_content)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def write_temporary_path_dependency_files
|
|
125
|
+
path_dependency_files.each do |file|
|
|
126
|
+
path = file.name
|
|
127
|
+
dir = Pathname.new(path).dirname
|
|
128
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
|
129
|
+
File.write(file.name, prepared_path_dependency_content(file))
|
|
130
|
+
|
|
131
|
+
FileUtils.mkdir_p(File.join(dir, "src"))
|
|
132
|
+
File.write(File.join(dir, "src/lib.rs"), dummy_app_content)
|
|
133
|
+
File.write(File.join(dir, "src/main.rs"), dummy_app_content)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def prepared_manifest_content(file)
|
|
138
|
+
content = updated_manifest_content(file)
|
|
139
|
+
content = pin_version(content) unless git_dependency?
|
|
140
|
+
content = replace_ssh_urls(content)
|
|
141
|
+
content
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def prepared_path_dependency_content(file)
|
|
145
|
+
content = file.content.dup
|
|
146
|
+
content = replace_ssh_urls(content)
|
|
147
|
+
content
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def updated_manifest_content(file)
|
|
151
|
+
ManifestUpdater.new(
|
|
152
|
+
dependencies: dependencies,
|
|
153
|
+
manifest: file
|
|
154
|
+
).updated_manifest_content
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def pin_version(content)
|
|
158
|
+
parsed_manifest = TomlRB.parse(content)
|
|
159
|
+
|
|
160
|
+
Cargo::FileParser::DEPENDENCY_TYPES.each do |type|
|
|
161
|
+
next unless (req = parsed_manifest.dig(type, dependency.name))
|
|
162
|
+
|
|
163
|
+
updated_req = "=#{dependency.version}"
|
|
164
|
+
|
|
165
|
+
if req.is_a?(Hash)
|
|
166
|
+
parsed_manifest[type][dependency.name]["version"] = updated_req
|
|
167
|
+
else
|
|
168
|
+
parsed_manifest[type][dependency.name] = updated_req
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
TomlRB.dump(parsed_manifest)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def replace_ssh_urls(content)
|
|
176
|
+
git_ssh_requirements_to_swap.each do |ssh_url, https_url|
|
|
177
|
+
content = content.gsub(ssh_url, https_url)
|
|
178
|
+
end
|
|
179
|
+
content
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def post_process_lockfile(content)
|
|
183
|
+
git_ssh_requirements_to_swap.each do |ssh_url, https_url|
|
|
184
|
+
content = content.gsub(https_url, ssh_url)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
content
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def git_ssh_requirements_to_swap
|
|
191
|
+
return @git_ssh_requirements_to_swap if @git_ssh_requirements_to_swap
|
|
192
|
+
|
|
193
|
+
@git_ssh_requirements_to_swap = {}
|
|
194
|
+
|
|
195
|
+
[*manifest_files, *path_dependency_files].each do |manifest|
|
|
196
|
+
parsed_manifest = TomlRB.parse(manifest.content)
|
|
197
|
+
|
|
198
|
+
Cargo::FileParser::DEPENDENCY_TYPES.each do |type|
|
|
199
|
+
(parsed_manifest[type] || {}).each do |_, details|
|
|
200
|
+
next unless details.is_a?(Hash)
|
|
201
|
+
next unless details["git"]&.match?(%r{ssh://git@(.*?)/})
|
|
202
|
+
|
|
203
|
+
@git_ssh_requirements_to_swap[details["git"]] =
|
|
204
|
+
details["git"].gsub(%r{ssh://git@(.*?)/}, 'https://\1/')
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
@git_ssh_requirements_to_swap
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def dummy_app_content
|
|
213
|
+
%{fn main() {\nprintln!("Hello, world!");\n}}
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def git_dependency?
|
|
217
|
+
GitCommitChecker.new(
|
|
218
|
+
dependency: dependency,
|
|
219
|
+
credentials: credentials
|
|
220
|
+
).git_dependency?
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def manifest_files
|
|
224
|
+
@manifest_files ||=
|
|
225
|
+
dependency_files.
|
|
226
|
+
select { |f| f.name.end_with?("Cargo.toml") }.
|
|
227
|
+
reject { |f| f.type == "path_dependency" }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def path_dependency_files
|
|
231
|
+
@path_dependency_files ||=
|
|
232
|
+
dependency_files.
|
|
233
|
+
select { |f| f.type == "path_dependency" }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def lockfile
|
|
237
|
+
@lockfile ||= dependency_files.find { |f| f.name == "Cargo.lock" }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def toolchain
|
|
241
|
+
@toolchain ||=
|
|
242
|
+
dependency_files.find { |f| f.name == "rust-toolchain" }
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dependabot/cargo/file_updater"
|
|
4
|
+
|
|
5
|
+
module Dependabot
|
|
6
|
+
module Cargo
|
|
7
|
+
class FileUpdater
|
|
8
|
+
class ManifestUpdater
|
|
9
|
+
def initialize(dependencies:, manifest:)
|
|
10
|
+
@dependencies = dependencies
|
|
11
|
+
@manifest = manifest
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def updated_manifest_content
|
|
15
|
+
dependencies.
|
|
16
|
+
select { |dep| requirement_changed?(manifest, dep) }.
|
|
17
|
+
reduce(manifest.content.dup) do |content, dep|
|
|
18
|
+
updated_content = content
|
|
19
|
+
|
|
20
|
+
updated_content = update_requirements(
|
|
21
|
+
content: updated_content,
|
|
22
|
+
filename: manifest.name,
|
|
23
|
+
dependency: dep
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
updated_content = update_git_pin(
|
|
27
|
+
content: updated_content,
|
|
28
|
+
filename: manifest.name,
|
|
29
|
+
dependency: dep
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
raise "Expected content to change!" if content == updated_content
|
|
33
|
+
|
|
34
|
+
updated_content
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
attr_reader :dependencies, :manifest
|
|
41
|
+
|
|
42
|
+
def requirement_changed?(file, dependency)
|
|
43
|
+
changed_requirements =
|
|
44
|
+
dependency.requirements - dependency.previous_requirements
|
|
45
|
+
|
|
46
|
+
changed_requirements.any? { |f| f[:file] == file.name }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def update_requirements(content:, filename:, dependency:)
|
|
50
|
+
updated_content = content.dup
|
|
51
|
+
|
|
52
|
+
# The UpdateChecker ensures the order of requirements is preserved
|
|
53
|
+
# when updating, so we can zip them together in new/old pairs.
|
|
54
|
+
reqs = dependency.requirements.
|
|
55
|
+
zip(dependency.previous_requirements).
|
|
56
|
+
reject { |new_req, old_req| new_req == old_req }
|
|
57
|
+
|
|
58
|
+
# Loop through each changed requirement
|
|
59
|
+
reqs.each do |new_req, old_req|
|
|
60
|
+
raise "Bad req match" unless new_req[:file] == old_req[:file]
|
|
61
|
+
next if new_req[:requirement] == old_req[:requirement]
|
|
62
|
+
next unless new_req[:file] == filename
|
|
63
|
+
|
|
64
|
+
updated_content = update_manifest_req(
|
|
65
|
+
content: updated_content,
|
|
66
|
+
dep: dependency,
|
|
67
|
+
old_req: old_req.fetch(:requirement),
|
|
68
|
+
new_req: new_req.fetch(:requirement)
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
updated_content
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def update_git_pin(content:, filename:, dependency:)
|
|
76
|
+
updated_pin =
|
|
77
|
+
dependency.requirements.
|
|
78
|
+
find { |r| r[:file] == filename }&.
|
|
79
|
+
dig(:source, :ref)
|
|
80
|
+
|
|
81
|
+
old_pin =
|
|
82
|
+
dependency.previous_requirements.
|
|
83
|
+
find { |r| r[:file] == filename }&.
|
|
84
|
+
dig(:source, :ref)
|
|
85
|
+
|
|
86
|
+
return content unless old_pin
|
|
87
|
+
|
|
88
|
+
update_manifest_pin(
|
|
89
|
+
content: content,
|
|
90
|
+
dep: dependency,
|
|
91
|
+
old_pin: old_pin,
|
|
92
|
+
new_pin: updated_pin
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def update_manifest_req(content:, dep:, old_req:, new_req:)
|
|
97
|
+
simple_declaration = content.scan(declaration_regex(dep)).
|
|
98
|
+
find { |m| m.include?(old_req) }
|
|
99
|
+
|
|
100
|
+
if simple_declaration
|
|
101
|
+
content.gsub(simple_declaration) do |line|
|
|
102
|
+
line.gsub(old_req, new_req)
|
|
103
|
+
end
|
|
104
|
+
elsif content.match?(feature_declaration_version_regex(dep))
|
|
105
|
+
content.gsub(feature_declaration_version_regex(dep)) do |part|
|
|
106
|
+
line = content.match(feature_declaration_version_regex(dep)).
|
|
107
|
+
named_captures.fetch("version_declaration")
|
|
108
|
+
new_line = line.gsub(old_req, new_req)
|
|
109
|
+
part.gsub(line, new_line)
|
|
110
|
+
end
|
|
111
|
+
else
|
|
112
|
+
content
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def update_manifest_pin(content:, dep:, old_pin:, new_pin:)
|
|
117
|
+
simple_declaration = content.scan(declaration_regex(dep)).
|
|
118
|
+
find { |m| m.include?(old_pin) }
|
|
119
|
+
|
|
120
|
+
if simple_declaration
|
|
121
|
+
content.gsub(simple_declaration) do |line|
|
|
122
|
+
line.gsub(old_pin, new_pin)
|
|
123
|
+
end
|
|
124
|
+
elsif content.match?(feature_declaration_pin_regex(dep))
|
|
125
|
+
content.gsub(feature_declaration_pin_regex(dep)) do |part|
|
|
126
|
+
line = content.match(feature_declaration_pin_regex(dep)).
|
|
127
|
+
named_captures.fetch("pin_declaration")
|
|
128
|
+
new_line = line.gsub(old_pin, new_pin)
|
|
129
|
+
part.gsub(line, new_line)
|
|
130
|
+
end
|
|
131
|
+
else
|
|
132
|
+
content
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def declaration_regex(dep)
|
|
137
|
+
/(?:^|["'])#{Regexp.escape(dep.name)}["']?\s*=.*$/i
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def feature_declaration_version_regex(dep)
|
|
141
|
+
/
|
|
142
|
+
#{Regexp.quote("dependencies.#{dep.name}]")}
|
|
143
|
+
(?:(?!^\[).)+
|
|
144
|
+
(?<version_declaration>version\s*=[^\[]*)$
|
|
145
|
+
/mx
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def feature_declaration_pin_regex(dep)
|
|
149
|
+
/
|
|
150
|
+
#{Regexp.quote("dependencies.#{dep.name}]")}
|
|
151
|
+
(?:(?!^\[).)+
|
|
152
|
+
(?<pin_declaration>(?:tag|rev)\s*=[^\[]*)$
|
|
153
|
+
/mx
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|