addressable 2.4.0 → 2.9.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.
@@ -1,6 +1,7 @@
1
- # encoding:utf-8
1
+ # frozen_string_literal: true
2
+
2
3
  #--
3
- # Copyright (C) 2006-2015 Bob Aman
4
+ # Copyright (C) Bob Aman
4
5
  #
5
6
  # Licensed under the Apache License, Version 2.0 (the "License");
6
7
  # you may not use this file except in compliance with the License.
@@ -18,6 +19,7 @@
18
19
 
19
20
  require "addressable/version"
20
21
  require "addressable/idna"
22
+ require "public_suffix"
21
23
 
22
24
  ##
23
25
  # Addressable is a library for processing links and URIs.
@@ -35,20 +37,48 @@ module Addressable
35
37
  ##
36
38
  # Container for the character classes specified in
37
39
  # <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>.
40
+ #
41
+ # Note: Concatenated and interpolated `String`s are not affected by the
42
+ # `frozen_string_literal` directive and must be frozen explicitly.
43
+ #
44
+ # Interpolated `String`s *were* frozen this way before Ruby 3.0:
45
+ # https://bugs.ruby-lang.org/issues/17104
38
46
  module CharacterClasses
39
47
  ALPHA = "a-zA-Z"
40
48
  DIGIT = "0-9"
41
49
  GEN_DELIMS = "\\:\\/\\?\\#\\[\\]\\@"
42
50
  SUB_DELIMS = "\\!\\$\\&\\'\\(\\)\\*\\+\\,\\;\\="
43
- RESERVED = GEN_DELIMS + SUB_DELIMS
44
- UNRESERVED = ALPHA + DIGIT + "\\-\\.\\_\\~"
45
- PCHAR = UNRESERVED + SUB_DELIMS + "\\:\\@"
46
- SCHEME = ALPHA + DIGIT + "\\-\\+\\."
47
- HOST = ALPHA + DIGIT + "\\-\\.\\[\\:\\]"
48
- AUTHORITY = PCHAR
49
- PATH = PCHAR + "\\/"
50
- QUERY = PCHAR + "\\/\\?"
51
- FRAGMENT = PCHAR + "\\/\\?"
51
+ RESERVED = (GEN_DELIMS + SUB_DELIMS).freeze
52
+ UNRESERVED = (ALPHA + DIGIT + "\\-\\.\\_\\~").freeze
53
+ RESERVED_AND_UNRESERVED = RESERVED + UNRESERVED
54
+ PCHAR = (UNRESERVED + SUB_DELIMS + "\\:\\@").freeze
55
+ SCHEME = (ALPHA + DIGIT + "\\-\\+\\.").freeze
56
+ HOST = (UNRESERVED + SUB_DELIMS + "\\[\\:\\]").freeze
57
+ AUTHORITY = (PCHAR + "\\[\\]").freeze
58
+ PATH = (PCHAR + "\\/").freeze
59
+ QUERY = (PCHAR + "\\/\\?").freeze
60
+ FRAGMENT = (PCHAR + "\\/\\?").freeze
61
+ end
62
+
63
+ module NormalizeCharacterClasses
64
+ HOST = /[^#{CharacterClasses::HOST}]/
65
+ UNRESERVED = /[^#{CharacterClasses::UNRESERVED}]/
66
+ PCHAR = /[^#{CharacterClasses::PCHAR}]/
67
+ SCHEME = /[^#{CharacterClasses::SCHEME}]/
68
+ FRAGMENT = /[^#{CharacterClasses::FRAGMENT}]/
69
+ QUERY = %r{[^a-zA-Z0-9\-\.\_\~\!\$\'\(\)\*\+\,\=\:\@\/\?%]|%(?!2B|2b)}
70
+ end
71
+
72
+ module CharacterClassesRegexps
73
+ AUTHORITY = /[^#{CharacterClasses::AUTHORITY}]/
74
+ FRAGMENT = /[^#{CharacterClasses::FRAGMENT}]/
75
+ HOST = /[^#{CharacterClasses::HOST}]/
76
+ PATH = /[^#{CharacterClasses::PATH}]/
77
+ QUERY = /[^#{CharacterClasses::QUERY}]/
78
+ RESERVED = /[^#{CharacterClasses::RESERVED}]/
79
+ RESERVED_AND_UNRESERVED = /[^#{CharacterClasses::RESERVED_AND_UNRESERVED}]/
80
+ SCHEME = /[^#{CharacterClasses::SCHEME}]/
81
+ UNRESERVED = /[^#{CharacterClasses::UNRESERVED}]/
52
82
  end
53
83
 
54
84
  SLASH = '/'
@@ -70,7 +100,7 @@ module Addressable
70
100
  "wais" => 210,
71
101
  "ldap" => 389,
72
102
  "prospero" => 1525
73
- }
103
+ }.freeze
74
104
 
75
105
  ##
76
106
  # Returns a URI object based on the parsed string.
@@ -100,7 +130,7 @@ module Addressable
100
130
  uri = uri.to_str
101
131
  rescue TypeError, NoMethodError
102
132
  raise TypeError, "Can't convert #{uri.class} into String."
103
- end if not uri.is_a? String
133
+ end unless uri.is_a?(String)
104
134
 
105
135
  # This Regexp supplied as an example in RFC 3986, and it works great.
106
136
  scan = uri.scan(URIREGEX)
@@ -121,15 +151,15 @@ module Addressable
121
151
  user = userinfo.strip[/^([^:]*):?/, 1]
122
152
  password = userinfo.strip[/:(.*)$/, 1]
123
153
  end
124
- host = authority.gsub(
154
+
155
+ host = authority.sub(
125
156
  /^([^\[\]]*)@/, EMPTY_STR
126
- ).gsub(
157
+ ).sub(
127
158
  /:([^:@\[\]]*?)$/, EMPTY_STR
128
159
  )
160
+
129
161
  port = authority[/:([^:@\[\]]*?)$/, 1]
130
- end
131
- if port == EMPTY_STR
132
- port = nil
162
+ port = nil if port == EMPTY_STR
133
163
  end
134
164
 
135
165
  return new(
@@ -172,37 +202,54 @@ module Addressable
172
202
  uri = uri.to_s
173
203
  end
174
204
 
175
- if !uri.respond_to?(:to_str)
205
+ unless uri.respond_to?(:to_str)
176
206
  raise TypeError, "Can't convert #{uri.class} into String."
177
207
  end
178
208
  # Otherwise, convert to a String
179
- uri = uri.to_str.dup
209
+ uri = uri.to_str.dup.strip
180
210
  hints = {
181
211
  :scheme => "http"
182
212
  }.merge(hints)
183
213
  case uri
184
- when /^http:\/+/
185
- uri.gsub!(/^http:\/+/, "http://")
186
- when /^https:\/+/
187
- uri.gsub!(/^https:\/+/, "https://")
188
- when /^feed:\/+http:\/+/
189
- uri.gsub!(/^feed:\/+http:\/+/, "feed:http://")
190
- when /^feed:\/+/
191
- uri.gsub!(/^feed:\/+/, "feed://")
192
- when /^file:\/+/
193
- uri.gsub!(/^file:\/+/, "file:///")
214
+ when /^http:\//i
215
+ uri.sub!(/^http:\/+/i, "http://")
216
+ when /^https:\//i
217
+ uri.sub!(/^https:\/+/i, "https://")
218
+ when /^feed:\/+http:\//i
219
+ uri.sub!(/^feed:\/+http:\/+/i, "feed:http://")
220
+ when /^feed:\//i
221
+ uri.sub!(/^feed:\/+/i, "feed://")
222
+ when %r[^file:/{4}]i
223
+ uri.sub!(%r[^file:/+]i, "file:////")
224
+ when %r[^file://localhost/]i
225
+ uri.sub!(%r[^file://localhost/+]i, "file:///")
226
+ when %r[^file:/+]i
227
+ uri.sub!(%r[^file:/+]i, "file:///")
194
228
  when /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/
195
- uri.gsub!(/^/, hints[:scheme] + "://")
229
+ uri.sub!(/^/, hints[:scheme] + "://")
230
+ when /\A\d+\..*:\d+\z/
231
+ uri = "#{hints[:scheme]}://#{uri}"
232
+ end
233
+ match = uri.match(URIREGEX)
234
+ fragments = match.captures
235
+ authority = fragments[3]
236
+ if authority && authority.length > 0
237
+ new_authority = authority.tr("\\", "/").gsub(" ", "%20")
238
+ # NOTE: We want offset 4, not 3!
239
+ offset = match.offset(4)
240
+ uri = uri.dup
241
+ uri[offset[0]...offset[1]] = new_authority
196
242
  end
197
243
  parsed = self.parse(uri)
198
244
  if parsed.scheme =~ /^[^\/?#\.]+\.[^\/?#]+$/
199
245
  parsed = self.parse(hints[:scheme] + "://" + uri)
200
246
  end
201
247
  if parsed.path.include?(".")
202
- new_host = parsed.path[/^([^\/]+\.[^\/]*)/, 1]
203
- if new_host
248
+ if parsed.path[/\b@\b/]
249
+ parsed.scheme = "mailto" unless parsed.scheme
250
+ elsif new_host = parsed.path[/^([^\/]+\.[^\/]*)/, 1]
204
251
  parsed.defer_validation do
205
- new_path = parsed.path.gsub(
252
+ new_path = parsed.path.sub(
206
253
  Regexp.new("^" + Regexp.escape(new_host)), EMPTY_STR)
207
254
  parsed.host = new_host
208
255
  parsed.path = new_path
@@ -247,30 +294,30 @@ module Addressable
247
294
  return nil unless path
248
295
  # If a URI object is passed, just return itself.
249
296
  return path if path.kind_of?(self)
250
- if !path.respond_to?(:to_str)
297
+ unless path.respond_to?(:to_str)
251
298
  raise TypeError, "Can't convert #{path.class} into String."
252
299
  end
253
300
  # Otherwise, convert to a String
254
301
  path = path.to_str.strip
255
302
 
256
- path.gsub!(/^file:\/?\/?/, EMPTY_STR) if path =~ /^file:\/?\/?/
303
+ path.sub!(/^file:\/?\/?/, EMPTY_STR) if path =~ /^file:\/?\/?/
257
304
  path = SLASH + path if path =~ /^([a-zA-Z])[\|:]/
258
305
  uri = self.parse(path)
259
306
 
260
307
  if uri.scheme == nil
261
308
  # Adjust windows-style uris
262
- uri.path.gsub!(/^\/?([a-zA-Z])[\|:][\\\/]/) do
309
+ uri.path.sub!(/^\/?([a-zA-Z])[\|:][\\\/]/) do
263
310
  "/#{$1.downcase}:/"
264
311
  end
265
- uri.path.gsub!(/\\/, SLASH)
312
+ uri.path.tr!("\\", SLASH)
266
313
  if File.exist?(uri.path) &&
267
314
  File.stat(uri.path).directory?
268
- uri.path.gsub!(/\/$/, EMPTY_STR)
315
+ uri.path.chomp!(SLASH)
269
316
  uri.path = uri.path + '/'
270
317
  end
271
318
 
272
319
  # If the path is absolute, set the scheme and host.
273
- if uri.path =~ /^\//
320
+ if uri.path.start_with?(SLASH)
274
321
  uri.scheme = "file"
275
322
  uri.host = EMPTY_STR
276
323
  end
@@ -295,18 +342,29 @@ module Addressable
295
342
  # #=> #<Addressable::URI:0xcab390 URI:http://example.com/relative/path>
296
343
  def self.join(*uris)
297
344
  uri_objects = uris.collect do |uri|
298
- if !uri.respond_to?(:to_str)
345
+ unless uri.respond_to?(:to_str)
299
346
  raise TypeError, "Can't convert #{uri.class} into String."
300
347
  end
301
348
  uri.kind_of?(self) ? uri : self.parse(uri.to_str)
302
349
  end
303
350
  result = uri_objects.shift.dup
304
- for uri in uri_objects
351
+ uri_objects.each do |uri|
305
352
  result.join!(uri)
306
353
  end
307
354
  return result
308
355
  end
309
356
 
357
+ ##
358
+ # Tables used to optimize encoding operations in `self.encode_component`
359
+ # and `self.normalize_component`
360
+ SEQUENCE_ENCODING_TABLE = (0..255).map do |byte|
361
+ format("%02x", byte).freeze
362
+ end.freeze
363
+
364
+ SEQUENCE_UPCASED_PERCENT_ENCODING_TABLE = (0..255).map do |byte|
365
+ format("%%%02X", byte).freeze
366
+ end.freeze
367
+
310
368
  ##
311
369
  # Percent encodes a URI component.
312
370
  #
@@ -342,9 +400,7 @@ module Addressable
342
400
  # "simple/example", Addressable::URI::CharacterClasses::UNRESERVED
343
401
  # )
344
402
  # => "simple%2Fexample"
345
- def self.encode_component(component, character_class=
346
- CharacterClasses::RESERVED + CharacterClasses::UNRESERVED,
347
- upcase_encoded='')
403
+ def self.encode_component(component, character_class=CharacterClassesRegexps::RESERVED_AND_UNRESERVED, upcase_encoded='')
348
404
  return nil if component.nil?
349
405
 
350
406
  begin
@@ -367,26 +423,27 @@ module Addressable
367
423
  if character_class.kind_of?(String)
368
424
  character_class = /[^#{character_class}]/
369
425
  end
370
- if component.respond_to?(:force_encoding)
371
- # We can't perform regexps on invalid UTF sequences, but
372
- # here we need to, so switch to ASCII.
373
- component = component.dup
374
- component.force_encoding(Encoding::ASCII_8BIT)
375
- end
426
+ # We can't perform regexps on invalid UTF sequences, but
427
+ # here we need to, so switch to ASCII.
428
+ component = component.dup
429
+ component.force_encoding(Encoding::ASCII_8BIT)
376
430
  # Avoiding gsub! because there are edge cases with frozen strings
377
- component = component.gsub(character_class) do |sequence|
378
- (sequence.unpack('C*').map { |c| "%" + ("%02x" % c).upcase }).join
431
+ component = component.gsub(character_class) do |char|
432
+ SEQUENCE_UPCASED_PERCENT_ENCODING_TABLE[char.ord]
379
433
  end
380
434
  if upcase_encoded.length > 0
381
- component = component.gsub(/%(#{upcase_encoded.chars.map do |char|
382
- char.unpack('C*').map { |c| '%02x' % c }.join
383
- end.join('|')})/i) { |s| s.upcase }
435
+ upcase_encoded_chars = upcase_encoded.bytes.map do |byte|
436
+ SEQUENCE_ENCODING_TABLE[byte]
437
+ end
438
+ component = component.gsub(/%(#{upcase_encoded_chars.join('|')})/,
439
+ &:upcase)
384
440
  end
441
+
385
442
  return component
386
443
  end
387
444
 
388
445
  class << self
389
- alias_method :encode_component, :encode_component
446
+ alias_method :escape_component, :encode_component
390
447
  end
391
448
 
392
449
  ##
@@ -425,16 +482,14 @@ module Addressable
425
482
  "Expected Class (String or Addressable::URI), " +
426
483
  "got #{return_type.inspect}"
427
484
  end
428
- uri = uri.dup
429
- # Seriously, only use UTF-8. I'm really not kidding!
430
- uri.force_encoding("utf-8") if uri.respond_to?(:force_encoding)
431
- leave_encoded.force_encoding("utf-8") if leave_encoded.respond_to?(:force_encoding)
432
- result = uri.gsub(/%[0-9a-f]{2}/iu) do |sequence|
485
+
486
+ result = uri.gsub(/%[0-9a-f]{2}/i) do |sequence|
433
487
  c = sequence[1..3].to_i(16).chr
434
- c.force_encoding("utf-8") if c.respond_to?(:force_encoding)
488
+ c.force_encoding(sequence.encoding)
435
489
  leave_encoded.include?(c) ? sequence : c
436
490
  end
437
- result.force_encoding("utf-8") if result.respond_to?(:force_encoding)
491
+
492
+ result.force_encoding(Encoding::UTF_8)
438
493
  if return_type == String
439
494
  return result
440
495
  elsif return_type == ::Addressable::URI
@@ -495,7 +550,7 @@ module Addressable
495
550
  # )
496
551
  # => "one two%2Fthree&four"
497
552
  def self.normalize_component(component, character_class=
498
- CharacterClasses::RESERVED + CharacterClasses::UNRESERVED,
553
+ CharacterClassesRegexps::RESERVED_AND_UNRESERVED,
499
554
  leave_encoded='')
500
555
  return nil if component.nil?
501
556
 
@@ -513,33 +568,32 @@ module Addressable
513
568
  leave_re = if leave_encoded.length > 0
514
569
  character_class = "#{character_class}%" unless character_class.include?('%')
515
570
 
516
- "|%(?!#{leave_encoded.chars.map do |char|
517
- seq = char.unpack('C*').map { |c| '%02x' % c }.join
518
- [seq.upcase, seq.downcase]
519
- end.flatten.join('|')})"
571
+ bytes = leave_encoded.bytes
572
+ leave_encoded_pattern = bytes.map { |b| SEQUENCE_ENCODING_TABLE[b] }.join('|')
573
+ "|%(?!#{leave_encoded_pattern}|#{leave_encoded_pattern.upcase})"
520
574
  end
521
575
 
522
- character_class = /[^#{character_class}]#{leave_re}/
523
- end
524
- if component.respond_to?(:force_encoding)
525
- # We can't perform regexps on invalid UTF sequences, but
526
- # here we need to, so switch to ASCII.
527
- component = component.dup
528
- component.force_encoding(Encoding::ASCII_8BIT)
576
+ character_class = if leave_re
577
+ /[^#{character_class}]#{leave_re}/
578
+ else
579
+ /[^#{character_class}]/
580
+ end
529
581
  end
582
+ # We can't perform regexps on invalid UTF sequences, but
583
+ # here we need to, so switch to ASCII.
584
+ component = component.dup
585
+ component.force_encoding(Encoding::ASCII_8BIT)
530
586
  unencoded = self.unencode_component(component, String, leave_encoded)
531
587
  begin
532
588
  encoded = self.encode_component(
533
- Addressable::IDNA.unicode_normalize_kc(unencoded),
589
+ unencoded.unicode_normalize(:nfc),
534
590
  character_class,
535
591
  leave_encoded
536
592
  )
537
593
  rescue ArgumentError
538
594
  encoded = self.encode_component(unencoded)
539
595
  end
540
- if encoded.respond_to?(:force_encoding)
541
- encoded.force_encoding(Encoding::UTF_8)
542
- end
596
+ encoded.force_encoding(Encoding::UTF_8)
543
597
  return encoded
544
598
  end
545
599
 
@@ -576,15 +630,15 @@ module Addressable
576
630
  uri_object = uri.kind_of?(self) ? uri : self.parse(uri)
577
631
  encoded_uri = Addressable::URI.new(
578
632
  :scheme => self.encode_component(uri_object.scheme,
579
- Addressable::URI::CharacterClasses::SCHEME),
633
+ Addressable::URI::CharacterClassesRegexps::SCHEME),
580
634
  :authority => self.encode_component(uri_object.authority,
581
- Addressable::URI::CharacterClasses::AUTHORITY),
635
+ Addressable::URI::CharacterClassesRegexps::AUTHORITY),
582
636
  :path => self.encode_component(uri_object.path,
583
- Addressable::URI::CharacterClasses::PATH),
637
+ Addressable::URI::CharacterClassesRegexps::PATH),
584
638
  :query => self.encode_component(uri_object.query,
585
- Addressable::URI::CharacterClasses::QUERY),
639
+ Addressable::URI::CharacterClassesRegexps::QUERY),
586
640
  :fragment => self.encode_component(uri_object.fragment,
587
- Addressable::URI::CharacterClasses::FRAGMENT)
641
+ Addressable::URI::CharacterClassesRegexps::FRAGMENT)
588
642
  )
589
643
  if return_type == String
590
644
  return encoded_uri.to_s
@@ -640,8 +694,7 @@ module Addressable
640
694
  components.each do |key, value|
641
695
  if value != nil
642
696
  begin
643
- components[key] =
644
- Addressable::IDNA.unicode_normalize_kc(value.to_str)
697
+ components[key] = value.to_str.unicode_normalize(:nfc)
645
698
  rescue ArgumentError
646
699
  # Likely a malformed UTF-8 character, skip unicode normalization
647
700
  components[key] = value.to_str
@@ -650,19 +703,19 @@ module Addressable
650
703
  end
651
704
  encoded_uri = Addressable::URI.new(
652
705
  :scheme => self.encode_component(components[:scheme],
653
- Addressable::URI::CharacterClasses::SCHEME),
706
+ Addressable::URI::CharacterClassesRegexps::SCHEME),
654
707
  :user => self.encode_component(components[:user],
655
- Addressable::URI::CharacterClasses::UNRESERVED),
708
+ Addressable::URI::CharacterClassesRegexps::UNRESERVED),
656
709
  :password => self.encode_component(components[:password],
657
- Addressable::URI::CharacterClasses::UNRESERVED),
710
+ Addressable::URI::CharacterClassesRegexps::UNRESERVED),
658
711
  :host => components[:host],
659
712
  :port => components[:port],
660
713
  :path => self.encode_component(components[:path],
661
- Addressable::URI::CharacterClasses::PATH),
714
+ Addressable::URI::CharacterClassesRegexps::PATH),
662
715
  :query => self.encode_component(components[:query],
663
- Addressable::URI::CharacterClasses::QUERY),
716
+ Addressable::URI::CharacterClassesRegexps::QUERY),
664
717
  :fragment => self.encode_component(components[:fragment],
665
- Addressable::URI::CharacterClasses::FRAGMENT)
718
+ Addressable::URI::CharacterClassesRegexps::FRAGMENT)
666
719
  )
667
720
  if return_type == String
668
721
  return encoded_uri.to_s
@@ -713,11 +766,11 @@ module Addressable
713
766
  [
714
767
  self.encode_component(
715
768
  key.gsub(/(\r\n|\n|\r)/, "\r\n"),
716
- CharacterClasses::UNRESERVED
769
+ CharacterClassesRegexps::UNRESERVED
717
770
  ).gsub("%20", "+"),
718
771
  self.encode_component(
719
772
  value.gsub(/(\r\n|\n|\r)/, "\r\n"),
720
- CharacterClasses::UNRESERVED
773
+ CharacterClassesRegexps::UNRESERVED
721
774
  ).gsub("%20", "+")
722
775
  ]
723
776
  end
@@ -789,7 +842,9 @@ module Addressable
789
842
  end
790
843
  end
791
844
 
792
- self.defer_validation do
845
+ reset_ivs
846
+
847
+ defer_validation do
793
848
  # Bunch of crazy logic required because of the composite components
794
849
  # like userinfo and authority.
795
850
  self.scheme = options[:scheme] if options[:scheme]
@@ -804,7 +859,8 @@ module Addressable
804
859
  self.query_values = options[:query_values] if options[:query_values]
805
860
  self.fragment = options[:fragment] if options[:fragment]
806
861
  end
807
- self.to_s
862
+
863
+ to_s # force path validation
808
864
  end
809
865
 
810
866
  ##
@@ -831,9 +887,7 @@ module Addressable
831
887
  # The scheme component for this URI.
832
888
  #
833
889
  # @return [String] The scheme component.
834
- def scheme
835
- return defined?(@scheme) ? @scheme : nil
836
- end
890
+ attr_reader :scheme
837
891
 
838
892
  ##
839
893
  # The scheme component for this URI, normalized.
@@ -841,16 +895,19 @@ module Addressable
841
895
  # @return [String] The scheme component, normalized.
842
896
  def normalized_scheme
843
897
  return nil unless self.scheme
844
- @normalized_scheme ||= begin
845
- if self.scheme =~ /^\s*ssh\+svn\s*$/i
846
- "svn+ssh"
898
+ if @normalized_scheme == NONE
899
+ @normalized_scheme = if self.scheme =~ /^\s*ssh\+svn\s*$/i
900
+ "svn+ssh".dup
847
901
  else
848
902
  Addressable::URI.normalize_component(
849
903
  self.scheme.strip.downcase,
850
- Addressable::URI::CharacterClasses::SCHEME
904
+ Addressable::URI::NormalizeCharacterClasses::SCHEME
851
905
  )
852
906
  end
853
907
  end
908
+ # All normalized values should be UTF-8
909
+ force_utf8_encoding_if_needed(@normalized_scheme)
910
+ @normalized_scheme
854
911
  end
855
912
 
856
913
  ##
@@ -864,13 +921,13 @@ module Addressable
864
921
  new_scheme = new_scheme.to_str
865
922
  end
866
923
  if new_scheme && new_scheme !~ /\A[a-z][a-z0-9\.\+\-]*\z/i
867
- raise InvalidURIError, "Invalid scheme format: #{new_scheme}"
924
+ raise InvalidURIError, "Invalid scheme format: '#{new_scheme}'"
868
925
  end
869
926
  @scheme = new_scheme
870
927
  @scheme = nil if @scheme.to_s.strip.empty?
871
928
 
872
929
  # Reset dependent values
873
- remove_instance_variable(:@normalized_scheme) if defined?(@normalized_scheme)
930
+ @normalized_scheme = NONE
874
931
  remove_composite_values
875
932
 
876
933
  # Ensure we haven't created an invalid URI
@@ -881,9 +938,7 @@ module Addressable
881
938
  # The user component for this URI.
882
939
  #
883
940
  # @return [String] The user component.
884
- def user
885
- return defined?(@user) ? @user : nil
886
- end
941
+ attr_reader :user
887
942
 
888
943
  ##
889
944
  # The user component for this URI, normalized.
@@ -891,18 +946,21 @@ module Addressable
891
946
  # @return [String] The user component, normalized.
892
947
  def normalized_user
893
948
  return nil unless self.user
894
- return @normalized_user if defined?(@normalized_user)
895
- @normalized_user ||= begin
949
+ return @normalized_user unless @normalized_user == NONE
950
+ @normalized_user = begin
896
951
  if normalized_scheme =~ /https?/ && self.user.strip.empty? &&
897
952
  (!self.password || self.password.strip.empty?)
898
953
  nil
899
954
  else
900
955
  Addressable::URI.normalize_component(
901
956
  self.user.strip,
902
- Addressable::URI::CharacterClasses::UNRESERVED
957
+ Addressable::URI::NormalizeCharacterClasses::UNRESERVED
903
958
  )
904
959
  end
905
960
  end
961
+ # All normalized values should be UTF-8
962
+ force_utf8_encoding_if_needed(@normalized_user)
963
+ @normalized_user
906
964
  end
907
965
 
908
966
  ##
@@ -917,14 +975,14 @@ module Addressable
917
975
 
918
976
  # You can't have a nil user with a non-nil password
919
977
  if password != nil
920
- @user = EMPTY_STR if @user.nil?
978
+ @user = EMPTY_STR unless user
921
979
  end
922
980
 
923
981
  # Reset dependent values
924
- remove_instance_variable(:@userinfo) if defined?(@userinfo)
925
- remove_instance_variable(:@normalized_userinfo) if defined?(@normalized_userinfo)
926
- remove_instance_variable(:@authority) if defined?(@authority)
927
- remove_instance_variable(:@normalized_user) if defined?(@normalized_user)
982
+ @userinfo = nil
983
+ @normalized_userinfo = NONE
984
+ @authority = nil
985
+ @normalized_user = NONE
928
986
  remove_composite_values
929
987
 
930
988
  # Ensure we haven't created an invalid URI
@@ -935,9 +993,7 @@ module Addressable
935
993
  # The password component for this URI.
936
994
  #
937
995
  # @return [String] The password component.
938
- def password
939
- return defined?(@password) ? @password : nil
940
- end
996
+ attr_reader :password
941
997
 
942
998
  ##
943
999
  # The password component for this URI, normalized.
@@ -945,18 +1001,21 @@ module Addressable
945
1001
  # @return [String] The password component, normalized.
946
1002
  def normalized_password
947
1003
  return nil unless self.password
948
- return @normalized_password if defined?(@normalized_password)
949
- @normalized_password ||= begin
1004
+ return @normalized_password unless @normalized_password == NONE
1005
+ @normalized_password = begin
950
1006
  if self.normalized_scheme =~ /https?/ && self.password.strip.empty? &&
951
1007
  (!self.user || self.user.strip.empty?)
952
1008
  nil
953
1009
  else
954
1010
  Addressable::URI.normalize_component(
955
1011
  self.password.strip,
956
- Addressable::URI::CharacterClasses::UNRESERVED
1012
+ Addressable::URI::NormalizeCharacterClasses::UNRESERVED
957
1013
  )
958
1014
  end
959
1015
  end
1016
+ # All normalized values should be UTF-8
1017
+ force_utf8_encoding_if_needed(@normalized_password)
1018
+ @normalized_password
960
1019
  end
961
1020
 
962
1021
  ##
@@ -970,17 +1029,15 @@ module Addressable
970
1029
  @password = new_password ? new_password.to_str : nil
971
1030
 
972
1031
  # You can't have a nil user with a non-nil password
973
- @password ||= nil
974
- @user ||= nil
975
1032
  if @password != nil
976
- @user = EMPTY_STR if @user.nil?
1033
+ self.user = EMPTY_STR if user.nil?
977
1034
  end
978
1035
 
979
1036
  # Reset dependent values
980
- remove_instance_variable(:@userinfo) if defined?(@userinfo)
981
- remove_instance_variable(:@normalized_userinfo) if defined?(@normalized_userinfo)
982
- remove_instance_variable(:@authority) if defined?(@authority)
983
- remove_instance_variable(:@normalized_password) if defined?(@normalized_password)
1037
+ @userinfo = nil
1038
+ @normalized_userinfo = NONE
1039
+ @authority = nil
1040
+ @normalized_password = NONE
984
1041
  remove_composite_values
985
1042
 
986
1043
  # Ensure we haven't created an invalid URI
@@ -1010,18 +1067,21 @@ module Addressable
1010
1067
  # @return [String] The userinfo component, normalized.
1011
1068
  def normalized_userinfo
1012
1069
  return nil unless self.userinfo
1013
- return @normalized_userinfo if defined?(@normalized_userinfo)
1014
- @normalized_userinfo ||= begin
1070
+ return @normalized_userinfo unless @normalized_userinfo == NONE
1071
+ @normalized_userinfo = begin
1015
1072
  current_user = self.normalized_user
1016
1073
  current_password = self.normalized_password
1017
1074
  if !current_user && !current_password
1018
1075
  nil
1019
1076
  elsif current_user && current_password
1020
- "#{current_user}:#{current_password}"
1077
+ "#{current_user}:#{current_password}".dup
1021
1078
  elsif current_user && !current_password
1022
- "#{current_user}"
1079
+ "#{current_user}".dup
1023
1080
  end
1024
1081
  end
1082
+ # All normalized values should be UTF-8
1083
+ force_utf8_encoding_if_needed(@normalized_userinfo)
1084
+ @normalized_userinfo
1025
1085
  end
1026
1086
 
1027
1087
  ##
@@ -1046,7 +1106,7 @@ module Addressable
1046
1106
  self.user = new_user
1047
1107
 
1048
1108
  # Reset dependent values
1049
- remove_instance_variable(:@authority) if defined?(@authority)
1109
+ @authority = nil
1050
1110
  remove_composite_values
1051
1111
 
1052
1112
  # Ensure we haven't created an invalid URI
@@ -1057,9 +1117,7 @@ module Addressable
1057
1117
  # The host component for this URI.
1058
1118
  #
1059
1119
  # @return [String] The host component.
1060
- def host
1061
- return defined?(@host) ? @host : nil
1062
- end
1120
+ attr_reader :host
1063
1121
 
1064
1122
  ##
1065
1123
  # The host component for this URI, normalized.
@@ -1067,6 +1125,7 @@ module Addressable
1067
1125
  # @return [String] The host component, normalized.
1068
1126
  def normalized_host
1069
1127
  return nil unless self.host
1128
+
1070
1129
  @normalized_host ||= begin
1071
1130
  if !self.host.strip.empty?
1072
1131
  result = ::Addressable::IDNA.to_ascii(
@@ -1078,12 +1137,16 @@ module Addressable
1078
1137
  end
1079
1138
  result = Addressable::URI.normalize_component(
1080
1139
  result,
1081
- CharacterClasses::HOST)
1140
+ NormalizeCharacterClasses::HOST
1141
+ )
1082
1142
  result
1083
1143
  else
1084
- EMPTY_STR
1144
+ EMPTY_STR.dup
1085
1145
  end
1086
1146
  end
1147
+ # All normalized values should be UTF-8
1148
+ force_utf8_encoding_if_needed(@normalized_host)
1149
+ @normalized_host
1087
1150
  end
1088
1151
 
1089
1152
  ##
@@ -1096,17 +1159,9 @@ module Addressable
1096
1159
  end
1097
1160
  @host = new_host ? new_host.to_str : nil
1098
1161
 
1099
- unreserved = CharacterClasses::UNRESERVED
1100
- sub_delims = CharacterClasses::SUB_DELIMS
1101
- if !@host.nil? && (@host =~ /[<>{}\/\?\#\@"[[:space:]]]/ ||
1102
- (@host[/^\[(.*)\]$/, 1] != nil && @host[/^\[(.*)\]$/, 1] !~
1103
- Regexp.new("^[#{unreserved}#{sub_delims}:]*$")))
1104
- raise InvalidURIError, "Invalid character in host: '#{@host.to_s}'"
1105
- end
1106
-
1107
1162
  # Reset dependent values
1108
- remove_instance_variable(:@authority) if defined?(@authority)
1109
- remove_instance_variable(:@normalized_host) if defined?(@normalized_host)
1163
+ @authority = nil
1164
+ @normalized_host = nil
1110
1165
  remove_composite_values
1111
1166
 
1112
1167
  # Ensure we haven't created an invalid URI
@@ -1144,6 +1199,33 @@ module Addressable
1144
1199
  self.host = v
1145
1200
  end
1146
1201
 
1202
+ ##
1203
+ # Returns the top-level domain for this host.
1204
+ #
1205
+ # @example
1206
+ # Addressable::URI.parse("http://www.example.co.uk").tld # => "co.uk"
1207
+ def tld
1208
+ PublicSuffix.parse(self.host, ignore_private: true).tld
1209
+ end
1210
+
1211
+ ##
1212
+ # Sets the top-level domain for this URI.
1213
+ #
1214
+ # @param [String, #to_str] new_tld The new top-level domain.
1215
+ def tld=(new_tld)
1216
+ replaced_tld = host.sub(/#{tld}\z/, new_tld)
1217
+ self.host = PublicSuffix::Domain.new(replaced_tld).to_s
1218
+ end
1219
+
1220
+ ##
1221
+ # Returns the public suffix domain for this host.
1222
+ #
1223
+ # @example
1224
+ # Addressable::URI.parse("http://www.example.co.uk").domain # => "example.co.uk"
1225
+ def domain
1226
+ PublicSuffix.domain(self.host, ignore_private: true)
1227
+ end
1228
+
1147
1229
  ##
1148
1230
  # The authority component for this URI.
1149
1231
  # Combines the user, password, host, and port components.
@@ -1151,7 +1233,7 @@ module Addressable
1151
1233
  # @return [String] The authority component.
1152
1234
  def authority
1153
1235
  self.host && @authority ||= begin
1154
- authority = ""
1236
+ authority = String.new
1155
1237
  if self.userinfo != nil
1156
1238
  authority << "#{self.userinfo}@"
1157
1239
  end
@@ -1170,7 +1252,7 @@ module Addressable
1170
1252
  def normalized_authority
1171
1253
  return nil unless self.authority
1172
1254
  @normalized_authority ||= begin
1173
- authority = ""
1255
+ authority = String.new
1174
1256
  if self.normalized_userinfo != nil
1175
1257
  authority << "#{self.normalized_userinfo}@"
1176
1258
  end
@@ -1180,6 +1262,9 @@ module Addressable
1180
1262
  end
1181
1263
  authority
1182
1264
  end
1265
+ # All normalized values should be UTF-8
1266
+ force_utf8_encoding_if_needed(@normalized_authority)
1267
+ @normalized_authority
1183
1268
  end
1184
1269
 
1185
1270
  ##
@@ -1197,9 +1282,9 @@ module Addressable
1197
1282
  new_user = new_userinfo.strip[/^([^:]*):?/, 1]
1198
1283
  new_password = new_userinfo.strip[/:(.*)$/, 1]
1199
1284
  end
1200
- new_host = new_authority.gsub(
1285
+ new_host = new_authority.sub(
1201
1286
  /^([^\[\]]*)@/, EMPTY_STR
1202
- ).gsub(
1287
+ ).sub(
1203
1288
  /:([^:@\[\]]*?)$/, EMPTY_STR
1204
1289
  )
1205
1290
  new_port =
@@ -1207,14 +1292,14 @@ module Addressable
1207
1292
  end
1208
1293
 
1209
1294
  # Password assigned first to ensure validity in case of nil
1210
- self.password = defined?(new_password) ? new_password : nil
1211
- self.user = defined?(new_user) ? new_user : nil
1212
- self.host = defined?(new_host) ? new_host : nil
1213
- self.port = defined?(new_port) ? new_port : nil
1295
+ self.password = new_password
1296
+ self.user = new_user
1297
+ self.host = new_host
1298
+ self.port = new_port
1214
1299
 
1215
1300
  # Reset dependent values
1216
- remove_instance_variable(:@userinfo) if defined?(@userinfo)
1217
- remove_instance_variable(:@normalized_userinfo) if defined?(@normalized_userinfo)
1301
+ @userinfo = nil
1302
+ @normalized_userinfo = NONE
1218
1303
  remove_composite_values
1219
1304
 
1220
1305
  # Ensure we haven't created an invalid URI
@@ -1262,16 +1347,16 @@ module Addressable
1262
1347
  new_port = new_origin[/:([^:@\[\]\/]*?)$/, 1]
1263
1348
  end
1264
1349
 
1265
- self.scheme = defined?(new_scheme) ? new_scheme : nil
1266
- self.host = defined?(new_host) ? new_host : nil
1267
- self.port = defined?(new_port) ? new_port : nil
1350
+ self.scheme = new_scheme
1351
+ self.host = new_host
1352
+ self.port = new_port
1268
1353
  self.userinfo = nil
1269
1354
 
1270
1355
  # Reset dependent values
1271
- remove_instance_variable(:@userinfo) if defined?(@userinfo)
1272
- remove_instance_variable(:@normalized_userinfo) if defined?(@normalized_userinfo)
1273
- remove_instance_variable(:@authority) if defined?(@authority)
1274
- remove_instance_variable(:@normalized_authority) if defined?(@normalized_authority)
1356
+ @userinfo = nil
1357
+ @normalized_userinfo = NONE
1358
+ @authority = nil
1359
+ @normalized_authority = nil
1275
1360
  remove_composite_values
1276
1361
 
1277
1362
  # Ensure we haven't created an invalid URI
@@ -1298,9 +1383,7 @@ module Addressable
1298
1383
  # infer port numbers from default values.
1299
1384
  #
1300
1385
  # @return [Integer] The port component.
1301
- def port
1302
- return defined?(@port) ? @port : nil
1303
- end
1386
+ attr_reader :port
1304
1387
 
1305
1388
  ##
1306
1389
  # The port component for this URI, normalized.
@@ -1308,8 +1391,8 @@ module Addressable
1308
1391
  # @return [Integer] The port component, normalized.
1309
1392
  def normalized_port
1310
1393
  return nil unless self.port
1311
- return @normalized_port if defined?(@normalized_port)
1312
- @normalized_port ||= begin
1394
+ return @normalized_port unless @normalized_port == NONE
1395
+ @normalized_port = begin
1313
1396
  if URI.port_mapping[self.normalized_scheme] == self.port
1314
1397
  nil
1315
1398
  else
@@ -1340,8 +1423,8 @@ module Addressable
1340
1423
  @port = nil if @port == 0
1341
1424
 
1342
1425
  # Reset dependent values
1343
- remove_instance_variable(:@authority) if defined?(@authority)
1344
- remove_instance_variable(:@normalized_port) if defined?(@normalized_port)
1426
+ @authority = nil
1427
+ @normalized_port = NONE
1345
1428
  remove_composite_values
1346
1429
 
1347
1430
  # Ensure we haven't created an invalid URI
@@ -1383,7 +1466,7 @@ module Addressable
1383
1466
  # @return [String] The components that identify a site.
1384
1467
  def site
1385
1468
  (self.scheme || self.authority) && @site ||= begin
1386
- site_string = ""
1469
+ site_string = "".dup
1387
1470
  site_string << "#{self.scheme}:" if self.scheme != nil
1388
1471
  site_string << "//#{self.authority}" if self.authority != nil
1389
1472
  site_string
@@ -1402,7 +1485,7 @@ module Addressable
1402
1485
  def normalized_site
1403
1486
  return nil unless self.site
1404
1487
  @normalized_site ||= begin
1405
- site_string = ""
1488
+ site_string = "".dup
1406
1489
  if self.normalized_scheme != nil
1407
1490
  site_string << "#{self.normalized_scheme}:"
1408
1491
  end
@@ -1411,6 +1494,9 @@ module Addressable
1411
1494
  end
1412
1495
  site_string
1413
1496
  end
1497
+ # All normalized values should be UTF-8
1498
+ force_utf8_encoding_if_needed(@normalized_site)
1499
+ @normalized_site
1414
1500
  end
1415
1501
 
1416
1502
  ##
@@ -1439,9 +1525,7 @@ module Addressable
1439
1525
  # The path component for this URI.
1440
1526
  #
1441
1527
  # @return [String] The path component.
1442
- def path
1443
- return defined?(@path) ? @path : EMPTY_STR
1444
- end
1528
+ attr_reader :path
1445
1529
 
1446
1530
  NORMPATH = /^(?!\/)[^\/:]*:.*$/
1447
1531
  ##
@@ -1455,22 +1539,25 @@ module Addressable
1455
1539
  # Relative paths with colons in the first segment are ambiguous.
1456
1540
  path = path.sub(":", "%2F")
1457
1541
  end
1458
- # String#split(delimeter, -1) uses the more strict splitting behavior
1542
+ # String#split(delimiter, -1) uses the more strict splitting behavior
1459
1543
  # found by default in Python.
1460
1544
  result = path.strip.split(SLASH, -1).map do |segment|
1461
1545
  Addressable::URI.normalize_component(
1462
1546
  segment,
1463
- Addressable::URI::CharacterClasses::PCHAR
1547
+ Addressable::URI::NormalizeCharacterClasses::PCHAR
1464
1548
  )
1465
1549
  end.join(SLASH)
1466
1550
 
1467
1551
  result = URI.normalize_path(result)
1468
1552
  if result.empty? &&
1469
1553
  ["http", "https", "ftp", "tftp"].include?(self.normalized_scheme)
1470
- result = SLASH
1554
+ result = SLASH.dup
1471
1555
  end
1472
1556
  result
1473
1557
  end
1558
+ # All normalized values should be UTF-8
1559
+ force_utf8_encoding_if_needed(@normalized_path)
1560
+ @normalized_path
1474
1561
  end
1475
1562
 
1476
1563
  ##
@@ -1487,8 +1574,11 @@ module Addressable
1487
1574
  end
1488
1575
 
1489
1576
  # Reset dependent values
1490
- remove_instance_variable(:@normalized_path) if defined?(@normalized_path)
1577
+ @normalized_path = nil
1491
1578
  remove_composite_values
1579
+
1580
+ # Ensure we haven't created an invalid URI
1581
+ validate()
1492
1582
  end
1493
1583
 
1494
1584
  ##
@@ -1497,7 +1587,7 @@ module Addressable
1497
1587
  # @return [String] The path's basename.
1498
1588
  def basename
1499
1589
  # Path cannot be nil
1500
- return File.basename(self.path).gsub(/;[^\/]*$/, EMPTY_STR)
1590
+ return File.basename(self.path).sub(/;[^\/]*$/, EMPTY_STR)
1501
1591
  end
1502
1592
 
1503
1593
  ##
@@ -1514,9 +1604,7 @@ module Addressable
1514
1604
  # The query component for this URI.
1515
1605
  #
1516
1606
  # @return [String] The query component.
1517
- def query
1518
- return defined?(@query) ? @query : nil
1519
- end
1607
+ attr_reader :query
1520
1608
 
1521
1609
  ##
1522
1610
  # The query component for this URI, normalized.
@@ -1524,18 +1612,26 @@ module Addressable
1524
1612
  # @return [String] The query component, normalized.
1525
1613
  def normalized_query(*flags)
1526
1614
  return nil unless self.query
1527
- return @normalized_query if defined?(@normalized_query)
1528
- @normalized_query ||= begin
1615
+ return @normalized_query unless @normalized_query == NONE
1616
+ @normalized_query = begin
1529
1617
  modified_query_class = Addressable::URI::CharacterClasses::QUERY.dup
1530
1618
  # Make sure possible key-value pair delimiters are escaped.
1531
1619
  modified_query_class.sub!("\\&", "").sub!("\\;", "")
1532
- pairs = (self.query || "").split("&", -1)
1620
+ pairs = (query || "").split("&", -1)
1621
+ pairs.delete_if(&:empty?).uniq! if flags.include?(:compacted)
1533
1622
  pairs.sort! if flags.include?(:sorted)
1534
1623
  component = pairs.map do |pair|
1535
- Addressable::URI.normalize_component(pair, modified_query_class, "+")
1624
+ Addressable::URI.normalize_component(
1625
+ pair,
1626
+ Addressable::URI::NormalizeCharacterClasses::QUERY,
1627
+ "+"
1628
+ )
1536
1629
  end.join("&")
1537
1630
  component == "" ? nil : component
1538
1631
  end
1632
+ # All normalized values should be UTF-8
1633
+ force_utf8_encoding_if_needed(@normalized_query)
1634
+ @normalized_query
1539
1635
  end
1540
1636
 
1541
1637
  ##
@@ -1549,7 +1645,7 @@ module Addressable
1549
1645
  @query = new_query ? new_query.to_str : nil
1550
1646
 
1551
1647
  # Reset dependent values
1552
- remove_instance_variable(:@normalized_query) if defined?(@normalized_query)
1648
+ @normalized_query = NONE
1553
1649
  remove_composite_values
1554
1650
  end
1555
1651
 
@@ -1588,11 +1684,13 @@ module Addressable
1588
1684
  # so it's best to make all changes in-place.
1589
1685
  pair[0] = URI.unencode_component(pair[0])
1590
1686
  if pair[1].respond_to?(:to_str)
1687
+ value = pair[1].to_str
1591
1688
  # I loathe the fact that I have to do this. Stupid HTML 4.01.
1592
1689
  # Treating '+' as a space was just an unbelievably bad idea.
1593
1690
  # There was nothing wrong with '%20'!
1594
1691
  # If it ain't broke, don't fix it!
1595
- pair[1] = URI.unencode_component(pair[1].to_str.gsub(/\+/, " "))
1692
+ value = value.tr("+", " ") if ["http", "https", nil].include?(scheme)
1693
+ pair[1] = URI.unencode_component(value)
1596
1694
  end
1597
1695
  if return_type == Hash
1598
1696
  accu[pair[0]] = pair[1]
@@ -1644,23 +1742,23 @@ module Addressable
1644
1742
  end
1645
1743
 
1646
1744
  # new_query_values have form [['key1', 'value1'], ['key2', 'value2']]
1647
- buffer = ""
1745
+ buffer = "".dup
1648
1746
  new_query_values.each do |key, value|
1649
1747
  encoded_key = URI.encode_component(
1650
- key, CharacterClasses::UNRESERVED
1748
+ key, CharacterClassesRegexps::UNRESERVED
1651
1749
  )
1652
1750
  if value == nil
1653
1751
  buffer << "#{encoded_key}&"
1654
1752
  elsif value.kind_of?(Array)
1655
1753
  value.each do |sub_value|
1656
1754
  encoded_value = URI.encode_component(
1657
- sub_value, CharacterClasses::UNRESERVED
1755
+ sub_value, CharacterClassesRegexps::UNRESERVED
1658
1756
  )
1659
1757
  buffer << "#{encoded_key}=#{encoded_value}&"
1660
1758
  end
1661
1759
  else
1662
1760
  encoded_value = URI.encode_component(
1663
- value, CharacterClasses::UNRESERVED
1761
+ value, CharacterClassesRegexps::UNRESERVED
1664
1762
  )
1665
1763
  buffer << "#{encoded_key}=#{encoded_value}&"
1666
1764
  end
@@ -1674,7 +1772,7 @@ module Addressable
1674
1772
  #
1675
1773
  # @return [String] The request URI required for an HTTP request.
1676
1774
  def request_uri
1677
- return nil if self.absolute? && self.scheme !~ /^https?$/
1775
+ return nil if self.absolute? && self.scheme !~ /^https?$/i
1678
1776
  return (
1679
1777
  (!self.path.empty? ? self.path : SLASH) +
1680
1778
  (self.query ? "?#{self.query}" : EMPTY_STR)
@@ -1689,12 +1787,12 @@ module Addressable
1689
1787
  if !new_request_uri.respond_to?(:to_str)
1690
1788
  raise TypeError, "Can't convert #{new_request_uri.class} into String."
1691
1789
  end
1692
- if self.absolute? && self.scheme !~ /^https?$/
1790
+ if self.absolute? && self.scheme !~ /^https?$/i
1693
1791
  raise InvalidURIError,
1694
1792
  "Cannot set an HTTP request URI for a non-HTTP URI."
1695
1793
  end
1696
1794
  new_request_uri = new_request_uri.to_str
1697
- path_component = new_request_uri[/^([^\?]*)\?(?:.*)$/, 1]
1795
+ path_component = new_request_uri[/^([^\?]*)\??(?:.*)$/, 1]
1698
1796
  query_component = new_request_uri[/^(?:[^\?]*)\?(.*)$/, 1]
1699
1797
  path_component = path_component.to_s
1700
1798
  path_component = (!path_component.empty? ? path_component : SLASH)
@@ -1709,9 +1807,7 @@ module Addressable
1709
1807
  # The fragment component for this URI.
1710
1808
  #
1711
1809
  # @return [String] The fragment component.
1712
- def fragment
1713
- return defined?(@fragment) ? @fragment : nil
1714
- end
1810
+ attr_reader :fragment
1715
1811
 
1716
1812
  ##
1717
1813
  # The fragment component for this URI, normalized.
@@ -1719,14 +1815,17 @@ module Addressable
1719
1815
  # @return [String] The fragment component, normalized.
1720
1816
  def normalized_fragment
1721
1817
  return nil unless self.fragment
1722
- return @normalized_fragment if defined?(@normalized_fragment)
1723
- @normalized_fragment ||= begin
1818
+ return @normalized_fragment unless @normalized_fragment == NONE
1819
+ @normalized_fragment = begin
1724
1820
  component = Addressable::URI.normalize_component(
1725
1821
  self.fragment,
1726
- Addressable::URI::CharacterClasses::FRAGMENT
1822
+ Addressable::URI::NormalizeCharacterClasses::FRAGMENT
1727
1823
  )
1728
1824
  component == "" ? nil : component
1729
1825
  end
1826
+ # All normalized values should be UTF-8
1827
+ force_utf8_encoding_if_needed(@normalized_fragment)
1828
+ @normalized_fragment
1730
1829
  end
1731
1830
 
1732
1831
  ##
@@ -1740,7 +1839,7 @@ module Addressable
1740
1839
  @fragment = new_fragment ? new_fragment.to_str : nil
1741
1840
 
1742
1841
  # Reset dependent values
1743
- remove_instance_variable(:@normalized_fragment) if defined?(@normalized_fragment)
1842
+ @normalized_fragment = NONE
1744
1843
  remove_composite_values
1745
1844
 
1746
1845
  # Ensure we haven't created an invalid URI
@@ -1844,8 +1943,8 @@ module Addressable
1844
1943
  # Section 5.2.3 of RFC 3986
1845
1944
  #
1846
1945
  # Removes the right-most path segment from the base path.
1847
- if base_path =~ /\//
1848
- base_path.gsub!(/\/[^\/]+$/, SLASH)
1946
+ if base_path.include?(SLASH)
1947
+ base_path.sub!(/\/[^\/]+$/, SLASH)
1849
1948
  else
1850
1949
  base_path = EMPTY_STR
1851
1950
  end
@@ -1906,7 +2005,7 @@ module Addressable
1906
2005
  #
1907
2006
  # @see Hash#merge
1908
2007
  def merge(hash)
1909
- if !hash.respond_to?(:to_hash)
2008
+ unless hash.respond_to?(:to_hash)
1910
2009
  raise TypeError, "Can't convert #{hash.class} into Hash."
1911
2010
  end
1912
2011
  hash = hash.to_hash
@@ -2246,15 +2345,13 @@ module Addressable
2246
2345
  "Cannot assemble URI string with ambiguous path: '#{self.path}'"
2247
2346
  end
2248
2347
  @uri_string ||= begin
2249
- uri_string = ""
2348
+ uri_string = String.new
2250
2349
  uri_string << "#{self.scheme}:" if self.scheme != nil
2251
2350
  uri_string << "//#{self.authority}" if self.authority != nil
2252
2351
  uri_string << self.path.to_s
2253
2352
  uri_string << "?#{self.query}" if self.query != nil
2254
2353
  uri_string << "##{self.fragment}" if self.fragment != nil
2255
- if uri_string.respond_to?(:force_encoding)
2256
- uri_string.force_encoding(Encoding::UTF_8)
2257
- end
2354
+ uri_string.force_encoding(Encoding::UTF_8)
2258
2355
  uri_string
2259
2356
  end
2260
2357
  end
@@ -2285,7 +2382,7 @@ module Addressable
2285
2382
  #
2286
2383
  # @return [String] The URI object's state, as a <code>String</code>.
2287
2384
  def inspect
2288
- sprintf("#<%s:%#0x URI:%s>", URI.to_s, self.object_id, self.to_s)
2385
+ sprintf("#<%s:%#0x URI:%s>", self.class.to_s, self.object_id, self.to_s)
2289
2386
  end
2290
2387
 
2291
2388
  ##
@@ -2296,13 +2393,33 @@ module Addressable
2296
2393
  #
2297
2394
  # @param [Proc] block
2298
2395
  # A set of operations to perform on a given URI.
2299
- def defer_validation(&block)
2300
- raise LocalJumpError, "No block given." unless block
2396
+ def defer_validation
2397
+ raise LocalJumpError, "No block given." unless block_given?
2301
2398
  @validation_deferred = true
2302
- block.call()
2399
+ yield
2303
2400
  @validation_deferred = false
2304
2401
  validate
2305
- return nil
2402
+ ensure
2403
+ @validation_deferred = false
2404
+ end
2405
+
2406
+ def encode_with(coder)
2407
+ instance_variables.each do |ivar|
2408
+ value = instance_variable_get(ivar)
2409
+ if value != NONE
2410
+ key = ivar.to_s.slice(1..-1)
2411
+ coder[key] = value
2412
+ end
2413
+ end
2414
+ nil
2415
+ end
2416
+
2417
+ def init_with(coder)
2418
+ reset_ivs
2419
+ coder.map.each do |key, value|
2420
+ instance_variable_set("@#{key}", value)
2421
+ end
2422
+ nil
2306
2423
  end
2307
2424
 
2308
2425
  protected
@@ -2323,30 +2440,35 @@ module Addressable
2323
2440
  def self.normalize_path(path)
2324
2441
  # Section 5.2.4 of RFC 3986
2325
2442
 
2326
- return nil if path.nil?
2443
+ return if path.nil?
2327
2444
  normalized_path = path.dup
2328
- begin
2329
- mod = nil
2445
+ loop do
2330
2446
  mod ||= normalized_path.gsub!(RULE_2A, SLASH)
2331
2447
 
2332
2448
  pair = normalized_path.match(RULE_2B_2C)
2333
- parent, current = pair[1], pair[2] if pair
2449
+ if pair
2450
+ parent = pair[1]
2451
+ current = pair[2]
2452
+ else
2453
+ parent = nil
2454
+ current = nil
2455
+ end
2456
+
2457
+ regexp = "/#{Regexp.escape(parent.to_s)}/\\.\\./|"
2458
+ regexp += "(/#{Regexp.escape(current.to_s)}/\\.\\.$)"
2459
+
2334
2460
  if pair && ((parent != SELF_REF && parent != PARENT) ||
2335
2461
  (current != SELF_REF && current != PARENT))
2336
- mod ||= normalized_path.gsub!(
2337
- Regexp.new(
2338
- "/#{Regexp.escape(parent.to_s)}/\\.\\./|" +
2339
- "(/#{Regexp.escape(current.to_s)}/\\.\\.$)"
2340
- ), SLASH
2341
- )
2462
+ mod ||= normalized_path.gsub!(Regexp.new(regexp), SLASH)
2342
2463
  end
2343
2464
 
2344
2465
  mod ||= normalized_path.gsub!(RULE_2D, EMPTY_STR)
2345
2466
  # Non-standard, removes prefixed dotted segments from path.
2346
2467
  mod ||= normalized_path.gsub!(RULE_PREFIXED_PARENT, SLASH)
2347
- end until mod.nil?
2468
+ break if mod.nil?
2469
+ end
2348
2470
 
2349
- return normalized_path
2471
+ normalized_path
2350
2472
  end
2351
2473
 
2352
2474
  ##
@@ -2371,6 +2493,19 @@ module Addressable
2371
2493
  raise InvalidURIError,
2372
2494
  "Cannot have a relative path with an authority set: '#{self.to_s}'"
2373
2495
  end
2496
+ if self.path != nil && !self.path.empty? &&
2497
+ self.path[0..1] == SLASH + SLASH && self.authority == nil
2498
+ raise InvalidURIError,
2499
+ "Cannot have a path with two leading slashes " +
2500
+ "without an authority set: '#{self.to_s}'"
2501
+ end
2502
+ unreserved = CharacterClasses::UNRESERVED
2503
+ sub_delims = CharacterClasses::SUB_DELIMS
2504
+ if !self.host.nil? && (self.host =~ /[<>{}\/\\\?\#\@"[[:space:]]]/ ||
2505
+ (self.host[/^\[(.*)\]$/, 1] != nil && self.host[/^\[(.*)\]$/, 1] !~
2506
+ Regexp.new("^[#{unreserved}#{sub_delims}:]*$")))
2507
+ raise InvalidURIError, "Invalid character in host: '#{self.host.to_s}'"
2508
+ end
2374
2509
  return nil
2375
2510
  end
2376
2511
 
@@ -2383,9 +2518,7 @@ module Addressable
2383
2518
  # @return [Addressable::URI] <code>self</code>.
2384
2519
  def replace_self(uri)
2385
2520
  # Reset dependent values
2386
- instance_variables.each do |var|
2387
- remove_instance_variable(var) if instance_variable_defined?(var)
2388
- end
2521
+ reset_ivs
2389
2522
 
2390
2523
  @scheme = uri.scheme
2391
2524
  @user = uri.user
@@ -2417,8 +2550,53 @@ module Addressable
2417
2550
  #
2418
2551
  # @api private
2419
2552
  def remove_composite_values
2420
- remove_instance_variable(:@uri_string) if defined?(@uri_string)
2421
- remove_instance_variable(:@hash) if defined?(@hash)
2553
+ @uri_string = nil
2554
+ @hash = nil
2422
2555
  end
2556
+
2557
+ ##
2558
+ # Converts the string to be UTF-8 if it is not already UTF-8
2559
+ #
2560
+ # @api private
2561
+ def force_utf8_encoding_if_needed(str)
2562
+ if str && str.encoding != Encoding::UTF_8
2563
+ str.force_encoding(Encoding::UTF_8)
2564
+ end
2565
+ end
2566
+
2567
+ private
2568
+
2569
+ ##
2570
+ # Resets instance variables
2571
+ #
2572
+ # @api private
2573
+ def reset_ivs
2574
+ @scheme = nil
2575
+ @user = nil
2576
+ @normalized_scheme = NONE
2577
+ @normalized_user = NONE
2578
+ @uri_string = nil
2579
+ @hash = nil
2580
+ @userinfo = nil
2581
+ @normalized_userinfo = NONE
2582
+ @authority = nil
2583
+ @password = nil
2584
+ @normalized_authority = nil
2585
+ @port = nil
2586
+ @normalized_password = NONE
2587
+ @host = nil
2588
+ @normalized_host = nil
2589
+ @normalized_port = NONE
2590
+ @path = EMPTY_STR
2591
+ @normalized_path = nil
2592
+ @normalized_query = NONE
2593
+ @fragment = nil
2594
+ @normalized_fragment = NONE
2595
+ @query = nil
2596
+ end
2597
+
2598
+ NONE = Module.new.freeze
2599
+
2600
+ private_constant :NONE
2423
2601
  end
2424
2602
  end