Skip to content

Commit 27a0394

Browse files
authored
Fix parsing errors (#19)
* Fix incorrect parsing of PURLs beginning with pkg:/ * Fix escaping of subpaths * Relax Rubocop configuration * Fix linting errors
1 parent 1f2d6a0 commit 27a0394

File tree

3 files changed

+101
-6
lines changed

3 files changed

+101
-6
lines changed

.rubocop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ Metrics/MethodLength:
1919
Metrics/ParameterLists:
2020
Max: 7
2121
Metrics/PerceivedComplexity:
22-
Max: 20
22+
Max: 30
2323
Style/ConditionalAssignment:
2424
Enabled: false

lib/package_url.rb

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,14 @@ def self.parse(string)
9393
# - This is the subpath
9494
case string.rpartition('#')
9595
in String => remainder, separator, String => subpath unless separator.empty?
96-
components[:subpath] = subpath.split('/').select do |segment|
97-
!segment.empty? && segment != '.' && segment != '..'
98-
end.compact.join('/')
96+
subpath_components = []
97+
subpath.split('/').each do |segment|
98+
next if segment.empty? || segment == '.' || segment == '..'
99+
100+
subpath_components << URI.decode_www_form_component(segment)
101+
end
102+
103+
components[:subpath] = subpath_components.compact.join('/')
99104

100105
string = remainder
101106
else
@@ -152,10 +157,11 @@ def self.parse(string)
152157
end
153158

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

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

349361
unless segments.empty?

spec/package_url_spec.rb

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,89 @@
187187

188188
it { should have_description 'pkg:rpm/fedora/[email protected]?arch=i386&distro=fedora-25' }
189189
end
190+
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+
236+
context 'with URLs containing extra slashes after scheme' do
237+
it 'should parse pkg:/type/namespace/name correctly' do
238+
purl = PackageURL.parse('pkg:/maven/org.apache.commons/io')
239+
expect(purl).to have_attributes(
240+
type: 'maven',
241+
namespace: 'org.apache.commons',
242+
name: 'io',
243+
version: nil,
244+
qualifiers: nil,
245+
subpath: nil
246+
)
247+
end
248+
249+
it 'should parse pkg://type/namespace/name correctly' do
250+
purl = PackageURL.parse('pkg://maven/org.apache.commons/io')
251+
expect(purl).to have_attributes(
252+
type: 'maven',
253+
namespace: 'org.apache.commons',
254+
name: 'io',
255+
version: nil,
256+
qualifiers: nil,
257+
subpath: nil
258+
)
259+
end
260+
261+
it 'should parse pkg:///type/namespace/name correctly' do
262+
purl = PackageURL.parse('pkg:///maven/org.apache.commons/io')
263+
expect(purl).to have_attributes(
264+
type: 'maven',
265+
namespace: 'org.apache.commons',
266+
name: 'io',
267+
version: nil,
268+
qualifiers: nil,
269+
subpath: nil
270+
)
271+
end
272+
end
190273
end
191274

192275
describe 'pattern matching' do

0 commit comments

Comments
 (0)