diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-16 21:11:26 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-16 21:11:26 +0300 |
commit | 8fa0c53e26c947ac647b8067fde3e9673b77b1a6 (patch) | |
tree | da32e7224125973e9e87d3856fb7e672ff41c8b1 /lib/sbom | |
parent | 0552020767452da44de2bf5424096f2cb2ea6bf5 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'lib/sbom')
-rw-r--r-- | lib/sbom/package_url.rb | 11 | ||||
-rw-r--r-- | lib/sbom/package_url/argument_validator.rb | 90 | ||||
-rw-r--r-- | lib/sbom/package_url/decoder.rb | 35 | ||||
-rw-r--r-- | lib/sbom/package_url/encoder.rb | 8 | ||||
-rw-r--r-- | lib/sbom/package_url/normalizer.rb | 47 | ||||
-rw-r--r-- | lib/sbom/package_url/string_utils.rb | 6 |
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 |