dependabot-cargo 0.81.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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