protocol-url 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67f58bb48277d53b3817bb38b2b7c4546a8a8e1f3df62cea531c577705c4c833
4
- data.tar.gz: 55bcfd924be9ae1eada0b2e5f8ee93fcd4a1a24c0693986ed3f8818b26072fd1
3
+ metadata.gz: 9ff041406f4bc298081aaa3f7150a374bf937f87e0f2783458082c4635d51488
4
+ data.tar.gz: 90aa117ed86f9df29b3278420b8c3b450a8f6fc7b30cf6473e5fbf68bb939d40
5
5
  SHA512:
6
- metadata.gz: 7df97ce90f4ce934023217fbedf9cbf864febeee311b9b25e3ee952e647d75d8cc041f16665b84884fec1e1d2dad859f9f3d21a0bfdccf40f832938158a21924
7
- data.tar.gz: bb2c1f9fece1eda9cb5ee8b96d216b28e0e15c6fe9605c6ec0d43a615b5b9a247f9bc299553e1d46778d9e6c5971e34d56bbd77f172202002ad63d7be89b53c3
6
+ metadata.gz: 371901582d694902f2610377663c7aca91835c3e04fde1a06854a3b2a67456bff313d71a347686e3d7e80b1dc35cd0103379fe0abf28c6050967534f0d3ae888
7
+ data.tar.gz: 8446857f07d8aa19317a1ce85e067b623df958e2a56b4df9b9110d52de1cc4eb70115d1af6ed068c5cbb9e2d78ebefd02d406ab551e36258e8acfb99276c9694
checksums.yaml.gz.sig CHANGED
Binary file
@@ -20,7 +20,8 @@ module Protocol
20
20
 
21
21
  EMPTY_SEGMENTS = [].freeze
22
22
  ROOT_SEGMENTS = ["", ""].freeze
23
- private_constant :EMPTY_SEGMENTS, :ROOT_SEGMENTS
23
+ NORMALIZATION_PATTERN = /%[0-9A-Fa-f]{2}|%|[^a-zA-Z0-9_.~!$&'()*+,;=:@-]/
24
+ private_constant :EMPTY_SEGMENTS, :ROOT_SEGMENTS, :NORMALIZATION_PATTERN
24
25
 
25
26
  # Coerce an encoded string or encoded segment array into a path.
26
27
  #
@@ -207,14 +208,18 @@ module Protocol
207
208
  end
208
209
 
209
210
  # @parameter other [Object] The value to compare with this path.
210
- # @returns [Boolean] Whether both paths have the same encoded representation.
211
+ # @returns [Boolean] Whether both values are the same.
211
212
  def ==(other)
212
- eql?(other)
213
+ if other.is_a?(String)
214
+ return encoded == other
215
+ else
216
+ return eql?(other)
217
+ end
213
218
  end
214
219
 
215
220
  # Compare this path with another path using exact encoded string identity.
216
221
  # @parameter other [Object] The value to compare with this path.
217
- # @returns [Boolean] Whether both paths have equal encoded strings.
222
+ # @returns [Boolean] Whether both paths are the same.
218
223
  def eql?(other)
219
224
  other.is_a?(Path) && encoded.eql?(other.encoded)
220
225
  end
@@ -252,6 +257,42 @@ module Protocol
252
257
  alias to_s encoded
253
258
  alias to_str encoded
254
259
 
260
+ # Normalize the encoded spelling of this path.
261
+ #
262
+ # Percent-encoded unreserved characters are decoded, retained percent escapes
263
+ # use uppercase hexadecimal digits, and literal characters outside the path
264
+ # segment grammar are percent encoded. Reserved characters retain their
265
+ # encoded or literal form because those forms are not generally equivalent.
266
+ #
267
+ # This operation preserves the path structure. Use {simplify} separately when
268
+ # application semantics permit resolving dot segments or collapsing repeated separators.
269
+ #
270
+ # @returns [Path] The normalized path, or this path if already normalized.
271
+ # @raises [ArgumentError] If the path contains malformed percent encoding, NUL, or invalid string encoding.
272
+ def normalize
273
+ encoded = self.encoded
274
+ unless encoded.valid_encoding? && encoded.encoding.ascii_compatible?
275
+ raise ArgumentError, "Path segment has invalid encoding!"
276
+ end
277
+
278
+ segments = self.segments
279
+ normalized_segments = nil
280
+
281
+ segments.each_with_index do |segment, index|
282
+ next unless NORMALIZATION_PATTERN.match?(segment)
283
+
284
+ normalized = normalize_segment(segment)
285
+ next if normalized == segment
286
+
287
+ normalized_segments ||= segments.dup
288
+ normalized_segments[index] = normalized
289
+ end
290
+
291
+ return self unless normalized_segments
292
+
293
+ return self.class.new(nil, normalized_segments)
294
+ end
295
+
255
296
  # Simplify this path in place by resolving literal or percent-encoded dot segments and repeated separators.
256
297
  #
257
298
  # @returns [Path | Nil] This path when changed, otherwise `nil`.
@@ -342,6 +383,42 @@ module Protocol
342
383
 
343
384
  private
344
385
 
386
+ # Normalize one encoded path segment:
387
+ def normalize_segment(segment)
388
+ return segment.gsub(NORMALIZATION_PATTERN) do |character|
389
+ byte = character.getbyte(0)
390
+
391
+ if byte == 0
392
+ raise ArgumentError, "Path segment contains NUL!"
393
+ elsif byte == 0x25
394
+ if character.bytesize == 1
395
+ raise ArgumentError, "String contains malformed percent encoding!"
396
+ end
397
+
398
+ byte = character.byteslice(1, 2).to_i(16)
399
+ if byte == 0
400
+ raise ArgumentError, "Path segment contains NUL!"
401
+ elsif unreserved_byte?(byte)
402
+ byte.chr
403
+ else
404
+ character.upcase
405
+ end
406
+ else
407
+ Encoding.escape(character)
408
+ end
409
+ end
410
+ end
411
+
412
+ # Whether the byte represents an unreserved URI character:
413
+ def unreserved_byte?(byte)
414
+ case byte
415
+ when 0x30..0x39, 0x41..0x5A, 0x61..0x7A, 0x2D, 0x2E, 0x5F, 0x7E
416
+ return true
417
+ else
418
+ return false
419
+ end
420
+ end
421
+
345
422
  # Identify dot segments, including percent-encoded spellings. RFC 3986 treats
346
423
  # percent-encoded unreserved characters as equivalent to their literal forms;
347
424
  # the WHATWG URL Standard explicitly recognizes `%2e`, `.%2e`, `%2e.`, and
@@ -415,9 +492,9 @@ module Protocol
415
492
  offset += 1
416
493
  end
417
494
  elsif segment == "" && index != last_index
418
- # Collapse repeated separators.
495
+ # Collapse repeated separators:
419
496
  elsif dot == ".." && offset > 0 && dot_segment(segments[offset - 1]) != ".."
420
- # Pop a component, but never pop the absolute-path root.
497
+ # Pop a component, but never pop the absolute-path root:
421
498
  offset -= 1 if segments[offset - 1] != ""
422
499
 
423
500
  # A trailing parent reference also denotes a directory.
@@ -132,12 +132,17 @@ module Protocol
132
132
  self.class.new(path || @path, query, fragment)
133
133
  end
134
134
 
135
- # Normalize the path by resolving "." and ".." segments and removing duplicate slashes.
135
+ # Normalize the encoded path and simplify its structure.
136
136
  #
137
- # This modifies the URL in-place by simplifying the path component:
137
+ # This modifies the URL in-place by normalizing and simplifying the path component:
138
+ # - Decodes percent-encoded unreserved characters
139
+ # - Uses uppercase hexadecimal digits for retained percent escapes
138
140
  # - Removes "." segments (current directory)
139
141
  # - Resolves ".." segments (parent directory)
140
- # - Collapses multiple consecutive slashes to single slashes (except at start)
142
+ # - Collapses empty path segments represented by consecutive slashes
143
+ #
144
+ # Normalization is intentionally lossy. Callers that need to preserve the
145
+ # original path structure should retain the parsed URL and avoid this method.
141
146
  #
142
147
  # @returns [self] The normalized URL.
143
148
  #
@@ -146,7 +151,7 @@ module Protocol
146
151
  # url.normalize!
147
152
  # url.path.to_s # => "/foo/bar/qux"
148
153
  def normalize!
149
- @path = @path.simplify
154
+ @path = @path.normalize.simplify
150
155
 
151
156
  return self
152
157
  end
@@ -7,6 +7,6 @@
7
7
  module Protocol
8
8
  # @namespace
9
9
  module URL
10
- VERSION = "0.12.0"
10
+ VERSION = "0.14.0"
11
11
  end
12
12
  end
data/readme.md CHANGED
@@ -34,6 +34,104 @@ bundle exec sus
34
34
 
35
35
  Please see the [project releases](https://socketry.github.io/protocol-url/releases/index) for all releases.
36
36
 
37
+ ### v0.14.0
38
+
39
+ - Allow paths to compare with their encoded string representation.
40
+
41
+ ### v0.13.0
42
+
43
+ - Add conservative normalization of encoded URL paths.
44
+
45
+ ### v0.12.0
46
+
47
+ - Allow unfrozen relative and absolute URLs to replace their components.
48
+
49
+ ### v0.10.0
50
+
51
+ - Rename `Protocol::URL::FormData::Parser::CONTENT_TYPE` to `MEDIA_TYPE`.
52
+
53
+ ### v0.9.0
54
+
55
+ - Add `Protocol::URL::LimitError` for configured processing limits.
56
+
57
+ ### v0.8.0
58
+
59
+ - Use consistent limit naming for form data parser constraints.
60
+
61
+ ### v0.7.0
62
+
63
+ - Allow `Protocol::URL::FormData::Parser#parse` to populate a supplied result object.
64
+
65
+ ### v0.6.0
66
+
67
+ - Add `Protocol::URL::FormData::Parser` for incremental, limited parsing of `application/x-www-form-urlencoded` form data.
68
+ - Add `Protocol::URL::FormData::Nested` for consistently building nested form data while preserving absent and empty values.
69
+
70
+ ### v0.5.0
71
+
72
+ - Add `Protocol::URL::Encoding.decode_www_form` for decoding HTML form data where `+` represents a space.
73
+
74
+ ### v0.4.0
75
+
76
+ - Add comparison methods to `Protocol::URL::Relative` (and by inheritance to `Protocol::URL::Absolute`):
77
+ - `#==` for structural equality comparison (compares path, query, fragment components).
78
+ - `#===` for string equality comparison (enables case statement matching).
79
+ - `#<=>` for ordering and sorting.
80
+ - `#hash` for hash key support.
81
+ - `#equal?` for component-based equality checking.
82
+ - Add JSON serialization support to `Protocol::URL::Relative`:
83
+ - `#as_json` returns the string representation.
84
+ - `#to_json` returns a JSON-encoded string.
85
+
86
+ ### v0.13.0
87
+
88
+ - Add conservative normalization of encoded URL paths.
89
+
90
+ ### v0.12.0
91
+
92
+ - Allow unfrozen relative and absolute URLs to replace their components.
93
+
94
+ ### v0.10.0
95
+
96
+ - Rename `Protocol::URL::FormData::Parser::CONTENT_TYPE` to `MEDIA_TYPE`.
97
+
98
+ ### v0.9.0
99
+
100
+ - Add `Protocol::URL::LimitError` for configured processing limits.
101
+
102
+ ### v0.8.0
103
+
104
+ - Use consistent limit naming for form data parser constraints.
105
+
106
+ ### v0.7.0
107
+
108
+ - Allow `Protocol::URL::FormData::Parser#parse` to populate a supplied result object.
109
+
110
+ ### v0.6.0
111
+
112
+ - Add `Protocol::URL::FormData::Parser` for incremental, limited parsing of `application/x-www-form-urlencoded` form data.
113
+ - Add `Protocol::URL::FormData::Nested` for consistently building nested form data while preserving absent and empty values.
114
+
115
+ ### v0.5.0
116
+
117
+ - Add `Protocol::URL::Encoding.decode_www_form` for decoding HTML form data where `+` represents a space.
118
+
119
+ ### v0.4.0
120
+
121
+ - Add comparison methods to `Protocol::URL::Relative` (and by inheritance to `Protocol::URL::Absolute`):
122
+ - `#==` for structural equality comparison (compares path, query, fragment components).
123
+ - `#===` for string equality comparison (enables case statement matching).
124
+ - `#<=>` for ordering and sorting.
125
+ - `#hash` for hash key support.
126
+ - `#equal?` for component-based equality checking.
127
+ - Add JSON serialization support to `Protocol::URL::Relative`:
128
+ - `#as_json` returns the string representation.
129
+ - `#to_json` returns a JSON-encoded string.
130
+
131
+ ### v0.3.0
132
+
133
+ - Add `relative(target, from)` for computing relative paths between URLs.
134
+
37
135
  ### v0.12.0
38
136
 
39
137
  - Allow unfrozen relative and absolute URLs to replace their components.
data/releases.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Releases
2
2
 
3
+ ## v0.14.0
4
+
5
+ - Allow paths to compare with their encoded string representation.
6
+
7
+ ## v0.13.0
8
+
9
+ - Add conservative normalization of encoded URL paths.
10
+
3
11
  ## v0.12.0
4
12
 
5
13
  - Allow unfrozen relative and absolute URLs to replace their components.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-url
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file