dependabot-nuget 0.242.0 → 0.243.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/.editorconfig +37 -28
  3. data/helpers/lib/NuGetUpdater/.gitignore +1 -0
  4. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.CommandLine/AssemblyMetadataExtractor.cs +2 -1
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs +2 -2
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +178 -176
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs +2 -1
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs +1 -0
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs +5 -4
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs +1 -0
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs +10 -5
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs +16 -12
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +18 -17
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs +7 -7
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs +13 -20
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs +9 -3
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +32 -16
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +42 -22
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +32 -13
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs +47 -0
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs +55 -0
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs +12 -9
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +50 -42
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +16 -3
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs +6 -6
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs +11 -0
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs +18 -9
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/CompatibilityCheckerFacts.cs +2 -2
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/FrameworkCompatibilityServiceFacts.cs +7 -7
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/SupportedFrameworkFacts.cs +1 -1
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackagesConfigUpdaterTests.cs +9 -9
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorker.DirsProj.cs +81 -80
  33. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +22 -9
  34. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.DotNetTools.cs +140 -104
  35. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.GlobalJson.cs +25 -25
  36. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs +8 -9
  37. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +198 -22
  38. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs +401 -399
  39. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/JsonHelperTests.cs +17 -15
  40. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +111 -42
  41. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterTests.cs +161 -82
  42. data/lib/dependabot/nuget/file_fetcher.rb +3 -23
  43. data/lib/dependabot/nuget/file_parser/project_file_parser.rb +47 -60
  44. data/lib/dependabot/nuget/file_parser.rb +24 -6
  45. data/lib/dependabot/nuget/file_updater.rb +42 -6
  46. data/lib/dependabot/nuget/native_helpers.rb +27 -8
  47. data/lib/dependabot/nuget/nuget_client.rb +130 -24
  48. data/lib/dependabot/nuget/nuget_config_credential_helpers.rb +7 -3
  49. data/lib/dependabot/nuget/update_checker/compatibility_checker.rb +63 -59
  50. data/lib/dependabot/nuget/update_checker/dependency_finder.rb +2 -2
  51. data/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb +1 -1
  52. data/lib/dependabot/nuget/update_checker/nuspec_fetcher.rb +22 -17
  53. data/lib/dependabot/nuget/update_checker/repository_finder.rb +292 -270
  54. data/lib/dependabot/nuget/update_checker/tfm_comparer.rb +11 -13
  55. data/lib/dependabot/nuget/update_checker/tfm_finder.rb +80 -82
  56. data/lib/dependabot/nuget/update_checker/version_finder.rb +4 -7
  57. data/lib/dependabot/nuget/version.rb +18 -7
  58. data/lib/dependabot/nuget.rb +0 -2
  59. metadata +7 -5
@@ -5,10 +5,13 @@ require "dependabot/dependency_file"
5
5
  require "dependabot/file_updaters"
6
6
  require "dependabot/file_updaters/base"
7
7
  require "dependabot/nuget/native_helpers"
8
+ require "sorbet-runtime"
8
9
 
9
10
  module Dependabot
10
11
  module Nuget
11
12
  class FileUpdater < Dependabot::FileUpdaters::Base
13
+ extend T::Sig
14
+
12
15
  require_relative "file_updater/property_value_updater"
13
16
  require_relative "file_parser/project_file_parser"
14
17
  require_relative "file_parser/dotnet_tools_json_parser"
@@ -54,6 +57,7 @@ module Dependabot
54
57
 
55
58
  def try_update_projects(dependency)
56
59
  update_ran = T.let(false, T::Boolean)
60
+ checked_files = Set.new
57
61
 
58
62
  # run update for each project file
59
63
  project_files.each do |project_file|
@@ -62,9 +66,17 @@ module Dependabot
62
66
 
63
67
  next unless project_dependencies.any? { |dep| dep.name.casecmp(dependency.name).zero? }
64
68
 
65
- NativeHelpers.run_nuget_updater_tool(repo_root: repo_contents_path, proj_path: proj_path,
66
- dependency: dependency, is_transitive: !dependency.top_level?,
67
- credentials: credentials)
69
+ next unless repo_contents_path
70
+
71
+ checked_key = "#{project_file.name}-#{dependency.name}#{dependency.version}"
72
+ call_nuget_updater_tool(dependency, proj_path) unless checked_files.include?(checked_key)
73
+
74
+ checked_files.add(checked_key)
75
+ # We need to check the downstream references even though we're already evaluated the file
76
+ downstream_files = project_file_parser.downstream_file_references(project_file: project_file)
77
+ downstream_files.each do |downstream_file|
78
+ checked_files.add("#{downstream_file}-#{dependency.name}#{dependency.version}")
79
+ end
68
80
  update_ran = true
69
81
  end
70
82
 
@@ -79,15 +91,39 @@ module Dependabot
79
91
  project_file = project_files.first
80
92
  proj_path = dependency_file_path(project_file)
81
93
 
82
- NativeHelpers.run_nuget_updater_tool(repo_root: repo_contents_path, proj_path: proj_path,
83
- dependency: dependency, is_transitive: !dependency.top_level?,
84
- credentials: credentials)
94
+ return false unless repo_contents_path
95
+
96
+ call_nuget_updater_tool(dependency, proj_path)
85
97
  return true
86
98
  end
87
99
 
88
100
  false
89
101
  end
90
102
 
103
+ sig { params(dependency: Dependency, proj_path: String).void }
104
+ def call_nuget_updater_tool(dependency, proj_path)
105
+ NativeHelpers.run_nuget_updater_tool(repo_root: T.must(repo_contents_path), proj_path: proj_path,
106
+ dependency: dependency, is_transitive: !dependency.top_level?,
107
+ credentials: credentials)
108
+
109
+ # Tests need to track how many times we call the tooling updater to ensure we don't recurse needlessly
110
+ # Ideally we should find a way to not run this code in prod
111
+ # (or a better way to track calls made to NativeHelpers)
112
+ @update_tooling_calls ||= {}
113
+ key = proj_path + dependency.name
114
+ if @update_tooling_calls[key]
115
+ @update_tooling_calls[key] += 1
116
+ else
117
+ @update_tooling_calls[key] = 1
118
+ end
119
+ end
120
+
121
+ # Don't call this from outside tests, we're only checking that we aren't recursing needlessly
122
+ sig { returns(T.nilable(T::Hash[String, Integer])) }
123
+ def testonly_update_tooling_calls
124
+ @update_tooling_calls
125
+ end
126
+
91
127
  def project_dependencies(project_file)
92
128
  # Collect all dependencies from the project file and associated packages.config
93
129
  dependencies = project_file_parser.dependency_set(project_file: project_file).dependencies
@@ -1,11 +1,17 @@
1
- # typed: true
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "shellwords"
5
+ require "sorbet-runtime"
6
+
4
7
  require_relative "nuget_config_credential_helpers"
5
8
 
6
9
  module Dependabot
7
10
  module Nuget
8
11
  module NativeHelpers
12
+ extend T::Sig
13
+
14
+ sig { returns(String) }
9
15
  def self.native_helpers_root
10
16
  helpers_root = ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", nil)
11
17
  return File.join(helpers_root, "nuget") unless helpers_root.nil?
@@ -13,9 +19,10 @@ module Dependabot
13
19
  File.expand_path("../../../helpers", __dir__)
14
20
  end
15
21
 
22
+ sig { params(project_tfms: T::Array[String], package_tfms: T::Array[String]).returns(T::Boolean) }
16
23
  def self.run_nuget_framework_check(project_tfms, package_tfms)
17
24
  exe_path = File.join(native_helpers_root, "NuGetUpdater", "NuGetUpdater.Cli")
18
- command = [
25
+ command_parts = [
19
26
  exe_path,
20
27
  "framework-check",
21
28
  "--project-tfms",
@@ -23,7 +30,8 @@ module Dependabot
23
30
  "--package-tfms",
24
31
  *package_tfms,
25
32
  "--verbose"
26
- ].join(" ")
33
+ ]
34
+ command = Shellwords.join(command_parts)
27
35
 
28
36
  fingerprint = [
29
37
  exe_path,
@@ -48,9 +56,18 @@ module Dependabot
48
56
  end
49
57
 
50
58
  # rubocop:disable Metrics/MethodLength
59
+ sig do
60
+ params(
61
+ repo_root: String,
62
+ proj_path: String,
63
+ dependency: Dependency,
64
+ is_transitive: T::Boolean,
65
+ credentials: T::Array[T.untyped]
66
+ ).void
67
+ end
51
68
  def self.run_nuget_updater_tool(repo_root:, proj_path:, dependency:, is_transitive:, credentials:)
52
69
  exe_path = File.join(native_helpers_root, "NuGetUpdater", "NuGetUpdater.Cli")
53
- command = [
70
+ command_parts = [
54
71
  exe_path,
55
72
  "update",
56
73
  "--repo-root",
@@ -63,9 +80,11 @@ module Dependabot
63
80
  dependency.version,
64
81
  "--previous-version",
65
82
  dependency.previous_version,
66
- is_transitive ? "--transitive" : "",
83
+ is_transitive ? "--transitive" : nil,
67
84
  "--verbose"
68
- ].join(" ")
85
+ ].compact
86
+
87
+ command = Shellwords.join(command_parts)
69
88
 
70
89
  fingerprint = [
71
90
  exe_path,
@@ -80,9 +99,9 @@ module Dependabot
80
99
  "<new-version>",
81
100
  "--previous-version",
82
101
  "<previous-version>",
83
- is_transitive ? "--transitive" : "",
102
+ is_transitive ? "--transitive" : nil,
84
103
  "--verbose"
85
- ].join(" ")
104
+ ].compact.join(" ")
86
105
 
87
106
  puts "running NuGet updater:\n" + command
88
107
 
@@ -1,13 +1,35 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/nuget/cache_manager"
5
5
  require "dependabot/nuget/update_checker/repository_finder"
6
+ require "sorbet-runtime"
6
7
 
7
8
  module Dependabot
8
9
  module Nuget
9
10
  class NugetClient
10
- def self.get_package_versions_v3(dependency_name, repository_details)
11
+ extend T::Sig
12
+
13
+ sig do
14
+ params(dependency_name: String, repository_details: T::Hash[Symbol, String])
15
+ .returns(T.nilable(T::Set[String]))
16
+ end
17
+ def self.get_package_versions(dependency_name, repository_details)
18
+ repository_type = repository_details.fetch(:repository_type)
19
+ if repository_type == "v3"
20
+ get_package_versions_v3(dependency_name, repository_details)
21
+ elsif repository_type == "v2"
22
+ get_package_versions_v2(dependency_name, repository_details)
23
+ else
24
+ raise "Unknown repository type: #{repository_type}"
25
+ end
26
+ end
27
+
28
+ sig do
29
+ params(dependency_name: String, repository_details: T::Hash[Symbol, String])
30
+ .returns(T.nilable(T::Set[String]))
31
+ end
32
+ private_class_method def self.get_package_versions_v3(dependency_name, repository_details)
11
33
  # Use the registration URL if possible because it is fast and correct
12
34
  if repository_details[:registration_url]
13
35
  get_versions_from_registration_v3(repository_details)
@@ -17,37 +39,60 @@ module Dependabot
17
39
  # Otherwise, use the versions URL (fast but wrong because it includes unlisted versions)
18
40
  elsif repository_details[:versions_url]
19
41
  get_versions_from_versions_url_v3(repository_details)
42
+ else
43
+ raise "No version sources were available for #{dependency_name} in #{repository_details}"
44
+ end
45
+ end
46
+
47
+ sig do
48
+ params(dependency_name: String, repository_details: T::Hash[Symbol, String])
49
+ .returns(T.nilable(T::Set[String]))
50
+ end
51
+ private_class_method def self.get_package_versions_v2(dependency_name, repository_details)
52
+ doc = execute_xml_nuget_request(repository_details.fetch(:versions_url), repository_details)
53
+ return unless doc
54
+
55
+ id_nodes = doc.xpath("/feed/entry/properties/Id")
56
+ matching_versions = Set.new
57
+ id_nodes.each do |id_node|
58
+ return nil unless id_node.text
59
+
60
+ next unless id_node.text.casecmp?(dependency_name)
61
+
62
+ version_node = id_node.parent.xpath("Version")
63
+ matching_versions << version_node.text if version_node && version_node.text
20
64
  end
65
+
66
+ matching_versions
21
67
  end
22
68
 
69
+ sig { params(repository_details: T::Hash[Symbol, String]).returns(T.nilable(T::Set[String])) }
23
70
  private_class_method def self.get_versions_from_versions_url_v3(repository_details)
24
- body = execute_search_for_dependency_url(repository_details[:versions_url], repository_details)
25
- body&.fetch("versions")
71
+ body = execute_json_nuget_request(repository_details.fetch(:versions_url), repository_details)
72
+ ver_array = T.let(body&.fetch("versions"), T.nilable(T::Array[String]))
73
+ ver_array&.to_set
26
74
  end
27
75
 
76
+ sig { params(repository_details: T::Hash[Symbol, String]).returns(T.nilable(T::Set[String])) }
28
77
  private_class_method def self.get_versions_from_registration_v3(repository_details)
29
- url = repository_details[:registration_url]
30
- body = execute_search_for_dependency_url(url, repository_details)
78
+ url = repository_details.fetch(:registration_url)
79
+ body = execute_json_nuget_request(url, repository_details)
31
80
 
32
81
  return unless body
33
82
 
34
83
  pages = body.fetch("items")
35
- versions = Set.new
84
+ versions = T.let(Set.new, T::Set[String])
36
85
  pages.each do |page|
37
86
  items = page["items"]
38
87
  if items
39
88
  # inlined entries
40
- items.each do |item|
41
- catalog_entry = item["catalogEntry"]
42
- if catalog_entry["listed"] == true
43
- vers = catalog_entry["version"]
44
- versions << vers
45
- end
46
- end
89
+ get_versions_from_inline_page(items, versions)
47
90
  else
48
91
  # paged entries
49
92
  page_url = page["@id"]
50
- page_body = execute_search_for_dependency_url(page_url, repository_details)
93
+ page_body = execute_json_nuget_request(page_url, repository_details)
94
+ next unless page_body
95
+
51
96
  items = page_body.fetch("items")
52
97
  items.each do |item|
53
98
  catalog_entry = item.fetch("catalogEntry")
@@ -59,9 +104,28 @@ module Dependabot
59
104
  versions
60
105
  end
61
106
 
107
+ sig { params(items: T::Array[T::Hash[String, T.untyped]], versions: T::Set[String]).void }
108
+ private_class_method def self.get_versions_from_inline_page(items, versions)
109
+ items.each do |item|
110
+ catalog_entry = item["catalogEntry"]
111
+
112
+ # a package is considered listed if the `listed` property is either `true` or missing
113
+ listed_property = catalog_entry["listed"]
114
+ is_listed = listed_property.nil? || listed_property == true
115
+ if is_listed
116
+ vers = catalog_entry["version"]
117
+ versions << vers
118
+ end
119
+ end
120
+ end
121
+
122
+ sig do
123
+ params(repository_details: T::Hash[Symbol, String], dependency_name: String)
124
+ .returns(T.nilable(T::Set[String]))
125
+ end
62
126
  private_class_method def self.get_versions_from_search_url_v3(repository_details, dependency_name)
63
- search_url = repository_details[:search_url]
64
- body = execute_search_for_dependency_url(search_url, repository_details)
127
+ search_url = repository_details.fetch(:search_url)
128
+ body = execute_json_nuget_request(search_url, repository_details)
65
129
 
66
130
  body&.fetch("data")
67
131
  &.find { |d| d.fetch("id").casecmp(dependency_name.downcase).zero? }
@@ -69,26 +133,68 @@ module Dependabot
69
133
  &.map { |d| d.fetch("version") }
70
134
  end
71
135
 
72
- private_class_method def self.execute_search_for_dependency_url(url, repository_details)
73
- cache = CacheManager.cache("dependency_url_search_cache")
74
- cache[url] ||= Dependabot::RegistryClient.get(
136
+ sig do
137
+ params(url: String, repository_details: T::Hash[Symbol, T.untyped]).returns(T.nilable(Nokogiri::XML::Document))
138
+ end
139
+ private_class_method def self.execute_xml_nuget_request(url, repository_details)
140
+ response = execute_nuget_request_internal(
75
141
  url: url,
76
- headers: repository_details[:auth_header]
142
+ auth_header: repository_details.fetch(:auth_header),
143
+ repository_url: repository_details.fetch(:repository_url)
77
144
  )
145
+ return unless response.status == 200
78
146
 
79
- response = cache[url]
147
+ doc = Nokogiri::XML(response.body)
148
+ doc.remove_namespaces!
149
+ doc
150
+ end
80
151
 
152
+ sig do
153
+ params(url: String,
154
+ repository_details: T::Hash[Symbol, T.untyped])
155
+ .returns(T.nilable(T::Hash[T.untyped, T.untyped]))
156
+ end
157
+ private_class_method def self.execute_json_nuget_request(url, repository_details)
158
+ response = execute_nuget_request_internal(
159
+ url: url,
160
+ auth_header: repository_details.fetch(:auth_header),
161
+ repository_url: repository_details.fetch(:repository_url)
162
+ )
81
163
  return unless response.status == 200
82
164
 
83
165
  body = remove_wrapping_zero_width_chars(response.body)
84
166
  JSON.parse(body)
167
+ end
168
+
169
+ sig do
170
+ params(url: String, auth_header: T::Hash[Symbol, T.untyped], repository_url: String).returns(Excon::Response)
171
+ end
172
+ private_class_method def self.execute_nuget_request_internal(url:, auth_header:, repository_url:)
173
+ cache = CacheManager.cache("dependency_url_search_cache")
174
+ if cache[url].nil?
175
+ response = Dependabot::RegistryClient.get(
176
+ url: url,
177
+ headers: auth_header
178
+ )
179
+
180
+ if [401, 402, 403].include?(response.status)
181
+ raise Dependabot::PrivateSourceAuthenticationFailure, repository_url
182
+ end
183
+
184
+ cache[url] = response if !CacheManager.caching_disabled? && response.status == 200
185
+ else
186
+ response = cache[url]
187
+ end
188
+
189
+ response
85
190
  rescue Excon::Error::Timeout, Excon::Error::Socket
86
- repo_url = repository_details[:repository_url]
87
- raise if repo_url == Dependabot::Nuget::UpdateChecker::RepositoryFinder::DEFAULT_REPOSITORY_URL
191
+ repo_url = repository_url
192
+ raise if repo_url == Dependabot::Nuget::RepositoryFinder::DEFAULT_REPOSITORY_URL
88
193
 
89
194
  raise PrivateSourceTimedOut, repo_url
90
195
  end
91
196
 
197
+ sig { params(string: String).returns(String) }
92
198
  private_class_method def self.remove_wrapping_zero_width_chars(string)
93
199
  string.force_encoding("UTF-8").encode
94
200
  .gsub(/\A[\u200B-\u200D\uFEFF]/, "")
@@ -55,17 +55,21 @@ module Dependabot
55
55
  File.rename(temporary_nuget_config_path, user_nuget_config_path)
56
56
  end
57
57
 
58
- # rubocop:disable Lint/SuppressedException
59
58
  def self.patch_nuget_config_for_action(credentials, &_block)
60
59
  add_credentials_to_nuget_config(credentials)
61
60
  begin
62
61
  yield
63
- rescue StandardError
62
+ rescue StandardError => e
63
+ Dependabot.logger.error(
64
+ <<~LOG_MESSAGE
65
+ Block argument of NuGetConfigCredentialHelpers::patch_nuget_config_for_action causes an exception #{e}:
66
+ #{e.message}
67
+ LOG_MESSAGE
68
+ )
64
69
  ensure
65
70
  restore_user_nuget_config
66
71
  end
67
72
  end
68
- # rubocop:enable Lint/SuppressedException
69
73
  end
70
74
  end
71
75
  end
@@ -5,80 +5,84 @@ require "dependabot/update_checkers/base"
5
5
 
6
6
  module Dependabot
7
7
  module Nuget
8
- class UpdateChecker < Dependabot::UpdateCheckers::Base
9
- class CompatibilityChecker
10
- require_relative "nuspec_fetcher"
11
- require_relative "nupkg_fetcher"
12
- require_relative "tfm_finder"
13
- require_relative "tfm_comparer"
14
-
15
- def initialize(dependency_urls:, dependency:, tfm_finder:)
16
- @dependency_urls = dependency_urls
17
- @dependency = dependency
18
- @tfm_finder = tfm_finder
19
- end
8
+ class CompatibilityChecker
9
+ require_relative "nuspec_fetcher"
10
+ require_relative "nupkg_fetcher"
11
+ require_relative "tfm_finder"
12
+ require_relative "tfm_comparer"
13
+
14
+ def initialize(dependency_urls:, dependency:, tfm_finder:)
15
+ @dependency_urls = dependency_urls
16
+ @dependency = dependency
17
+ @tfm_finder = tfm_finder
18
+ end
20
19
 
21
- def compatible?(version)
22
- nuspec_xml = NuspecFetcher.fetch_nuspec(dependency_urls, dependency.name, version)
23
- return false unless nuspec_xml
20
+ def compatible?(version)
21
+ nuspec_xml = NuspecFetcher.fetch_nuspec(dependency_urls, dependency.name, version)
22
+ return false unless nuspec_xml
24
23
 
25
- # development dependencies are packages such as analyzers which need to be
26
- # compatible with the compiler not the project itself.
27
- return true if development_dependency?(nuspec_xml)
24
+ # development dependencies are packages such as analyzers which need to be compatible with the compiler not the
25
+ # project itself, but some packages that report themselves as development dependencies still contain target
26
+ # framework dependencies and should be checked for compatibility through the regular means
27
+ return true if pure_development_dependency?(nuspec_xml)
28
28
 
29
- package_tfms = parse_package_tfms(nuspec_xml)
30
- package_tfms = fetch_package_tfms(version) if package_tfms.empty?
31
- # nil is a special return value that indicates that the package is likely a development dependency
32
- return true if package_tfms.nil?
33
- return false if package_tfms.empty?
29
+ package_tfms = parse_package_tfms(nuspec_xml)
30
+ package_tfms = fetch_package_tfms(version) if package_tfms.empty?
31
+ # nil is a special return value that indicates that the package is likely a development dependency
32
+ return true if package_tfms.nil?
33
+ return false if package_tfms.empty?
34
34
 
35
- return false if project_tfms.nil? || project_tfms.empty?
35
+ return false if project_tfms.nil? || project_tfms.empty?
36
36
 
37
- TfmComparer.are_frameworks_compatible?(project_tfms, package_tfms)
38
- end
37
+ TfmComparer.are_frameworks_compatible?(project_tfms, package_tfms)
38
+ end
39
39
 
40
- private
40
+ private
41
41
 
42
- attr_reader :dependency_urls, :dependency, :tfm_finder
42
+ attr_reader :dependency_urls, :dependency, :tfm_finder
43
43
 
44
- def development_dependency?(nuspec_xml)
45
- contents = nuspec_xml.at_xpath("package/metadata/developmentDependency")&.content&.strip
46
- return false unless contents
44
+ def pure_development_dependency?(nuspec_xml)
45
+ contents = nuspec_xml.at_xpath("package/metadata/developmentDependency")&.content&.strip
46
+ return false unless contents # no `developmentDependency` element
47
47
 
48
- contents.casecmp("true").zero?
49
- end
48
+ self_reports_as_development_dependency = contents.casecmp?("true")
49
+ return false unless self_reports_as_development_dependency
50
50
 
51
- def parse_package_tfms(nuspec_xml)
52
- nuspec_xml.xpath("//dependencies/group").map do |group|
53
- group.attribute("targetFramework")
54
- end
55
- end
51
+ # even though a package self-reports as a development dependency, it might not be if it has dependency groups
52
+ # with a target framework
53
+ dependency_groups_with_target_framework =
54
+ nuspec_xml.at_xpath("/package/metadata/dependencies/group[@targetFramework]")
55
+ dependency_groups_with_target_framework.to_a.empty?
56
+ end
57
+
58
+ def parse_package_tfms(nuspec_xml)
59
+ nuspec_xml.xpath("//dependencies/group").filter_map { |group| group.attribute("targetFramework") }
60
+ end
56
61
 
57
- def project_tfms
58
- return @project_tfms if defined?(@project_tfms)
62
+ def project_tfms
63
+ return @project_tfms if defined?(@project_tfms)
59
64
 
60
- @project_tfms = tfm_finder.frameworks(dependency)
61
- end
65
+ @project_tfms = tfm_finder.frameworks(dependency)
66
+ end
62
67
 
63
- def fetch_package_tfms(dependency_version)
64
- nupkg_buffer = NupkgFetcher.fetch_nupkg_buffer(dependency_urls, dependency.name, dependency_version)
65
- return [] unless nupkg_buffer
66
-
67
- # Parse tfms from the folders beneath the lib folder
68
- folder_name = "lib/"
69
- tfms = Set.new
70
- Zip::File.open_buffer(nupkg_buffer) do |zip|
71
- lib_file_entries = zip.select { |entry| entry.name.start_with?(folder_name) }
72
- # If there is no lib folder in this package, assume it is a development dependency
73
- return nil if lib_file_entries.empty?
74
-
75
- lib_file_entries.each do |entry|
76
- _, tfm = entry.name.split("/").first(2)
77
- tfms << tfm
78
- end
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
79
83
  end
80
- tfms.to_a
81
84
  end
85
+ tfms.to_a
82
86
  end
83
87
  end
84
88
  end
@@ -121,12 +121,12 @@ module Dependabot
121
121
 
122
122
  def dependency_urls
123
123
  @dependency_urls ||=
124
- UpdateChecker::RepositoryFinder.new(
124
+ RepositoryFinder.new(
125
125
  dependency: @dependency,
126
126
  credentials: @credentials,
127
127
  config_files: nuget_configs
128
128
  ).dependency_urls
129
- .select { |url| url.fetch(:repository_type) == "v3" }
129
+ .select { |url| url.fetch(:repository_type) == "v3" }
130
130
  end
131
131
 
132
132
  def fetch_transitive_dependencies(package_id, package_version)
@@ -73,7 +73,7 @@ module Dependabot
73
73
  response_block: response_block
74
74
  )
75
75
 
76
- if response.status == 303
76
+ if response.status == 303 || response.status == 307
77
77
  current_redirects += 1
78
78
  return nil if current_redirects > max_redirects
79
79
 
@@ -26,17 +26,9 @@ module Dependabot
26
26
 
27
27
  nuspec_xml = nil
28
28
 
29
- if azure_package_feed?(feed_url)
30
- # this is an azure devops url we can extract the nuspec from the nupkg
31
- package_data = NupkgFetcher.fetch_nupkg_buffer_from_repository(repository_details, package_id,
32
- package_version)
33
- return if package_data.nil?
34
-
35
- nuspec_string = extract_nuspec(package_data, package_id)
36
- nuspec_xml = Nokogiri::XML(nuspec_string)
37
- else
29
+ if feed_supports_nuspec_download?(feed_url)
38
30
  # we can use the normal nuget apis to get the nuspec and list out the dependencies
39
- base_url = feed_url.gsub("/index.json", "-flatcontainer")
31
+ base_url = repository_details[:base_url].delete_suffix("/")
40
32
  package_id_downcased = package_id.downcase
41
33
  nuspec_url = "#{base_url}/#{package_id_downcased}/#{package_version}/#{package_id_downcased}.nuspec"
42
34
 
@@ -47,22 +39,32 @@ module Dependabot
47
39
 
48
40
  return unless nuspec_response.status == 200
49
41
 
50
- nuspec_response_body = remove_wrapping_zero_width_chars(nuspec_response.body)
42
+ nuspec_response_body = remove_invalid_characters(nuspec_response.body)
51
43
  nuspec_xml = Nokogiri::XML(nuspec_response_body)
44
+ else
45
+ # no guarantee we can directly query the .nuspec; fall back to extracting it from the .nupkg
46
+ package_data = NupkgFetcher.fetch_nupkg_buffer_from_repository(repository_details, package_id,
47
+ package_version)
48
+ return if package_data.nil?
49
+
50
+ nuspec_string = extract_nuspec(package_data, package_id)
51
+ nuspec_xml = Nokogiri::XML(nuspec_string)
52
52
  end
53
53
 
54
54
  nuspec_xml.remove_namespaces!
55
55
  nuspec_xml
56
56
  end
57
57
 
58
- def self.azure_package_feed?(feed_url)
59
- # if url is azure devops
60
- azure_devops_regexs = [
58
+ def self.feed_supports_nuspec_download?(feed_url)
59
+ feed_regexs = [
60
+ # nuget
61
+ %r{https://api\.nuget\.org/v3/index\.json},
62
+ # azure devops
61
63
  %r{https://pkgs\.dev\.azure\.com/(?<organization>[^/]+)/(?<project>[^/]+)/_packaging/(?<feedId>[^/]+)/nuget/v3/index\.json},
62
64
  %r{https://pkgs\.dev\.azure\.com/(?<organization>[^/]+)/_packaging/(?<feedId>[^/]+)/nuget/v3/index\.json(?<project>)},
63
65
  %r{https://(?<organization>[^\.\/]+)\.pkgs\.visualstudio\.com/_packaging/(?<feedId>[^/]+)/nuget/v3/index\.json(?<project>)}
64
66
  ]
65
- azure_devops_regexs.any? { |reg| reg.match(feed_url) }
67
+ feed_regexs.any? { |reg| reg.match(feed_url) }
66
68
  end
67
69
 
68
70
  def self.extract_nuspec(zip_stream, package_id)
@@ -73,8 +75,11 @@ module Dependabot
73
75
  nil
74
76
  end
75
77
 
76
- def self.remove_wrapping_zero_width_chars(string)
77
- string.force_encoding("UTF-8").encode
78
+ def self.remove_invalid_characters(string)
79
+ string.dup
80
+ .force_encoding(Encoding::UTF_8)
81
+ .encode
82
+ .scrub("")
78
83
  .gsub(/\A[\u200B-\u200D\uFEFF]/, "")
79
84
  .gsub(/[\u200B-\u200D\uFEFF]\Z/, "")
80
85
  end