protocol-url 0.12.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: 67f58bb48277d53b3817bb38b2b7c4546a8a8e1f3df62cea531c577705c4c833
4
- data.tar.gz: 55bcfd924be9ae1eada0b2e5f8ee93fcd4a1a24c0693986ed3f8818b26072fd1
3
+ metadata.gz: 4a8be4027ad2b93f7707396fe9a53a47846b53e78cde0269be4fdd3dbdb448e3
4
+ data.tar.gz: 89ae574fc48ca43bcd0b224bdbe91ab0da1354869f6c8dbc87bdfbe9e6cd45b0
5
5
  SHA512:
6
- metadata.gz: 7df97ce90f4ce934023217fbedf9cbf864febeee311b9b25e3ee952e647d75d8cc041f16665b84884fec1e1d2dad859f9f3d21a0bfdccf40f832938158a21924
7
- data.tar.gz: bb2c1f9fece1eda9cb5ee8b96d216b28e0e15c6fe9605c6ec0d43a615b5b9a247f9bc299553e1d46778d9e6c5971e34d56bbd77f172202002ad63d7be89b53c3
6
+ metadata.gz: fb21e1c877b41f75975183cacab2a9dbea71e09c5fa4e11f66767f713e50fe0c27b63f07b1daa83feb00309a41132703544620541c794c598ce1efc9c402a830
7
+ data.tar.gz: 7c0703c48866182719b0a12edee9b45447f2ce3233519058af33c171ddf729fa9f8615cb76ae0aaf8355383703e21937fa912e94eff05428c6007d62c4e763cd
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
  #
@@ -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.
@@ -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.13.0"
11
11
  end
12
12
  end
data/readme.md CHANGED
@@ -34,6 +34,55 @@ 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
+
37
86
  ### v0.12.0
38
87
 
39
88
  - Allow unfrozen relative and absolute URLs to replace their components.
data/releases.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v0.13.0
4
+
5
+ - Add conservative normalization of encoded URL paths.
6
+
3
7
  ## v0.12.0
4
8
 
5
9
  - Allow unfrozen relative and absolute URLs to replace their components.
data.tar.gz.sig CHANGED
@@ -1,3 +1 @@
1
- $7�s����>G��\+�..��
2
- X�{2�� �D�8���pj�(���h�_����j����E��= �����H�y�q�3�E1�?�c�.{-�R�ͻ�/�8�9�ͣ��j*�~o���3��,�t����à���)F���C���GD��B����<r��ோ�\ xGyM�)`?>1��� ��~W��1B���Dvօ��Q��)��Ÿ��
3
- �'�M���$Hl�*d������ר��ly���h�.�� 4�\f�Ŭ�@X�<�9Y��H55Cv�����FU'���Hb~M�hd�u��QJ�G���������,D�Q�~Lٜ�TX�;HI�_�]��
1
+ �ɑ��2v�~�lK
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.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file