dependabot-cargo 0.81.0

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