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
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sbom/package_url/decoder.rb')
-rw-r--r--lib/sbom/package_url/decoder.rb181
1 files changed, 181 insertions, 0 deletions
diff --git a/lib/sbom/package_url/decoder.rb b/lib/sbom/package_url/decoder.rb
new file mode 100644
index 00000000000..ceadc36660c
--- /dev/null
+++ b/lib/sbom/package_url/decoder.rb
@@ -0,0 +1,181 @@
+# frozen_string_literal: true
+
+# MIT License
+#
+# Copyright (c) 2021 package-url
+# Portions Copyright 2022 Gitlab B.V.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+module Sbom
+ class PackageUrl
+ class Decoder
+ include StringUtils
+
+ def initialize(string)
+ @string = string
+ end
+
+ def decode!
+ raise ArgumentError, "expected String but given #{@string.class}" unless @string.is_a?(::String)
+
+ decode_subpath!
+ decode_qualifiers!
+ decode_scheme!
+ decode_type!
+ decode_version!
+ decode_name!
+ decode_namespace!
+
+ 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
+
+ def decode_subpath!
+ # Split the purl string once from right on '#'
+ # Given the string: `scheme:type/namespace/name@version?qualifiers#subpath`
+ # - The left side is the remainder: `scheme:type/namespace/name@version?qualifiers`
+ # - The right side will be parsed as the subpath: `subpath`
+ @subpath, @string = partition(@string, '#', from: :right) do |subpath|
+ decode_segments(subpath) do |segment|
+ # Discard segments which are blank, `.`, or `..`
+ segment.empty? || segment == '.' || segment == '..'
+ end
+ end
+ end
+
+ def decode_qualifiers!
+ # Split the remainder once from right on '?'
+ # Given string: `scheme:type/namespace/name@version?qualifiers`
+ # - The left side is the remainder: `scheme:type/namespace/name@version`
+ # - The right side is the qualifiers string: `qualifiers`
+ @qualifiers, @string = partition(@string, '?', from: :right) do |qualifiers|
+ parse_qualifiers(qualifiers)
+ end
+ end
+
+ def decode_scheme!
+ # Split the remainder once from left on ':'
+ # Given the string: `scheme:type/namespace/name@version`
+ # - 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'
+ end
+
+ def decode_type!
+ # Strip the remainder from leading and trailing '/'
+ @string = strip(@string, '/')
+ # Split this once from left on '/'
+ # 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, &:downcase)
+ end
+
+ def decode_version!
+ # Split the remainder once from right on '@'
+ # Given the string: `namespace/name@version`
+ # - The left side is the remainder: `namespace/name`
+ # - The right side is the version: `version`
+ # - The version must be URI decoded
+ @version, @string = partition(@string, '@', from: :right) do |version|
+ URI.decode_www_form_component(version)
+ end
+ end
+
+ def decode_name!
+ # Split the remainder once from right on '/'
+ # Given the string: `namespace/name`
+ # - The left side is the remainder: `namespace`
+ # - The right size is the name: `name`
+ # - The name must be URI decoded
+ @name, @string = partition(@string, '/', from: :right, require_separator: false) do |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 `/`.
+ 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?('/')
+
+ decoded
+ end
+
+ def decode_segments(string)
+ string.split('/').filter_map do |segment|
+ next if block_given? && yield(segment)
+
+ decode_segment(segment)
+ end.join('/')
+ end
+
+ def parse_qualifiers(raw_qualifiers)
+ # - Split the qualifiers on '&'. Each part is a key=value pair
+ # - For each pair, split the key=value once from left on '=':
+ # - The key is the lowercase left side
+ # - The value is the percent-decoded right side
+ # - Discard any key/value pairs where the value is empty
+ # - If the key is checksums,
+ # split the value on ',' to create a list of checksums
+ # - This list of key/value is the qualifiers object
+ raw_qualifiers.split('&').each_with_object({}) do |pair, memo|
+ key, separator, value = pair.partition('=')
+
+ next if separator.empty?
+
+ key = key.downcase
+ value = URI.decode_www_form_component(value)
+
+ next if value.empty?
+
+ memo[key] = case key
+ when 'checksums'
+ value.split(',')
+ else
+ value
+ end
+ end
+ end
+ end
+ end
+end