Skip to content
Open
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
8 changes: 8 additions & 0 deletions guides/getting-started/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,14 @@ messy.to_s # => "https://example.com/a/c/d"

If the original path structure is significant, retain the parsed URL and do not call `normalize!`.

To normalize its encoding and resolve dot segments while preserving repeated separators, compose the path operations explicitly:

``` ruby
url = Protocol::URL["https://example.com/a//b/./c"]
url.path = url.path.normalize.simplify(preserve_empty: true)
url.to_s # => "https://example.com/a//b/c"
```

## Best Practices

### Choose the Right Class
Expand Down
38 changes: 25 additions & 13 deletions lib/protocol/url/path.rb
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,10 @@ def normalize

# Simplify this path in place by resolving literal or percent-encoded dot segments and repeated separators.
#
# @parameter preserve_empty [Boolean] Whether to preserve empty path segments represented by repeated separators.
# @returns [Path | Nil] This path when changed, otherwise `nil`.
def simplify!
simplified = simplify
def simplify!(preserve_empty: false)
simplified = simplify(preserve_empty: preserve_empty)
return nil if simplified.equal?(self)

@encoded = simplified.encoded
Expand All @@ -307,9 +308,10 @@ def simplify!
# Absolute paths do not retain parent components above the root. Relative paths
# retain leading parent components which cannot be resolved locally.
#
# @parameter preserve_empty [Boolean] Whether to preserve empty path segments represented by repeated separators.
# @returns [Path] The simplified path, or this path if already canonical.
def simplify
segments = simplify_segments
def simplify(preserve_empty: false)
segments = simplify_segments(preserve_empty: preserve_empty)
return self unless segments

return self.class.new(nil, segments)
Expand Down Expand Up @@ -432,7 +434,7 @@ def dot_segment(segment)
end

# Find the first encoded segment which requires simplification.
def simplification_index(segments)
def simplification_index(segments, preserve_empty: false)
absolute = segments.first == ""
regular_segment = false
last_index = segments.size - 1
Expand All @@ -444,7 +446,9 @@ def simplification_index(segments)
return index
elsif segment == ""
# Leading and trailing empty components are significant.
return index if index > 0 && index < last_index
if !preserve_empty && index > 0 && index < last_index
return index
end
elsif dot == ".."
# Absolute paths cannot retain parent components. Relative paths
# can retain them only before the first regular component.
Expand All @@ -458,19 +462,19 @@ def simplification_index(segments)
end

# Return simplified encoded segments, or nil if they are already canonical.
def simplify_segments
def simplify_segments(preserve_empty: false)
segments = self.segments
return nil unless start_index = simplification_index(segments)
return nil unless start_index = simplification_index(segments, preserve_empty: preserve_empty)

segments = segments.dup
simplify_segments!(segments, start_index)
simplify_segments!(segments, start_index, preserve_empty: preserve_empty)

return segments
end

# Simplify the given encoded segments in place.
def simplify_segments!(segments, start_index = nil)
start_index ||= simplification_index(segments)
def simplify_segments!(segments, start_index = nil, preserve_empty: false)
start_index ||= simplification_index(segments, preserve_empty: preserve_empty)
return nil unless start_index

offset = start_index
Expand All @@ -488,10 +492,18 @@ def simplify_segments!(segments, start_index = nil)
offset += 1
end
elsif segment == "" && index != last_index
# Collapse repeated separators:
# Preserve or collapse repeated separators according to the requested policy:
if preserve_empty
segments[offset] = segment if offset < index
offset += 1
end
elsif dot == ".." && offset > 0 && dot_segment(segments[offset - 1]) != ".."
# Pop a component, but never pop the absolute-path root:
offset -= 1 if segments[offset - 1] != ""
if preserve_empty
offset -= 1 unless segments.first == "" && offset == 1
elsif segments[offset - 1] != ""
offset -= 1
end

# A trailing parent reference also denotes a directory.
if index == last_index
Expand Down
1 change: 1 addition & 0 deletions releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- Add conservative normalization of encoded URL paths.
- Add an option to preserve empty path segments while simplifying paths.

## v0.12.0

Expand Down
25 changes: 25 additions & 0 deletions test/protocol/url/path.rb
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,24 @@

expect(path.simplify.encoded).to be == "/b"
end

it "can preserve repeated separators" do
path = Protocol::URL::Path["/a//b///c"]

expect(path.simplify(preserve_empty: true)).to be_equal(path)
end

it "resolves dot segments while preserving repeated separators" do
path = Protocol::URL::Path["/a//b/./c/../d"]

expect(path.simplify(preserve_empty: true).encoded).to be == "/a//b/d"
end

it "resolves a parent against an empty segment when preserving repeated separators" do
path = Protocol::URL::Path["/a//../b"]

expect(path.simplify(preserve_empty: true).encoded).to be == "/a/b"
end
end

with "#normalize" do
Expand Down Expand Up @@ -426,6 +444,13 @@
path.simplify!
expect(path.components).to be == ["", "a", ""]
end

it "can preserve repeated separators" do
path = Protocol::URL::Path["/a//b/./c"]

path.simplify!(preserve_empty: true)
expect(path.encoded).to be == "/a//b/c"
end
end

with "#join" do
Expand Down
Loading