dependabot-nuget 0.246.0 → 0.248.0

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