Class: Protocol::URL::Path
- Inherits:
-
Object
- Object
- Protocol::URL::Path
- Includes:
- Comparable
- Defined in:
- lib/protocol/url/path.rb
Overview
Represents a URL path without losing its encoded segment boundaries.
String input is interpreted as an encoded URL path. A literal / is structural,
while %2F remains encoded data within a single segment. Decoding is explicit and
controlled by the encoding object passed to #components.
Constant Summary collapse
- SEPARATOR =
The path separator.
"/"
Class Method Summary collapse
-
.[](path) ⇒ Object
Coerce an encoded string or encoded segment array into a path.
-
.for(components, encoding: Encoding) ⇒ Object
Construct a path from decoded components.
-
.relative(target, from) ⇒ Object
Calculate the relative path from one absolute path to another.
Instance Method Summary collapse
-
#<=>(other) ⇒ Object
Paths compare by their exact encoded representation.
- #==(other) ⇒ Object
- #absolute? ⇒ Boolean
-
#basename(extension: true) ⇒ Object
The final decoded component.
-
#components(encoding = Encoding) ⇒ Object
Decode the path segments using the given encoding.
- #directory? ⇒ Boolean
- #empty? ⇒ Boolean
- #encoded ⇒ Object (also: #to_s, #to_str)
-
#eql?(other) ⇒ Boolean
Compare this path with another path using exact encoded string identity.
-
#freeze ⇒ Object
Freeze the path and materialize both lossless representations.
- #hash ⇒ Object
-
#initialize(encoded, segments = nil) ⇒ Path
constructor
Initialize a path from either its complete encoded representation or encoded segments.
-
#join(other, pop: true, simplify: true) ⇒ Object
Resolve another path relative to this path.
-
#local_path(root) ⇒ Object
Resolve a URL path beneath a local filesystem root.
-
#normalize ⇒ Object
Normalize the encoded spelling of this path.
-
#parent(level = 1) ⇒ Object
Return a path with its final component removed.
-
#relative(from) ⇒ Object
Calculate this path relative to another path.
- #relative? ⇒ Boolean
- #segments ⇒ Object
-
#simplify ⇒ Object
Return a canonical path by resolving literal or percent-encoded dot segments and repeated separators.
-
#simplify! ⇒ Object
Simplify this path in place by resolving literal or percent-encoded dot segments and repeated separators.
Constructor Details
#initialize(encoded, segments = nil) ⇒ Path
Initialize a path from either its complete encoded representation or encoded segments.
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/protocol/url/path.rb', line 88 def initialize(encoded, segments = nil) if encoded @encoded = -encoded end if encoded.nil? && segments.nil? segments = EMPTY_SEGMENTS elsif segments segments.each do |segment| unless segment.is_a?(String) && !segment.include?(SEPARATOR) raise ArgumentError, "Path contains an invalid encoded segment!" end end segments = segments.map(&:-@).freeze end @segments = segments end |
Class Method Details
.[](path) ⇒ Object
Coerce an encoded string or encoded segment array into a path.
30 31 32 33 34 35 36 37 38 |
# File 'lib/protocol/url/path.rb', line 30 def self.[](path) if path.is_a?(self) return path elsif path.is_a?(Array) return self.new(nil, path) else return self.new(path.to_s) end end |
.for(components, encoding: Encoding) ⇒ Object
Construct a path from decoded components.
Each component is escaped independently, so decoded / characters remain data
inside one encoded segment rather than becoming structural separators.
49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/protocol/url/path.rb', line 49 def self.for(components, encoding: Encoding) segments = components.map do |component| segment = encoding.escape(component) unless segment.is_a?(String) && !segment.include?(SEPARATOR) raise ArgumentError, "Path encoding produced an invalid segment!" end segment end return self.new(nil, segments) end |
.relative(target, from) ⇒ Object
Calculate the relative path from one absolute path to another.
This is useful for generating relative URLs from one location to another, such as creating page-specific import maps or relative links.
79 80 81 |
# File 'lib/protocol/url/path.rb', line 79 def self.relative(target, from) return Path[target].relative(from).to_s end |
Instance Method Details
#<=>(other) ⇒ Object
Paths compare by their exact encoded representation.
204 205 206 207 208 |
# File 'lib/protocol/url/path.rb', line 204 def <=>(other) return nil unless other.is_a?(Path) encoded <=> other.encoded end |
#==(other) ⇒ Object
212 213 214 |
# File 'lib/protocol/url/path.rb', line 212 def ==(other) eql?(other) end |
#absolute? ⇒ Boolean
120 121 122 |
# File 'lib/protocol/url/path.rb', line 120 def absolute? encoded.start_with?(SEPARATOR) end |
#basename(extension: true) ⇒ Object
The final decoded component. A path with a trailing separator has an empty basename.
138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/protocol/url/path.rb', line 138 def basename(extension: true) component = self.components.last return component if extension || component.nil? if index = component.rindex(".") basename = component[0...index] return basename if basename.b.match?(/[^.]/n) end return component end |
#components(encoding = Encoding) ⇒ Object
Decode the path segments using the given encoding.
The result is not cached because different encoding objects can produce different
component values. In particular, a decoded component may contain / without
changing its boundary in the returned array.
189 190 191 |
# File 'lib/protocol/url/path.rb', line 189 def components(encoding = Encoding) segments.map{|segment| encoding.unescape(segment)} end |
#directory? ⇒ Boolean
130 131 132 |
# File 'lib/protocol/url/path.rb', line 130 def directory? encoded.end_with?(SEPARATOR) end |
#empty? ⇒ Boolean
199 200 201 |
# File 'lib/protocol/url/path.rb', line 199 def empty? encoded.empty? end |
#encoded ⇒ Object Also known as: to_s, to_str
194 195 196 |
# File 'lib/protocol/url/path.rb', line 194 def encoded @encoded ||= @segments.join(SEPARATOR).freeze end |
#eql?(other) ⇒ Boolean
Compare this path with another path using exact encoded string identity.
219 220 221 |
# File 'lib/protocol/url/path.rb', line 219 def eql?(other) other.is_a?(Path) && encoded.eql?(other.encoded) end |
#freeze ⇒ Object
Freeze the path and materialize both lossless representations.
110 111 112 113 114 115 116 117 |
# File 'lib/protocol/url/path.rb', line 110 def freeze return self if frozen? self.segments self.encoded return super end |
#hash ⇒ Object
224 225 226 |
# File 'lib/protocol/url/path.rb', line 224 def hash encoded.hash end |
#join(other, pop: true, simplify: true) ⇒ Object
Resolve another path relative to this path.
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 |
# File 'lib/protocol/url/path.rb', line 324 def join(other, pop: true, simplify: true) other = Path[other] return self if other.empty? if other.absolute? return simplify ? other.simplify : other end segments = self.segments.dup # RFC2396 Section 5.2: # 6) a) All but the last segment of the base URI's path component is # copied to the buffer. In other words, any characters after the # last (right-most) slash character, if any, are excluded. if pop and dot_segment(segments.last) != ".." segments.pop elsif segments.last == "" segments.pop end segments.concat(other.segments) if simplify simplify_segments!(segments) end return Path.new(nil, segments) end |
#local_path(root) ⇒ Object
Resolve a URL path beneath a local filesystem root.
Each decoded URL component must map to exactly one local path component. Components
containing NUL or a platform path separator cannot be represented and are rejected.
Absolute URL paths are interpreted relative to root, not the filesystem root.
This establishes lexical containment only. It does not resolve symbolic links or prevent filesystem races while a returned path is subsequently opened.
240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'lib/protocol/url/path.rb', line 240 def local_path(root) root = File.(root) root_prefix = root.end_with?(File::SEPARATOR) ? root : root + File::SEPARATOR components = self.components(Encoding::System) components.shift if components.first == "" path = File.(File.join(root, *components)) return path if path == root || path.start_with?(root_prefix) raise ArgumentError, "Path escapes the specified root!" end |
#normalize ⇒ Object
Normalize the encoded spelling of this path.
Percent-encoded unreserved characters are decoded, retained percent escapes use uppercase hexadecimal digits, and literal characters outside the path segment grammar are percent encoded. Reserved characters retain their encoded or literal form because those forms are not generally equivalent.
This operation preserves the path structure. Use #simplify separately when application semantics permit resolving dot segments or collapsing repeated separators.
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/protocol/url/path.rb', line 268 def normalize encoded = self.encoded unless encoded.valid_encoding? && encoded.encoding.ascii_compatible? raise ArgumentError, "Path segment has invalid encoding!" end segments = self.segments normalized_segments = nil segments.each_with_index do |segment, index| next unless NORMALIZATION_PATTERN.match?(segment) normalized = normalize_segment(segment) next if normalized == segment normalized_segments ||= segments.dup normalized_segments[index] = normalized end return self unless normalized_segments return self.class.new(nil, normalized_segments) end |
#parent(level = 1) ⇒ Object
Return a path with its final component removed.
The empty path and absolute root are their own parents. For a directory path, this removes the trailing empty component which represents its separator.
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/protocol/url/path.rb', line 158 def parent(level = 1) unless level.is_a?(Integer) && level >= 0 raise ArgumentError, "Path parent level must be a non-negative integer!" end segments = self.segments return self if level == 0 || segments.empty? || segments == ROOT_SEGMENTS remaining = segments.size - level if absolute? segments = remaining <= 1 ? ROOT_SEGMENTS : segments.first(remaining) else segments = remaining <= 0 ? EMPTY_SEGMENTS : segments.first(remaining) end return self.class.new(nil, segments) end |
#relative(from) ⇒ Object
Calculate this path relative to another path.
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 |
# File 'lib/protocol/url/path.rb', line 357 def relative(from) target_segments = self.segments from_segments = Path[from].segments # Remove the last component from 'from' to get the directory from_segments = from_segments[0...-1] if from_segments.size > 0 # Find the common prefix common_length = 0 [target_segments.size, from_segments.size].min.times do |i| break if target_segments[i] != from_segments[i] common_length = i + 1 end # Calculate how many levels to go up up_levels = from_segments.size - common_length # Build the relative path segments relative_segments = [".."] * up_levels + target_segments[common_length..-1] return Path.new(nil, relative_segments) end |
#relative? ⇒ Boolean
125 126 127 |
# File 'lib/protocol/url/path.rb', line 125 def relative? !absolute? end |
#segments ⇒ Object
177 178 179 |
# File 'lib/protocol/url/path.rb', line 177 def segments @segments ||= @encoded.split(SEPARATOR, -1).map!(&:-@).freeze end |
#simplify ⇒ Object
Return a canonical path by resolving literal or percent-encoded dot segments and repeated separators.
Absolute paths do not retain parent components above the root. Relative paths retain leading parent components which cannot be resolved locally.
311 312 313 314 315 316 |
# File 'lib/protocol/url/path.rb', line 311 def simplify segments = simplify_segments return self unless segments return self.class.new(nil, segments) end |
#simplify! ⇒ Object
Simplify this path in place by resolving literal or percent-encoded dot segments and repeated separators.
295 296 297 298 299 300 301 302 303 |
# File 'lib/protocol/url/path.rb', line 295 def simplify! simplified = simplify return nil if simplified.equal?(self) @encoded = simplified.encoded @segments = simplified.segments return self end |