debian-mirror-gitlab/lib/sbom/package_url/decoder.rb
2023-01-12 18:35:48 +00:00

181 lines
6.5 KiB
Ruby

# 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