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
path: root/lib
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-11-16 21:11:26 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-11-16 21:11:26 +0300
commit8fa0c53e26c947ac647b8067fde3e9673b77b1a6 (patch)
treeda32e7224125973e9e87d3856fb7e672ff41c8b1 /lib
parent0552020767452da44de2bf5424096f2cb2ea6bf5 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'lib')
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/branches.rb2
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/api/entities/basic_project_details.rb2
-rw-r--r--lib/api/entities/issuable_entity.rb14
-rw-r--r--lib/api/entities/issue_basic.rb16
-rw-r--r--lib/api/entities/project.rb2
-rw-r--r--lib/api/entities/release.rb10
-rw-r--r--lib/api/files.rb2
-rw-r--r--lib/api/issue_links.rb55
-rw-r--r--lib/api/lint.rb2
-rw-r--r--lib/api/releases.rb10
-rw-r--r--lib/api/repositories.rb4
-rw-r--r--lib/api/tags.rb2
-rw-r--r--lib/gitlab/ci/parsers/sbom/cyclonedx.rb2
-rw-r--r--lib/gitlab/gitaly_client.rb2
-rw-r--r--lib/gitlab/gon_helper.rb13
-rw-r--r--lib/sbom/package_url.rb11
-rw-r--r--lib/sbom/package_url/argument_validator.rb90
-rw-r--r--lib/sbom/package_url/decoder.rb35
-rw-r--r--lib/sbom/package_url/encoder.rb8
-rw-r--r--lib/sbom/package_url/normalizer.rb47
-rw-r--r--lib/sbom/package_url/string_utils.rb6
23 files changed, 262 insertions, 77 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 94dfb7f598c..ffb0cdf8991 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -218,6 +218,7 @@ module API
mount ::API::ImportGithub
mount ::API::Integrations
mount ::API::Invitations
+ mount ::API::IssueLinks
mount ::API::Keys
mount ::API::Lint
mount ::API::Markdown
@@ -291,7 +292,6 @@ module API
mount ::API::Groups
mount ::API::HelmPackages
mount ::API::Integrations::JiraConnect::Subscriptions
- mount ::API::IssueLinks
mount ::API::Issues
mount ::API::Labels
mount ::API::MavenPackages
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index a7418deb88a..845e42c2ed8 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -14,7 +14,7 @@ module API
before do
require_repository_enabled!
- authorize! :download_code, user_project
+ authorize! :read_code, user_project
end
rescue_from Gitlab::Git::Repository::NoRepository do
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 3b122fb23a2..63a13b83a9b 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -9,7 +9,7 @@ module API
before do
require_repository_enabled!
- authorize! :download_code, user_project
+ authorize! :read_code, user_project
verify_pagination_params!
end
diff --git a/lib/api/entities/basic_project_details.rb b/lib/api/entities/basic_project_details.rb
index f8f5b59cdc1..2585b2d0b6d 100644
--- a/lib/api/entities/basic_project_details.rb
+++ b/lib/api/entities/basic_project_details.rb
@@ -6,7 +6,7 @@ module API
include ::API::ProjectsRelationBuilder
include Gitlab::Utils::StrongMemoize
- expose :default_branch_or_main, documentation: { type: 'string', example: 'main' }, as: :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
+ expose :default_branch_or_main, documentation: { type: 'string', example: 'main' }, as: :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :read_code, project) }
# Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770
expose :topic_names, as: :tag_list, documentation: { type: 'string', is_array: true, example: 'tag' }
diff --git a/lib/api/entities/issuable_entity.rb b/lib/api/entities/issuable_entity.rb
index e2c674c0b8b..4e70f945a48 100644
--- a/lib/api/entities/issuable_entity.rb
+++ b/lib/api/entities/issuable_entity.rb
@@ -3,10 +3,16 @@
module API
module Entities
class IssuableEntity < Grape::Entity
- expose :id, :iid
- expose(:project_id) { |entity| entity&.project.try(:id) }
- expose :title, :description
- expose :state, :created_at, :updated_at
+ expose :id, documentation: { type: 'integer', example: 84 }
+ expose :iid, documentation: { type: 'integer', example: 14 }
+ expose :project_id, documentation: { type: 'integer', example: 4 } do |entity|
+ entity&.project.try(:id)
+ end
+ expose :title, documentation: { type: 'string', example: 'Impedit et ut et dolores vero provident ullam est' }
+ expose :description, documentation: { type: 'string', example: 'Repellendus impedit et vel velit dignissimos.' }
+ expose :state, documentation: { type: 'string', example: 'closed' }
+ expose :created_at, documentation: { type: 'dateTime', example: '2022-08-17T12:46:35.053Z' }
+ expose :updated_at, documentation: { type: 'dateTime', example: '2022-11-14T17:22:01.470Z' }
def presented
lazy_issuable_metadata
diff --git a/lib/api/entities/issue_basic.rb b/lib/api/entities/issue_basic.rb
index 20f66c026e6..89fb8bbe1c0 100644
--- a/lib/api/entities/issue_basic.rb
+++ b/lib/api/entities/issue_basic.rb
@@ -7,10 +7,10 @@ module API
item.upcase if item.respond_to?(:upcase)
end
- expose :closed_at
+ expose :closed_at, documentation: { type: 'dateTime', example: '2022-11-15T08:30:55.232Z' }
expose :closed_by, using: Entities::UserBasic
- expose :labels do |issue, options|
+ expose :labels, documentation: { type: 'string', is_array: true, example: 'bug' } do |issue, options|
if options[:with_labels_details]
::API::Entities::LabelBasic.represent(issue.labels.sort_by(&:title))
else
@@ -23,7 +23,7 @@ module API
expose :issue_type,
as: :type,
format_with: :upcase,
- documentation: { type: "String", desc: "One of #{::WorkItems::Type.allowed_types_for_issues.map(&:upcase)}" }
+ documentation: { type: 'String', example: 'ISSUE', desc: "One of #{::WorkItems::Type.allowed_types_for_issues.map(&:upcase)}" }
expose :assignee, using: ::API::Entities::UserBasic do |issue|
issue.assignees.first
@@ -33,12 +33,12 @@ module API
expose(:merge_requests_count) { |issue, options| issuable_metadata.merge_requests_count }
expose(:upvotes) { |issue, options| issuable_metadata.upvotes }
expose(:downvotes) { |issue, options| issuable_metadata.downvotes }
- expose :due_date
- expose :confidential
- expose :discussion_locked
- expose :issue_type
+ expose :due_date, documentation: { type: 'date', example: '2022-11-20' }
+ expose :confidential, documentation: { type: 'boolean' }
+ expose :discussion_locked, documentation: { type: 'boolean' }
+ expose :issue_type, documentation: { type: 'string', example: 'issue' }
- expose :web_url do |issue|
+ expose :web_url, documentation: { type: 'string', example: 'http://example.com/example/example/issues/14' } do |issue|
Gitlab::UrlBuilder.build(issue)
end
diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb
index 947b0a3c0c1..1c1bafbf161 100644
--- a/lib/api/entities/project.rb
+++ b/lib/api/entities/project.rb
@@ -114,7 +114,7 @@ module API
end
expose :build_timeout, documentation: { type: 'integer', example: 3600 }
expose :auto_cancel_pending_pipelines, documentation: { type: 'string', example: 'enabled' }
- expose :ci_config_path, documentation: { type: 'string', example: '' }, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
+ expose :ci_config_path, documentation: { type: 'string', example: '' }, if: -> (project, options) { Ability.allowed?(options[:current_user], :read_code, project) }
expose :shared_with_groups, documentation: { is_array: true } do |project, options|
user = options[:current_user]
diff --git a/lib/api/entities/release.rb b/lib/api/entities/release.rb
index 5feb1edbff6..c1a48a46d64 100644
--- a/lib/api/entities/release.rb
+++ b/lib/api/entities/release.rb
@@ -9,7 +9,7 @@ module API
MarkupHelper.markdown_field(entity, :description, current_user: options[:current_user])
end
expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? }
- expose :commit, using: Entities::Commit, if: ->(_, _) { can_download_code? }
+ expose :commit, using: Entities::Commit, if: ->(_, _) { can_read_code? }
expose :milestones,
using: Entities::MilestoneWithStats,
if: -> (release, _) { release.milestones.present? && can_read_milestone? } do |release, _|
@@ -23,10 +23,10 @@ module API
expose :assets do
expose :assets_count, documentation: { type: 'integer', example: 2 }, as: :count
- expose :sources, using: Entities::Releases::Source, if: ->(_, _) { can_download_code? }
+ expose :sources, using: Entities::Releases::Source, if: ->(_, _) { can_read_code? }
expose :sorted_links, as: :links, using: Entities::Releases::Link
end
- expose :evidences, using: Entities::Releases::Evidence, expose_nil: false, if: ->(_, _) { can_download_code? }
+ expose :evidences, using: Entities::Releases::Evidence, expose_nil: false, if: ->(_, _) { can_read_code? }
expose :_links do
expose :self_url, as: :self, expose_nil: false
expose :edit_url, expose_nil: false
@@ -34,8 +34,8 @@ module API
private
- def can_download_code?
- Ability.allowed?(options[:current_user], :download_code, object.project)
+ def can_read_code?
+ Ability.allowed?(options[:current_user], :read_code, object.project)
end
def can_read_milestone?
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 68dd2647703..fa749299b9a 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -30,7 +30,7 @@ module API
end
def assign_file_vars!
- authorize! :download_code, user_project
+ authorize! :read_code, user_project
@commit = user_project.commit(params[:ref])
not_found!('Commit') unless @commit
diff --git a/lib/api/issue_links.rb b/lib/api/issue_links.rb
index 0f92f7aeb91..020b02248a0 100644
--- a/lib/api/issue_links.rb
+++ b/lib/api/issue_links.rb
@@ -6,16 +6,27 @@ module API
before { authenticate! }
+ ISSUE_LINKS_TAGS = %w[issue_links].freeze
+
feature_category :team_planning
urgency :low
params do
- requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
- requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
+ requires :id, types: [String, Integer],
+ desc: 'The ID or URL-encoded path of the project owned by the authenticated user'
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project’s issue'
end
resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Get related issues' do
+ desc 'List issue relations' do
+ detail 'Get a list of a given issue’s linked issues, sorted by the relationship creation datetime (ascending).'\
+ 'Issues are filtered according to the user authorizations.'
success Entities::RelatedIssue
+ is_array true
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 404, message: 'Not found' }
+ ]
+ tags ISSUE_LINKS_TAGS
end
get ':id/issues/:issue_iid/links' do
source_issue = find_project_issue(params[:issue_iid])
@@ -30,14 +41,23 @@ module API
include_subscribed: false
end
- desc 'Relate issues' do
+ desc 'Create an issue link' do
+ detail 'Creates a two-way relation between two issues.'\
+ 'The user must be allowed to update both issues to succeed.'
success Entities::IssueLink
+ failure [
+ { code: 400, message: 'Bad Request' },
+ { code: 401, message: 'Unauthorized' }
+ ]
+ tags ISSUE_LINKS_TAGS
end
params do
- requires :target_project_id, type: String, desc: 'The ID of the target project'
- requires :target_issue_iid, type: Integer, desc: 'The IID of the target issue'
+ requires :target_project_id, types: [String, Integer],
+ desc: 'The ID or URL-encoded path of a target project'
+ requires :target_issue_iid, types: [String, Integer], desc: 'The internal ID of a target project’s issue'
optional :link_type, type: String, values: IssueLink.link_types.keys,
- desc: 'The type of the relation'
+ desc: 'The type of the relation (“relates_to”, “blocks”, “is_blocked_by”),'\
+ 'defaults to “relates_to”)'
end
# rubocop: disable CodeReuse/ActiveRecord
post ':id/issues/:issue_iid/links' do
@@ -61,12 +81,17 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
- desc 'Get issues relation' do
- detail 'This feature was introduced in GitLab 15.1.'
+ desc 'Get an issue link' do
+ detail 'Gets details about an issue link. This feature was introduced in GitLab 15.1.'
success Entities::IssueLink
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 404, message: 'Not found' }
+ ]
+ tags ISSUE_LINKS_TAGS
end
params do
- requires :issue_link_id, type: Integer, desc: 'The ID of an issue link'
+ requires :issue_link_id, types: [String, Integer], desc: 'ID of an issue relationship'
end
get ':id/issues/:issue_iid/links/:issue_link_id' do
issue = find_project_issue(params[:issue_iid])
@@ -77,11 +102,17 @@ module API
present issue_link, with: Entities::IssueLink
end
- desc 'Remove issues relation' do
+ desc 'Delete an issue link' do
+ detail 'Deletes an issue link, thus removes the two-way relationship.'
success Entities::IssueLink
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 404, message: 'Not found' }
+ ]
+ tags ISSUE_LINKS_TAGS
end
params do
- requires :issue_link_id, type: Integer, desc: 'The ID of an issue link'
+ requires :issue_link_id, types: [String, Integer], desc: 'The ID of an issue relationship'
end
delete ':id/issues/:issue_iid/links/:issue_link_id' do
issue = find_project_issue(params[:issue_iid])
diff --git a/lib/api/lint.rb b/lib/api/lint.rb
index 1d19d653d8b..89787ba00c2 100644
--- a/lib/api/lint.rb
+++ b/lib/api/lint.rb
@@ -56,7 +56,7 @@ module API
end
get ':id/ci/lint', urgency: :low do
- authorize! :download_code, user_project
+ authorize! :read_code, user_project
if user_project.commit.present?
content = user_project.repository.gitlab_ci_yml_for(user_project.commit.id, user_project.ci_config_path_or_default)
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index ec9907b18f9..e6884e66200 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -131,7 +131,7 @@ module API
end
route_setting :authentication, job_token_allowed: true
get ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do
- authorize_download_code!
+ authorize_read_code!
not_found! unless release
@@ -157,7 +157,7 @@ module API
end
route_setting :authentication, job_token_allowed: true
get ':id/releases/:tag_name/downloads/*file_path', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do
- authorize_download_code!
+ authorize_read_code!
not_found! unless release
@@ -185,7 +185,7 @@ module API
end
route_setting :authentication, job_token_allowed: true
get ':id/releases/permalink/latest(/)(*suffix_path)', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do
- authorize_download_code!
+ authorize_read_code!
# Try to find the latest release
latest_release = find_latest_release
@@ -373,6 +373,10 @@ module API
authorize! :download_code, user_project
end
+ def authorize_read_code!
+ authorize! :read_code, user_project
+ end
+
def authorize_create_evidence!
# extended in EE
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index beba2842316..70535496b12 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -41,7 +41,7 @@ module API
end
end
- before { authorize! :download_code, user_project }
+ before { authorize! :read_code, user_project }
feature_category :source_code_management
@@ -63,7 +63,7 @@ module API
end
def assign_blob_vars!(limit:)
- authorize! :download_code, user_project
+ authorize! :read_code, user_project
@repo = user_project.repository
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 0022b51bd92..b412a17bc6f 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -7,7 +7,7 @@ module API
TAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(tag_name: API::NO_SLASH_URL_PART_REGEX)
before do
- authorize! :download_code, user_project
+ authorize! :read_code, user_project
not_found! unless user_project.repo_exists?
end
diff --git a/lib/gitlab/ci/parsers/sbom/cyclonedx.rb b/lib/gitlab/ci/parsers/sbom/cyclonedx.rb
index f1a07af1bf9..bc62fbe55ec 100644
--- a/lib/gitlab/ci/parsers/sbom/cyclonedx.rb
+++ b/lib/gitlab/ci/parsers/sbom/cyclonedx.rb
@@ -70,7 +70,7 @@ module Gitlab
)
report.add_component(component) if component.ingestible?
- rescue ::Sbom::PackageUrl::InvalidPackageURL
+ rescue ::Sbom::PackageUrl::InvalidPackageUrl
report.add_error("/components/#{index}/purl is invalid")
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index c68c096bea1..735c7fcf80c 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -371,8 +371,6 @@ module Gitlab
end
def self.expected_server_version
- return ENV[SERVER_VERSION_FILE] if ENV[SERVER_VERSION_FILE]
-
path = Rails.root.join(SERVER_VERSION_FILE)
path.read.chomp
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index a0daa03bbed..ecb57bfc1a2 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -17,14 +17,15 @@ module Gitlab
gon.markdown_surround_selection = current_user&.markdown_surround_selection
gon.markdown_automatic_lists = current_user&.markdown_automatic_lists
- # Support for Sentry setup via configuration will be removed in 16.0
- # in favor of Gitlab::CurrentSettings.
- if Feature.enabled?(:enable_old_sentry_clientside_integration) && Gitlab.config.sentry.enabled
- gon.sentry_dsn = Gitlab.config.sentry.clientside_dsn
- gon.sentry_environment = Gitlab.config.sentry.environment
+ if Gitlab.config.sentry.enabled
+ gon.sentry_dsn = Gitlab.config.sentry.clientside_dsn
+ gon.sentry_environment = Gitlab.config.sentry.environment
end
- if Feature.enabled?(:enable_new_sentry_clientside_integration) && Gitlab::CurrentSettings.sentry_enabled
+ # Support for Sentry setup via configuration files will be removed in 16.0
+ # in favor of Gitlab::CurrentSettings.
+ if Feature.enabled?(:enable_new_sentry_clientside_integration,
+ current_user) && Gitlab::CurrentSettings.sentry_enabled
gon.sentry_dsn = Gitlab::CurrentSettings.sentry_clientside_dsn
gon.sentry_environment = Gitlab::CurrentSettings.sentry_environment
end
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