@@ -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?
0 commit comments