dependabot-nuget 0.80.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.
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ require "dependabot/dependency"
6
+ require "dependabot/file_parsers"
7
+ require "dependabot/file_parsers/base"
8
+
9
+ # For details on how dotnet handles version constraints, see:
10
+ # https://docs.microsoft.com/en-us/nuget/reference/package-versioning
11
+ module Dependabot
12
+ module Nuget
13
+ class FileParser < Dependabot::FileParsers::Base
14
+ require "dependabot/file_parsers/base/dependency_set"
15
+ require_relative "file_parser/project_file_parser"
16
+ require_relative "file_parser/packages_config_parser"
17
+
18
+ PACKAGE_CONF_DEPENDENCY_SELECTOR = "packages > packages"
19
+
20
+ def parse
21
+ dependency_set = DependencySet.new
22
+ dependency_set += project_file_dependencies
23
+ dependency_set += packages_config_dependencies
24
+ dependency_set.dependencies
25
+ end
26
+
27
+ private
28
+
29
+ def project_file_dependencies
30
+ dependency_set = DependencySet.new
31
+
32
+ (project_files + project_import_files).each do |file|
33
+ parser = project_file_parser
34
+ dependency_set += parser.dependency_set(project_file: file)
35
+ end
36
+
37
+ dependency_set
38
+ end
39
+
40
+ def packages_config_dependencies
41
+ dependency_set = DependencySet.new
42
+
43
+ packages_config_files.each do |file|
44
+ parser = PackagesConfigParser.new(packages_config: file)
45
+ dependency_set += parser.dependency_set
46
+ end
47
+
48
+ dependency_set
49
+ end
50
+
51
+ def project_file_parser
52
+ @project_file_parser ||=
53
+ ProjectFileParser.new(dependency_files: dependency_files)
54
+ end
55
+
56
+ def project_files
57
+ dependency_files.select { |df| df.name.match?(/\.[a-z]{2}proj$/) }
58
+ end
59
+
60
+ def packages_config_files
61
+ dependency_files.select do |f|
62
+ f.name.split("/").last.casecmp("packages.config").zero?
63
+ end
64
+ end
65
+
66
+ def project_import_files
67
+ dependency_files -
68
+ project_files -
69
+ packages_config_files -
70
+ [nuget_config]
71
+ end
72
+
73
+ def nuget_config
74
+ dependency_files.find { |f| f.name.casecmp("nuget.config").zero? }
75
+ end
76
+
77
+ def check_required_files
78
+ return if project_files.any? || packages_config_files.any?
79
+
80
+ raise "No project file or packages.config!"
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ Dependabot::FileParsers.register("nuget", Dependabot::Nuget::FileParser)
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "dependabot/nuget/file_updater"
5
+
6
+ module Dependabot
7
+ module Nuget
8
+ class FileUpdater
9
+ class PackagesConfigDeclarationFinder
10
+ DECLARATION_REGEX =
11
+ %r{<package [^>]*?/>|
12
+ <package [^>]*?[^/]>.*?</package>}mx.freeze
13
+
14
+ attr_reader :dependency_name, :declaring_requirement,
15
+ :packages_config
16
+
17
+ def initialize(dependency_name:, packages_config:,
18
+ declaring_requirement:)
19
+ @dependency_name = dependency_name
20
+ @packages_config = packages_config
21
+ @declaring_requirement = declaring_requirement
22
+
23
+ if declaring_requirement[:file].split("/").last.
24
+ casecmp("packages.config").zero?
25
+ return
26
+ end
27
+
28
+ raise "Requirement not from packages.config!"
29
+ end
30
+
31
+ def declaration_strings
32
+ @declaration_strings ||= fetch_declaration_strings
33
+ end
34
+
35
+ def declaration_nodes
36
+ declaration_strings.map do |declaration_string|
37
+ Nokogiri::XML(declaration_string)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def fetch_declaration_strings
44
+ deep_find_declarations(packages_config.content).select do |nd|
45
+ node = Nokogiri::XML(nd)
46
+ node.remove_namespaces!
47
+ node = node.at_xpath("/package")
48
+
49
+ node_name = node.attribute("id")&.value&.strip ||
50
+ node.at_xpath("./id")&.content&.strip
51
+ next false unless node_name == dependency_name
52
+
53
+ node_requirement = node.attribute("version")&.value&.strip ||
54
+ node.at_xpath("./version")&.content&.strip
55
+ node_requirement == declaring_requirement.fetch(:requirement)
56
+ end
57
+ end
58
+
59
+ def deep_find_declarations(string)
60
+ string.scan(DECLARATION_REGEX).flat_map do |matching_node|
61
+ [matching_node, *deep_find_declarations(matching_node[0..-2])]
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "dependabot/nuget/file_updater"
5
+
6
+ module Dependabot
7
+ module Nuget
8
+ class FileUpdater
9
+ class ProjectFileDeclarationFinder
10
+ DECLARATION_REGEX =
11
+ %r{
12
+ <PackageReference [^>]*?/>|
13
+ <PackageReference [^>]*?[^/]>.*?</PackageReference>|
14
+ <Dependency [^>]*?/>|
15
+ <Dependency [^>]*?[^/]>.*?</Dependency>|
16
+ <DevelopmentDependency [^>]*?/>|
17
+ <DevelopmentDependency [^>]*?[^/]>.*?</DevelopmentDependency>
18
+ }mx.freeze
19
+
20
+ attr_reader :dependency_name, :declaring_requirement,
21
+ :dependency_files
22
+
23
+ def initialize(dependency_name:, dependency_files:,
24
+ declaring_requirement:)
25
+ @dependency_name = dependency_name
26
+ @dependency_files = dependency_files
27
+ @declaring_requirement = declaring_requirement
28
+ end
29
+
30
+ def declaration_strings
31
+ @declaration_strings ||= fetch_declaration_strings
32
+ end
33
+
34
+ def declaration_nodes
35
+ declaration_strings.map do |declaration_string|
36
+ Nokogiri::XML(declaration_string)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def fetch_declaration_strings
43
+ deep_find_declarations(declaring_file.content).select do |nd|
44
+ node = Nokogiri::XML(nd)
45
+ node.remove_namespaces!
46
+ node = node.at_xpath("/PackageReference") ||
47
+ node.at_xpath("/Dependency") ||
48
+ node.at_xpath("/DevelopmentDependency")
49
+
50
+ node_name = node.attribute("Include")&.value&.strip ||
51
+ node.at_xpath("./Include")&.content&.strip
52
+ next false unless node_name == dependency_name
53
+
54
+ node_requirement = node.attribute("Version")&.value&.strip ||
55
+ node.at_xpath("./Version")&.content&.strip
56
+ node_requirement == declaring_requirement.fetch(:requirement)
57
+ end
58
+ end
59
+
60
+ def deep_find_declarations(string)
61
+ string.scan(DECLARATION_REGEX).flat_map do |matching_node|
62
+ [matching_node, *deep_find_declarations(matching_node[0..-2])]
63
+ end
64
+ end
65
+
66
+ def declaring_file
67
+ filename = declaring_requirement.fetch(:file)
68
+ declaring_file = dependency_files.find { |f| f.name == filename }
69
+ return declaring_file if declaring_file
70
+
71
+ raise "No file found with name #{filename}!"
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ require "dependabot/dependency_file"
6
+ require "dependabot/nuget/file_updater"
7
+ require "dependabot/nuget/file_parser/property_value_finder"
8
+
9
+ module Dependabot
10
+ module Nuget
11
+ class FileUpdater
12
+ class PropertyValueUpdater
13
+ def initialize(dependency_files:)
14
+ @dependency_files = dependency_files
15
+ end
16
+
17
+ def update_files_for_property_change(property_name:, updated_value:,
18
+ callsite_file:)
19
+ declaration_details =
20
+ property_value_finder.
21
+ property_details(
22
+ property_name: property_name,
23
+ callsite_file: callsite_file
24
+ )
25
+
26
+ declaration_file = dependency_files.find do |f|
27
+ declaration_details.fetch(:file) == f.name
28
+ end
29
+ node = declaration_details.fetch(:node)
30
+
31
+ updated_content = declaration_file.content.sub(
32
+ %r{<#{Regexp.quote(node.name)}>
33
+ \s*#{Regexp.quote(node.content)}\s*
34
+ </#{Regexp.quote(node.name)}>}xm,
35
+ "<#{node.name}>#{updated_value}</#{node.name}>"
36
+ )
37
+
38
+ files = dependency_files.dup
39
+ files[files.index(declaration_file)] =
40
+ update_file(file: declaration_file, content: updated_content)
41
+ files
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :dependency_files
47
+
48
+ def property_value_finder
49
+ @property_value_finder ||=
50
+ Nuget::FileParser::PropertyValueFinder.
51
+ new(dependency_files: dependency_files)
52
+ end
53
+
54
+ def update_file(file:, content:)
55
+ updated_file = file.dup
56
+ updated_file.content = content
57
+ updated_file
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/file_updaters"
4
+ require "dependabot/file_updaters/base"
5
+
6
+ module Dependabot
7
+ module Nuget
8
+ class FileUpdater < Dependabot::FileUpdaters::Base
9
+ require_relative "file_updater/packages_config_declaration_finder"
10
+ require_relative "file_updater/project_file_declaration_finder"
11
+ require_relative "file_updater/property_value_updater"
12
+
13
+ def self.updated_files_regex
14
+ [
15
+ %r{^[^/]*\.[a-z]{2}proj$},
16
+ /^packages\.config$/i
17
+ ]
18
+ end
19
+
20
+ def updated_dependency_files
21
+ updated_files = dependency_files.dup
22
+
23
+ # Loop through each of the changed requirements, applying changes to
24
+ # all files for that change. Note that the logic is different here
25
+ # to other languages because donet has property inheritance across
26
+ # files
27
+ dependencies.each do |dependency|
28
+ updated_files = update_files_for_dependency(
29
+ files: updated_files,
30
+ dependency: dependency
31
+ )
32
+ end
33
+
34
+ updated_files.reject! { |f| dependency_files.include?(f) }
35
+
36
+ raise "No files changed!" if updated_files.none?
37
+
38
+ updated_files
39
+ end
40
+
41
+ private
42
+
43
+ def project_files
44
+ dependency_files.select { |df| df.name.match?(/\.[a-z]{2}proj$/) }
45
+ end
46
+
47
+ def packages_config_files
48
+ dependency_files.select do |f|
49
+ f.name.split("/").last.casecmp("packages.config").zero?
50
+ end
51
+ end
52
+
53
+ def check_required_files
54
+ return if project_files.any? || packages_config_files.any?
55
+
56
+ raise "No project file or packages.config!"
57
+ end
58
+
59
+ def update_files_for_dependency(files:, dependency:)
60
+ # The UpdateChecker ensures the order of requirements is preserved
61
+ # when updating, so we can zip them together in new/old pairs.
62
+ reqs = dependency.requirements.zip(dependency.previous_requirements).
63
+ reject { |new_req, old_req| new_req == old_req }
64
+
65
+ # Loop through each changed requirement and update the files
66
+ reqs.each do |new_req, old_req|
67
+ raise "Bad req match" unless new_req[:file] == old_req[:file]
68
+ next if new_req[:requirement] == old_req[:requirement]
69
+
70
+ file = files.find { |f| f.name == new_req.fetch(:file) }
71
+
72
+ files =
73
+ if new_req.dig(:metadata, :property_name)
74
+ update_property_value(files, file, new_req)
75
+ else
76
+ update_declaration(files, dependency, file, old_req, new_req)
77
+ end
78
+ end
79
+
80
+ files
81
+ end
82
+
83
+ def update_property_value(files, file, req)
84
+ files = files.dup
85
+ property_name = req.fetch(:metadata).fetch(:property_name)
86
+
87
+ PropertyValueUpdater.
88
+ new(dependency_files: files).
89
+ update_files_for_property_change(
90
+ property_name: property_name,
91
+ updated_value: req.fetch(:requirement),
92
+ callsite_file: file
93
+ )
94
+ end
95
+
96
+ def update_declaration(files, dependency, file, old_req, new_req)
97
+ files = files.dup
98
+
99
+ updated_content = file.content
100
+
101
+ original_declarations(dependency, old_req).each do |old_dec|
102
+ updated_content = updated_content.gsub(
103
+ old_dec,
104
+ updated_declaration(old_dec, old_req, new_req)
105
+ )
106
+ end
107
+
108
+ raise "Expected content to change!" if updated_content == file.content
109
+
110
+ files[files.index(file)] =
111
+ updated_file(file: file, content: updated_content)
112
+ files
113
+ end
114
+
115
+ def original_declarations(dependency, requirement)
116
+ declaration_finder(dependency, requirement).declaration_strings
117
+ end
118
+
119
+ def declaration_finder(dependency, requirement)
120
+ @declaration_finders ||= {}
121
+
122
+ requirement_fn = requirement.fetch(:file)
123
+ @declaration_finders[dependency.hash + requirement.hash] ||=
124
+ if requirement_fn.split("/").last.casecmp("packages.config").zero?
125
+ PackagesConfigDeclarationFinder.new(
126
+ dependency_name: dependency.name,
127
+ declaring_requirement: requirement,
128
+ packages_config:
129
+ packages_config_files.find { |f| f.name == requirement_fn }
130
+ )
131
+ else
132
+ ProjectFileDeclarationFinder.new(
133
+ dependency_name: dependency.name,
134
+ declaring_requirement: requirement,
135
+ dependency_files: dependency_files
136
+ )
137
+ end
138
+ end
139
+
140
+ def updated_declaration(old_declaration, previous_req, requirement)
141
+ original_req_string = previous_req.fetch(:requirement)
142
+
143
+ old_declaration.gsub(
144
+ original_req_string,
145
+ requirement.fetch(:requirement)
146
+ )
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ Dependabot::FileUpdaters.register("nuget", Dependabot::Nuget::FileUpdater)
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "dependabot/metadata_finders"
5
+ require "dependabot/metadata_finders/base"
6
+
7
+ module Dependabot
8
+ module Nuget
9
+ class MetadataFinder < Dependabot::MetadataFinders::Base
10
+ private
11
+
12
+ def look_up_source
13
+ return Source.from_url(dependency_source_url) if dependency_source_url
14
+
15
+ look_up_source_in_nuspec(dependency_nuspec_file)
16
+ end
17
+
18
+ def look_up_source_in_nuspec(nuspec)
19
+ potential_source_urls = [
20
+ nuspec.at_css("package > metadata > repository")&.
21
+ attribute("url")&.value,
22
+ nuspec.at_css("package > metadata > repository > url")&.content,
23
+ nuspec.at_css("package > metadata > projectUrl")&.content,
24
+ nuspec.at_css("package > metadata > licenseUrl")&.content
25
+ ].compact
26
+
27
+ source_url = potential_source_urls.find { |url| Source.from_url(url) }
28
+ source_url ||= source_from_anywhere_in_nuspec(nuspec)
29
+
30
+ Source.from_url(source_url)
31
+ end
32
+
33
+ def source_from_anywhere_in_nuspec(nuspec)
34
+ github_urls = []
35
+ nuspec.to_s.scan(Source::SOURCE_REGEX) do
36
+ github_urls << Regexp.last_match.to_s
37
+ end
38
+
39
+ github_urls.find do |url|
40
+ repo = Source.from_url(url).repo
41
+ repo.downcase.end_with?(dependency.name.downcase)
42
+ end
43
+ end
44
+
45
+ def dependency_nuspec_file
46
+ return @dependency_nuspec_file unless @dependency_nuspec_file.nil?
47
+
48
+ response = Excon.get(
49
+ dependency_nuspec_url,
50
+ headers: auth_header,
51
+ idempotent: true,
52
+ **SharedHelpers.excon_defaults
53
+ )
54
+
55
+ @dependency_nuspec_file = Nokogiri::XML(response.body)
56
+ end
57
+
58
+ # rubocop:disable Metrics/AbcSize
59
+ def dependency_nuspec_url
60
+ source = dependency.requirements.
61
+ find { |r| r&.fetch(:source) }&.fetch(:source)
62
+
63
+ if source&.key?(:nuspec_url)
64
+ source.fetch(:nuspec_url) ||
65
+ "https://api.nuget.org/v3-flatcontainer/"\
66
+ "#{dependency.name.downcase}/#{dependency.version}/"\
67
+ "#{dependency.name.downcase}.nuspec"
68
+ elsif source&.key?(:nuspec_url)
69
+ source.fetch("nuspec_url") ||
70
+ "https://api.nuget.org/v3-flatcontainer/"\
71
+ "#{dependency.name.downcase}/#{dependency.version}/"\
72
+ "#{dependency.name.downcase}.nuspec"
73
+ else
74
+ "https://api.nuget.org/v3-flatcontainer/"\
75
+ "#{dependency.name.downcase}/#{dependency.version}/"\
76
+ "#{dependency.name.downcase}.nuspec"
77
+ end
78
+ end
79
+ # rubocop:enable Metrics/AbcSize
80
+
81
+ def dependency_source_url
82
+ source = dependency.requirements.
83
+ find { |r| r&.fetch(:source) }&.fetch(:source)
84
+
85
+ return unless source
86
+ return source.fetch(:source_url) if source.key?(:source_url)
87
+
88
+ source.fetch("source_url")
89
+ end
90
+
91
+ def auth_header
92
+ source = dependency.requirements.
93
+ find { |r| r&.fetch(:source) }&.fetch(:source)
94
+ url = source&.fetch(:url, nil) || source&.fetch("url")
95
+
96
+ token = credentials.
97
+ select { |cred| cred["type"] == "nuget_feed" }.
98
+ find { |cred| cred["url"] == url }&.
99
+ fetch("token", nil)
100
+
101
+ return {} unless token
102
+
103
+ if token.include?(":")
104
+ encoded_token = Base64.encode64(token).delete("\n")
105
+ { "Authorization" => "Basic #{encoded_token}" }
106
+ elsif Base64.decode64(token).ascii_only? &&
107
+ Base64.decode64(token).include?(":")
108
+ { "Authorization" => "Basic #{token.delete("\n")}" }
109
+ else
110
+ { "Authorization" => "Bearer #{token}" }
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ Dependabot::MetadataFinders.register("nuget", Dependabot::Nuget::MetadataFinder)
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/utils"
4
+ require "dependabot/nuget/version"
5
+
6
+ # For details on .NET version constraints see:
7
+ # https://docs.microsoft.com/en-us/nuget/reference/package-versioning
8
+ module Dependabot
9
+ module Nuget
10
+ class Requirement < Gem::Requirement
11
+ def self.parse(obj)
12
+ return ["=", Nuget::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
13
+
14
+ unless (matches = PATTERN.match(obj.to_s))
15
+ msg = "Illformed requirement [#{obj.inspect}]"
16
+ raise BadRequirementError, msg
17
+ end
18
+
19
+ return DefaultRequirement if matches[1] == ">=" && matches[2] == "0"
20
+
21
+ [matches[1] || "=", Nuget::Version.new(matches[2])]
22
+ end
23
+
24
+ # For consistency with other langauges, we define a requirements array.
25
+ # Dotnet doesn't have an `OR` separator for requirements, so it always
26
+ # contains a single element.
27
+ def self.requirements_array(requirement_string)
28
+ [new(requirement_string)]
29
+ end
30
+
31
+ def initialize(*requirements)
32
+ requirements = requirements.flatten.flat_map do |req_string|
33
+ convert_dotnet_constraint_to_ruby_constraint(req_string)
34
+ end
35
+
36
+ super(requirements)
37
+ end
38
+
39
+ def satisfied_by?(version)
40
+ version = Nuget::Version.new(version.to_s)
41
+ super
42
+ end
43
+
44
+ private
45
+
46
+ def convert_dotnet_constraint_to_ruby_constraint(req_string)
47
+ return unless req_string
48
+
49
+ if req_string&.start_with?("(", "[")
50
+ return convert_dotnet_range_to_ruby_range(req_string)
51
+ end
52
+
53
+ return req_string.split(",").map(&:strip) if req_string.include?(",")
54
+ return req_string unless req_string.include?("*")
55
+
56
+ convert_wildcard_req(req_string)
57
+ end
58
+
59
+ def convert_dotnet_range_to_ruby_range(req_string)
60
+ lower_b, upper_b = req_string.split(",").map(&:strip)
61
+
62
+ lower_b =
63
+ if ["(", "["].include?(lower_b) then nil
64
+ elsif lower_b.start_with?("(") then "> #{lower_b.sub(/\(\s*/, '')}"
65
+ else ">= #{lower_b.sub(/\[\s*/, '').strip}"
66
+ end
67
+
68
+ upper_b =
69
+ if [")", "]"].include?(upper_b) then nil
70
+ elsif upper_b.end_with?(")") then "< #{upper_b.sub(/\s*\)/, '')}"
71
+ else "<= #{upper_b.sub(/\s*\]/, '').strip}"
72
+ end
73
+
74
+ [lower_b, upper_b].compact
75
+ end
76
+
77
+ def convert_wildcard_req(req_string)
78
+ return ">= 0" if req_string.start_with?("*")
79
+
80
+ defined_part = req_string.split("*").first
81
+ suffix = defined_part.end_with?(".") ? "0" : "a"
82
+ version = defined_part + suffix
83
+ "~> #{version}"
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ Dependabot::Utils.
90
+ register_requirement_class("nuget", Dependabot::Nuget::Requirement)