Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib/sbom
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-11-16 21:11:26 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-11-16 21:11:26 +0300
commit8fa0c53e26c947ac647b8067fde3e9673b77b1a6 (patch)
treeda32e7224125973e9e87d3856fb7e672ff41c8b1 /lib/sbom
parent0552020767452da44de2bf5424096f2cb2ea6bf5 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'lib/sbom')
-rw-r--r--lib/sbom/package_url.rb11
-rw-r--r--lib/sbom/package_url/argument_validator.rb90
-rw-r--r--lib/sbom/package_url/decoder.rb35
-rw-r--r--lib/sbom/package_url/encoder.rb8
-rw-r--r--lib/sbom/package_url/normalizer.rb47
-rw-r--r--lib/sbom/package_url/string_utils.rb6
6 files changed, 171 insertions, 26 deletions
diff --git a/lib/sbom/package_url.rb b/lib/sbom/package_url.rb
index 3b545ebebf2..d8f4e876b82 100644
--- a/lib/sbom/package_url.rb
+++ b/lib/sbom/package_url.rb
@@ -44,7 +44,7 @@ module Sbom
class PackageUrl
# Raised when attempting to parse an invalid package URL string.
# @see #parse
- InvalidPackageURL = Class.new(ArgumentError)
+ InvalidPackageUrl = Class.new(ArgumentError)
# The URL scheme, which has a constant value of `"pkg"`.
def scheme
@@ -79,20 +79,19 @@ module Sbom
# @param qualifiers [Hash] Extra qualifying data for a package, specific to the type of package.
# @param subpath [String] An extra subpath within a package, relative to the package root.
def initialize(type:, name:, namespace: nil, version: nil, qualifiers: nil, subpath: nil)
- raise ArgumentError, 'type is required' unless type.present?
- raise ArgumentError, 'name is required' unless name.present?
-
- @type = type.downcase
+ @type = type&.downcase
@namespace = namespace
@name = name
@version = version
@qualifiers = qualifiers
@subpath = subpath
+
+ ArgumentValidator.new(self).validate!
end
# Creates a new PackageUrl from a string.
# @param [String] string The package URL string.
- # @raise [InvalidPackageURL] If the string is not a valid package URL.
+ # @raise [InvalidPackageUrl] If the string is not a valid package URL.
# @return [PackageUrl]
def self.parse(string)
Decoder.new(string).decode!
diff --git a/lib/sbom/package_url/argument_validator.rb b/lib/sbom/package_url/argument_validator.rb
new file mode 100644
index 00000000000..639ee9f89b6
--- /dev/null
+++ b/lib/sbom/package_url/argument_validator.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+module Sbom
+ class PackageUrl
+ class ArgumentValidator
+ QUALIFIER_KEY_REGEXP = /^[A-Za-z\d._-]+$/.freeze
+ START_WITH_NUMBER_REGEXP = /^\d/.freeze
+
+ def initialize(package)
+ @type = package.type
+ @namespace = package.namespace
+ @name = package.name
+ @version = package.version
+ @qualifiers = package.qualifiers
+ @errors = []
+ end
+
+ def validate!
+ validate_type
+ validate_name
+ validate_qualifiers
+ validate_by_type
+
+ raise ArgumentError, formatted_errors if invalid?
+ end
+
+ private
+
+ def invalid?
+ errors.present?
+ end
+
+ attr_reader :type, :namespace, :name, :version, :qualifiers, :errors
+
+ def formatted_errors
+ errors.join(', ')
+ end
+
+ def validate_type
+ errors.push('Type is required') if type.blank?
+ end
+
+ def validate_name
+ errors.push('Name is required') if name.blank?
+ end
+
+ def validate_qualifiers
+ return if qualifiers.nil?
+
+ keys = qualifiers.keys
+ errors.push('Qualifier keys must be unique') unless keys.uniq.size == keys.size
+
+ keys.each do |key|
+ errors.push(key_error(key, 'contains illegal characters')) unless key.match?(QUALIFIER_KEY_REGEXP)
+ errors.push(key_error(key, 'may not start with a number')) if key.match?(START_WITH_NUMBER_REGEXP)
+ end
+ end
+
+ def key_error(key, text)
+ "Qualifier key `#{key}` #{text}"
+ end
+
+ def validate_by_type
+ case type
+ when 'conan'
+ validate_conan
+ when 'cran'
+ validate_cran
+ when 'swift'
+ validate_swift
+ end
+ end
+
+ def validate_conan
+ return unless namespace.blank? ^ (qualifiers.nil? || qualifiers.exclude?('channel'))
+
+ errors.push('Conan packages require the channel be present if published in a namespace and vice-versa')
+ end
+
+ def validate_cran
+ errors.push('Cran packages require a version') if version.blank?
+ end
+
+ def validate_swift
+ errors.push('Swift packages require a namespace') if namespace.blank?
+ errors.push('Swift packages require a version') if version.blank?
+ end
+ end
+ end
+end
diff --git a/lib/sbom/package_url/decoder.rb b/lib/sbom/package_url/decoder.rb
index 5a31343995d..ceadc36660c 100644
--- a/lib/sbom/package_url/decoder.rb
+++ b/lib/sbom/package_url/decoder.rb
@@ -43,14 +43,18 @@ module Sbom
decode_name!
decode_namespace!
- PackageUrl.new(
- type: @type,
- name: @name,
- namespace: @namespace,
- version: @version,
- qualifiers: @qualifiers,
- subpath: @subpath
- )
+ begin
+ PackageUrl.new(
+ type: @type,
+ name: @name,
+ namespace: @namespace,
+ version: @version,
+ qualifiers: @qualifiers,
+ subpath: @subpath
+ )
+ rescue ArgumentError => e
+ raise InvalidPackageUrl, e.message
+ end
end
private
@@ -84,7 +88,7 @@ module Sbom
# - The left side lowercased is the scheme: `scheme`
# - The right side is the remainder: `type/namespace/name@version`
@scheme, @string = partition(@string, ':', from: :left)
- raise InvalidPackageURL, 'invalid or missing "pkg:" URL scheme' unless @scheme == 'pkg'
+ raise InvalidPackageUrl, 'invalid or missing "pkg:" URL scheme' unless @scheme == 'pkg'
end
def decode_type!
@@ -94,8 +98,7 @@ module Sbom
# Given the string: `type/namespace/name@version`
# - The left side lowercased is the type: `type`
# - The right side is the remainder: `namespace/name@version`
- @type, @string = partition(@string, '/', from: :left)
- raise InvalidPackageURL, 'invalid or missing package type' if @type.blank?
+ @type, @string = partition(@string, '/', from: :left, &:downcase)
end
def decode_version!
@@ -116,20 +119,24 @@ module Sbom
# - The right size is the name: `name`
# - The name must be URI decoded
@name, @string = partition(@string, '/', from: :right, require_separator: false) do |name|
- URI.decode_www_form_component(name)
+ decoded_name = URI.decode_www_form_component(name)
+ Normalizer.new(type: @type, text: decoded_name).normalize_name
end
end
def decode_namespace!
# If there is anything remaining, this is the namespace.
# The namespace may contain multiple segments delimited by `/`.
- @namespace = decode_segments(@string, &:empty?) if @string.present?
+ return if @string.blank?
+
+ @namespace = decode_segments(@string, &:empty?)
+ @namespace = Normalizer.new(type: @type, text: @namespace).normalize_namespace
end
def decode_segment(segment)
decoded = URI.decode_www_form_component(segment)
- raise InvalidPackageURL, 'slash-separated segments may not contain `/`' if decoded.include?('/')
+ raise InvalidPackageUrl, 'slash-separated segments may not contain `/`' if decoded.include?('/')
decoded
end
diff --git a/lib/sbom/package_url/encoder.rb b/lib/sbom/package_url/encoder.rb
index 1412824b76f..9cf05095571 100644
--- a/lib/sbom/package_url/encoder.rb
+++ b/lib/sbom/package_url/encoder.rb
@@ -84,11 +84,11 @@ module Sbom
# - UTF-8-encode the name if needed in your programming language
# - Append the percent-encoded name to the purl
if @namespace.nil?
- io.write(URI.encode_www_form_component(@name))
+ io.write(URI.encode_www_form_component(@name, Encoding::UTF_8))
else
io.write(encode_segments(@namespace, &:empty?))
io.write('/')
- io.write(URI.encode_www_form_component(strip(@name, '/')))
+ io.write(URI.encode_www_form_component(strip(@name, '/'), Encoding::UTF_8))
end
end
@@ -99,7 +99,7 @@ module Sbom
# - UTF-8-encode the version if needed in your programming language
# - Append the percent-encoded version to the purl
io.write('@')
- io.write(URI.encode_www_form_component(@version))
+ io.write(URI.encode_www_form_component(@version, Encoding::UTF_8))
end
def encode_qualifiers!
@@ -115,7 +115,7 @@ module Sbom
next "#{key.downcase}=#{value.join(',')}" if key == 'checksums' && value.is_a?(::Array)
- "#{key.downcase}=#{URI.encode_www_form_component(value)}"
+ "#{key.downcase}=#{URI.encode_www_form_component(value, Encoding::UTF_8)}"
end.sort.join('&')
end
diff --git a/lib/sbom/package_url/normalizer.rb b/lib/sbom/package_url/normalizer.rb
new file mode 100644
index 00000000000..663df6f72a5
--- /dev/null
+++ b/lib/sbom/package_url/normalizer.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Sbom
+ class PackageUrl
+ class Normalizer
+ def initialize(type:, text:)
+ @type = type
+ @text = text
+ end
+
+ def normalize_namespace
+ return if text.nil?
+
+ normalize
+ end
+
+ def normalize_name
+ raise ArgumentError, 'Name is required' if text.nil?
+
+ normalize
+ end
+
+ private
+
+ def normalize
+ case type
+ when 'bitbucket', 'github'
+ downcase
+ when 'pypi'
+ normalize_pypi
+ else
+ text
+ end
+ end
+
+ attr_reader :type, :text
+
+ def downcase
+ text.downcase
+ end
+
+ def normalize_pypi
+ downcase.tr('_', '-')
+ end
+ end
+ end
+end
diff --git a/lib/sbom/package_url/string_utils.rb b/lib/sbom/package_url/string_utils.rb
index 7b476292c72..c1ea8de95b2 100644
--- a/lib/sbom/package_url/string_utils.rb
+++ b/lib/sbom/package_url/string_utils.rb
@@ -29,7 +29,9 @@ module Sbom
private
def strip(string, char)
- string.delete_prefix(char).delete_suffix(char)
+ string = string.delete_prefix(char) while string.start_with?(char)
+ string = string.delete_suffix(char) while string.end_with?(char)
+ string
end
def split_segments(string)
@@ -66,7 +68,7 @@ module Sbom
return [nil, value] if separator.empty? && require_separator
- value = yield(value, remainder) if block_given?
+ value = yield(value) if block_given?
[value, remainder]
end