addressable 2.9.0 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ffaab8b78df30a2126058a425f168d76b26bc33e60849cabc1a6beabae24464d
4
- data.tar.gz: 841ab2bd18fbcf3ff746cb85ea1661e628e278e6c7bc5ad96cd480e36a54f067
3
+ metadata.gz: a6d91298f649477b2a758f6205bd3598d071f8c6ee6c063e6a906833d7b1c22d
4
+ data.tar.gz: '0911936040e27dda609f926393afbd2b5791ec217e25abd02cb744d512e6cf32'
5
5
  SHA512:
6
- metadata.gz: f5884313cb2c68ea73d25e8f7f0a76200030bb3fb7abad8b31532180b1ae3c1df10f8db614a273dba115c87860682d227fdf046c743370e020a577cae667d026
7
- data.tar.gz: c97dd91446991ce20400e9b4c2586aefbc1dca9fd1d49efca9606fc505ff0d49807686ab1fdfd7d4dba447c9c4bf8fb6c0bbd5a1d3d69a4952207d035e912dc2
6
+ metadata.gz: 82bc575aff52883ab8834cd6e588db9e086f9721ef86dc93a43120ac4ae7b3ea88da1d74c05ca1f96ad3890854152f363acf02064c2a754dcd02101f5b509b90
7
+ data.tar.gz: f5da7b05d4d49ed517a5e3773adbb3146069492e69ee6666285fc9f298925b2805227003489abb5ee353270f23197c165e1b4c6d5b5588d03f887a5a44f4822b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,71 @@
1
1
  # Changelog
2
2
 
3
+ ## Addressable 2.9.1 <a name="v2.9.1">
4
+ - fixes denial of service in `Addressable::URI.normalize_path`, which took
5
+ quadratic time on paths with many dot segments; `URI#normalize`, `#join`,
6
+ `#==` and `#===` were affected (GHSA-qq38-m9mg-xjj5)
7
+ - fixes `Addressable::URI.normalize_path` keeping segments that a `..` should
8
+ have removed after a `..` above the root, e.g.
9
+ `/../../public/x/../../secret` normalized to `/public/x/secret` instead of
10
+ `/secret` (GHSA-8gmh-2hhj-2hv9)
11
+ - fixes `Addressable::URI.normalize_path` treating a raw line feed as a
12
+ segment boundary, e.g. joining `..\n/secret` onto `/public/` gave
13
+ `/\n/secret`
14
+ - deprecates `Addressable::URI::RULE_2A`, `RULE_2B_2C`, `RULE_2D` and
15
+ `RULE_PREFIXED_PARENT`, which are no longer used; they will be removed in
16
+ 2.10.0
17
+ - fixes denial of service in `Addressable::Template#match` and `#extract`,
18
+ which still backtracked exponentially in 2.9.0 for exploded `/`, `.`, `;`,
19
+ `?` and `&` expressions (e.g. `{/path*}`) on regex engines without match
20
+ memoization, such as JRuby, TruffleRuby and MRI before 3.2; match results
21
+ are unchanged (GHSA-j82w-28jv-3pmj)
22
+ - fixes denial of service in `Addressable::Template#match` and `#extract`,
23
+ which backtracked polynomially or exponentially on the same regex engines
24
+ for expressions with several variables (e.g. `{/owner,repo,ref}`, or
25
+ `{+a,b,c}` with many variables), for `;` expressions, and for adjacent
26
+ expressions (e.g. `{+a}{+b}{+c}` or `{+a}{b}`); match results are unchanged
27
+ (GHSA-9f42-57jx-vpp3)
28
+ - fixes denial of service in `Addressable::Template#match` and `#extract`,
29
+ which backtracked polynomially for expressions with several exploded
30
+ variables (e.g. `{+a*,b*}`), including on MRI 3.2; match results are
31
+ unchanged (GHSA-4w5q-wg7v-w6gp)
32
+ - fixes `Addressable::IDNA.to_unicode` in the pure-Ruby backend decoding
33
+ Punycode labels that aren't valid A-labels: labels that decode to only
34
+ ASCII characters (e.g. `xn--google-` decoded to `google`) and labels that
35
+ don't convert back to themselves; such a label is now returned unchanged,
36
+ as with libidn, so `Addressable::URI#display_uri` no longer shows a host
37
+ that differs from `#normalized_host` (GHSA-9crm-g4mf-wc5j)
38
+ - fixes `Addressable::IDNA.to_unicode` decoding Punycode labels with
39
+ characters that change how a host displays without being visible
40
+ themselves, such as bidi overrides and isolates (e.g. U+202E and U+2066),
41
+ zero width characters and control characters; such a label is now
42
+ returned unchanged, so `Addressable::URI#display_uri` no longer shows
43
+ them. The pure-Ruby backend also no longer decodes labels with other
44
+ characters that nameprep prohibits, as with libidn (GHSA-539f-xcgw-2gjg)
45
+ - fixes `Addressable::URI#route_from` and `#route_to` raising
46
+ `InvalidURIError` when the route's first segment contains a colon
47
+ (e.g. `/a:b` from `/a`); the route is now `./a:b` ([#625])
48
+ - fixes `Addressable::IDNA.to_unicode` raising in the pure-Ruby backend for
49
+ Punycode labels that are too long, overflow the decoder, or decode past
50
+ U+10FFFF; the label is now returned unchanged, as with libidn, so
51
+ `Addressable::URI#display_uri` no longer raises for such hosts ([#620])
52
+ - fixes `Addressable::URI#display_uri` raising `InvalidURIError` for URIs
53
+ without a host, such as `abc` or `mailto:user@example.com`; relative URIs
54
+ are also no longer displayed with an empty authority (e.g. `/path/to` was
55
+ displayed as `///path/to`) ([#610])
56
+ - fixes `Addressable::Template#match` and `#extract` raising
57
+ `Addressable::URI::InvalidURIError` for a string that isn't a valid URI;
58
+ they now return `nil`, as for any other URI that doesn't match ([#621])
59
+ - fixes `Addressable::URI#origin=` raising `InvalidURIError` for an origin
60
+ with an empty port (e.g. `http://example.com:`), which is the same as no
61
+ port ([#627])
62
+
63
+ [#610]: https://github.com/sporkmonger/addressable/pull/610
64
+ [#620]: https://github.com/sporkmonger/addressable/pull/620
65
+ [#621]: https://github.com/sporkmonger/addressable/pull/621
66
+ [#625]: https://github.com/sporkmonger/addressable/pull/625
67
+ [#627]: https://github.com/sporkmonger/addressable/pull/627
68
+
3
69
  ## Addressable 2.9.0 <a name="v2.9.0">
4
70
  - fixes ReDoS vulnerability in Addressable::Template#match (fixes incomplete
5
71
  remediation in 2.8.10)
@@ -51,10 +51,18 @@ module Addressable
51
51
  end.join('.')
52
52
  end
53
53
 
54
+ # Characters that change how a label displays without being visible
55
+ # themselves: controls, format characters such as bidi overrides and
56
+ # isolates, and default ignorable code points. libidn rejects most of
57
+ # them, but not those added to Unicode after the version it checks
58
+ # against, such as U+2066 to U+2069.
59
+ DISPLAY_CHANGING = /[\p{Cc}\p{Cf}\p{Default_Ignorable_Code_Point}]/
60
+
54
61
  def self.to_unicode(value)
55
62
  value.to_s.split('.', -1).map do |segment|
56
63
  if segment.size > 0 && segment.size < 64
57
- IDN::Idna.toUnicode(segment, IDN::Idna::ALLOW_UNASSIGNED)
64
+ decoded = IDN::Idna.toUnicode(segment, IDN::Idna::ALLOW_UNASSIGNED)
65
+ decoded =~ DISPLAY_CHANGING ? segment : decoded
58
66
  elsif segment.size >= 64
59
67
  segment
60
68
  else
@@ -55,6 +55,39 @@ module Addressable
55
55
  | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
56
56
  )/mnx
57
57
 
58
+ # Characters that nameprep doesn't allow in a label (Section 5 of
59
+ # RFC 3491), or that it maps to nothing, so they can't be in a label that
60
+ # toASCII produces.
61
+ NAMEPREP_PROHIBITED = Regexp.new("[" + [
62
+ # Table C.1.2, non-ASCII space characters
63
+ "\u00A0\u1680\u2000-\u200B\u202F\u205F\u3000",
64
+ # Table C.2.2, non-ASCII control characters
65
+ "\u0080-\u009F\u06DD\u070F\u180E\u200C\u200D\u2028\u2029" +
66
+ "\u2060-\u2063\u206A-\u206F\uFEFF\uFFF9-\uFFFC\u{1D173}-\u{1D17A}",
67
+ # Table C.3, private use
68
+ "\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}",
69
+ # Table C.4, non-character code points
70
+ "\uFDD0-\uFDEF" + (0..16).map do |plane|
71
+ [plane * 0x10000 + 0xFFFE, 0x2D, plane * 0x10000 + 0xFFFF].pack("U*")
72
+ end.join,
73
+ # Table C.6, inappropriate for plain text
74
+ "\uFFF9-\uFFFD",
75
+ # Table C.7, inappropriate for canonical representation
76
+ "\u2FF0-\u2FFB",
77
+ # Table C.8, change display properties or are deprecated
78
+ "\u0340\u0341\u200E\u200F\u202A-\u202E\u206A-\u206F",
79
+ # Table C.9, tagging characters
80
+ "\u{E0001}\u{E0020}-\u{E007F}",
81
+ # Table B.1, commonly mapped to nothing
82
+ "\u00AD\u034F\u1806\u180B-\u180D\u200B-\u200D\u2060\uFE00-\uFE0F\uFEFF"
83
+ ].join + "]")
84
+
85
+ # Characters that change how a label displays without being visible
86
+ # themselves: controls, format characters such as bidi overrides and
87
+ # isolates, and default ignorable code points, including those added to
88
+ # Unicode after the version nameprep is based on.
89
+ DISPLAY_CHANGING = /[\p{Cc}\p{Cf}\p{Default_Ignorable_Code_Point}]/
90
+
58
91
  # :startdoc:
59
92
 
60
93
  # Converts from a Unicode internationalized domain name to an ASCII
@@ -91,8 +124,22 @@ module Addressable
91
124
  parts.map! do |part|
92
125
  if part =~ /^#{ACE_PREFIX}(.+)/
93
126
  begin
94
- punycode_decode(part[/^#{ACE_PREFIX}(.+)/, 1])
95
- rescue Addressable::IDNA::PunycodeBadInput
127
+ decoded = punycode_decode(part[/^#{ACE_PREFIX}(.+)/, 1])
128
+ # A label that doesn't convert back to itself isn't a valid
129
+ # A-label (Section 4.2 of RFC 3490, steps 6 and 7). That includes
130
+ # a label that decodes to only ASCII characters. A label with a
131
+ # character that would change how it displays is returned
132
+ # unchanged too.
133
+ if to_ascii(decoded).downcase != part.downcase ||
134
+ decoded =~ NAMEPREP_PROHIBITED || decoded =~ DISPLAY_CHANGING
135
+ part
136
+ else
137
+ decoded
138
+ end
139
+ rescue Addressable::IDNA::PunycodeBadInput,
140
+ Addressable::IDNA::PunycodeBigOutput,
141
+ Addressable::IDNA::PunycodeOverflow,
142
+ RangeError, ArgumentError
96
143
  # toUnicode is explicitly defined as never-fails by the spec
97
144
  part
98
145
  end
@@ -413,15 +413,25 @@ module Addressable
413
413
  # match.captures
414
414
  # #=> ["a", ["b", "c"]]
415
415
  def match(uri, processor=nil)
416
- uri = Addressable::URI.parse(uri) unless uri.is_a?(Addressable::URI)
416
+ unless uri.is_a?(Addressable::URI)
417
+ begin
418
+ uri = Addressable::URI.parse(uri)
419
+ rescue Addressable::URI::InvalidURIError
420
+ # Per the documented contract, match returns nil for any input
421
+ # that doesn't match the template - including a string that
422
+ # isn't even a valid URI.
423
+ return nil
424
+ end
425
+ end
417
426
  mapping = {}
418
427
 
419
428
  # First, we need to process the pattern, and extract the values.
420
429
  expansions, expansion_regexp =
421
430
  parse_template_pattern(pattern, processor)
422
431
 
423
- return nil unless uri.to_str.match(expansion_regexp)
424
- unparsed_values = uri.to_str.scan(expansion_regexp).flatten
432
+ matched = uri.to_str.match(expansion_regexp)
433
+ return nil unless matched
434
+ unparsed_values = matched.captures
425
435
 
426
436
  if uri.to_str == pattern
427
437
  return Addressable::Template::MatchData.new(uri, self, mapping)
@@ -958,6 +968,77 @@ module Addressable
958
968
  end
959
969
  end
960
970
 
971
+ ##
972
+ # Describes what follows each expression in a template pattern.
973
+ #
974
+ # @param [String] escaped_pattern The escaped URI template pattern.
975
+ #
976
+ # @return [Array<Array>] For each expression, the <tt>MatchData</tt> of
977
+ # the expression directly after it (or <tt>nil</tt>), and whether the
978
+ # text after it could start with a comma.
979
+ def expression_followers(escaped_pattern)
980
+ expressions = escaped_pattern.to_enum(:scan, EXPRESSION).map do
981
+ Regexp.last_match
982
+ end
983
+ expressions.each_with_index.map do |current, index|
984
+ following = expressions[index + 1]
985
+ following = nil unless following && following.begin(0) == current.end(0)
986
+ # Expressions can match nothing, so look past any that can't start
987
+ # with a comma.
988
+ position = current.end(0)
989
+ comma = expressions[(index + 1)..-1].any? do |later|
990
+ break false unless later.begin(0) == position
991
+ position = later.end(0)
992
+ may_start_with_comma?(later[1], later[2])
993
+ end
994
+ [following, comma || escaped_pattern[position] == ',']
995
+ end
996
+ end
997
+
998
+ ##
999
+ # Whether an expression could match a string that starts with a comma.
1000
+ #
1001
+ # @param [String, nil] operator The expression's operator.
1002
+ # @param [String] varlist The expression's variable list.
1003
+ #
1004
+ # @return [TrueClass, FalseClass]
1005
+ def may_start_with_comma?(operator, varlist)
1006
+ return false unless LEADERS.fetch(operator, '').empty?
1007
+ operator == '+' || !(varlist =~ VARSPEC && $2 != '*')
1008
+ end
1009
+
1010
+ ##
1011
+ # Whether an expression, followed directly by another one, could only
1012
+ # match characters that the other one could also match, in any amount.
1013
+ #
1014
+ # @param [MatchData] following The expression that follows.
1015
+ # @param [String, nil] operator The operator of the first expression.
1016
+ # @param [Array<String>] varspecs The varspecs of the first expression.
1017
+ #
1018
+ # @return [TrueClass, FalseClass]
1019
+ def absorbed_by?(following, operator, varspecs)
1020
+ last = following[2].split(',').last
1021
+ case following[1]
1022
+ when '+'
1023
+ # An unexploded last variable matches any characters that an
1024
+ # expression can, and can end anywhere.
1025
+ last[VARSPEC, 2] != '*'
1026
+ when nil
1027
+ if last[VARSPEC, 2] == '*'
1028
+ # Matches any unreserved characters and commas.
1029
+ operator.nil? || operator == '.'
1030
+ elsif following[2] == last
1031
+ # Matches any unreserved characters.
1032
+ operator == '.' || (operator.nil? && varspecs.size == 1 &&
1033
+ varspecs.first[VARSPEC, 2] != '*')
1034
+ else
1035
+ false
1036
+ end
1037
+ else
1038
+ false
1039
+ end
1040
+ end
1041
+
961
1042
  ##
962
1043
  # Generates the <tt>Regexp</tt> that parses a template pattern.
963
1044
  #
@@ -978,20 +1059,36 @@ module Addressable
978
1059
  end
979
1060
 
980
1061
  expansions = []
1062
+ # Processor patterns can match anything, so they get 2.9.0's choices.
1063
+ plain_processor = !(processor && processor.respond_to?(:match))
1064
+ followers = expression_followers(escaped_pattern)
981
1065
 
982
1066
  # Create a regular expression that captures the values of the
983
1067
  # variables in the URI.
984
1068
  regexp_string = escaped_pattern.gsub( EXPRESSION ) do |expansion|
985
1069
 
1070
+ index = expansions.size
986
1071
  expansions << expansion
987
1072
  _, operator, varlist = *expansion.match(EXPRESSION)
988
1073
  leader = Regexp.escape(LEADERS.fetch(operator, ''))
989
1074
  joiner = Regexp.escape(JOINERS.fetch(operator, ','))
990
1075
  varspecs = varlist.split(',')
991
- combined = varspecs.map do |varspec|
1076
+ results = varspecs.map do |varspec|
1077
+ name = varspec[VARSPEC, 1]
1078
+ processor && processor.respond_to?(:match) ? processor.match(name) : nil
1079
+ end
1080
+ # Without processor patterns, any variable in the expression can take
1081
+ # the characters that an earlier variable leaves, which lets the
1082
+ # variables after the first one have fewer choices below.
1083
+ plain = results.compact.empty?
1084
+ explodes = varspecs.map { |varspec| varspec[VARSPEC, 2] == '*' }
1085
+ first_explode = explodes.index(true)
1086
+ not_after_value =
1087
+ "(?<![#{Addressable::URI::CharacterClasses::UNRESERVED}])"
1088
+ combined = varspecs.each_with_index.map do |varspec, index|
992
1089
  _, name, modifier = *varspec.match(VARSPEC)
993
1090
 
994
- result = processor && processor.respond_to?(:match) ? processor.match(name) : nil
1091
+ result = results[index]
995
1092
  if result
996
1093
  "(?<#{name}>#{ result })"
997
1094
  else
@@ -1005,7 +1102,9 @@ module Addressable
1005
1102
  when '.'
1006
1103
  "#{ UNRESERVED.gsub('\.', '') }*?"
1007
1104
  when ';'
1008
- "#{ UNRESERVED }*=?#{ UNRESERVED }*?"
1105
+ # The same strings, tried in the same order, as 2.9.0's
1106
+ # "U*=?U*?", but a name without "=" can only match one way.
1107
+ "(?:#{UNRESERVED}*=#{UNRESERVED}*?|#{UNRESERVED}*)"
1009
1108
  when '?'
1010
1109
  "#{ UNRESERVED }*=#{ UNRESERVED }*?"
1011
1110
  when '&'
@@ -1014,20 +1113,117 @@ module Addressable
1014
1113
  "#{ UNRESERVED }*?"
1015
1114
  end
1016
1115
  if modifier == '*'
1116
+ # The ; ? and & members match the same strings, in the same
1117
+ # order of preference, as 2.9.0's members joined by an optional
1118
+ # joiner, but each character can only belong to one member.
1017
1119
  seg = case operator
1018
1120
  when '+', '#' then "#{RESERVED_NO_COMMA}*+"
1121
+ when ';' then "(?:#{UNRESERVED}|=)*"
1122
+ when '?', '&'
1123
+ "#{UNRESERVED}*=(?:#{UNRESERVED}*=)*#{UNRESERVED}*?"
1019
1124
  else group
1020
1125
  end
1021
- joiner_pattern = operator.nil? ? joiner : "#{joiner}?"
1022
- "(?<#{name}>#{seg}(?:#{joiner_pattern}#{seg})*)?"
1126
+ # The joiner between exploded members must be mandatory. With
1127
+ # an optional joiner, a run of members can be partitioned in
1128
+ # exponentially many ways, which backtracks catastrophically on
1129
+ # engines without match memoization (MRI < 3.2, JRuby,
1130
+ # TruffleRuby).
1131
+ value = "#{seg}(?:#{joiner}#{seg})*"
1132
+ end
1133
+ if plain && first_explode && index > first_explode &&
1134
+ (operator == '+' || operator == '#' || operator == ';')
1135
+ # Exploded values were tried longest first, so whatever this
1136
+ # variable could match after the first exploded one, that one
1137
+ # already tried matching itself. Only the last unexploded + or
1138
+ # # variable can stop inside a member, where an exploded
1139
+ # variable can't. As in 2.9.0, a variable is the last one if
1140
+ # its varspec is the same as the last varspec.
1141
+ if operator == ';'
1142
+ "(?<#{name}>)"
1143
+ elsif modifier != '*' && varspec == varspecs.last
1144
+ "(?<#{name}>#{RESERVED_NO_COMMA}*?)?"
1145
+ else
1146
+ # 2.9.0 skipped the variable when it couldn't be empty.
1147
+ "(?:(?<#{name}>)(?!#{RESERVED_NO_COMMA}))?"
1148
+ end
1149
+ elsif plain && modifier == '*' && explodes[(index + 1)..-1].any? &&
1150
+ (operator.nil? || operator == '/' || operator == '.')
1151
+ # Exploded values were tried shortest first, and a later
1152
+ # exploded variable can match whatever this one leaves, so once
1153
+ # a split fails, every split where this variable is longer
1154
+ # fails too. Of the values tried, only the leading joiners are
1155
+ # shorter than every value tried before them.
1156
+ "(?<#{name}>(?:#{joiner})*)"
1157
+ elsif plain && index > 0 && explodes[index - 1] &&
1158
+ (operator == '?' || operator == '&')
1159
+ # Whatever this variable could match after an exploded one,
1160
+ # that one already tried matching itself, unless it couldn't:
1161
+ # an exploded member can't be empty, so this variable may only
1162
+ # start right after an empty one.
1163
+ "(?<#{name}>(?<=#{leader}#{joiner}|#{joiner}#{joiner})" \
1164
+ "#{value || group})?"
1165
+ elsif operator == ';' && plain && index > 0
1166
+ # Values were tried longest first, and splitting an earlier
1167
+ # variable's run of characters with this one never gave a
1168
+ # different result, so this one may only start after that run.
1169
+ "(?<#{name}>(?:#{not_after_value}#{value || group})?)"
1170
+ elsif modifier == '*'
1171
+ "(?<#{name}>#{value})?"
1023
1172
  elsif varspecs.size > 1 && (operator == '+' || operator == '#') &&
1024
1173
  varspec != varspecs.last
1025
1174
  "(?<#{name}>#{RESERVED_NO_COMMA}*+)?"
1175
+ elsif plain && index < varspecs.size - 1 &&
1176
+ (operator.nil? || operator == '/' || operator == '.')
1177
+ # Values were tried shortest first, so a run of characters
1178
+ # without a joiner went to a later variable. The only other
1179
+ # value that could change the result is the whole run up to the
1180
+ # next joiner.
1181
+ "(?<#{name}>(?:#{group}(?=#{joiner}))??)"
1026
1182
  else
1027
1183
  "(?<#{name}>#{group})?"
1028
1184
  end
1029
1185
  end
1030
- end.join("#{joiner}?")
1186
+ end
1187
+ following, comma_follows = followers[index]
1188
+ single = varspecs.size == 1 && varspecs.first[VARSPEC, 2] != '*'
1189
+ if plain_processor && following &&
1190
+ absorbed_by?(following, operator, varspecs)
1191
+ # Each expression tried its empty alternative first. When the next
1192
+ # one follows it directly and can match anything it could, followed
1193
+ # by anything the next one could match on its own, every other
1194
+ # choice ends where the empty one can also end, so the empty
1195
+ # alternative always gave the first match.
1196
+ leader = "(?!)#{leader}"
1197
+ combined = combined.join("#{joiner}?")
1198
+ elsif plain_processor && single && following &&
1199
+ following[1].nil? && following[2] =~ VARSPEC && $2 != '*' &&
1200
+ (operator == '+' || operator == '#')
1201
+ # Values were tried shortest first, and the next expression can
1202
+ # match any unreserved characters, so the value only needs to end
1203
+ # after a character that the next expression couldn't match.
1204
+ combined = "(?<#{varspecs.first[VARSPEC, 1]}>" \
1205
+ "#{RESERVED}*?#{not_after_value})?"
1206
+ elsif plain && varspecs.size > 1 &&
1207
+ (operator == '+' || operator == '#') &&
1208
+ !comma_follows &&
1209
+ varspecs.none? { |varspec| varspec[VARSPEC, 2] == '*' } &&
1210
+ !varspecs[0...-1].include?(varspecs.last)
1211
+ # 2.9.0 tried each value with the joiner after it first, then the
1212
+ # value on its own, then skipping the variable, and every other
1213
+ # choice gave a value to a later variable that an earlier one had
1214
+ # already been tried with. A variable after a skipped one is
1215
+ # skipped too. Stopping before a joiner is left out, so this is
1216
+ # only used when no joiner can follow the expression. As in 2.9.0,
1217
+ # a variable is the last one if its varspec is the same as the
1218
+ # last varspec.
1219
+ nested = varspecs[0...-1].reverse.inject("") do |inner, varspec|
1220
+ "(?:(?<#{varspec[VARSPEC, 1]}>#{RESERVED_NO_COMMA}*+)" \
1221
+ "#{joiner}?+#{inner}|(?=#{RESERVED_NO_COMMA}))"
1222
+ end
1223
+ combined = nested + combined.last
1224
+ else
1225
+ combined = combined.join("#{joiner}?")
1226
+ end
1031
1227
  "(?:|#{leader}#{combined})"
1032
1228
  end
1033
1229
 
@@ -1345,6 +1345,10 @@ module Addressable
1345
1345
  raise InvalidURIError, 'An origin cannot omit the host.'
1346
1346
  end
1347
1347
  new_port = new_origin[/:([^:@\[\]\/]*?)$/, 1]
1348
+ # An empty port is the same as no port (Section 3.2.3 of RFC 3986).
1349
+ if new_port == EMPTY_STR && new_origin =~ /\A[^:\/?#]+:\/\/[^\/?#:@\[\]]+:\z/
1350
+ new_port = nil
1351
+ end
1348
1352
  end
1349
1353
 
1350
1354
  self.scheme = new_scheme
@@ -2126,6 +2130,11 @@ module Addressable
2126
2130
  if components[:host] != nil
2127
2131
  components[:scheme] = normalized_self.scheme
2128
2132
  end
2133
+ # A first segment containing a colon would be read as a scheme.
2134
+ if components[:scheme].nil? && components[:path] &&
2135
+ components[:path][/\A[^\/]*/].include?(":")
2136
+ components[:path] = "./" + components[:path]
2137
+ end
2129
2138
  return Addressable::URI.new(
2130
2139
  :scheme => components[:scheme],
2131
2140
  :user => components[:user],
@@ -2200,7 +2209,7 @@ module Addressable
2200
2209
  # @return [Addressable::URI] A URI suitable for display purposes.
2201
2210
  def display_uri
2202
2211
  display_uri = self.normalize
2203
- display_uri.host = ::Addressable::IDNA.to_unicode(display_uri.host)
2212
+ display_uri.host = ::Addressable::IDNA.to_unicode(display_uri.host) if display_uri.host
2204
2213
  return display_uri
2205
2214
  end
2206
2215
 
@@ -2394,13 +2403,14 @@ module Addressable
2394
2403
  # @param [Proc] block
2395
2404
  # A set of operations to perform on a given URI.
2396
2405
  def defer_validation
2406
+ validation_was_deferred = !!@validation_deferred
2397
2407
  raise LocalJumpError, "No block given." unless block_given?
2398
2408
  @validation_deferred = true
2399
2409
  yield
2400
- @validation_deferred = false
2401
- validate
2410
+ @validation_deferred = validation_was_deferred
2411
+ validate unless validation_was_deferred
2402
2412
  ensure
2403
- @validation_deferred = false
2413
+ @validation_deferred = validation_was_deferred
2404
2414
  end
2405
2415
 
2406
2416
  def encode_with(coder)
@@ -2426,10 +2436,14 @@ module Addressable
2426
2436
  SELF_REF = '.'
2427
2437
  PARENT = '..'
2428
2438
 
2439
+ # Deprecated. No longer used by normalize_path; will be removed in 2.10.0.
2429
2440
  RULE_2A = /\/\.\/|\/\.$/
2430
2441
  RULE_2B_2C = /\/([^\/]*)\/\.\.\/|\/([^\/]*)\/\.\.$/
2431
2442
  RULE_2D = /^\.\.?\/?/
2432
2443
  RULE_PREFIXED_PARENT = /^\/\.\.?\/|^(\/\.\.?)+\/?$/
2444
+ if respond_to?(:deprecate_constant, true)
2445
+ deprecate_constant :RULE_2A, :RULE_2B_2C, :RULE_2D, :RULE_PREFIXED_PARENT
2446
+ end
2433
2447
 
2434
2448
  ##
2435
2449
  # Resolves paths to their simplest form.
@@ -2438,37 +2452,52 @@ module Addressable
2438
2452
  #
2439
2453
  # @return [String] The normalized path.
2440
2454
  def self.normalize_path(path)
2441
- # Section 5.2.4 of RFC 3986
2455
+ # Section 5.2.4 of RFC 3986, as a single pass over the segments so that
2456
+ # the running time is linear in the length of the path.
2442
2457
 
2443
2458
  return if path.nil?
2444
- normalized_path = path.dup
2445
- loop do
2446
- mod ||= normalized_path.gsub!(RULE_2A, SLASH)
2447
-
2448
- pair = normalized_path.match(RULE_2B_2C)
2449
- if pair
2450
- parent = pair[1]
2451
- current = pair[2]
2459
+ segments = path.split(SLASH, -1)
2460
+ return path.dup if segments.empty?
2461
+
2462
+ # The first segment is never removed by a parent reference. For an
2463
+ # absolute path it is the empty segment before the leading slash.
2464
+ output = [segments.first]
2465
+ last = segments.size - 1
2466
+ (1..last).each do |index|
2467
+ segment = segments[index]
2468
+ if segment == SELF_REF
2469
+ output << EMPTY_STR if index == last
2470
+ elsif segment == PARENT
2471
+ if output.size > 1
2472
+ output.pop
2473
+ output << EMPTY_STR if index == last
2474
+ else
2475
+ output << PARENT
2476
+ end
2452
2477
  else
2453
- parent = nil
2454
- current = nil
2478
+ output << segment
2455
2479
  end
2456
-
2457
- regexp = "/#{Regexp.escape(parent.to_s)}/\\.\\./|"
2458
- regexp += "(/#{Regexp.escape(current.to_s)}/\\.\\.$)"
2459
-
2460
- if pair && ((parent != SELF_REF && parent != PARENT) ||
2461
- (current != SELF_REF && current != PARENT))
2462
- mod ||= normalized_path.gsub!(Regexp.new(regexp), SLASH)
2480
+ end
2481
+ normalized_path = output.join(SLASH)
2482
+
2483
+ # Non-standard, removes prefixed dotted segments from path. At this
2484
+ # point an absolute path can only start with a single "/..", and a
2485
+ # relative path keeps its leading dots.
2486
+ start = 0
2487
+ if normalized_path.start_with?(SLASH)
2488
+ if normalized_path == "/#{PARENT}"
2489
+ return normalized_path[0, 1]
2490
+ elsif normalized_path.start_with?("/#{PARENT}/")
2491
+ start = PARENT.size + 1
2492
+ end
2493
+ else
2494
+ while normalized_path[start] == SELF_REF
2495
+ start += 1
2496
+ start += 1 if normalized_path[start] == SELF_REF
2497
+ start += 1 if normalized_path[start] == SLASH
2463
2498
  end
2464
-
2465
- mod ||= normalized_path.gsub!(RULE_2D, EMPTY_STR)
2466
- # Non-standard, removes prefixed dotted segments from path.
2467
- mod ||= normalized_path.gsub!(RULE_PREFIXED_PARENT, SLASH)
2468
- break if mod.nil?
2469
2499
  end
2470
-
2471
- normalized_path
2500
+ start.zero? ? normalized_path : normalized_path[start..-1]
2472
2501
  end
2473
2502
 
2474
2503
  ##
@@ -23,7 +23,7 @@ if !defined?(Addressable::VERSION)
23
23
  module VERSION
24
24
  MAJOR = 2
25
25
  MINOR = 9
26
- TINY = 0
26
+ TINY = 1
27
27
 
28
28
  STRING = [MAJOR, MINOR, TINY].join('.')
29
29
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: addressable
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.9.0
4
+ version: 2.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Aman
@@ -53,7 +53,7 @@ homepage: https://github.com/sporkmonger/addressable
53
53
  licenses:
54
54
  - Apache-2.0
55
55
  metadata:
56
- changelog_uri: https://github.com/sporkmonger/addressable/blob/main/CHANGELOG.md#v2.9.0
56
+ changelog_uri: https://github.com/sporkmonger/addressable/blob/main/CHANGELOG.md#v2.9.1
57
57
  rdoc_options:
58
58
  - "--main"
59
59
  - README.md
@@ -70,7 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
70
70
  - !ruby/object:Gem::Version
71
71
  version: '0'
72
72
  requirements: []
73
- rubygems_version: 4.0.6
73
+ rubygems_version: 4.0.10
74
74
  specification_version: 4
75
75
  summary: URI Implementation
76
76
  test_files: []