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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9a15f5a5286fc161d2f722711b599016486490abb64c446631eeed673a9a7742
4
+ data.tar.gz: d01689805661704379ea8b2ccd1795dd674fcf7c5c6a8c0de8680d64dfd08a31
5
+ SHA512:
6
+ metadata.gz: e3bcff1df9f77bc8b94230cff603352c84dab68e7c4f2648c952e32b02ad2a186a2052e4cd5d862c7e59cb19c247e1772b720e3ccf69c81dbfd94943406151b0
7
+ data.tar.gz: cf7de24ba36776e1b0377caf4a73f99eeb172f432c32c2f9ae18dad0c04b08ea8b3652aee5319f4a2a70f12bc630ea7419ac40b75349c2217260a6bac0d46244
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # These all need to be required so the various classes can be registered in a
4
+ # lookup table of package manager names to concrete classes.
5
+ require "dependabot/cargo/file_fetcher"
6
+ require "dependabot/cargo/file_parser"
7
+ require "dependabot/cargo/update_checker"
8
+ require "dependabot/cargo/file_updater"
9
+ require "dependabot/cargo/metadata_finder"
10
+ require "dependabot/cargo/requirement"
11
+ require "dependabot/cargo/version"
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "toml-rb"
5
+
6
+ require "dependabot/file_fetchers"
7
+ require "dependabot/file_fetchers/base"
8
+ require "dependabot/cargo/file_parser"
9
+
10
+ # Docs on Cargo workspaces:
11
+ # https://doc.rust-lang.org/cargo/reference/manifest.html#the-workspace-section
12
+ module Dependabot
13
+ module Cargo
14
+ class FileFetcher < Dependabot::FileFetchers::Base
15
+ def self.required_files_in?(filenames)
16
+ filenames.include?("Cargo.toml")
17
+ end
18
+
19
+ def self.required_files_message
20
+ "Repo must contain a Cargo.toml."
21
+ end
22
+
23
+ private
24
+
25
+ def fetch_files
26
+ fetched_files = []
27
+ fetched_files << cargo_toml
28
+ fetched_files << cargo_lock if cargo_lock
29
+ fetched_files << rust_toolchain if rust_toolchain
30
+ fetched_files += workspace_files
31
+ fetched_files += path_dependency_files
32
+ fetched_files
33
+ end
34
+
35
+ def workspace_files
36
+ @workspace_files ||=
37
+ fetch_workspace_files(
38
+ file: cargo_toml,
39
+ previously_fetched_files: []
40
+ )
41
+ end
42
+
43
+ def path_dependency_files
44
+ @path_dependency_files ||=
45
+ begin
46
+ fetched_path_dependency_files = []
47
+ [cargo_toml, *workspace_files].each do |file|
48
+ fetched_path_dependency_files +=
49
+ fetch_path_dependency_files(
50
+ file: file,
51
+ previously_fetched_files: [cargo_toml, *workspace_files] +
52
+ fetched_path_dependency_files
53
+ )
54
+ end
55
+
56
+ fetched_path_dependency_files
57
+ end
58
+ end
59
+
60
+ def fetch_workspace_files(file:, previously_fetched_files:)
61
+ current_dir = file.name.split("/")[0..-2].join("/")
62
+ current_dir = nil if current_dir == ""
63
+
64
+ workspace_dependency_paths_from_file(file).flat_map do |path|
65
+ path = File.join(current_dir, path) unless current_dir.nil?
66
+ path = Pathname.new(path).cleanpath.to_path
67
+
68
+ next if previously_fetched_files.map(&:name).include?(path)
69
+ next if file.name == path
70
+
71
+ fetched_file = fetch_file_from_host(path)
72
+ previously_fetched_files << fetched_file
73
+ grandchild_requirement_files =
74
+ fetch_workspace_files(
75
+ file: fetched_file,
76
+ previously_fetched_files: previously_fetched_files
77
+ )
78
+ [fetched_file, *grandchild_requirement_files]
79
+ end.compact
80
+ end
81
+
82
+ def fetch_path_dependency_files(
83
+ file:,
84
+ previously_fetched_files:
85
+ )
86
+ current_dir = file.name.split("/")[0..-2].join("/")
87
+ current_dir = nil if current_dir == ""
88
+
89
+ path_dependency_paths_from_file(file).flat_map do |path|
90
+ path = File.join(current_dir, path) unless current_dir.nil?
91
+ path = Pathname.new(path).cleanpath.to_path
92
+
93
+ next if previously_fetched_files.map(&:name).include?(path)
94
+ next if file.name == path
95
+
96
+ fetched_file = fetch_file_from_host(path, type: "path_dependency").
97
+ tap { |f| f.support_file = true }
98
+ previously_fetched_files << fetched_file
99
+ grandchild_requirement_files =
100
+ fetch_path_dependency_files(
101
+ file: fetched_file,
102
+ previously_fetched_files: previously_fetched_files
103
+ )
104
+ [fetched_file, *grandchild_requirement_files]
105
+ rescue Dependabot::DependencyFileNotFound
106
+ raise if required_path?(file, path)
107
+ end.compact
108
+ end
109
+
110
+ def path_dependency_paths_from_file(file)
111
+ paths = []
112
+
113
+ # Paths specified in dependency declaration
114
+ Cargo::FileParser::DEPENDENCY_TYPES.each do |type|
115
+ parsed_file(file).fetch(type, {}).each do |_, details|
116
+ next unless details.is_a?(Hash)
117
+ next unless details["path"]
118
+
119
+ paths << File.join(details["path"], "Cargo.toml")
120
+ end
121
+ end
122
+
123
+ # Paths specified for target-specific dependencies
124
+ parsed_file(file).fetch("target", {}).each do |_, t_details|
125
+ Cargo::FileParser::DEPENDENCY_TYPES.each do |type|
126
+ t_details.fetch(type, {}).each do |_, details|
127
+ next unless details.is_a?(Hash)
128
+ next unless details["path"]
129
+
130
+ paths << File.join(details["path"], "Cargo.toml")
131
+ end
132
+ end
133
+ end
134
+
135
+ # Paths specified as replacements
136
+ parsed_file(file).fetch("replace", {}).each do |_, details|
137
+ next unless details.is_a?(Hash)
138
+ next unless details["path"]
139
+
140
+ paths << File.join(details["path"], "Cargo.toml")
141
+ end
142
+
143
+ paths
144
+ end
145
+
146
+ def workspace_dependency_paths_from_file(file)
147
+ workspace_paths = parsed_file(file).dig("workspace", "members")
148
+ return [] unless workspace_paths&.any?
149
+
150
+ # Expand any workspace paths that specify a `*`
151
+ workspace_paths = workspace_paths.flat_map do |path|
152
+ path.end_with?("*") ? expand_workspaces(path) : [path]
153
+ end
154
+
155
+ # Excluded paths, to be subtracted for the workspaces array
156
+ excluded_paths = parsed_file(file).dig("workspace", "excluded_paths")
157
+
158
+ (workspace_paths - (excluded_paths || [])).map do |path|
159
+ File.join(path, "Cargo.toml")
160
+ end
161
+ end
162
+
163
+ # Check whether a path is required or not. It will not be required if
164
+ # an alternative source (i.e., a git source) is also specified
165
+ # rubocop:disable Metrics/AbcSize
166
+ # rubocop:disable Metrics/CyclomaticComplexity
167
+ # rubocop:disable Metrics/PerceivedComplexity
168
+ def required_path?(file, path)
169
+ # Paths specified in dependency declaration
170
+ Cargo::FileParser::DEPENDENCY_TYPES.each do |type|
171
+ parsed_file(file).fetch(type, {}).each do |_, details|
172
+ next unless details.is_a?(Hash)
173
+ next unless details["path"]
174
+ next unless path == File.join(details["path"], "Cargo.toml")
175
+
176
+ return true if details["git"].nil?
177
+ end
178
+ end
179
+
180
+ # Paths specified for target-specific dependencies
181
+ parsed_file(file).fetch("target", {}).each do |_, t_details|
182
+ Cargo::FileParser::DEPENDENCY_TYPES.each do |type|
183
+ t_details.fetch(type, {}).each do |_, details|
184
+ next unless details.is_a?(Hash)
185
+ next unless details["path"]
186
+ next unless path == File.join(details["path"], "Cargo.toml")
187
+
188
+ return true if details["git"].nil?
189
+ end
190
+ end
191
+ end
192
+
193
+ # Paths specified as replacements
194
+ parsed_file(file).fetch("replace", {}).each do |_, details|
195
+ next unless details.is_a?(Hash)
196
+ next unless details["path"]
197
+ next unless path == File.join(details["path"], "Cargo.toml")
198
+
199
+ return true if details["git"].nil?
200
+ end
201
+
202
+ false
203
+ end
204
+ # rubocop:enable Metrics/AbcSize
205
+ # rubocop:enable Metrics/CyclomaticComplexity
206
+ # rubocop:enable Metrics/PerceivedComplexity
207
+
208
+ def expand_workspaces(path)
209
+ path = Pathname.new(path).cleanpath.to_path
210
+ dir = directory.gsub(%r{(^/|/$)}, "")
211
+ unglobbed_path = path.split("*").first.gsub(%r{(?<=/)[^/]*$}, "")
212
+
213
+ repo_contents(dir: unglobbed_path, raise_errors: false).
214
+ select { |file| file.type == "dir" }.
215
+ map { |f| f.path.gsub(%r{^/?#{Regexp.escape(dir)}/?}, "") }.
216
+ select { |filename| File.fnmatch?(path, filename) }
217
+ end
218
+
219
+ def parsed_file(file)
220
+ TomlRB.parse(file.content)
221
+ rescue TomlRB::ParseError
222
+ raise Dependabot::DependencyFileNotParseable, file.path
223
+ end
224
+
225
+ def cargo_toml
226
+ @cargo_toml ||= fetch_file_from_host("Cargo.toml")
227
+ end
228
+
229
+ def cargo_lock
230
+ @cargo_lock ||= fetch_file_if_present("Cargo.lock")
231
+ end
232
+
233
+ def rust_toolchain
234
+ @rust_toolchain ||= fetch_file_if_present("rust-toolchain")&.
235
+ tap { |f| f.support_file = true }
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+ Dependabot::FileFetchers.register("cargo", Dependabot::Cargo::FileFetcher)
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "toml-rb"
4
+
5
+ require "dependabot/dependency"
6
+ require "dependabot/file_parsers"
7
+ require "dependabot/file_parsers/base"
8
+ require "dependabot/cargo/requirement"
9
+ require "dependabot/cargo/version"
10
+ require "dependabot/errors"
11
+
12
+ # Relevant Cargo docs can be found at:
13
+ # - https://doc.rust-lang.org/cargo/reference/manifest.html
14
+ # - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
15
+ module Dependabot
16
+ module Cargo
17
+ class FileParser < Dependabot::FileParsers::Base
18
+ require "dependabot/file_parsers/base/dependency_set"
19
+
20
+ DEPENDENCY_TYPES =
21
+ %w(dependencies dev-dependencies build-dependencies).freeze
22
+
23
+ def parse
24
+ check_rust_workspace_root
25
+
26
+ dependency_set = DependencySet.new
27
+ dependency_set += manifest_dependencies
28
+ dependency_set += lockfile_dependencies if lockfile
29
+
30
+ dependencies = dependency_set.dependencies
31
+
32
+ # TODO: Handle patched dependencies
33
+ dependencies.reject! { |d| patched_dependencies.include?(d.name) }
34
+
35
+ # TODO: Currently, Dependabot can't handle dependencies that have
36
+ # multiple source types. Fix that!
37
+ dependencies.reject do |dep|
38
+ dep.requirements.map { |r| r.dig(:source, :type) }.uniq.count > 1
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def check_rust_workspace_root
45
+ cargo_toml = dependency_files.find { |f| f.name == "Cargo.toml" }
46
+ workspace_root = parsed_file(cargo_toml).dig("package", "workspace")
47
+ return unless workspace_root
48
+
49
+ msg = "This project is part of a Rust workspace but is not the "\
50
+ "workspace root."\
51
+
52
+ if cargo_toml.directory != "/"
53
+ msg += "Please update your settings so Dependabot points at the "\
54
+ "workspace root instead of #{cargo_toml.directory}."
55
+ end
56
+ raise Dependabot::DependencyFileNotEvaluatable, msg
57
+ end
58
+
59
+ def manifest_dependencies
60
+ dependency_set = DependencySet.new
61
+
62
+ DEPENDENCY_TYPES.each do |type|
63
+ manifest_files.each do |file|
64
+ parsed_file(file).fetch(type, {}).each do |name, requirement|
65
+ next if lockfile && !version_from_lockfile(name, requirement)
66
+
67
+ dependency_set << Dependency.new(
68
+ name: name,
69
+ version: version_from_lockfile(name, requirement),
70
+ package_manager: "cargo",
71
+ requirements: [{
72
+ requirement: requirement_from_declaration(requirement),
73
+ file: file.name,
74
+ groups: [type],
75
+ source: source_from_declaration(requirement)
76
+ }]
77
+ )
78
+ end
79
+ end
80
+ end
81
+
82
+ dependency_set
83
+ end
84
+
85
+ def lockfile_dependencies
86
+ dependency_set = DependencySet.new
87
+ return dependency_set unless lockfile
88
+
89
+ parsed_file(lockfile).fetch("package", []).each do |package_details|
90
+ next unless package_details["source"]
91
+
92
+ # TODO: This isn't quite right, as it will only give us one
93
+ # version of each dependency (when in fact there are many)
94
+ dependency_set << Dependency.new(
95
+ name: package_details["name"],
96
+ version: version_from_lockfile_details(package_details),
97
+ package_manager: "cargo",
98
+ requirements: []
99
+ )
100
+ end
101
+
102
+ dependency_set
103
+ end
104
+
105
+ def patched_dependencies
106
+ root_manifest = manifest_files.find { |f| f.name == "Cargo.toml" }
107
+ return [] unless parsed_file(root_manifest)["patch"]
108
+
109
+ parsed_file(root_manifest)["patch"].values.flat_map(&:keys)
110
+ end
111
+
112
+ def requirement_from_declaration(declaration)
113
+ if declaration.is_a?(String)
114
+ return declaration == "" ? nil : declaration
115
+ end
116
+ unless declaration.is_a?(Hash)
117
+ raise "Unexpected dependency declaration: #{declaration}"
118
+ end
119
+ return declaration["version"] if declaration["version"]
120
+
121
+ nil
122
+ end
123
+
124
+ def source_from_declaration(declaration)
125
+ return if declaration.is_a?(String)
126
+ unless declaration.is_a?(Hash)
127
+ raise "Unexpected dependency declaration: #{declaration}"
128
+ end
129
+
130
+ return git_source_details(declaration) if declaration["git"]
131
+ return { type: "path" } if declaration["path"]
132
+ end
133
+
134
+ def version_from_lockfile(name, declaration)
135
+ return unless lockfile
136
+
137
+ candidate_packages =
138
+ parsed_file(lockfile).fetch("package", []).
139
+ select { |p| p["name"] == name }
140
+
141
+ if (req = requirement_from_declaration(declaration))
142
+ req = Cargo::Requirement.new(req)
143
+
144
+ candidate_packages =
145
+ candidate_packages.
146
+ select { |p| req.satisfied_by?(version_class.new(p["version"])) }
147
+ end
148
+
149
+ candidate_packages =
150
+ candidate_packages.
151
+ select do |p|
152
+ git_req?(declaration) ^ !p["source"]&.start_with?("git+")
153
+ end
154
+
155
+ package =
156
+ candidate_packages.
157
+ max_by { |p| version_class.new(p["version"]) }
158
+
159
+ return unless package
160
+
161
+ version_from_lockfile_details(package)
162
+ end
163
+
164
+ def git_req?(declaration)
165
+ source_from_declaration(declaration)&.fetch(:type, nil) == "git"
166
+ end
167
+
168
+ def git_source_details(declaration)
169
+ {
170
+ type: "git",
171
+ url: declaration["git"],
172
+ branch: declaration["branch"],
173
+ ref: declaration["tag"] || declaration["rev"]
174
+ }
175
+ end
176
+
177
+ def version_from_lockfile_details(package_details)
178
+ unless package_details["source"]&.start_with?("git+")
179
+ return package_details["version"]
180
+ end
181
+
182
+ package_details["source"].split("#").last
183
+ end
184
+
185
+ def check_required_files
186
+ raise "No Cargo.toml!" unless get_original_file("Cargo.toml")
187
+ end
188
+
189
+ def parsed_file(file)
190
+ @parsed_file ||= {}
191
+ @parsed_file[file.name] ||= TomlRB.parse(file.content)
192
+ rescue TomlRB::ParseError
193
+ raise Dependabot::DependencyFileNotParseable, file.path
194
+ end
195
+
196
+ def manifest_files
197
+ @manifest_files ||=
198
+ dependency_files.
199
+ select { |f| f.name.end_with?("Cargo.toml") }.
200
+ reject { |f| f.type == "path_dependency" }
201
+ end
202
+
203
+ def lockfile
204
+ @lockfile ||= get_original_file("Cargo.lock")
205
+ end
206
+
207
+ def version_class
208
+ Cargo::Version
209
+ end
210
+ end
211
+ end
212
+ end
213
+
214
+ Dependabot::FileParsers.register("cargo", Dependabot::Cargo::FileParser)