diff options
Diffstat (limited to 'lib/sbom/package_url/encoder.rb')
-rw-r--r-- | lib/sbom/package_url/encoder.rb | 137 |
1 files changed, 137 insertions, 0 deletions
diff --git a/lib/sbom/package_url/encoder.rb b/lib/sbom/package_url/encoder.rb new file mode 100644 index 00000000000..9cf05095571 --- /dev/null +++ b/lib/sbom/package_url/encoder.rb @@ -0,0 +1,137 @@ +# 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 Encoder + include StringUtils + + def initialize(package) + @type = package.type + @namespace = package.namespace + @name = package.name + @version = package.version + @qualifiers = package.qualifiers + @subpath = package.subpath + @io = StringIO.new + end + + def encode + encode_scheme! + encode_type! + encode_name! + encode_version! + encode_qualifiers! + encode_subpath! + + io.string + end + + private + + attr_reader :io + + def encode_scheme! + io.write('pkg:') + end + + def encode_type! + # Append the type string to the purl as a lowercase ASCII string + # Append '/' to the purl + io.write(@type) + io.write('/') + end + + def encode_name! + # If the namespace is empty: + # - Apply type-specific normalization to the name if needed + # - UTF-8-encode the name if needed in your programming language + # - Append the percent-encoded name to the purl + # + # If the namespace is not empty: + # - Strip the namespace from leading and trailing '/' + # - Split on '/' as segments + # - Apply type-specific normalization to each segment if needed + # - UTF-8-encode each segment if needed in your programming language + # - Percent-encode each segment + # - Join the segments with '/' + # - Append this to the purl + # - Append '/' to the purl + # - Strip the name from leading and trailing '/' + # - Apply type-specific normalization to the name if needed + # - 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, Encoding::UTF_8)) + else + io.write(encode_segments(@namespace, &:empty?)) + io.write('/') + io.write(URI.encode_www_form_component(strip(@name, '/'), Encoding::UTF_8)) + end + end + + def encode_version! + return if @version.nil? + + # - Append '@' to the purl + # - 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, Encoding::UTF_8)) + end + + def encode_qualifiers! + return if @qualifiers.nil? || encoded_qualifiers.empty? + + io.write('?') + io.write(encoded_qualifiers) + end + + def encoded_qualifiers + @encoded_qualifiers ||= @qualifiers.filter_map do |key, value| + next if value.empty? + + next "#{key.downcase}=#{value.join(',')}" if key == 'checksums' && value.is_a?(::Array) + + "#{key.downcase}=#{URI.encode_www_form_component(value, Encoding::UTF_8)}" + end.sort.join('&') + end + + def encode_subpath! + return if @subpath.nil? || encoded_subpath.empty? + + io.write('#') + io.write(encoded_subpath) + end + + def encoded_subpath + @encoded_subpath ||= encode_segments(@subpath) do |segment| + # Discard segments which are blank, `.`, or `..` + segment.empty? || segment == '.' || segment == '..' + end + end + end + end +end |