138 lines
4.4 KiB
Ruby
138 lines
4.4 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 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
|