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
16 changes: 14 additions & 2 deletions lib/uri/generic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1021,9 +1021,21 @@ def merge_path(base, rel)

# RFC2396, Section 5.2, 6), a)
base_path << '' if base_path.last == '..'
while i = base_path.index('..')
base_path.slice!(i - 1, 2)
# Remove "<segment>/.." pairs in a single left-to-right pass (O(n)).
# This deliberately differs from the relative-path stack handling
# below: a leading ".." (one with no preceding segment left to
# cancel) discards the whole base path, reproducing the previous
# index/slice! implementation exactly.
reduced = []
base_path.each do |seg|
if seg == '..'
break if reduced.empty?
reduced.pop
else
reduced << seg
end
end
base_path = reduced

if (first = rel_path.first) and first.empty?
base_path.clear
Expand Down
37 changes: 37 additions & 0 deletions test/uri/test_generic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,43 @@ def test_merge
assert_equal(u0, u1)
end

def test_merge_path_dot_dot_removal
# Base-path ".." removal (RFC2396 5.2 6a) is handled by a single
# left-to-right pass. These lock in the exact, historically observed
# semantics, which differ from the relative-path stack: a leading ".."
# (or a ".." exposed as leading after earlier cancellations) discards
# the remaining base path rather than being kept.
{
'http://h/a/../../b' => { 'x' => 'http://h/x' },
'http://h/../a' => { 'x' => 'http://h/x' },
'http://h/a/..' => { 'x' => 'http://h/x' },
'http://h/../x' => { 'y' => 'http://h/y' },
'http://h/foo/bar/..' => { './' => 'http://h/foo/' },
'http://h/foo/bar/../..' => { './' => 'http://h/' },
'http://h/a/b/c' => {
'../../g' => 'http://h/g',
'../../../g' => 'http://h/g',
'../../../../g' => 'http://h/g',
},
'http://h/p//q/..' => { 'r' => 'http://h/p//r' },
'http://h/a/../..//y' => { 'z' => 'http://h/z' },
}.each { |base, map|
map.each { |rel, expected|
assert_equal(expected, URI.parse(base).merge(rel).to_s,
"<#{base}> + #{rel.inspect}")
}
}
end

def test_merge_path_dot_dot_removal_is_linear
# Regression guard for the previous O(n^2) base-path ".." removal:
# merging a base full of "a/../" segments must scale linearly.
pre = ->(n) {URI.parse('http://example.com/' + 'a/../' * n)}
assert_linear_performance((1..5).map {|i| 10 ** i}, pre: pre) do |base|
assert_equal('http://example.com/x', base.merge('x').to_s)
end
end

def test_merge_authority
u = URI.parse('http://user:pass@example.com:8080')
u0 = URI.parse('http://new.example.org/path')
Expand Down