diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-16 21:11:26 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-16 21:11:26 +0300 |
commit | 8fa0c53e26c947ac647b8067fde3e9673b77b1a6 (patch) | |
tree | da32e7224125973e9e87d3856fb7e672ff41c8b1 /lib | |
parent | 0552020767452da44de2bf5424096f2cb2ea6bf5 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'lib')
-rw-r--r-- | lib/api/api.rb | 2 | ||||
-rw-r--r-- | lib/api/branches.rb | 2 | ||||
-rw-r--r-- | lib/api/commits.rb | 2 | ||||
-rw-r--r-- | lib/api/entities/basic_project_details.rb | 2 | ||||
-rw-r--r-- | lib/api/entities/issuable_entity.rb | 14 | ||||
-rw-r--r-- | lib/api/entities/issue_basic.rb | 16 | ||||
-rw-r--r-- | lib/api/entities/project.rb | 2 | ||||
-rw-r--r-- | lib/api/entities/release.rb | 10 | ||||
-rw-r--r-- | lib/api/files.rb | 2 | ||||
-rw-r--r-- | lib/api/issue_links.rb | 55 | ||||
-rw-r--r-- | lib/api/lint.rb | 2 | ||||
-rw-r--r-- | lib/api/releases.rb | 10 | ||||
-rw-r--r-- | lib/api/repositories.rb | 4 | ||||
-rw-r--r-- | lib/api/tags.rb | 2 | ||||
-rw-r--r-- | lib/gitlab/ci/parsers/sbom/cyclonedx.rb | 2 | ||||
-rw-r--r-- | lib/gitlab/gitaly_client.rb | 2 | ||||
-rw-r--r-- | lib/gitlab/gon_helper.rb | 13 | ||||
-rw-r--r-- | lib/sbom/package_url.rb | 11 | ||||
-rw-r--r-- | lib/sbom/package_url/argument_validator.rb | 90 | ||||
-rw-r--r-- | lib/sbom/package_url/decoder.rb | 35 | ||||
-rw-r--r-- | lib/sbom/package_url/encoder.rb | 8 | ||||
-rw-r--r-- | lib/sbom/package_url/normalizer.rb | 47 | ||||
-rw-r--r-- | lib/sbom/package_url/string_utils.rb | 6 |
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 |