dependabot-nuget 0.246.0 → 0.248.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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +40 -6
  3. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +27 -0
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs +18 -0
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +110 -0
  6. data/lib/dependabot/nuget/cache_manager.rb +9 -3
  7. data/lib/dependabot/nuget/file_fetcher/import_paths_finder.rb +15 -12
  8. data/lib/dependabot/nuget/file_fetcher/sln_project_paths_finder.rb +13 -3
  9. data/lib/dependabot/nuget/file_fetcher.rb +89 -37
  10. data/lib/dependabot/nuget/file_parser/dotnet_tools_json_parser.rb +10 -2
  11. data/lib/dependabot/nuget/file_parser/global_json_parser.rb +10 -2
  12. data/lib/dependabot/nuget/file_parser/packages_config_parser.rb +11 -2
  13. data/lib/dependabot/nuget/file_parser/project_file_parser.rb +140 -41
  14. data/lib/dependabot/nuget/file_parser/property_value_finder.rb +57 -5
  15. data/lib/dependabot/nuget/file_parser.rb +13 -3
  16. data/lib/dependabot/nuget/file_updater/property_value_updater.rb +25 -8
  17. data/lib/dependabot/nuget/file_updater.rb +74 -38
  18. data/lib/dependabot/nuget/http_response_helpers.rb +6 -1
  19. data/lib/dependabot/nuget/metadata_finder.rb +27 -3
  20. data/lib/dependabot/nuget/nuget_client.rb +23 -0
  21. data/lib/dependabot/nuget/nuget_config_credential_helpers.rb +10 -1
  22. data/lib/dependabot/nuget/requirement.rb +21 -9
  23. data/lib/dependabot/nuget/update_checker/compatibility_checker.rb +26 -15
  24. data/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb +87 -21
  25. data/lib/dependabot/nuget/update_checker/nuspec_fetcher.rb +25 -3
  26. data/lib/dependabot/nuget/update_checker/repository_finder.rb +25 -3
  27. data/lib/dependabot/nuget/update_checker/requirements_updater.rb +32 -9
  28. data/lib/dependabot/nuget/update_checker/tfm_finder.rb +2 -2
  29. data/lib/dependabot/nuget/update_checker/version_finder.rb +178 -64
  30. data/lib/dependabot/nuget/update_checker.rb +76 -32
  31. data/lib/dependabot/nuget/version.rb +7 -2
  32. metadata +19 -5
@@ -66,23 +66,34 @@ module Dependabot
66
66
  end
67
67
 
68
68
  def fetch_package_tfms(dependency_version)
69
- nupkg_buffer = NupkgFetcher.fetch_nupkg_buffer(dependency_urls, dependency.name, dependency_version)
70
- return [] unless nupkg_buffer
71
-
72
- # Parse tfms from the folders beneath the lib folder
73
- folder_name = "lib/"
74
- tfms = Set.new
75
- Zip::File.open_buffer(nupkg_buffer) do |zip|
76
- lib_file_entries = zip.select { |entry| entry.name.start_with?(folder_name) }
77
- # If there is no lib folder in this package, assume it is a development dependency
78
- return nil if lib_file_entries.empty?
79
-
80
- lib_file_entries.each do |entry|
81
- _, tfm = entry.name.split("/").first(2)
82
- tfms << tfm
69
+ cache = CacheManager.cache("compatibility_checker_tfms_cache")
70
+ key = "#{dependency.name}::#{dependency_version}"
71
+
72
+ cache[key] ||= begin
73
+ nupkg_buffer = NupkgFetcher.fetch_nupkg_buffer(dependency_urls, dependency.name, dependency_version)
74
+ return [] unless nupkg_buffer
75
+
76
+ # Parse tfms from the folders beneath the lib folder
77
+ folder_name = "lib/"
78
+ tfms = Set.new
79
+ Zip::File.open_buffer(nupkg_buffer) do |zip|
80
+ lib_file_entries = zip.select { |entry| entry.name.start_with?(folder_name) }
81
+ # If there is no lib folder in this package, assume it is a development dependency
82
+ return nil if lib_file_entries.empty?
83
+
84
+ lib_file_entries.each do |entry|
85
+ _, tfm = entry.name.split("/").first(2)
86
+
87
+ # some zip compressors create empty directory entries (in this case `lib/`) which can cause the string
88
+ # split to return `nil`, so we have to explicitly guard against that
89
+ tfms << tfm if tfm
90
+ end
83
91
  end
92
+
93
+ tfms.to_a
84
94
  end
85
- tfms.to_a
95
+
96
+ cache[key]
86
97
  end
87
98
  end
88
99
  end
@@ -1,23 +1,43 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "nokogiri"
5
- require "zip"
6
5
  require "stringio"
6
+ require "sorbet-runtime"
7
+ require "zip"
8
+
7
9
  require "dependabot/nuget/http_response_helpers"
8
10
 
9
11
  module Dependabot
10
12
  module Nuget
11
13
  class NupkgFetcher
14
+ extend T::Sig
15
+
12
16
  require_relative "repository_finder"
13
17
 
18
+ sig do
19
+ params(
20
+ dependency_urls: T::Array[T::Hash[Symbol, String]],
21
+ package_id: String,
22
+ package_version: String
23
+ )
24
+ .returns(T.nilable(String))
25
+ end
14
26
  def self.fetch_nupkg_buffer(dependency_urls, package_id, package_version)
15
27
  # check all repositories for the first one that has the nupkg
16
- dependency_urls.reduce(nil) do |nupkg_buffer, repository_details|
28
+ dependency_urls.reduce(T.let(nil, T.nilable(String))) do |nupkg_buffer, repository_details|
17
29
  nupkg_buffer || fetch_nupkg_buffer_from_repository(repository_details, package_id, package_version)
18
30
  end
19
31
  end
20
32
 
33
+ sig do
34
+ params(
35
+ repository_details: T::Hash[Symbol, T.untyped],
36
+ package_id: T.nilable(String),
37
+ package_version: T.nilable(String)
38
+ )
39
+ .returns(T.nilable(String))
40
+ end
21
41
  def self.fetch_nupkg_url_from_repository(repository_details, package_id, package_version)
22
42
  return unless package_id && package_version && !package_version.empty?
23
43
 
@@ -35,6 +55,14 @@ module Dependabot
35
55
  package_url
36
56
  end
37
57
 
58
+ sig do
59
+ params(
60
+ repository_details: T::Hash[Symbol, T.untyped],
61
+ package_id: String,
62
+ package_version: String
63
+ )
64
+ .returns(T.nilable(String))
65
+ end
38
66
  def self.fetch_nupkg_buffer_from_repository(repository_details, package_id, package_version)
39
67
  package_url = fetch_nupkg_url_from_repository(repository_details, package_id, package_version)
40
68
  return unless package_url
@@ -43,6 +71,14 @@ module Dependabot
43
71
  fetch_stream(package_url, auth_header)
44
72
  end
45
73
 
74
+ sig do
75
+ params(
76
+ repository_details: T::Hash[Symbol, T.untyped],
77
+ package_id: String,
78
+ package_version: String
79
+ )
80
+ .returns(T.nilable(String))
81
+ end
46
82
  def self.get_nuget_v3_package_url(repository_details, package_id, package_version)
47
83
  base_url = repository_details[:base_url]
48
84
  unless base_url
@@ -57,15 +93,23 @@ module Dependabot
57
93
 
58
94
  # rubocop:disable Metrics/CyclomaticComplexity
59
95
  # rubocop:disable Metrics/PerceivedComplexity
96
+ sig do
97
+ params(
98
+ repository_details: T::Hash[Symbol, T.untyped],
99
+ package_id: String,
100
+ package_version: String
101
+ )
102
+ .returns(T.nilable(String))
103
+ end
60
104
  def self.get_nuget_v3_package_url_from_search(repository_details, package_id, package_version)
61
105
  search_url = repository_details[:search_url]
62
106
  return nil unless search_url
63
107
 
64
108
  # get search result
65
109
  search_result_response = fetch_url(search_url, repository_details)
66
- return nil unless search_result_response.status == 200
110
+ return nil unless search_result_response&.status == 200
67
111
 
68
- search_response_body = HttpResponseHelpers.remove_wrapping_zero_width_chars(search_result_response.body)
112
+ search_response_body = HttpResponseHelpers.remove_wrapping_zero_width_chars(T.must(search_result_response).body)
69
113
  search_results = JSON.parse(search_response_body)
70
114
 
71
115
  # find matching package and version
@@ -90,15 +134,23 @@ module Dependabot
90
134
  # rubocop:enable Metrics/PerceivedComplexity
91
135
  # rubocop:enable Metrics/CyclomaticComplexity
92
136
 
137
+ sig do
138
+ params(
139
+ repository_details: T::Hash[Symbol, T.untyped],
140
+ package_id: String,
141
+ package_version: String
142
+ )
143
+ .returns(T.nilable(String))
144
+ end
93
145
  def self.get_nuget_v2_package_url(repository_details, package_id, package_version)
94
146
  # get package XML
95
147
  base_url = repository_details[:base_url].delete_suffix("/")
96
148
  package_url = "#{base_url}/Packages(Id='#{package_id}',Version='#{package_version}')"
97
149
  response = fetch_url(package_url, repository_details)
98
- return nil unless response.status == 200
150
+ return nil unless response&.status == 200
99
151
 
100
152
  # find relevant element
101
- doc = Nokogiri::XML(response.body)
153
+ doc = Nokogiri::XML(T.must(response).body)
102
154
  doc.remove_namespaces!
103
155
 
104
156
  content_element = doc.xpath("/entry/content")
@@ -106,46 +158,60 @@ module Dependabot
106
158
  nupkg_url
107
159
  end
108
160
 
161
+ sig do
162
+ params(
163
+ stream_url: String,
164
+ auth_header: T::Hash[String, String],
165
+ max_redirects: Integer
166
+ )
167
+ .returns(T.nilable(String))
168
+ end
109
169
  def self.fetch_stream(stream_url, auth_header, max_redirects = 5)
110
170
  current_url = stream_url
111
171
  current_redirects = 0
112
172
 
113
173
  loop do
114
- connection = Excon.new(current_url, persistent: true)
115
-
116
- package_data = StringIO.new
117
- response_block = lambda do |chunk, _remaining_bytes, _total_bytes|
118
- package_data.write(chunk)
119
- end
120
-
121
- response = connection.request(
122
- method: :get,
174
+ # Directly download the stream without any additional settings _except_ for `omit_default_port: true` which
175
+ # is necessary to not break the URL signing that some NuGet feeds use.
176
+ response = Excon.get(
177
+ current_url,
123
178
  headers: auth_header,
124
- response_block: response_block
179
+ omit_default_port: true
125
180
  )
126
181
 
127
182
  # redirect the HTTP response as appropriate based on documentation here:
128
183
  # https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections
129
184
  case response.status
130
185
  when 200
131
- package_data.rewind
132
- return package_data
186
+ return response.body
133
187
  when 301, 302, 303, 307, 308
134
188
  current_redirects += 1
135
189
  return nil if current_redirects > max_redirects
136
190
 
137
- current_url = response.headers["Location"]
191
+ current_url = T.must(response.headers["Location"])
138
192
  else
139
193
  return nil
140
194
  end
141
195
  end
142
196
  end
143
197
 
198
+ sig do
199
+ params(
200
+ url: String,
201
+ repository_details: T::Hash[Symbol, T.untyped]
202
+ )
203
+ .returns(T.nilable(Excon::Response))
204
+ end
144
205
  def self.fetch_url(url, repository_details)
206
+ fetch_url_with_auth(url, repository_details.fetch(:auth_header))
207
+ end
208
+
209
+ sig { params(url: String, auth_header: T::Hash[T.any(String, Symbol), T.untyped]).returns(Excon::Response) }
210
+ def self.fetch_url_with_auth(url, auth_header)
145
211
  cache = CacheManager.cache("nupkg_fetcher_cache")
146
212
  cache[url] ||= Dependabot::RegistryClient.get(
147
213
  url: url,
148
- headers: repository_details.fetch(:auth_header)
214
+ headers: auth_header
149
215
  )
150
216
 
151
217
  cache[url]
@@ -1,23 +1,42 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "nokogiri"
5
- require "zip"
6
5
  require "stringio"
6
+ require "sorbet-runtime"
7
+ require "zip"
7
8
 
8
9
  module Dependabot
9
10
  module Nuget
10
11
  class NuspecFetcher
12
+ extend T::Sig
13
+
11
14
  require_relative "nupkg_fetcher"
12
15
  require_relative "repository_finder"
13
16
 
17
+ sig do
18
+ params(
19
+ dependency_urls: T::Array[T::Hash[Symbol, String]],
20
+ package_id: String,
21
+ package_version: String
22
+ )
23
+ .returns(T.nilable(Nokogiri::XML::Document))
24
+ end
14
25
  def self.fetch_nuspec(dependency_urls, package_id, package_version)
15
26
  # check all repositories for the first one that has the nuspec
16
- dependency_urls.reduce(nil) do |nuspec_xml, repository_details|
27
+ dependency_urls.reduce(T.let(nil, T.nilable(Nokogiri::XML::Document))) do |nuspec_xml, repository_details|
17
28
  nuspec_xml || fetch_nuspec_from_repository(repository_details, package_id, package_version)
18
29
  end
19
30
  end
20
31
 
32
+ sig do
33
+ params(
34
+ repository_details: T::Hash[Symbol, T.untyped],
35
+ package_id: T.nilable(String),
36
+ package_version: T.nilable(String)
37
+ )
38
+ .returns(T.nilable(Nokogiri::XML::Document))
39
+ end
21
40
  def self.fetch_nuspec_from_repository(repository_details, package_id, package_version)
22
41
  return unless package_id && package_version && !package_version.empty?
23
42
 
@@ -55,6 +74,7 @@ module Dependabot
55
74
  nuspec_xml
56
75
  end
57
76
 
77
+ sig { params(feed_url: String).returns(T::Boolean) }
58
78
  def self.feed_supports_nuspec_download?(feed_url)
59
79
  feed_regexs = [
60
80
  # nuget
@@ -67,6 +87,7 @@ module Dependabot
67
87
  feed_regexs.any? { |reg| reg.match(feed_url) }
68
88
  end
69
89
 
90
+ sig { params(zip_stream: String, package_id: String).returns(T.nilable(String)) }
70
91
  def self.extract_nuspec(zip_stream, package_id)
71
92
  Zip::File.open_buffer(zip_stream) do |zip|
72
93
  nuspec_entry = zip.find { |entry| entry.name == "#{package_id}.nuspec" }
@@ -75,6 +96,7 @@ module Dependabot
75
96
  nil
76
97
  end
77
98
 
99
+ sig { params(string: String).returns(String) }
78
100
  def self.remove_invalid_characters(string)
79
101
  string.dup
80
102
  .force_encoding(Encoding::UTF_8)
@@ -72,6 +72,21 @@ module Dependabot
72
72
  end
73
73
 
74
74
  def build_url_for_details(repo_details)
75
+ url = repo_details.fetch(:url)
76
+ url_obj = URI.parse(url)
77
+ if url_obj.is_a?(URI::HTTP)
78
+ details = build_url_for_details_remote(repo_details)
79
+ elsif url_obj.is_a?(URI::File)
80
+ details = {
81
+ base_url: url,
82
+ repository_type: "local"
83
+ }
84
+ end
85
+
86
+ details
87
+ end
88
+
89
+ def build_url_for_details_remote(repo_details)
75
90
  response = get_repo_metadata(repo_details)
76
91
  check_repo_response(response, repo_details)
77
92
  return unless response.status == 200
@@ -205,6 +220,7 @@ module Dependabot
205
220
 
206
221
  # rubocop:disable Metrics/CyclomaticComplexity
207
222
  # rubocop:disable Metrics/PerceivedComplexity
223
+ # rubocop:disable Metrics/MethodLength
208
224
  # rubocop:disable Metrics/AbcSize
209
225
  def repos_from_config_file(config_file)
210
226
  doc = Nokogiri::XML(config_file.content)
@@ -223,7 +239,14 @@ module Dependabot
223
239
  key = node.attribute("key")&.value&.strip || node.at_xpath("./key")&.content&.strip
224
240
  url = node.attribute("value")&.value&.strip || node.at_xpath("./value")&.content&.strip
225
241
  url = expand_windows_style_environment_variables(url) if url
226
- sources << { url: url, key: key }
242
+
243
+ # if the path isn't absolute it's relative to the nuget.config file
244
+ if url
245
+ unless url.include?("://") || Pathname.new(url).absolute?
246
+ url = Pathname(config_file.directory).join(url).to_path
247
+ end
248
+ sources << { url: url, key: key }
249
+ end
227
250
  end
228
251
  end
229
252
 
@@ -246,14 +269,13 @@ module Dependabot
246
269
  known_urls.include?(s.fetch(:url))
247
270
  end
248
271
 
249
- sources.select! { |s| s.fetch(:url)&.include?("://") }
250
-
251
272
  add_config_file_credentials(sources: sources, doc: doc)
252
273
  sources.each { |details| details.delete(:key) }
253
274
 
254
275
  sources
255
276
  end
256
277
  # rubocop:enable Metrics/AbcSize
278
+ # rubocop:enable Metrics/MethodLength
257
279
  # rubocop:enable Metrics/PerceivedComplexity
258
280
  # rubocop:enable Metrics/CyclomaticComplexity
259
281
 
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  #######################################################################
@@ -6,6 +6,8 @@
6
6
  # https://docs.microsoft.com/en-us/nuget/reference/package-versioning #
7
7
  #######################################################################
8
8
 
9
+ require "sorbet-runtime"
10
+
9
11
  require "dependabot/update_checkers/base"
10
12
  require "dependabot/nuget/version"
11
13
 
@@ -13,14 +15,25 @@ module Dependabot
13
15
  module Nuget
14
16
  class UpdateChecker < Dependabot::UpdateCheckers::Base
15
17
  class RequirementsUpdater
18
+ extend T::Sig
19
+
20
+ sig do
21
+ params(
22
+ requirements: T::Array[T::Hash[Symbol, T.untyped]],
23
+ latest_version: T.nilable(T.any(String, Dependabot::Nuget::Version)),
24
+ source_details: T.nilable(T::Hash[Symbol, T.untyped])
25
+ )
26
+ .void
27
+ end
16
28
  def initialize(requirements:, latest_version:, source_details:)
17
29
  @requirements = requirements
18
30
  @source_details = source_details
19
31
  return unless latest_version
20
32
 
21
- @latest_version = version_class.new(latest_version)
33
+ @latest_version = T.let(version_class.new(latest_version), Dependabot::Nuget::Version)
22
34
  end
23
35
 
36
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
24
37
  def updated_requirements
25
38
  return requirements unless latest_version
26
39
 
@@ -52,32 +65,42 @@ module Dependabot
52
65
 
53
66
  private
54
67
 
55
- attr_reader :requirements, :latest_version, :source_details
68
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
69
+ attr_reader :requirements
70
+
71
+ sig { returns(T.nilable(Dependabot::Nuget::Version)) }
72
+ attr_reader :latest_version
73
+
74
+ sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
75
+ attr_reader :source_details
56
76
 
77
+ sig { returns(T.class_of(Dependabot::Nuget::Version)) }
57
78
  def version_class
58
- Nuget::Version
79
+ Dependabot::Nuget::Version
59
80
  end
60
81
 
82
+ sig { params(req_string: String).returns(String) }
61
83
  def update_wildcard_requirement(req_string)
62
84
  return req_string if req_string == "*-*"
63
85
 
64
86
  return req_string if req_string == "*"
65
87
 
66
- precision = req_string.split("*").first.split(/\.|\-/).count
88
+ precision = T.must(req_string.split("*").first).split(/\.|\-/).count
67
89
  wildcard_section = req_string.partition(/(?=[.\-]\*)/).last
68
90
 
69
- version_parts = latest_version.segments.first(precision)
91
+ version_parts = T.must(latest_version).segments.first(precision)
70
92
  version = version_parts.join(".")
71
93
 
72
94
  version + wildcard_section
73
95
  end
74
96
 
97
+ sig { returns(T::Hash[Symbol, T.untyped]) }
75
98
  def updated_source
76
99
  {
77
100
  type: "nuget_repo",
78
- url: source_details.fetch(:repo_url),
79
- nuspec_url: source_details.fetch(:nuspec_url),
80
- source_url: source_details.fetch(:source_url)
101
+ url: source_details&.fetch(:repo_url),
102
+ nuspec_url: source_details&.fetch(:nuspec_url),
103
+ source_url: source_details&.fetch(:source_url)
81
104
  }
82
105
  end
83
106
  end
@@ -52,13 +52,13 @@ module Dependabot
52
52
 
53
53
  config_parser = FileParser::PackagesConfigParser.new(packages_config: config_file)
54
54
  config_parser.dependency_set.dependencies.any? do |d|
55
- d.name.casecmp(dependency.name).zero?
55
+ d.name.casecmp(dependency.name)&.zero?
56
56
  end
57
57
  end
58
58
 
59
59
  def project_file_contains_dependency?(file, dependency)
60
60
  project_file_parser.dependency_set(project_file: file).dependencies.any? do |d|
61
- d.name.casecmp(dependency.name).zero?
61
+ d.name.casecmp(dependency.name)&.zero?
62
62
  end
63
63
  end
64
64