Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ Metrics/MethodLength:
Metrics/ParameterLists:
Max: 7
Metrics/PerceivedComplexity:
Max: 20
Max: 30
Style/ConditionalAssignment:
Enabled: false
22 changes: 17 additions & 5 deletions lib/package_url.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,14 @@ def self.parse(string)
# - This is the subpath
case string.rpartition('#')
in String => remainder, separator, String => subpath unless separator.empty?
components[:subpath] = subpath.split('/').select do |segment|
!segment.empty? && segment != '.' && segment != '..'
end.compact.join('/')
subpath_components = []
subpath.split('/').each do |segment|
next if segment.empty? || segment == '.' || segment == '..'

subpath_components << URI.decode_www_form_component(segment)
end

components[:subpath] = subpath_components.compact.join('/')

string = remainder
else
Expand Down Expand Up @@ -152,10 +157,11 @@ def self.parse(string)
end

# Strip the remainder from leading and trailing '/'
# Use gsub to remove ALL leading slashes instead of just one
string = string.gsub(%r{^/+}, '').delete_suffix('/')
# - Split this once from left on '/'
# - The left side lowercased is the type
# - The right side is the remainder
string = string.delete_suffix('/')
case string.partition('/')
in String => type, separator, remainder unless separator.empty?
components[:type] = type
Expand Down Expand Up @@ -343,7 +349,13 @@ def to_s
subpath.delete_prefix('/').delete_suffix('/').split('/').each do |segment|
next if segment.empty? || segment == '.' || segment == '..'

segments << URI.encode_www_form_component(segment)
# Custom encoding for URL fragment segments:
# 1. Explicitly encode % as %25 to prevent double-encoding issues
# 2. Percent-encode special characters according to URL fragment rules
# 3. This ensures proper round-trip encoding/decoding with the parse method
segments << segment.gsub(/%|[^A-Za-z0-9\-\._~]/) do |m|
m == '%' ? '%25' : format('%%%02X', m.ord)
end
end

unless segments.empty?
Expand Down
83 changes: 83 additions & 0 deletions spec/package_url_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,89 @@

it { should have_description 'pkg:rpm/fedora/[email protected]?arch=i386&distro=fedora-25' }
end

context 'with escaped subpath characters', url: 'pkg:type/name#path/with/%25/percent' do
it {
should have_attributes type: 'type',
namespace: nil,
name: 'name',
version: nil,
qualifiers: nil,
subpath: 'path/with/%/percent'
}

it 'should properly round-trip the URL' do
expect(subject.to_s).to eq('pkg:type/name#path/with/%25/percent')
end
end

context 'with multiple escaped subpath characters', url: 'pkg:type/name#path/%20space/%3Fquery/%25percent' do
it {
should have_attributes type: 'type',
namespace: nil,
name: 'name',
version: nil,
qualifiers: nil,
subpath: 'path/ space/?query/%percent'
}

it 'should properly round-trip the URL' do
expect(subject.to_s).to eq('pkg:type/name#path/%20space/%3Fquery/%25percent')
end
end

context 'with the specific issue case', url: 'pkg:t/n#%25' do
it {
should have_attributes type: 't',
namespace: nil,
name: 'n',
version: nil,
qualifiers: nil,
subpath: '%'
}

it 'should properly round-trip the URL' do
expect(subject.to_s).to eq('pkg:t/n#%25')
end
end

context 'with URLs containing extra slashes after scheme' do
it 'should parse pkg:/type/namespace/name correctly' do
purl = PackageURL.parse('pkg:/maven/org.apache.commons/io')
expect(purl).to have_attributes(
type: 'maven',
namespace: 'org.apache.commons',
name: 'io',
version: nil,
qualifiers: nil,
subpath: nil
)
end

it 'should parse pkg://type/namespace/name correctly' do
purl = PackageURL.parse('pkg://maven/org.apache.commons/io')
expect(purl).to have_attributes(
type: 'maven',
namespace: 'org.apache.commons',
name: 'io',
version: nil,
qualifiers: nil,
subpath: nil
)
end

it 'should parse pkg:///type/namespace/name correctly' do
purl = PackageURL.parse('pkg:///maven/org.apache.commons/io')
expect(purl).to have_attributes(
type: 'maven',
namespace: 'org.apache.commons',
name: 'io',
version: nil,
qualifiers: nil,
subpath: nil
)
end
end
end

describe 'pattern matching' do
Expand Down