dependabot-cargo 0.305.0 → 0.306.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7bb1d1d01e8722e5d2e2b4d7d5ebd81308db522828484bc57e3a5f7199acd16c
4
- data.tar.gz: 5c9fee65711dbb7fb55fa211d48578e46189053ad017b616576bee87530d066e
3
+ metadata.gz: 72b4039f8ac30bc8421818e11b43de5d2d9997cd5fdb5a4e53bba00b3bc7df15
4
+ data.tar.gz: c4a81d07aecd06538167a7c628e17c4f1aea1e0214816e5a8aaf29cad9796dc3
5
5
  SHA512:
6
- metadata.gz: da0510df1c5a53673c4cdae9278e8c5d8b674f44d88ae279eb79d97d14980d658fe48d2caba0c4390d472289f3788a78808f505d069803a3d19e5c86722f75e5
7
- data.tar.gz: 4634cef9a1f15590dba5127a23cfdf43d0661c10994cef61a0919a5dd2fd8b4f19f4e51acaf95d12a5e781453d0aa98f63218d0f3e5ae58eca331dd9304e64d2
6
+ metadata.gz: 0730b405c1f82cecdd1f1efb647e6eea37420b9d241fe46c6225bd41dd329388bff05b007051e6f901e110b27cd567b8a5dbe7a86600056f5f78d6dd657a4155
7
+ data.tar.gz: eb7cfa179819ae14421be61f9c847d1e79c94590ba90b0063d74a6c314e7b56740a2d9a3b9ef54ca68abeae8c2de72079426702ef641da9293dd1a4eab5c0df2
@@ -0,0 +1,231 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "time"
6
+ require "cgi"
7
+ require "excon"
8
+ require "nokogiri"
9
+ require "sorbet-runtime"
10
+ require "dependabot/registry_client"
11
+ require "dependabot/cargo"
12
+ require "dependabot/package/package_release"
13
+ require "dependabot/package/package_details"
14
+
15
+ module Dependabot
16
+ module Cargo
17
+ module Package
18
+ class PackageDetailsFetcher
19
+ extend T::Sig
20
+
21
+ CRATES_IO_API = "https://crates.io/api/v1/crates"
22
+ PACKAGE_TYPE = "gem"
23
+ PACKAGE_LANGUAGE = "rust"
24
+
25
+ # fallback for empty requirement
26
+ RUST_REQUIREMENT = "1.0"
27
+
28
+ sig do
29
+ params(
30
+ dependency: Dependabot::Dependency,
31
+ dependency_files: T::Array[Dependabot::DependencyFile],
32
+ credentials: T::Array[Dependabot::Credential]
33
+ ).void
34
+ end
35
+ def initialize(dependency:, dependency_files:, credentials:)
36
+ @dependency = dependency
37
+ @dependency_files = dependency_files
38
+ @credentials = credentials
39
+
40
+ @source_type = T.let(nil, T.nilable(String))
41
+ end
42
+
43
+ sig { returns(Dependabot::Dependency) }
44
+ attr_reader :dependency
45
+
46
+ sig { returns(T::Array[T.untyped]) }
47
+ attr_reader :dependency_files
48
+
49
+ sig { returns(T::Array[T.untyped]) }
50
+ attr_reader :credentials
51
+
52
+ sig do
53
+ returns(T.nilable(Dependabot::Package::PackageDetails))
54
+ end
55
+ def fetch
56
+ package_releases = crates_listing.fetch("versions", []).reject { |v| v["yanked"] }.map do |release|
57
+ package_release(
58
+ version: release["num"] || release["vers"],
59
+ released_at: release["created_at"] ? Time.parse(release["created_at"]) : nil,
60
+ downloads: release["downloads"],
61
+ url: "#{CRATES_IO_API}/#{release['dl_path']}",
62
+ rust_version: release["rust"]
63
+ )
64
+ end
65
+
66
+ package_details(package_releases)
67
+ end
68
+
69
+ private
70
+
71
+ sig do
72
+ returns(T.untyped)
73
+ end
74
+ def crates_listing
75
+ return @crates_listing unless @crates_listing.nil?
76
+
77
+ info = fetch_dependency_info
78
+ index = fetch_index(info)
79
+
80
+ hdrs = default_headers
81
+ hdrs.merge!(auth_headers(info)) if index != CRATES_IO_API
82
+
83
+ url = metadata_fetch_url(dependency, index)
84
+
85
+ Dependabot.logger.info("Calling #{url} to fetch metadata for #{dependency.name} from #{index}")
86
+
87
+ response = fetch_response(url, hdrs)
88
+ return {} if response.status == 404
89
+
90
+ @crates_listing = T.let(parse_response(response, index), T.nilable(T::Hash[T.untyped, T.untyped]))
91
+
92
+ Dependabot.logger.info("Fetched metadata for #{dependency.name} from #{index} successfully")
93
+
94
+ @crates_listing
95
+ end
96
+
97
+ sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
98
+ def fetch_dependency_info
99
+ dependency.requirements.filter_map { |r| r[:source] }.first
100
+ end
101
+
102
+ sig { params(info: T.untyped).returns(String) }
103
+ def fetch_index(info)
104
+ (info && info[:index]) || CRATES_IO_API
105
+ end
106
+
107
+ sig { returns(T::Hash[T.untyped, T.untyped]) }
108
+ def default_headers
109
+ { "User-Agent" => "Dependabot (dependabot.com)" }
110
+ end
111
+
112
+ sig { params(info: T.untyped).returns(T::Hash[T.untyped, T.untyped]) }
113
+ def auth_headers(info)
114
+ registry_creds = credentials.find do |cred|
115
+ cred["type"] == "cargo_registry" && cred["registry"] == info[:name]
116
+ end
117
+
118
+ return {} if registry_creds.nil?
119
+
120
+ token = registry_creds["token"] || "placeholder_token"
121
+ { "Authorization" => token }
122
+ end
123
+
124
+ sig { params(url: String, headers: T.untyped).returns(Excon::Response) }
125
+ def fetch_response(url, headers)
126
+ Excon.get(
127
+ url,
128
+ idempotent: true,
129
+ **SharedHelpers.excon_defaults(headers: headers)
130
+ )
131
+ end
132
+
133
+ sig { params(response: Excon::Response, index: T.untyped).returns(T::Hash[T.untyped, T.untyped]) }
134
+ def parse_response(response, index)
135
+ if index.start_with?("sparse+")
136
+ parsed_response = response.body.lines.map { |line| JSON.parse(line) }
137
+ { "versions" => parsed_response }
138
+ else
139
+ JSON.parse(response.body)
140
+ end
141
+ end
142
+
143
+ sig { params(dependency: T.untyped, index: T.untyped).returns(String) }
144
+ def metadata_fetch_url(dependency, index)
145
+ return "#{index}/#{dependency.name}" if index == CRATES_IO_API
146
+
147
+ # Determine cargo's index file path for the dependency
148
+ index = index.delete_prefix("sparse+")
149
+ name_length = dependency.name.length
150
+ dependency_path = case name_length
151
+ when 1, 2
152
+ "#{name_length}/#{dependency.name}"
153
+ when 3
154
+ "#{name_length}/#{dependency.name[0..1]}/#{dependency.name}"
155
+ else
156
+ "#{dependency.name[0..1]}/#{dependency.name[2..3]}/#{dependency.name}"
157
+ end
158
+
159
+ "#{index}#{'/' unless index.end_with?('/')}#{dependency_path}"
160
+ end
161
+
162
+ sig { returns(T::Boolean) }
163
+ def wants_prerelease?
164
+ return true if dependency.numeric_version&.prerelease?
165
+
166
+ dependency.requirements.any? do |req|
167
+ reqs = (req.fetch(:requirement) || "").split(",").map(&:strip)
168
+ reqs.any? { |r| r.match?(/[A-Za-z]/) }
169
+ end
170
+ end
171
+
172
+ sig { returns(T.class_of(Dependabot::Version)) }
173
+ def version_class
174
+ dependency.version_class
175
+ end
176
+
177
+ sig { returns(T.class_of(Dependabot::Requirement)) }
178
+ def requirement_class
179
+ dependency.requirement_class
180
+ end
181
+
182
+ sig do
183
+ params(
184
+ version: String,
185
+ released_at: T.nilable(Time),
186
+ downloads: T.nilable(Integer),
187
+ url: T.nilable(String),
188
+ rust_version: T.nilable(String),
189
+ yanked: T::Boolean
190
+ ).returns(Dependabot::Package::PackageRelease)
191
+ end
192
+ def package_release(version:, released_at:, downloads:, url:, rust_version:, yanked: false)
193
+ Dependabot::Package::PackageRelease.new(
194
+ version: Cargo::Version.new(version),
195
+ released_at: released_at,
196
+ yanked: yanked,
197
+ yanked_reason: nil,
198
+ downloads: downloads,
199
+ url: url,
200
+ package_type: PACKAGE_TYPE,
201
+ language: Dependabot::Package::PackageLanguage.new(
202
+ name: PACKAGE_LANGUAGE,
203
+ version: nil,
204
+ requirement: language_requirement(rust_version)
205
+ )
206
+ )
207
+ end
208
+
209
+ sig { params(req_string: T.nilable(String)).returns(T.nilable(Requirement)) }
210
+ def language_requirement(req_string)
211
+ return Requirement.new(req_string) if req_string && !req_string.empty?
212
+
213
+ nil
214
+ end
215
+
216
+ sig do
217
+ params(releases: T::Array[Dependabot::Package::PackageRelease])
218
+ .returns(Dependabot::Package::PackageDetails)
219
+ end
220
+ def package_details(releases)
221
+ @package_details ||= T.let(
222
+ Dependabot::Package::PackageDetails.new(
223
+ dependency: dependency,
224
+ releases: releases.reverse.uniq(&:version)
225
+ ), T.nilable(Dependabot::Package::PackageDetails)
226
+ )
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
@@ -5,16 +5,16 @@ require "excon"
5
5
  require "dependabot/cargo/update_checker"
6
6
  require "dependabot/update_checkers/version_filters"
7
7
  require "dependabot/registry_client"
8
+ require "dependabot/cargo/package/package_details_fetcher"
9
+ require "dependabot/package/package_latest_version_finder"
8
10
  require "sorbet-runtime"
9
11
 
10
12
  module Dependabot
11
13
  module Cargo
12
14
  class UpdateChecker
13
- class LatestVersionFinder
15
+ class LatestVersionFinder < Dependabot::Package::PackageLatestVersionFinder
14
16
  extend T::Sig
15
17
 
16
- CRATES_IO_API = "https://crates.io/api/v1/crates"
17
-
18
18
  def initialize(dependency:, dependency_files:, credentials:,
19
19
  ignored_versions:, raise_on_ignored: false,
20
20
  security_advisories:)
@@ -26,169 +26,28 @@ module Dependabot
26
26
  @security_advisories = security_advisories
27
27
  end
28
28
 
29
- def latest_version
30
- @latest_version ||= fetch_latest_version
31
- end
32
-
33
- def lowest_security_fix_version
34
- @lowest_security_fix_version ||= fetch_lowest_security_fix_version
35
- end
36
-
37
- private
38
-
39
- attr_reader :dependency
40
- attr_reader :dependency_files
41
- attr_reader :credentials
42
- attr_reader :ignored_versions
43
- attr_reader :security_advisories
44
-
45
- def fetch_latest_version
46
- versions = available_versions
47
- versions = filter_prerelease_versions(versions)
48
- versions = filter_ignored_versions(versions)
49
- versions.max
29
+ sig do
30
+ override.returns(T.nilable(Dependabot::Package::PackageDetails))
50
31
  end
51
-
52
- def fetch_lowest_security_fix_version
53
- versions = available_versions
54
- versions = filter_prerelease_versions(versions)
55
- versions = Dependabot::UpdateCheckers::VersionFilters.filter_vulnerable_versions(versions,
56
- security_advisories)
57
- versions = filter_ignored_versions(versions)
58
- versions = filter_lower_versions(versions)
59
-
60
- versions.min
32
+ def package_details
33
+ @package_details ||= Package::PackageDetailsFetcher.new(
34
+ dependency: dependency,
35
+ dependency_files: dependency_files,
36
+ credentials: credentials
37
+ ).fetch
61
38
  end
62
39
 
63
- sig { params(versions_array: T::Array[Gem::Version]).returns(T::Array[Gem::Version]) }
64
- def filter_prerelease_versions(versions_array)
65
- return versions_array if wants_prerelease?
66
-
67
- filtered = versions_array.reject(&:prerelease?)
68
- if versions_array.count > filtered.count
69
- Dependabot.logger.info("Filtered out #{versions_array.count - filtered.count} pre-release versions")
70
- end
71
- filtered
72
- end
73
-
74
- sig { params(versions_array: T::Array[Gem::Version]).returns(T::Array[Gem::Version]) }
75
- def filter_ignored_versions(versions_array)
76
- filtered = versions_array
77
- .reject { |v| ignore_requirements.any? { |r| r.satisfied_by?(v) } }
78
- if @raise_on_ignored && filter_lower_versions(filtered).empty? && filter_lower_versions(versions_array).any?
79
- raise Dependabot::AllVersionsIgnored
80
- end
81
-
82
- if versions_array.count > filtered.count
83
- Dependabot.logger.info("Filtered out #{versions_array.count - filtered.count} ignored versions")
84
- end
85
-
86
- filtered
87
- end
88
-
89
- def filter_lower_versions(versions_array)
90
- return versions_array unless dependency.numeric_version
91
-
92
- versions_array
93
- .select { |version| version > dependency.numeric_version }
94
- end
95
-
96
- def available_versions
97
- crates_listing
98
- .fetch("versions", [])
99
- .reject { |v| v["yanked"] }
100
- # Handle both default and sparse registry responses.
101
- # Default registry uses "num" for version number.
102
- # Sparse registry uses "vers" for version number.
103
- .map do |v|
104
- version_number = v["num"] || v["vers"]
105
- version_class.new(version_number)
106
- end
107
- end
108
-
109
- def crates_listing
110
- return @crates_listing unless @crates_listing.nil?
111
-
112
- info = fetch_dependency_info
113
- index = fetch_index(info)
114
-
115
- hdrs = default_headers
116
- hdrs.merge!(auth_headers(info)) if index != CRATES_IO_API
117
-
118
- url = metadata_fetch_url(dependency, index)
119
-
120
- # B4PR
121
- Dependabot.logger.info("Calling #{url} to fetch metadata for #{dependency.name} from #{index}")
122
-
123
- response = fetch_response(url, hdrs)
124
- return {} if response.status == 404
125
-
126
- @crates_listing = parse_response(response, index)
127
-
128
- # B4PR
129
- Dependabot.logger.info("Fetched metadata for #{dependency.name} from #{index} successfully")
130
-
131
- @crates_listing
132
- end
133
-
134
- def fetch_dependency_info
135
- dependency.requirements.filter_map { |r| r[:source] }.first
136
- end
137
-
138
- def fetch_index(info)
139
- (info && info[:index]) || CRATES_IO_API
140
- end
141
-
142
- def default_headers
143
- { "User-Agent" => "Dependabot (dependabot.com)" }
144
- end
145
-
146
- def auth_headers(info)
147
- registry_creds = credentials.find do |cred|
148
- cred["type"] == "cargo_registry" && cred["registry"] == info[:name]
149
- end
150
-
151
- return {} if registry_creds.nil?
152
-
153
- token = registry_creds["token"] || "placeholder_token"
154
- { "Authorization" => token }
155
- end
156
-
157
- def fetch_response(url, headers)
158
- Excon.get(
159
- url,
160
- idempotent: true,
161
- **SharedHelpers.excon_defaults(headers: headers)
162
- )
40
+ def latest_version
41
+ @latest_version ||= fetch_latest_version
163
42
  end
164
43
 
165
- def parse_response(response, index)
166
- if index.start_with?("sparse+")
167
- parsed_response = response.body.lines.map { |line| JSON.parse(line) }
168
- { "versions" => parsed_response }
169
- else
170
- JSON.parse(response.body)
171
- end
44
+ def lowest_security_fix_version
45
+ @lowest_security_fix_version ||= fetch_lowest_security_fix_version(language_version: nil)
172
46
  end
173
47
 
174
- def metadata_fetch_url(dependency, index)
175
- return "#{index}/#{dependency.name}" if index == CRATES_IO_API
176
-
177
- # Determine cargo's index file path for the dependency
178
- index = index.delete_prefix("sparse+")
179
- name_length = dependency.name.length
180
- dependency_path = case name_length
181
- when 1, 2
182
- "#{name_length}/#{dependency.name}"
183
- when 3
184
- "#{name_length}/#{dependency.name[0..1]}/#{dependency.name}"
185
- else
186
- "#{dependency.name[0..1]}/#{dependency.name[2..3]}/#{dependency.name}"
187
- end
188
-
189
- "#{index}#{'/' unless index.end_with?('/')}#{dependency_path}"
190
- end
48
+ protected
191
49
 
50
+ sig { override.returns(T::Boolean) }
192
51
  def wants_prerelease?
193
52
  return true if dependency.numeric_version&.prerelease?
194
53
 
@@ -198,16 +57,16 @@ module Dependabot
198
57
  end
199
58
  end
200
59
 
201
- def ignore_requirements
202
- ignored_versions.flat_map { |req| requirement_class.requirements_array(req) }
203
- end
60
+ private
204
61
 
205
- def version_class
206
- dependency.version_class
207
- end
62
+ attr_reader :dependency
63
+ attr_reader :dependency_files
64
+ attr_reader :credentials
65
+ attr_reader :ignored_versions
66
+ attr_reader :security_advisories
208
67
 
209
- def requirement_class
210
- dependency.requirement_class
68
+ def apply_post_fetch_lowest_security_fix_versions_filter(versions)
69
+ filter_prerelease_versions(versions)
211
70
  end
212
71
  end
213
72
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-cargo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.305.0
4
+ version: 0.306.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-06 00:00:00.000000000 Z
11
+ date: 2025-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dependabot-common
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.305.0
19
+ version: 0.306.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.305.0
26
+ version: 0.306.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: debug
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -251,6 +251,7 @@ files:
251
251
  - lib/dependabot/cargo/helpers.rb
252
252
  - lib/dependabot/cargo/language.rb
253
253
  - lib/dependabot/cargo/metadata_finder.rb
254
+ - lib/dependabot/cargo/package/package_details_fetcher.rb
254
255
  - lib/dependabot/cargo/package_manager.rb
255
256
  - lib/dependabot/cargo/registry_fetcher.rb
256
257
  - lib/dependabot/cargo/requirement.rb
@@ -265,7 +266,7 @@ licenses:
265
266
  - MIT
266
267
  metadata:
267
268
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
268
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.305.0
269
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.306.0
269
270
  post_install_message:
270
271
  rdoc_options: []
271
272
  require_paths: