addressable 2.5.0 → 2.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # encoding:utf-8
2
4
  #--
3
5
  # Copyright (C) Bob Aman
@@ -35,7 +37,7 @@ module Addressable
35
37
  Addressable::URI::CharacterClasses::DIGIT + '_'
36
38
 
37
39
  var_char =
38
- "(?:(?:[#{variable_char_class}]|%[a-fA-F0-9][a-fA-F0-9])+)"
40
+ "(?>(?:[#{variable_char_class}]|%[a-fA-F0-9][a-fA-F0-9])+)"
39
41
  RESERVED =
40
42
  "(?:[#{anything}]|%[a-fA-F0-9][a-fA-F0-9])"
41
43
  UNRESERVED =
@@ -410,7 +412,7 @@ module Addressable
410
412
  # match.captures
411
413
  # #=> ["a", ["b", "c"]]
412
414
  def match(uri, processor=nil)
413
- uri = Addressable::URI.parse(uri)
415
+ uri = Addressable::URI.parse(uri) unless uri.is_a?(Addressable::URI)
414
416
  mapping = {}
415
417
 
416
418
  # First, we need to process the pattern, and extract the values.
@@ -488,6 +490,8 @@ module Addressable
488
490
  # @param [Hash] mapping The mapping that corresponds to the pattern.
489
491
  # @param [#validate, #transform] processor
490
492
  # An optional processor object may be supplied.
493
+ # @param [Boolean] normalize_values
494
+ # Optional flag to enable/disable unicode normalization. Default: true
491
495
  #
492
496
  # The object should respond to either the <tt>validate</tt> or
493
497
  # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
@@ -518,11 +522,11 @@ module Addressable
518
522
  # "http://example.com/{?one,two,three}/"
519
523
  # ).partial_expand({"one" => "1", "three" => 3}).pattern
520
524
  # #=> "http://example.com/?one=1{&two}&three=3"
521
- def partial_expand(mapping, processor=nil)
525
+ def partial_expand(mapping, processor=nil, normalize_values=true)
522
526
  result = self.pattern.dup
523
527
  mapping = normalize_keys(mapping)
524
528
  result.gsub!( EXPRESSION ) do |capture|
525
- transform_partial_capture(mapping, capture, processor)
529
+ transform_partial_capture(mapping, capture, processor, normalize_values)
526
530
  end
527
531
  return Addressable::Template.new(result)
528
532
  end
@@ -533,6 +537,8 @@ module Addressable
533
537
  # @param [Hash] mapping The mapping that corresponds to the pattern.
534
538
  # @param [#validate, #transform] processor
535
539
  # An optional processor object may be supplied.
540
+ # @param [Boolean] normalize_values
541
+ # Optional flag to enable/disable unicode normalization. Default: true
536
542
  #
537
543
  # The object should respond to either the <tt>validate</tt> or
538
544
  # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
@@ -583,11 +589,11 @@ module Addressable
583
589
  # ExampleProcessor
584
590
  # ).to_str
585
591
  # #=> Addressable::Template::InvalidTemplateValueError
586
- def expand(mapping, processor=nil)
592
+ def expand(mapping, processor=nil, normalize_values=true)
587
593
  result = self.pattern.dup
588
594
  mapping = normalize_keys(mapping)
589
595
  result.gsub!( EXPRESSION ) do |capture|
590
- transform_capture(mapping, capture, processor)
596
+ transform_capture(mapping, capture, processor, normalize_values)
591
597
  end
592
598
  return Addressable::URI.parse(result)
593
599
  end
@@ -647,40 +653,6 @@ module Addressable
647
653
  self.to_regexp.named_captures
648
654
  end
649
655
 
650
- ##
651
- # Generates a route result for a given set of parameters.
652
- # Should only be used by rack-mount.
653
- #
654
- # @param params [Hash] The set of parameters used to expand the template.
655
- # @param recall [Hash] Default parameters used to expand the template.
656
- # @param options [Hash] Either a `:processor` or a `:parameterize` block.
657
- #
658
- # @api private
659
- def generate(params={}, recall={}, options={})
660
- merged = recall.merge(params)
661
- if options[:processor]
662
- processor = options[:processor]
663
- elsif options[:parameterize]
664
- # TODO: This is sending me into fits trying to shoe-horn this into
665
- # the existing API. I think I've got this backwards and processors
666
- # should be a set of 4 optional blocks named :validate, :transform,
667
- # :match, and :restore. Having to use a singleton here is a huge
668
- # code smell.
669
- processor = Object.new
670
- class <<processor
671
- attr_accessor :block
672
- def transform(name, value)
673
- block.call(name, value)
674
- end
675
- end
676
- processor.block = options[:parameterize]
677
- else
678
- processor = nil
679
- end
680
- result = self.expand(merged, processor)
681
- result.to_s if result
682
- end
683
-
684
656
  private
685
657
  def ordered_variable_defaults
686
658
  @ordered_variable_defaults ||= begin
@@ -704,6 +676,8 @@ module Addressable
704
676
  # The expression to expand
705
677
  # @param [#validate, #transform] processor
706
678
  # An optional processor object may be supplied.
679
+ # @param [Boolean] normalize_values
680
+ # Optional flag to enable/disable unicode normalization. Default: true
707
681
  #
708
682
  # The object should respond to either the <tt>validate</tt> or
709
683
  # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
@@ -718,56 +692,36 @@ module Addressable
718
692
  # after sending the value to the transform method.
719
693
  #
720
694
  # @return [String] The expanded expression
721
- def transform_partial_capture(mapping, capture, processor = nil)
695
+ def transform_partial_capture(mapping, capture, processor = nil,
696
+ normalize_values = true)
722
697
  _, operator, varlist = *capture.match(EXPRESSION)
723
698
 
724
- vars = varlist.split(',')
699
+ vars = varlist.split(",")
725
700
 
726
- if '?' == operator
701
+ if operator == "?"
727
702
  # partial expansion of form style query variables sometimes requires a
728
703
  # slight reordering of the variables to produce a valid url.
729
704
  first_to_expand = vars.find { |varspec|
730
705
  _, name, _ = *varspec.match(VARSPEC)
731
- mapping.key? name
706
+ mapping.key?(name) && !mapping[name].nil?
732
707
  }
733
708
 
734
709
  vars = [first_to_expand] + vars.reject {|varspec| varspec == first_to_expand} if first_to_expand
735
710
  end
736
711
 
737
- vars
738
- .zip(operator_sequence(operator).take(vars.length))
739
- .reduce("") do |acc, (varspec, op)|
712
+ vars.
713
+ inject("".dup) do |acc, varspec|
740
714
  _, name, _ = *varspec.match(VARSPEC)
741
-
742
- acc << if mapping.key? name
743
- transform_capture(mapping, "{#{op}#{varspec}}", processor)
744
- else
745
- "{#{op}#{varspec}}"
746
- end
747
- end
748
- end
749
-
750
- ##
751
- # Creates a lazy Enumerator of the operators that should be used to expand
752
- # variables in a varlist starting with `operator`. For example, an operator
753
- # `"?"` results in the sequence `"?","&","&"...`
754
- #
755
- # @param [String] operator from which to generate a sequence
756
- #
757
- # @return [Enumerator] sequence of operators
758
- def operator_sequence(operator)
759
- rest_operator = if "?" == operator
760
- "&"
761
- else
762
- operator
763
- end
764
- head_operator = operator
765
-
766
- Enumerator.new do |y|
767
- y << head_operator.to_s
768
- while true
769
- y << rest_operator.to_s
770
- end
715
+ next_val = if mapping.key? name
716
+ transform_capture(mapping, "{#{operator}#{varspec}}",
717
+ processor, normalize_values)
718
+ else
719
+ "{#{operator}#{varspec}}"
720
+ end
721
+ # If we've already expanded at least one '?' operator with non-empty
722
+ # value, change to '&'
723
+ operator = "&" if (operator == "?") && (next_val != "")
724
+ acc << next_val
771
725
  end
772
726
  end
773
727
 
@@ -780,6 +734,9 @@ module Addressable
780
734
  # The expression to replace
781
735
  # @param [#validate, #transform] processor
782
736
  # An optional processor object may be supplied.
737
+ # @param [Boolean] normalize_values
738
+ # Optional flag to enable/disable unicode normalization. Default: true
739
+ #
783
740
  #
784
741
  # The object should respond to either the <tt>validate</tt> or
785
742
  # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
@@ -794,7 +751,8 @@ module Addressable
794
751
  # after sending the value to the transform method.
795
752
  #
796
753
  # @return [String] The expanded expression
797
- def transform_capture(mapping, capture, processor=nil)
754
+ def transform_capture(mapping, capture, processor=nil,
755
+ normalize_values=true)
798
756
  _, operator, varlist = *capture.match(EXPRESSION)
799
757
  return_value = varlist.split(',').inject([]) do |acc, varspec|
800
758
  _, name, modifier = *varspec.match(VARSPEC)
@@ -814,7 +772,7 @@ module Addressable
814
772
  "Can't convert #{value.class} into String or Array."
815
773
  end
816
774
 
817
- value = normalize_value(value)
775
+ value = normalize_value(value) if normalize_values
818
776
 
819
777
  if processor == nil || !processor.respond_to?(:transform)
820
778
  # Handle percent escaping
@@ -877,7 +835,9 @@ module Addressable
877
835
  end
878
836
  if processor.respond_to?(:transform)
879
837
  transformed_value = processor.transform(name, value)
880
- transformed_value = normalize_value(transformed_value)
838
+ if normalize_values
839
+ transformed_value = normalize_value(transformed_value)
840
+ end
881
841
  end
882
842
  end
883
843
  acc << [name, transformed_value]
@@ -979,15 +939,35 @@ module Addressable
979
939
  end
980
940
  end
981
941
 
942
+ ##
943
+ # Generates the <tt>Regexp</tt> that parses a template pattern. Memoizes the
944
+ # value if template processor not set (processors may not be deterministic)
945
+ #
946
+ # @param [String] pattern The URI template pattern.
947
+ # @param [#match] processor The template processor to use.
948
+ #
949
+ # @return [Array, Regexp]
950
+ # An array of expansion variables nad a regular expression which may be
951
+ # used to parse a template pattern
952
+ def parse_template_pattern(pattern, processor = nil)
953
+ if processor.nil? && pattern == @pattern
954
+ @cached_template_parse ||=
955
+ parse_new_template_pattern(pattern, processor)
956
+ else
957
+ parse_new_template_pattern(pattern, processor)
958
+ end
959
+ end
960
+
982
961
  ##
983
962
  # Generates the <tt>Regexp</tt> that parses a template pattern.
984
963
  #
985
964
  # @param [String] pattern The URI template pattern.
986
965
  # @param [#match] processor The template processor to use.
987
966
  #
988
- # @return [Regexp]
989
- # A regular expression which may be used to parse a template pattern.
990
- def parse_template_pattern(pattern, processor=nil)
967
+ # @return [Array, Regexp]
968
+ # An array of expansion variables nad a regular expression which may be
969
+ # used to parse a template pattern
970
+ def parse_new_template_pattern(pattern, processor = nil)
991
971
  # Escape the pattern. The two gsubs restore the escaped curly braces
992
972
  # back to their original form. Basically, escape everything that isn't
993
973
  # within an expansion.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # encoding:utf-8
2
4
  #--
3
5
  # Copyright (C) Bob Aman
@@ -46,12 +48,21 @@ module Addressable
46
48
  PCHAR = UNRESERVED + SUB_DELIMS + "\\:\\@"
47
49
  SCHEME = ALPHA + DIGIT + "\\-\\+\\."
48
50
  HOST = UNRESERVED + SUB_DELIMS + "\\[\\:\\]"
49
- AUTHORITY = PCHAR
51
+ AUTHORITY = PCHAR + "\\[\\:\\]"
50
52
  PATH = PCHAR + "\\/"
51
53
  QUERY = PCHAR + "\\/\\?"
52
54
  FRAGMENT = PCHAR + "\\/\\?"
53
55
  end
54
56
 
57
+ module NormalizeCharacterClasses
58
+ HOST = /[^#{CharacterClasses::HOST}]/
59
+ UNRESERVED = /[^#{CharacterClasses::UNRESERVED}]/
60
+ PCHAR = /[^#{CharacterClasses::PCHAR}]/
61
+ SCHEME = /[^#{CharacterClasses::SCHEME}]/
62
+ FRAGMENT = /[^#{CharacterClasses::FRAGMENT}]/
63
+ QUERY = %r{[^a-zA-Z0-9\-\.\_\~\!\$\'\(\)\*\+\,\=\:\@\/\?%]|%(?!2B|2b)}
64
+ end
65
+
55
66
  SLASH = '/'
56
67
  EMPTY_STR = ''
57
68
 
@@ -71,7 +82,7 @@ module Addressable
71
82
  "wais" => 210,
72
83
  "ldap" => 389,
73
84
  "prospero" => 1525
74
- }
85
+ }.freeze
75
86
 
76
87
  ##
77
88
  # Returns a URI object based on the parsed string.
@@ -122,9 +133,9 @@ module Addressable
122
133
  user = userinfo.strip[/^([^:]*):?/, 1]
123
134
  password = userinfo.strip[/:(.*)$/, 1]
124
135
  end
125
- host = authority.gsub(
136
+ host = authority.sub(
126
137
  /^([^\[\]]*)@/, EMPTY_STR
127
- ).gsub(
138
+ ).sub(
128
139
  /:([^:@\[\]]*?)$/, EMPTY_STR
129
140
  )
130
141
  port = authority[/:([^:@\[\]]*?)$/, 1]
@@ -182,26 +193,33 @@ module Addressable
182
193
  :scheme => "http"
183
194
  }.merge(hints)
184
195
  case uri
185
- when /^http:\/+/
186
- uri.gsub!(/^http:\/+/, "http://")
187
- when /^https:\/+/
188
- uri.gsub!(/^https:\/+/, "https://")
189
- when /^feed:\/+http:\/+/
190
- uri.gsub!(/^feed:\/+http:\/+/, "feed:http://")
191
- when /^feed:\/+/
192
- uri.gsub!(/^feed:\/+/, "feed://")
193
- when /^file:\/+/
194
- uri.gsub!(/^file:\/+/, "file:///")
196
+ when /^http:\//i
197
+ uri.sub!(/^http:\/+/i, "http://")
198
+ when /^https:\//i
199
+ uri.sub!(/^https:\/+/i, "https://")
200
+ when /^feed:\/+http:\//i
201
+ uri.sub!(/^feed:\/+http:\/+/i, "feed:http://")
202
+ when /^feed:\//i
203
+ uri.sub!(/^feed:\/+/i, "feed://")
204
+ when %r[^file:/{4}]i
205
+ uri.sub!(%r[^file:/+]i, "file:////")
206
+ when %r[^file://localhost/]i
207
+ uri.sub!(%r[^file://localhost/+]i, "file:///")
208
+ when %r[^file:/+]i
209
+ uri.sub!(%r[^file:/+]i, "file:///")
195
210
  when /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/
196
- uri.gsub!(/^/, hints[:scheme] + "://")
211
+ uri.sub!(/^/, hints[:scheme] + "://")
212
+ when /\A\d+\..*:\d+\z/
213
+ uri = "#{hints[:scheme]}://#{uri}"
197
214
  end
198
215
  match = uri.match(URIREGEX)
199
216
  fragments = match.captures
200
217
  authority = fragments[3]
201
218
  if authority && authority.length > 0
202
- new_authority = authority.gsub(/\\/, '/').gsub(/ /, '%20')
219
+ new_authority = authority.tr("\\", "/").gsub(" ", "%20")
203
220
  # NOTE: We want offset 4, not 3!
204
221
  offset = match.offset(4)
222
+ uri = uri.dup
205
223
  uri[offset[0]...offset[1]] = new_authority
206
224
  end
207
225
  parsed = self.parse(uri)
@@ -209,10 +227,11 @@ module Addressable
209
227
  parsed = self.parse(hints[:scheme] + "://" + uri)
210
228
  end
211
229
  if parsed.path.include?(".")
212
- new_host = parsed.path[/^([^\/]+\.[^\/]*)/, 1]
213
- if new_host
230
+ if parsed.path[/\b@\b/]
231
+ parsed.scheme = "mailto" unless parsed.scheme
232
+ elsif new_host = parsed.path[/^([^\/]+\.[^\/]*)/, 1]
214
233
  parsed.defer_validation do
215
- new_path = parsed.path.gsub(
234
+ new_path = parsed.path.sub(
216
235
  Regexp.new("^" + Regexp.escape(new_host)), EMPTY_STR)
217
236
  parsed.host = new_host
218
237
  parsed.path = new_path
@@ -263,24 +282,24 @@ module Addressable
263
282
  # Otherwise, convert to a String
264
283
  path = path.to_str.strip
265
284
 
266
- path.gsub!(/^file:\/?\/?/, EMPTY_STR) if path =~ /^file:\/?\/?/
285
+ path.sub!(/^file:\/?\/?/, EMPTY_STR) if path =~ /^file:\/?\/?/
267
286
  path = SLASH + path if path =~ /^([a-zA-Z])[\|:]/
268
287
  uri = self.parse(path)
269
288
 
270
289
  if uri.scheme == nil
271
290
  # Adjust windows-style uris
272
- uri.path.gsub!(/^\/?([a-zA-Z])[\|:][\\\/]/) do
291
+ uri.path.sub!(/^\/?([a-zA-Z])[\|:][\\\/]/) do
273
292
  "/#{$1.downcase}:/"
274
293
  end
275
- uri.path.gsub!(/\\/, SLASH)
294
+ uri.path.tr!("\\", SLASH)
276
295
  if File.exist?(uri.path) &&
277
296
  File.stat(uri.path).directory?
278
- uri.path.gsub!(/\/$/, EMPTY_STR)
297
+ uri.path.chomp!(SLASH)
279
298
  uri.path = uri.path + '/'
280
299
  end
281
300
 
282
301
  # If the path is absolute, set the scheme and host.
283
- if uri.path =~ /^\//
302
+ if uri.path.start_with?(SLASH)
284
303
  uri.scheme = "file"
285
304
  uri.host = EMPTY_STR
286
305
  end
@@ -317,6 +336,21 @@ module Addressable
317
336
  return result
318
337
  end
319
338
 
339
+ ##
340
+ # Tables used to optimize encoding operations in `self.encode_component`
341
+ # and `self.normalize_component`
342
+ SEQUENCE_ENCODING_TABLE = Hash.new do |hash, sequence|
343
+ hash[sequence] = sequence.unpack("C*").map do |c|
344
+ format("%02x", c)
345
+ end.join
346
+ end
347
+
348
+ SEQUENCE_UPCASED_PERCENT_ENCODING_TABLE = Hash.new do |hash, sequence|
349
+ hash[sequence] = sequence.unpack("C*").map do |c|
350
+ format("%%%02X", c)
351
+ end.join
352
+ end
353
+
320
354
  ##
321
355
  # Percent encodes a URI component.
322
356
  #
@@ -383,18 +417,20 @@ module Addressable
383
417
  component.force_encoding(Encoding::ASCII_8BIT)
384
418
  # Avoiding gsub! because there are edge cases with frozen strings
385
419
  component = component.gsub(character_class) do |sequence|
386
- (sequence.unpack('C*').map { |c| "%" + ("%02x" % c).upcase }).join
420
+ SEQUENCE_UPCASED_PERCENT_ENCODING_TABLE[sequence]
387
421
  end
388
422
  if upcase_encoded.length > 0
389
- component = component.gsub(/%(#{upcase_encoded.chars.map do |char|
390
- char.unpack('C*').map { |c| '%02x' % c }.join
391
- end.join('|')})/i) { |s| s.upcase }
423
+ upcase_encoded_chars = upcase_encoded.chars.map do |char|
424
+ SEQUENCE_ENCODING_TABLE[char]
425
+ end
426
+ component = component.gsub(/%(#{upcase_encoded_chars.join('|')})/,
427
+ &:upcase)
392
428
  end
393
429
  return component
394
430
  end
395
431
 
396
432
  class << self
397
- alias_method :encode_component, :encode_component
433
+ alias_method :escape_component, :encode_component
398
434
  end
399
435
 
400
436
  ##
@@ -436,7 +472,11 @@ module Addressable
436
472
  uri = uri.dup
437
473
  # Seriously, only use UTF-8. I'm really not kidding!
438
474
  uri.force_encoding("utf-8")
439
- leave_encoded.force_encoding("utf-8")
475
+
476
+ unless leave_encoded.empty?
477
+ leave_encoded = leave_encoded.dup.force_encoding("utf-8")
478
+ end
479
+
440
480
  result = uri.gsub(/%[0-9a-f]{2}/iu) do |sequence|
441
481
  c = sequence[1..3].to_i(16).chr
442
482
  c.force_encoding("utf-8")
@@ -522,12 +562,16 @@ module Addressable
522
562
  character_class = "#{character_class}%" unless character_class.include?('%')
523
563
 
524
564
  "|%(?!#{leave_encoded.chars.map do |char|
525
- seq = char.unpack('C*').map { |c| '%02x' % c }.join
565
+ seq = SEQUENCE_ENCODING_TABLE[char]
526
566
  [seq.upcase, seq.downcase]
527
567
  end.flatten.join('|')})"
528
568
  end
529
569
 
530
- character_class = /[^#{character_class}]#{leave_re}/
570
+ character_class = if leave_re
571
+ /[^#{character_class}]#{leave_re}/
572
+ else
573
+ /[^#{character_class}]/
574
+ end
531
575
  end
532
576
  # We can't perform regexps on invalid UTF sequences, but
533
577
  # here we need to, so switch to ASCII.
@@ -847,11 +891,11 @@ module Addressable
847
891
  return nil unless self.scheme
848
892
  @normalized_scheme ||= begin
849
893
  if self.scheme =~ /^\s*ssh\+svn\s*$/i
850
- "svn+ssh"
894
+ "svn+ssh".dup
851
895
  else
852
896
  Addressable::URI.normalize_component(
853
897
  self.scheme.strip.downcase,
854
- Addressable::URI::CharacterClasses::SCHEME
898
+ Addressable::URI::NormalizeCharacterClasses::SCHEME
855
899
  )
856
900
  end
857
901
  end
@@ -871,7 +915,7 @@ module Addressable
871
915
  new_scheme = new_scheme.to_str
872
916
  end
873
917
  if new_scheme && new_scheme !~ /\A[a-z][a-z0-9\.\+\-]*\z/i
874
- raise InvalidURIError, "Invalid scheme format: #{new_scheme}"
918
+ raise InvalidURIError, "Invalid scheme format: '#{new_scheme}'"
875
919
  end
876
920
  @scheme = new_scheme
877
921
  @scheme = nil if @scheme.to_s.strip.empty?
@@ -906,7 +950,7 @@ module Addressable
906
950
  else
907
951
  Addressable::URI.normalize_component(
908
952
  self.user.strip,
909
- Addressable::URI::CharacterClasses::UNRESERVED
953
+ Addressable::URI::NormalizeCharacterClasses::UNRESERVED
910
954
  )
911
955
  end
912
956
  end
@@ -963,7 +1007,7 @@ module Addressable
963
1007
  else
964
1008
  Addressable::URI.normalize_component(
965
1009
  self.password.strip,
966
- Addressable::URI::CharacterClasses::UNRESERVED
1010
+ Addressable::URI::NormalizeCharacterClasses::UNRESERVED
967
1011
  )
968
1012
  end
969
1013
  end
@@ -1032,9 +1076,9 @@ module Addressable
1032
1076
  if !current_user && !current_password
1033
1077
  nil
1034
1078
  elsif current_user && current_password
1035
- "#{current_user}:#{current_password}"
1079
+ "#{current_user}:#{current_password}".dup
1036
1080
  elsif current_user && !current_password
1037
- "#{current_user}"
1081
+ "#{current_user}".dup
1038
1082
  end
1039
1083
  end
1040
1084
  # All normalized values should be UTF-8
@@ -1087,6 +1131,7 @@ module Addressable
1087
1131
  # @return [String] The host component, normalized.
1088
1132
  def normalized_host
1089
1133
  return nil unless self.host
1134
+
1090
1135
  @normalized_host ||= begin
1091
1136
  if !self.host.strip.empty?
1092
1137
  result = ::Addressable::IDNA.to_ascii(
@@ -1098,14 +1143,17 @@ module Addressable
1098
1143
  end
1099
1144
  result = Addressable::URI.normalize_component(
1100
1145
  result,
1101
- CharacterClasses::HOST)
1146
+ NormalizeCharacterClasses::HOST
1147
+ )
1102
1148
  result
1103
1149
  else
1104
- EMPTY_STR
1150
+ EMPTY_STR.dup
1105
1151
  end
1106
1152
  end
1107
1153
  # All normalized values should be UTF-8
1108
- @normalized_host.force_encoding(Encoding::UTF_8) if @normalized_host
1154
+ if @normalized_host && !@normalized_host.empty?
1155
+ @normalized_host.force_encoding(Encoding::UTF_8)
1156
+ end
1109
1157
  @normalized_host
1110
1158
  end
1111
1159
 
@@ -1163,16 +1211,25 @@ module Addressable
1163
1211
  # Returns the top-level domain for this host.
1164
1212
  #
1165
1213
  # @example
1166
- # Addressable::URI.parse("www.example.co.uk").tld # => "co.uk"
1214
+ # Addressable::URI.parse("http://www.example.co.uk").tld # => "co.uk"
1167
1215
  def tld
1168
1216
  PublicSuffix.parse(self.host, ignore_private: true).tld
1169
1217
  end
1170
1218
 
1219
+ ##
1220
+ # Sets the top-level domain for this URI.
1221
+ #
1222
+ # @param [String, #to_str] new_tld The new top-level domain.
1223
+ def tld=(new_tld)
1224
+ replaced_tld = host.sub(/#{tld}\z/, new_tld)
1225
+ self.host = PublicSuffix::Domain.new(replaced_tld).to_s
1226
+ end
1227
+
1171
1228
  ##
1172
1229
  # Returns the public suffix domain for this host.
1173
1230
  #
1174
1231
  # @example
1175
- # Addressable::URI.parse("www.example.co.uk").domain # => "example.co.uk"
1232
+ # Addressable::URI.parse("http://www.example.co.uk").domain # => "example.co.uk"
1176
1233
  def domain
1177
1234
  PublicSuffix.domain(self.host, ignore_private: true)
1178
1235
  end
@@ -1235,9 +1292,9 @@ module Addressable
1235
1292
  new_user = new_userinfo.strip[/^([^:]*):?/, 1]
1236
1293
  new_password = new_userinfo.strip[/:(.*)$/, 1]
1237
1294
  end
1238
- new_host = new_authority.gsub(
1295
+ new_host = new_authority.sub(
1239
1296
  /^([^\[\]]*)@/, EMPTY_STR
1240
- ).gsub(
1297
+ ).sub(
1241
1298
  /:([^:@\[\]]*?)$/, EMPTY_STR
1242
1299
  )
1243
1300
  new_port =
@@ -1421,7 +1478,7 @@ module Addressable
1421
1478
  # @return [String] The components that identify a site.
1422
1479
  def site
1423
1480
  (self.scheme || self.authority) && @site ||= begin
1424
- site_string = ""
1481
+ site_string = "".dup
1425
1482
  site_string << "#{self.scheme}:" if self.scheme != nil
1426
1483
  site_string << "//#{self.authority}" if self.authority != nil
1427
1484
  site_string
@@ -1440,7 +1497,7 @@ module Addressable
1440
1497
  def normalized_site
1441
1498
  return nil unless self.site
1442
1499
  @normalized_site ||= begin
1443
- site_string = ""
1500
+ site_string = "".dup
1444
1501
  if self.normalized_scheme != nil
1445
1502
  site_string << "#{self.normalized_scheme}:"
1446
1503
  end
@@ -1501,14 +1558,14 @@ module Addressable
1501
1558
  result = path.strip.split(SLASH, -1).map do |segment|
1502
1559
  Addressable::URI.normalize_component(
1503
1560
  segment,
1504
- Addressable::URI::CharacterClasses::PCHAR
1561
+ Addressable::URI::NormalizeCharacterClasses::PCHAR
1505
1562
  )
1506
1563
  end.join(SLASH)
1507
1564
 
1508
1565
  result = URI.normalize_path(result)
1509
1566
  if result.empty? &&
1510
1567
  ["http", "https", "ftp", "tftp"].include?(self.normalized_scheme)
1511
- result = SLASH
1568
+ result = SLASH.dup
1512
1569
  end
1513
1570
  result
1514
1571
  end
@@ -1544,7 +1601,7 @@ module Addressable
1544
1601
  # @return [String] The path's basename.
1545
1602
  def basename
1546
1603
  # Path cannot be nil
1547
- return File.basename(self.path).gsub(/;[^\/]*$/, EMPTY_STR)
1604
+ return File.basename(self.path).sub(/;[^\/]*$/, EMPTY_STR)
1548
1605
  end
1549
1606
 
1550
1607
  ##
@@ -1576,10 +1633,15 @@ module Addressable
1576
1633
  modified_query_class = Addressable::URI::CharacterClasses::QUERY.dup
1577
1634
  # Make sure possible key-value pair delimiters are escaped.
1578
1635
  modified_query_class.sub!("\\&", "").sub!("\\;", "")
1579
- pairs = (self.query || "").split("&", -1)
1636
+ pairs = (query || "").split("&", -1)
1637
+ pairs.delete_if(&:empty?).uniq! if flags.include?(:compacted)
1580
1638
  pairs.sort! if flags.include?(:sorted)
1581
1639
  component = pairs.map do |pair|
1582
- Addressable::URI.normalize_component(pair, modified_query_class, "+")
1640
+ Addressable::URI.normalize_component(
1641
+ pair,
1642
+ Addressable::URI::NormalizeCharacterClasses::QUERY,
1643
+ "+"
1644
+ )
1583
1645
  end.join("&")
1584
1646
  component == "" ? nil : component
1585
1647
  end
@@ -1638,11 +1700,13 @@ module Addressable
1638
1700
  # so it's best to make all changes in-place.
1639
1701
  pair[0] = URI.unencode_component(pair[0])
1640
1702
  if pair[1].respond_to?(:to_str)
1703
+ value = pair[1].to_str
1641
1704
  # I loathe the fact that I have to do this. Stupid HTML 4.01.
1642
1705
  # Treating '+' as a space was just an unbelievably bad idea.
1643
1706
  # There was nothing wrong with '%20'!
1644
1707
  # If it ain't broke, don't fix it!
1645
- pair[1] = URI.unencode_component(pair[1].to_str.gsub(/\+/, " "))
1708
+ value = value.tr("+", " ") if ["http", "https", nil].include?(scheme)
1709
+ pair[1] = URI.unencode_component(value)
1646
1710
  end
1647
1711
  if return_type == Hash
1648
1712
  accu[pair[0]] = pair[1]
@@ -1694,7 +1758,7 @@ module Addressable
1694
1758
  end
1695
1759
 
1696
1760
  # new_query_values have form [['key1', 'value1'], ['key2', 'value2']]
1697
- buffer = ""
1761
+ buffer = "".dup
1698
1762
  new_query_values.each do |key, value|
1699
1763
  encoded_key = URI.encode_component(
1700
1764
  key, CharacterClasses::UNRESERVED
@@ -1724,7 +1788,7 @@ module Addressable
1724
1788
  #
1725
1789
  # @return [String] The request URI required for an HTTP request.
1726
1790
  def request_uri
1727
- return nil if self.absolute? && self.scheme !~ /^https?$/
1791
+ return nil if self.absolute? && self.scheme !~ /^https?$/i
1728
1792
  return (
1729
1793
  (!self.path.empty? ? self.path : SLASH) +
1730
1794
  (self.query ? "?#{self.query}" : EMPTY_STR)
@@ -1739,12 +1803,12 @@ module Addressable
1739
1803
  if !new_request_uri.respond_to?(:to_str)
1740
1804
  raise TypeError, "Can't convert #{new_request_uri.class} into String."
1741
1805
  end
1742
- if self.absolute? && self.scheme !~ /^https?$/
1806
+ if self.absolute? && self.scheme !~ /^https?$/i
1743
1807
  raise InvalidURIError,
1744
1808
  "Cannot set an HTTP request URI for a non-HTTP URI."
1745
1809
  end
1746
1810
  new_request_uri = new_request_uri.to_str
1747
- path_component = new_request_uri[/^([^\?]*)\?(?:.*)$/, 1]
1811
+ path_component = new_request_uri[/^([^\?]*)\??(?:.*)$/, 1]
1748
1812
  query_component = new_request_uri[/^(?:[^\?]*)\?(.*)$/, 1]
1749
1813
  path_component = path_component.to_s
1750
1814
  path_component = (!path_component.empty? ? path_component : SLASH)
@@ -1773,7 +1837,7 @@ module Addressable
1773
1837
  @normalized_fragment ||= begin
1774
1838
  component = Addressable::URI.normalize_component(
1775
1839
  self.fragment,
1776
- Addressable::URI::CharacterClasses::FRAGMENT
1840
+ Addressable::URI::NormalizeCharacterClasses::FRAGMENT
1777
1841
  )
1778
1842
  component == "" ? nil : component
1779
1843
  end
@@ -1899,8 +1963,8 @@ module Addressable
1899
1963
  # Section 5.2.3 of RFC 3986
1900
1964
  #
1901
1965
  # Removes the right-most path segment from the base path.
1902
- if base_path =~ /\//
1903
- base_path.gsub!(/\/[^\/]+$/, SLASH)
1966
+ if base_path.include?(SLASH)
1967
+ base_path.sub!(/\/[^\/]+$/, SLASH)
1904
1968
  else
1905
1969
  base_path = EMPTY_STR
1906
1970
  end
@@ -2349,10 +2413,10 @@ module Addressable
2349
2413
  #
2350
2414
  # @param [Proc] block
2351
2415
  # A set of operations to perform on a given URI.
2352
- def defer_validation(&block)
2353
- raise LocalJumpError, "No block given." unless block
2416
+ def defer_validation
2417
+ raise LocalJumpError, "No block given." unless block_given?
2354
2418
  @validation_deferred = true
2355
- block.call()
2419
+ yield
2356
2420
  @validation_deferred = false
2357
2421
  validate
2358
2422
  return nil