addressable 2.5.0 → 2.8.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,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