rack 2.0.9 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

Files changed (188) hide show
  1. checksums.yaml +4 -4
  2. data/{HISTORY.md → CHANGELOG.md} +220 -155
  3. data/{COPYING → MIT-LICENSE} +4 -2
  4. data/README.rdoc +77 -117
  5. data/Rakefile +25 -18
  6. data/SPEC +3 -4
  7. data/bin/rackup +1 -0
  8. data/example/lobster.ru +2 -0
  9. data/example/protectedlobster.rb +3 -1
  10. data/example/protectedlobster.ru +2 -0
  11. data/lib/rack.rb +63 -60
  12. data/lib/rack/auth/abstract/handler.rb +3 -1
  13. data/lib/rack/auth/abstract/request.rb +2 -0
  14. data/lib/rack/auth/basic.rb +4 -1
  15. data/lib/rack/auth/digest/md5.rb +9 -7
  16. data/lib/rack/auth/digest/nonce.rb +6 -3
  17. data/lib/rack/auth/digest/params.rb +4 -2
  18. data/lib/rack/auth/digest/request.rb +2 -0
  19. data/lib/rack/body_proxy.rb +3 -6
  20. data/lib/rack/builder.rb +38 -15
  21. data/lib/rack/cascade.rb +6 -5
  22. data/lib/rack/chunked.rb +29 -6
  23. data/lib/rack/common_logger.rb +9 -8
  24. data/lib/rack/conditional_get.rb +3 -1
  25. data/lib/rack/config.rb +2 -0
  26. data/lib/rack/content_length.rb +3 -1
  27. data/lib/rack/content_type.rb +3 -1
  28. data/lib/rack/core_ext/regexp.rb +14 -0
  29. data/lib/rack/deflater.rb +28 -17
  30. data/lib/rack/directory.rb +17 -14
  31. data/lib/rack/etag.rb +3 -1
  32. data/lib/rack/events.rb +5 -3
  33. data/lib/rack/file.rb +5 -173
  34. data/lib/rack/files.rb +178 -0
  35. data/lib/rack/handler.rb +7 -2
  36. data/lib/rack/handler/cgi.rb +3 -1
  37. data/lib/rack/handler/fastcgi.rb +4 -2
  38. data/lib/rack/handler/lsws.rb +3 -1
  39. data/lib/rack/handler/scgi.rb +9 -6
  40. data/lib/rack/handler/thin.rb +3 -1
  41. data/lib/rack/handler/webrick.rb +4 -2
  42. data/lib/rack/head.rb +2 -0
  43. data/lib/rack/lint.rb +14 -11
  44. data/lib/rack/lobster.rb +7 -5
  45. data/lib/rack/lock.rb +2 -0
  46. data/lib/rack/logger.rb +2 -0
  47. data/lib/rack/media_type.rb +10 -5
  48. data/lib/rack/method_override.rb +4 -2
  49. data/lib/rack/mime.rb +9 -1
  50. data/lib/rack/mock.rb +74 -15
  51. data/lib/rack/multipart.rb +5 -3
  52. data/lib/rack/multipart/generator.rb +6 -7
  53. data/lib/rack/multipart/parser.rb +51 -45
  54. data/lib/rack/multipart/uploaded_file.rb +2 -0
  55. data/lib/rack/null_logger.rb +2 -0
  56. data/lib/rack/query_parser.rb +51 -25
  57. data/lib/rack/recursive.rb +7 -5
  58. data/lib/rack/reloader.rb +10 -4
  59. data/lib/rack/request.rb +79 -26
  60. data/lib/rack/response.rb +71 -31
  61. data/lib/rack/rewindable_input.rb +4 -2
  62. data/lib/rack/runtime.rb +4 -2
  63. data/lib/rack/sendfile.rb +15 -8
  64. data/lib/rack/server.rb +88 -16
  65. data/lib/rack/session/abstract/id.rb +40 -22
  66. data/lib/rack/session/cookie.rb +10 -9
  67. data/lib/rack/session/memcache.rb +4 -93
  68. data/lib/rack/session/pool.rb +4 -2
  69. data/lib/rack/show_exceptions.rb +15 -9
  70. data/lib/rack/show_status.rb +4 -2
  71. data/lib/rack/static.rb +15 -10
  72. data/lib/rack/tempfile_reaper.rb +2 -0
  73. data/lib/rack/urlmap.rb +11 -2
  74. data/lib/rack/utils.rb +54 -71
  75. data/rack.gemspec +17 -7
  76. metadata +30 -172
  77. data/test/builder/an_underscore_app.rb +0 -5
  78. data/test/builder/anything.rb +0 -5
  79. data/test/builder/comment.ru +0 -4
  80. data/test/builder/end.ru +0 -5
  81. data/test/builder/line.ru +0 -1
  82. data/test/builder/options.ru +0 -2
  83. data/test/cgi/assets/folder/test.js +0 -1
  84. data/test/cgi/assets/fonts/font.eot +0 -1
  85. data/test/cgi/assets/images/image.png +0 -1
  86. data/test/cgi/assets/index.html +0 -1
  87. data/test/cgi/assets/javascripts/app.js +0 -1
  88. data/test/cgi/assets/stylesheets/app.css +0 -1
  89. data/test/cgi/lighttpd.conf +0 -26
  90. data/test/cgi/rackup_stub.rb +0 -6
  91. data/test/cgi/sample_rackup.ru +0 -5
  92. data/test/cgi/test +0 -9
  93. data/test/cgi/test+directory/test+file +0 -1
  94. data/test/cgi/test.fcgi +0 -9
  95. data/test/cgi/test.gz +0 -0
  96. data/test/cgi/test.ru +0 -5
  97. data/test/gemloader.rb +0 -10
  98. data/test/helper.rb +0 -34
  99. data/test/multipart/bad_robots +0 -259
  100. data/test/multipart/binary +0 -0
  101. data/test/multipart/content_type_and_no_filename +0 -6
  102. data/test/multipart/empty +0 -10
  103. data/test/multipart/fail_16384_nofile +0 -814
  104. data/test/multipart/file1.txt +0 -1
  105. data/test/multipart/filename_and_modification_param +0 -7
  106. data/test/multipart/filename_and_no_name +0 -6
  107. data/test/multipart/filename_with_encoded_words +0 -7
  108. data/test/multipart/filename_with_escaped_quotes +0 -6
  109. data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
  110. data/test/multipart/filename_with_null_byte +0 -7
  111. data/test/multipart/filename_with_percent_escaped_quotes +0 -6
  112. data/test/multipart/filename_with_single_quote +0 -7
  113. data/test/multipart/filename_with_unescaped_percentages +0 -6
  114. data/test/multipart/filename_with_unescaped_percentages2 +0 -6
  115. data/test/multipart/filename_with_unescaped_percentages3 +0 -6
  116. data/test/multipart/filename_with_unescaped_quotes +0 -6
  117. data/test/multipart/ie +0 -6
  118. data/test/multipart/invalid_character +0 -6
  119. data/test/multipart/mixed_files +0 -21
  120. data/test/multipart/nested +0 -10
  121. data/test/multipart/none +0 -9
  122. data/test/multipart/quoted +0 -15
  123. data/test/multipart/rack-logo.png +0 -0
  124. data/test/multipart/semicolon +0 -6
  125. data/test/multipart/text +0 -15
  126. data/test/multipart/three_files_three_fields +0 -31
  127. data/test/multipart/unity3d_wwwform +0 -11
  128. data/test/multipart/webkit +0 -32
  129. data/test/rackup/config.ru +0 -31
  130. data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
  131. data/test/spec_auth_basic.rb +0 -89
  132. data/test/spec_auth_digest.rb +0 -260
  133. data/test/spec_body_proxy.rb +0 -85
  134. data/test/spec_builder.rb +0 -233
  135. data/test/spec_cascade.rb +0 -63
  136. data/test/spec_cgi.rb +0 -84
  137. data/test/spec_chunked.rb +0 -103
  138. data/test/spec_common_logger.rb +0 -95
  139. data/test/spec_conditional_get.rb +0 -103
  140. data/test/spec_config.rb +0 -23
  141. data/test/spec_content_length.rb +0 -86
  142. data/test/spec_content_type.rb +0 -46
  143. data/test/spec_deflater.rb +0 -375
  144. data/test/spec_directory.rb +0 -148
  145. data/test/spec_etag.rb +0 -108
  146. data/test/spec_events.rb +0 -133
  147. data/test/spec_fastcgi.rb +0 -85
  148. data/test/spec_file.rb +0 -264
  149. data/test/spec_handler.rb +0 -57
  150. data/test/spec_head.rb +0 -46
  151. data/test/spec_lint.rb +0 -515
  152. data/test/spec_lobster.rb +0 -59
  153. data/test/spec_lock.rb +0 -204
  154. data/test/spec_logger.rb +0 -24
  155. data/test/spec_media_type.rb +0 -42
  156. data/test/spec_method_override.rb +0 -110
  157. data/test/spec_mime.rb +0 -51
  158. data/test/spec_mock.rb +0 -359
  159. data/test/spec_multipart.rb +0 -722
  160. data/test/spec_null_logger.rb +0 -21
  161. data/test/spec_recursive.rb +0 -75
  162. data/test/spec_request.rb +0 -1407
  163. data/test/spec_response.rb +0 -528
  164. data/test/spec_rewindable_input.rb +0 -128
  165. data/test/spec_runtime.rb +0 -50
  166. data/test/spec_sendfile.rb +0 -125
  167. data/test/spec_server.rb +0 -193
  168. data/test/spec_session_abstract_id.rb +0 -31
  169. data/test/spec_session_abstract_session_hash.rb +0 -45
  170. data/test/spec_session_cookie.rb +0 -442
  171. data/test/spec_session_memcache.rb +0 -357
  172. data/test/spec_session_persisted_secure_secure_session_hash.rb +0 -73
  173. data/test/spec_session_pool.rb +0 -247
  174. data/test/spec_show_exceptions.rb +0 -93
  175. data/test/spec_show_status.rb +0 -104
  176. data/test/spec_static.rb +0 -184
  177. data/test/spec_tempfile_reaper.rb +0 -64
  178. data/test/spec_thin.rb +0 -96
  179. data/test/spec_urlmap.rb +0 -237
  180. data/test/spec_utils.rb +0 -742
  181. data/test/spec_version.rb +0 -11
  182. data/test/spec_webrick.rb +0 -206
  183. data/test/static/another/index.html +0 -1
  184. data/test/static/foo.html +0 -1
  185. data/test/static/index.html +0 -1
  186. data/test/testrequest.rb +0 -78
  187. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  188. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack/multipart/parser'
2
4
 
3
5
  module Rack
@@ -14,10 +16,10 @@ module Rack
14
16
  TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
15
17
  CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
16
18
  VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
17
- BROKEN_QUOTED = /^#{CONDISP}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
18
- BROKEN_UNQUOTED = /^#{CONDISP}.*;\sfilename=(#{TOKEN})/i
19
+ BROKEN_QUOTED = /^#{CONDISP}.*;\s*filename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
20
+ BROKEN_UNQUOTED = /^#{CONDISP}.*;\s*filename=(#{TOKEN})/i
19
21
  MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
20
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*\s+name=(#{VALUE})/ni
22
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni
21
23
  MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
22
24
  # Updated definitions from RFC 2231
23
25
  ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  module Multipart
3
5
  class Generator
@@ -27,21 +29,18 @@ module Rack
27
29
 
28
30
  private
29
31
  def multipart?
30
- multipart = false
31
-
32
32
  query = lambda { |value|
33
33
  case value
34
34
  when Array
35
- value.each(&query)
35
+ value.any?(&query)
36
36
  when Hash
37
- value.values.each(&query)
37
+ value.values.any?(&query)
38
38
  when Rack::Multipart::UploadedFile
39
- multipart = true
39
+ true
40
40
  end
41
41
  }
42
- @params.values.each(&query)
43
42
 
44
- multipart
43
+ @params.values.any?(&query)
45
44
  end
46
45
 
47
46
  def flattened_params
@@ -1,16 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack/utils'
4
+ require 'strscan'
5
+ require 'rack/core_ext/regexp'
2
6
 
3
7
  module Rack
4
8
  module Multipart
5
9
  class MultipartPartLimitError < Errno::EMFILE; end
6
10
 
7
11
  class Parser
8
- BUFSIZE = 16384
12
+ using ::Rack::RegexpExtensions
13
+
14
+ BUFSIZE = 1_048_576
9
15
  TEXT_PLAIN = "text/plain"
10
16
  TEMPFILE_FACTORY = lambda { |filename, content_type|
11
- Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0".freeze, '%00'.freeze))])
17
+ Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
12
18
  }
13
19
 
20
+ BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
21
+
14
22
  class BoundedIO # :nodoc:
15
23
  def initialize(io, content_length)
16
24
  @io = io
@@ -18,15 +26,15 @@ module Rack
18
26
  @cursor = 0
19
27
  end
20
28
 
21
- def read(size)
29
+ def read(size, outbuf = nil)
22
30
  return if @cursor >= @content_length
23
31
 
24
32
  left = @content_length - @cursor
25
33
 
26
34
  str = if left < size
27
- @io.read left
35
+ @io.read left, outbuf
28
36
  else
29
- @io.read size
37
+ @io.read size, outbuf
30
38
  end
31
39
 
32
40
  if str
@@ -61,13 +69,14 @@ module Rack
61
69
  return EMPTY unless boundary
62
70
 
63
71
  io = BoundedIO.new(io, content_length) if content_length
72
+ outbuf = String.new
64
73
 
65
74
  parser = new(boundary, tmpfile, bufsize, qp)
66
- parser.on_read io.read(bufsize)
75
+ parser.on_read io.read(bufsize, outbuf)
67
76
 
68
77
  loop do
69
78
  break if parser.state == :DONE
70
- parser.on_read io.read(bufsize)
79
+ parser.on_read io.read(bufsize, outbuf)
71
80
  end
72
81
 
73
82
  io.rewind
@@ -90,14 +99,14 @@ module Rack
90
99
  # those which give the lone filename.
91
100
  fn = filename.split(/[\/\\]/).last
92
101
 
93
- data = {:filename => fn, :type => content_type,
94
- :name => name, :tempfile => body, :head => head}
102
+ data = { filename: fn, type: content_type,
103
+ name: name, tempfile: body, head: head }
95
104
  elsif !filename && content_type && body.is_a?(IO)
96
105
  body.rewind
97
106
 
98
107
  # Generic multipart cases, not coming from a form
99
- data = {:type => content_type,
100
- :name => name, :tempfile => body, :head => head}
108
+ data = { type: content_type,
109
+ name: name, tempfile: body, head: head }
101
110
  end
102
111
 
103
112
  yield data
@@ -138,6 +147,7 @@ module Rack
138
147
  end
139
148
 
140
149
  @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
150
+
141
151
  check_open_files
142
152
  end
143
153
 
@@ -163,25 +173,26 @@ module Rack
163
173
  attr_reader :state
164
174
 
165
175
  def initialize(boundary, tempfile, bufsize, query_parser)
166
- @buf = String.new
167
-
168
176
  @query_parser = query_parser
169
177
  @params = query_parser.make_params
170
178
  @boundary = "--#{boundary}"
171
179
  @bufsize = bufsize
172
180
 
173
- @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
174
- @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
175
181
  @full_boundary = @boundary
176
182
  @end_boundary = @boundary + '--'
177
183
  @state = :FAST_FORWARD
178
184
  @mime_index = 0
179
185
  @collector = Collector.new tempfile
186
+
187
+ @sbuf = StringScanner.new("".dup)
188
+ @body_regex = /(.*?)(#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/m
189
+ @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
190
+ @head_regex = /(.*?#{EOL})#{EOL}/m
180
191
  end
181
192
 
182
193
  def on_read content
183
194
  handle_empty_content!(content)
184
- @buf << content
195
+ @sbuf.concat content
185
196
  run_parser
186
197
  end
187
198
 
@@ -192,7 +203,6 @@ module Rack
192
203
  @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
193
204
  end
194
205
  end
195
-
196
206
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
197
207
  end
198
208
 
@@ -219,7 +229,7 @@ module Rack
219
229
  if consume_boundary
220
230
  @state = :MIME_HEAD
221
231
  else
222
- raise EOFError, "bad content body" if @buf.bytesize >= @bufsize
232
+ raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
223
233
  :want_read
224
234
  end
225
235
  end
@@ -227,19 +237,16 @@ module Rack
227
237
  def handle_consume_token
228
238
  tok = consume_boundary
229
239
  # break if we're at the end of a buffer, but not if it is the end of a field
230
- if tok == :END_BOUNDARY || (@buf.empty? && tok != :BOUNDARY)
231
- @state = :DONE
240
+ @state = if tok == :END_BOUNDARY || (@sbuf.eos? && tok != :BOUNDARY)
241
+ :DONE
232
242
  else
233
- @state = :MIME_HEAD
243
+ :MIME_HEAD
234
244
  end
235
245
  end
236
246
 
237
247
  def handle_mime_head
238
- if @buf.index(EOL + EOL)
239
- i = @buf.index(EOL+EOL)
240
- head = @buf.slice!(0, i+2) # First \r\n
241
- @buf.slice!(0, 2) # Second \r\n
242
-
248
+ if @sbuf.scan_until(@head_regex)
249
+ head = @sbuf[1]
243
250
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
244
251
  if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
245
252
  name = Rack::Auth::Digest::Params::dequote(name)
@@ -250,7 +257,7 @@ module Rack
250
257
  filename = get_filename(head)
251
258
 
252
259
  if name.nil? || name.empty?
253
- name = filename || "#{content_type || TEXT_PLAIN}[]"
260
+ name = filename || "#{content_type || TEXT_PLAIN}[]".dup
254
261
  end
255
262
 
256
263
  @collector.on_mime_head @mime_index, head, filename, content_type, name
@@ -261,16 +268,19 @@ module Rack
261
268
  end
262
269
 
263
270
  def handle_mime_body
264
- if i = @buf.index(rx)
265
- # Save the rest.
266
- @collector.on_mime_body @mime_index, @buf.slice!(0, i)
267
- @buf.slice!(0, 2) # Remove \r\n after the content
271
+ if @sbuf.check_until(@body_regex) # check but do not advance the pointer yet
272
+ body = @sbuf[1]
273
+ @collector.on_mime_body @mime_index, body
274
+ @sbuf.pos += body.length + 2 # skip \r\n after the content
268
275
  @state = :CONSUME_TOKEN
269
276
  @mime_index += 1
270
277
  else
271
- # Save the read body part.
272
- if @rx_max_size < @buf.size
273
- @collector.on_mime_body @mime_index, @buf.slice!(0, @buf.size - @rx_max_size)
278
+ # Save what we have so far
279
+ if @rx_max_size < @sbuf.rest_size
280
+ delta = @sbuf.rest_size - @rx_max_size
281
+ @collector.on_mime_body @mime_index, @sbuf.peek(delta)
282
+ @sbuf.pos += delta
283
+ @sbuf.string = @sbuf.rest
274
284
  end
275
285
  :want_read
276
286
  end
@@ -278,16 +288,13 @@ module Rack
278
288
 
279
289
  def full_boundary; @full_boundary; end
280
290
 
281
- def rx; @rx; end
282
-
283
291
  def consume_boundary
284
- while @buf.gsub!(/\A([^\n]*(?:\n|\Z))/, '')
285
- read_buffer = $1
292
+ while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
286
293
  case read_buffer.strip
287
294
  when full_boundary then return :BOUNDARY
288
295
  when @end_boundary then return :END_BOUNDARY
289
296
  end
290
- return if @buf.empty?
297
+ return if @sbuf.eos?
291
298
  end
292
299
  end
293
300
 
@@ -308,8 +315,8 @@ module Rack
308
315
 
309
316
  return unless filename
310
317
 
311
- if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
312
- filename = Utils.unescape(filename)
318
+ if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
319
+ filename = Utils.unescape_path(filename)
313
320
  end
314
321
 
315
322
  filename.scrub!
@@ -325,7 +332,7 @@ module Rack
325
332
  filename
326
333
  end
327
334
 
328
- CHARSET = "charset"
335
+ CHARSET = "charset"
329
336
 
330
337
  def tag_multipart_encoding(filename, content_type, name, body)
331
338
  name = name.to_s
@@ -342,10 +349,10 @@ module Rack
342
349
  if TEXT_PLAIN == type_subtype
343
350
  rest = list.drop 1
344
351
  rest.each do |param|
345
- k,v = param.split('=', 2)
352
+ k, v = param.split('=', 2)
346
353
  k.strip!
347
354
  v.strip!
348
- v = v[1..-2] if v[0] == '"' && v[-1] == '"'
355
+ v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
349
356
  encoding = Encoding.find v if k == CHARSET
350
357
  end
351
358
  end
@@ -355,7 +362,6 @@ module Rack
355
362
  body.force_encoding(encoding)
356
363
  end
357
364
 
358
-
359
365
  def handle_empty_content!(content)
360
366
  if content.nil? || content.empty?
361
367
  raise EOFError
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  module Multipart
3
5
  class UploadedFile
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class NullLogger
3
5
  def initialize(app)
@@ -1,5 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'core_ext/regexp'
4
+
1
5
  module Rack
2
6
  class QueryParser
7
+ using ::Rack::RegexpExtensions
8
+
3
9
  DEFAULT_SEP = /[&;] */n
4
10
  COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
5
11
 
@@ -36,7 +42,7 @@ module Rack
36
42
 
37
43
  (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
38
44
  next if p.empty?
39
- k, v = p.split('='.freeze, 2).map!(&unescaper)
45
+ k, v = p.split('=', 2).map!(&unescaper)
40
46
 
41
47
  if cur = params[k]
42
48
  if cur.class == Array
@@ -49,7 +55,7 @@ module Rack
49
55
  end
50
56
  end
51
57
 
52
- return params.to_params_hash
58
+ return params.to_h
53
59
  end
54
60
 
55
61
  # parse_nested_query expands a query string into structural types. Supported
@@ -61,13 +67,13 @@ module Rack
61
67
  return {} if qs.nil? || qs.empty?
62
68
  params = make_params
63
69
 
64
- (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
65
- k, v = p.split('='.freeze, 2).map! { |s| unescape(s) }
70
+ qs.split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
71
+ k, v = p.split('=', 2).map! { |s| unescape(s) }
66
72
 
67
73
  normalize_params(params, k, v, param_depth_limit)
68
74
  end
69
75
 
70
- return params.to_params_hash
76
+ return params.to_h
71
77
  rescue ArgumentError => e
72
78
  raise InvalidParameterError, e.message
73
79
  end
@@ -79,22 +85,22 @@ module Rack
79
85
  raise RangeError if depth <= 0
80
86
 
81
87
  name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
82
- k = $1 || ''.freeze
83
- after = $' || ''.freeze
88
+ k = $1 || ''
89
+ after = $' || ''
84
90
 
85
91
  if k.empty?
86
- if !v.nil? && name == "[]".freeze
92
+ if !v.nil? && name == "[]"
87
93
  return Array(v)
88
94
  else
89
95
  return
90
96
  end
91
97
  end
92
98
 
93
- if after == ''.freeze
99
+ if after == ''
94
100
  params[k] = v
95
- elsif after == "[".freeze
101
+ elsif after == "["
96
102
  params[name] = v
97
- elsif after == "[]".freeze
103
+ elsif after == "[]"
98
104
  params[k] ||= []
99
105
  raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
100
106
  params[k] << v
@@ -135,7 +141,7 @@ module Rack
135
141
  end
136
142
 
137
143
  def params_hash_has_key?(hash, key)
138
- return false if key =~ /\[\]/
144
+ return false if /\[\]/.match?(key)
139
145
 
140
146
  key.split(/[\[\]]+/).inject(hash) do |h, part|
141
147
  next h if part == ''
@@ -171,22 +177,42 @@ module Rack
171
177
  @params.key?(key)
172
178
  end
173
179
 
174
- def to_params_hash
175
- hash = @params
176
- hash.keys.each do |key|
177
- value = hash[key]
178
- if value.kind_of?(self.class)
179
- if value.object_id == self.object_id
180
- hash[key] = hash
181
- else
182
- hash[key] = value.to_params_hash
183
- end
184
- elsif value.kind_of?(Array)
185
- value.map! {|x| x.kind_of?(self.class) ? x.to_params_hash : x}
180
+ # Recursively unwraps nested `Params` objects and constructs an object
181
+ # of the same shape, but using the objects' internal representations
182
+ # (Ruby hashes) in place of the objects. The result is a hash consisting
183
+ # purely of Ruby primitives.
184
+ #
185
+ # Mutation warning!
186
+ #
187
+ # 1. This method mutates the internal representation of the `Params`
188
+ # objects in order to save object allocations.
189
+ #
190
+ # 2. The value you get back is a reference to the internal hash
191
+ # representation, not a copy.
192
+ #
193
+ # 3. Because the `Params` object's internal representation is mutable
194
+ # through the `#[]=` method, it is not thread safe. The result of
195
+ # getting the hash representation while another thread is adding a
196
+ # key to it is non-deterministic.
197
+ #
198
+ def to_h
199
+ @params.each do |key, value|
200
+ case value
201
+ when self
202
+ # Handle circular references gracefully.
203
+ @params[key] = @params
204
+ when Params
205
+ @params[key] = value.to_h
206
+ when Array
207
+ value.map! { |v| v.kind_of?(Params) ? v.to_h : v }
208
+ else
209
+ # Ignore anything that is not a `Params` object or
210
+ # a collection that can contain one.
186
211
  end
187
212
  end
188
- hash
213
+ @params
189
214
  end
215
+ alias_method :to_params_hash, :to_h
190
216
  end
191
217
  end
192
218
  end