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.
@@ -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