Skip to content

Commit b2740de

Browse files
committed
Fix escaping of subpaths
1 parent 0b8ecf5 commit b2740de

File tree

2 files changed

+54
-3
lines changed

2 files changed

+54
-3
lines changed

lib/package_url.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def self.parse(string)
9595
in String => remainder, separator, String => subpath unless separator.empty?
9696
components[:subpath] = subpath.split('/').select do |segment|
9797
!segment.empty? && segment != '.' && segment != '..'
98-
end.compact.join('/')
98+
end.map { |segment| URI.decode_www_form_component(segment) }.compact.join('/')
9999

100100
string = remainder
101101
else
@@ -160,7 +160,7 @@ def self.parse(string)
160160
case string.partition('/')
161161
in String => type, separator, remainder unless separator.empty?
162162
components[:type] = type
163-
163+
164164
string = remainder
165165
else
166166
raise InvalidPackageURL, 'invalid or missing package type'
@@ -344,7 +344,13 @@ def to_s
344344
subpath.delete_prefix('/').delete_suffix('/').split('/').each do |segment|
345345
next if segment.empty? || segment == '.' || segment == '..'
346346

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

350356
unless segments.empty?

spec/package_url_spec.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,51 @@
188188
it { should have_description 'pkg:rpm/fedora/[email protected]?arch=i386&distro=fedora-25' }
189189
end
190190

191+
context 'with escaped subpath characters', url: 'pkg:type/name#path/with/%25/percent' do
192+
it {
193+
should have_attributes type: 'type',
194+
namespace: nil,
195+
name: 'name',
196+
version: nil,
197+
qualifiers: nil,
198+
subpath: 'path/with/%/percent'
199+
}
200+
201+
it 'should properly round-trip the URL' do
202+
expect(subject.to_s).to eq('pkg:type/name#path/with/%25/percent')
203+
end
204+
end
205+
206+
context 'with multiple escaped subpath characters', url: 'pkg:type/name#path/%20space/%3Fquery/%25percent' do
207+
it {
208+
should have_attributes type: 'type',
209+
namespace: nil,
210+
name: 'name',
211+
version: nil,
212+
qualifiers: nil,
213+
subpath: 'path/ space/?query/%percent'
214+
}
215+
216+
it 'should properly round-trip the URL' do
217+
expect(subject.to_s).to eq('pkg:type/name#path/%20space/%3Fquery/%25percent')
218+
end
219+
end
220+
221+
context 'with the specific issue case', url: 'pkg:t/n#%25' do
222+
it {
223+
should have_attributes type: 't',
224+
namespace: nil,
225+
name: 'n',
226+
version: nil,
227+
qualifiers: nil,
228+
subpath: '%'
229+
}
230+
231+
it 'should properly round-trip the URL' do
232+
expect(subject.to_s).to eq('pkg:t/n#%25')
233+
end
234+
end
235+
191236
context 'with URLs containing extra slashes after scheme' do
192237
it 'should parse pkg:/type/namespace/name correctly' do
193238
purl = PackageURL.parse('pkg:/maven/org.apache.commons/io')

0 commit comments

Comments
 (0)