protocol-url 0.11.0 → 0.13.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: 1b1993c620685f91b17b6a707eace1ff03219c1d6e2eace3d272e8ec8c723680
4
- data.tar.gz: 8969a20bccae90e49a5d541ea9f384127aaf817e7df12b1ac8b16a27ba450b1a
3
+ metadata.gz: 4a8be4027ad2b93f7707396fe9a53a47846b53e78cde0269be4fdd3dbdb448e3
4
+ data.tar.gz: 89ae574fc48ca43bcd0b224bdbe91ab0da1354869f6c8dbc87bdfbe9e6cd45b0
5
5
  SHA512:
6
- metadata.gz: 46b12bedaa0fca57e658677545a09ea2839ce6496c149eaa30c10b5cd213d3abbc517f850ddffce5cc159a8c5a73cb99be10c1737e06ed75974ace4448beca70
7
- data.tar.gz: 513ab9374d04bf04512d11f8610dbae6a14deea9c27a70b1d7a813df569c6642ca1eb679a5f51ad4b99319980f0b5f6c178d895c12de0c2037726337261dbce2
6
+ metadata.gz: fb21e1c877b41f75975183cacab2a9dbea71e09c5fa4e11f66767f713e50fe0c27b63f07b1daa83feb00309a41132703544620541c794c598ce1efc9c402a830
7
+ data.tar.gz: 7c0703c48866182719b0a12edee9b45447f2ce3233519058af33c171ddf729fa9f8615cb76ae0aaf8355383703e21937fa912e94eff05428c6007d62c4e763cd
checksums.yaml.gz.sig CHANGED
Binary file
@@ -15,8 +15,8 @@ module Protocol
15
15
  # @parameter scheme [String] The URL scheme (e.g., "https", "http").
16
16
  # @parameter authority [String] The authority component (e.g., "example.com", "user@host:port").
17
17
  # @parameter path [String | Path] The encoded path component (defaults to "/").
18
- # @parameter query [String, nil] The query string.
19
- # @parameter fragment [String, nil] The fragment identifier.
18
+ # @parameter query [String | Nil] The query string.
19
+ # @parameter fragment [String | Nil] The fragment identifier.
20
20
  def initialize(scheme, authority, path = "/", query = nil, fragment = nil)
21
21
  @scheme = scheme
22
22
  @authority = authority
@@ -36,11 +36,11 @@ module Protocol
36
36
  return super
37
37
  end
38
38
 
39
- # @attribute [String] The URL scheme.
40
- attr :scheme
39
+ # @attribute [String | Nil] The URL scheme.
40
+ attr_accessor :scheme
41
41
 
42
- # @attribute [String] The authority component.
43
- attr :authority
42
+ # @attribute [String | Nil] The authority component.
43
+ attr_accessor :authority
44
44
 
45
45
  # Check if the URL has a non-empty scheme.
46
46
  #
@@ -122,11 +122,11 @@ module Protocol
122
122
 
123
123
  # Create a new Absolute URL with modified components.
124
124
  #
125
- # @parameter scheme [String, nil] The scheme to use (nil to remove scheme).
126
- # @parameter authority [String, nil] The authority to use (nil to remove authority).
127
- # @parameter path [String, nil] The path to merge with the current path.
128
- # @parameter query [String, nil] The query string to use.
129
- # @parameter fragment [String, nil] The fragment to use.
125
+ # @parameter scheme [String | Nil] The scheme to use (nil to remove scheme).
126
+ # @parameter authority [String | Nil] The authority to use (nil to remove authority).
127
+ # @parameter path [String | Nil] The path to merge with the current path.
128
+ # @parameter query [String | Nil] The query string to use.
129
+ # @parameter fragment [String | Nil] The fragment to use.
130
130
  # @parameter pop [Boolean] Whether to pop the last path component before merging.
131
131
  # @returns [Absolute] A new Absolute URL with the modified components.
132
132
  #
@@ -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
  #
@@ -252,6 +253,42 @@ module Protocol
252
253
  alias to_s encoded
253
254
  alias to_str encoded
254
255
 
256
+ # Normalize the encoded spelling of this path.
257
+ #
258
+ # Percent-encoded unreserved characters are decoded, retained percent escapes
259
+ # use uppercase hexadecimal digits, and literal characters outside the path
260
+ # segment grammar are percent encoded. Reserved characters retain their
261
+ # encoded or literal form because those forms are not generally equivalent.
262
+ #
263
+ # This operation preserves the path structure. Use {simplify} separately when
264
+ # application semantics permit resolving dot segments or collapsing repeated separators.
265
+ #
266
+ # @returns [Path] The normalized path, or this path if already normalized.
267
+ # @raises [ArgumentError] If the path contains malformed percent encoding, NUL, or invalid string encoding.
268
+ def normalize
269
+ encoded = self.encoded
270
+ unless encoded.valid_encoding? && encoded.encoding.ascii_compatible?
271
+ raise ArgumentError, "Path segment has invalid encoding!"
272
+ end
273
+
274
+ segments = self.segments
275
+ normalized_segments = nil
276
+
277
+ segments.each_with_index do |segment, index|
278
+ next unless NORMALIZATION_PATTERN.match?(segment)
279
+
280
+ normalized = normalize_segment(segment)
281
+ next if normalized == segment
282
+
283
+ normalized_segments ||= segments.dup
284
+ normalized_segments[index] = normalized
285
+ end
286
+
287
+ return self unless normalized_segments
288
+
289
+ return self.class.new(nil, normalized_segments)
290
+ end
291
+
255
292
  # Simplify this path in place by resolving literal or percent-encoded dot segments and repeated separators.
256
293
  #
257
294
  # @returns [Path | Nil] This path when changed, otherwise `nil`.
@@ -342,6 +379,42 @@ module Protocol
342
379
 
343
380
  private
344
381
 
382
+ # Normalize one encoded path segment:
383
+ def normalize_segment(segment)
384
+ return segment.gsub(NORMALIZATION_PATTERN) do |character|
385
+ byte = character.getbyte(0)
386
+
387
+ if byte == 0
388
+ raise ArgumentError, "Path segment contains NUL!"
389
+ elsif byte == 0x25
390
+ if character.bytesize == 1
391
+ raise ArgumentError, "String contains malformed percent encoding!"
392
+ end
393
+
394
+ byte = character.byteslice(1, 2).to_i(16)
395
+ if byte == 0
396
+ raise ArgumentError, "Path segment contains NUL!"
397
+ elsif unreserved_byte?(byte)
398
+ byte.chr
399
+ else
400
+ character.upcase
401
+ end
402
+ else
403
+ Encoding.escape(character)
404
+ end
405
+ end
406
+ end
407
+
408
+ # Whether the byte represents an unreserved URI character:
409
+ def unreserved_byte?(byte)
410
+ case byte
411
+ when 0x30..0x39, 0x41..0x5A, 0x61..0x7A, 0x2D, 0x2E, 0x5F, 0x7E
412
+ return true
413
+ else
414
+ return false
415
+ end
416
+ end
417
+
345
418
  # Identify dot segments, including percent-encoded spellings. RFC 3986 treats
346
419
  # percent-encoded unreserved characters as equivalent to their literal forms;
347
420
  # the WHATWG URL Standard explicitly recognizes `%2e`, `.%2e`, `%2e.`, and
@@ -415,9 +488,9 @@ module Protocol
415
488
  offset += 1
416
489
  end
417
490
  elsif segment == "" && index != last_index
418
- # Collapse repeated separators.
491
+ # Collapse repeated separators:
419
492
  elsif dot == ".." && offset > 0 && dot_segment(segments[offset - 1]) != ".."
420
- # Pop a component, but never pop the absolute-path root.
493
+ # Pop a component, but never pop the absolute-path root:
421
494
  offset -= 1 if segments[offset - 1] != ""
422
495
 
423
496
  # A trailing parent reference also denotes a directory.
@@ -102,8 +102,8 @@ module Protocol
102
102
  @parameters = parameters
103
103
  end
104
104
 
105
- # @attribute [Hash] User supplied parameters that will be appended to the query part.
106
- attr :parameters
105
+ # @attribute [Hash | Nil] User supplied parameters that will be appended to the query part.
106
+ attr_accessor :parameters
107
107
 
108
108
  # Freeze the reference.
109
109
  #
@@ -15,8 +15,8 @@ module Protocol
15
15
  # Initialize a new relative URL.
16
16
  #
17
17
  # @parameter path [String | Path] The encoded path component.
18
- # @parameter query [String, nil] The query string.
19
- # @parameter fragment [String, nil] The fragment identifier.
18
+ # @parameter query [String | Nil] The query string.
19
+ # @parameter fragment [String | Nil] The fragment identifier.
20
20
  def initialize(path, query = nil, fragment = nil)
21
21
  @path = Path[path]
22
22
  @query = query
@@ -38,11 +38,18 @@ module Protocol
38
38
  # @attribute [Path] The path component of the URL.
39
39
  attr :path
40
40
 
41
- # @attribute [String, nil] The query string component.
42
- attr :query
41
+ # Replace the path component of this URL.
42
+ # @parameter path [String | Path] The encoded path component.
43
+ # @returns [Path] The assigned path component.
44
+ def path=(path)
45
+ @path = Path[path]
46
+ end
43
47
 
44
- # @attribute [String, nil] The fragment identifier.
45
- attr :fragment
48
+ # @attribute [String | Nil] The query string component.
49
+ attr_accessor :query
50
+
51
+ # @attribute [String | Nil] The fragment identifier.
52
+ attr_accessor :fragment
46
53
 
47
54
  # Resolve the URL path beneath a local filesystem root.
48
55
  #
@@ -104,9 +111,9 @@ module Protocol
104
111
 
105
112
  # Create a new Relative URL with modified components.
106
113
  #
107
- # @parameter path [String, nil] The path to merge with the current path.
108
- # @parameter query [String, nil] The query string to use.
109
- # @parameter fragment [String, nil] The fragment to use.
114
+ # @parameter path [String | Nil] The path to merge with the current path.
115
+ # @parameter query [String | Nil] The query string to use.
116
+ # @parameter fragment [String | Nil] The fragment to use.
110
117
  # @parameter pop [Boolean] Whether to pop the last path component before merging.
111
118
  # @returns [Relative] A new Relative URL with the modified components.
112
119
  #
@@ -125,12 +132,17 @@ module Protocol
125
132
  self.class.new(path || @path, query, fragment)
126
133
  end
127
134
 
128
- # Normalize the path by resolving "." and ".." segments and removing duplicate slashes.
135
+ # Normalize the encoded path and simplify its structure.
129
136
  #
130
- # 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
131
140
  # - Removes "." segments (current directory)
132
141
  # - Resolves ".." segments (parent directory)
133
- # - 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.
134
146
  #
135
147
  # @returns [self] The normalized URL.
136
148
  #
@@ -139,7 +151,7 @@ module Protocol
139
151
  # url.normalize!
140
152
  # url.path.to_s # => "/foo/bar/qux"
141
153
  def normalize!
142
- @path = @path.simplify
154
+ @path = @path.normalize.simplify
143
155
 
144
156
  return self
145
157
  end
@@ -7,6 +7,6 @@
7
7
  module Protocol
8
8
  # @namespace
9
9
  module URL
10
- VERSION = "0.11.0"
10
+ VERSION = "0.13.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.13.0
38
+
39
+ - Add conservative normalization of encoded URL paths.
40
+
41
+ ### v0.12.0
42
+
43
+ - Allow unfrozen relative and absolute URLs to replace their components.
44
+
45
+ ### v0.10.0
46
+
47
+ - Rename `Protocol::URL::FormData::Parser::CONTENT_TYPE` to `MEDIA_TYPE`.
48
+
49
+ ### v0.9.0
50
+
51
+ - Add `Protocol::URL::LimitError` for configured processing limits.
52
+
53
+ ### v0.8.0
54
+
55
+ - Use consistent limit naming for form data parser constraints.
56
+
57
+ ### v0.7.0
58
+
59
+ - Allow `Protocol::URL::FormData::Parser#parse` to populate a supplied result object.
60
+
61
+ ### v0.6.0
62
+
63
+ - Add `Protocol::URL::FormData::Parser` for incremental, limited parsing of `application/x-www-form-urlencoded` form data.
64
+ - Add `Protocol::URL::FormData::Nested` for consistently building nested form data while preserving absent and empty values.
65
+
66
+ ### v0.5.0
67
+
68
+ - Add `Protocol::URL::Encoding.decode_www_form` for decoding HTML form data where `+` represents a space.
69
+
70
+ ### v0.4.0
71
+
72
+ - Add comparison methods to `Protocol::URL::Relative` (and by inheritance to `Protocol::URL::Absolute`):
73
+ - `#==` for structural equality comparison (compares path, query, fragment components).
74
+ - `#===` for string equality comparison (enables case statement matching).
75
+ - `#<=>` for ordering and sorting.
76
+ - `#hash` for hash key support.
77
+ - `#equal?` for component-based equality checking.
78
+ - Add JSON serialization support to `Protocol::URL::Relative`:
79
+ - `#as_json` returns the string representation.
80
+ - `#to_json` returns a JSON-encoded string.
81
+
82
+ ### v0.3.0
83
+
84
+ - Add `relative(target, from)` for computing relative paths between URLs.
85
+
86
+ ### v0.12.0
87
+
88
+ - Allow unfrozen relative and absolute URLs to replace their components.
89
+
90
+ ### v0.10.0
91
+
92
+ - Rename `Protocol::URL::FormData::Parser::CONTENT_TYPE` to `MEDIA_TYPE`.
93
+
94
+ ### v0.9.0
95
+
96
+ - Add `Protocol::URL::LimitError` for configured processing limits.
97
+
98
+ ### v0.8.0
99
+
100
+ - Use consistent limit naming for form data parser constraints.
101
+
102
+ ### v0.7.0
103
+
104
+ - Allow `Protocol::URL::FormData::Parser#parse` to populate a supplied result object.
105
+
106
+ ### v0.6.0
107
+
108
+ - Add `Protocol::URL::FormData::Parser` for incremental, limited parsing of `application/x-www-form-urlencoded` form data.
109
+ - Add `Protocol::URL::FormData::Nested` for consistently building nested form data while preserving absent and empty values.
110
+
111
+ ### v0.5.0
112
+
113
+ - Add `Protocol::URL::Encoding.decode_www_form` for decoding HTML form data where `+` represents a space.
114
+
115
+ ### v0.4.0
116
+
117
+ - Add comparison methods to `Protocol::URL::Relative` (and by inheritance to `Protocol::URL::Absolute`):
118
+ - `#==` for structural equality comparison (compares path, query, fragment components).
119
+ - `#===` for string equality comparison (enables case statement matching).
120
+ - `#<=>` for ordering and sorting.
121
+ - `#hash` for hash key support.
122
+ - `#equal?` for component-based equality checking.
123
+ - Add JSON serialization support to `Protocol::URL::Relative`:
124
+ - `#as_json` returns the string representation.
125
+ - `#to_json` returns a JSON-encoded string.
126
+
127
+ ### v0.3.0
128
+
129
+ - Add `relative(target, from)` for computing relative paths between URLs.
130
+
131
+ ### v0.2.0
132
+
133
+ - Move `Protocol::URL::PATTERN` to `protocol/url/pattern.rb` so it can be shared more easily.
134
+
37
135
  ### v0.10.0
38
136
 
39
137
  - Rename `Protocol::URL::FormData::Parser::CONTENT_TYPE` to `MEDIA_TYPE`.
data/releases.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Releases
2
2
 
3
+ ## v0.13.0
4
+
5
+ - Add conservative normalization of encoded URL paths.
6
+
7
+ ## v0.12.0
8
+
9
+ - Allow unfrozen relative and absolute URLs to replace their components.
10
+
3
11
  ## v0.10.0
4
12
 
5
13
  - Rename `Protocol::URL::FormData::Parser::CONTENT_TYPE` to `MEDIA_TYPE`.
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.11.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file