rack 2.0.4 → 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 (187) 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 -119
  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 +9 -3
  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 +54 -51
  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 +89 -23
  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 +104 -21
  66. data/lib/rack/session/cookie.rb +21 -11
  67. data/lib/rack/session/memcache.rb +4 -87
  68. data/lib/rack/session/pool.rb +17 -8
  69. data/lib/rack/show_exceptions.rb +16 -10
  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 +55 -70
  75. data/rack.gemspec +19 -9
  76. metadata +32 -173
  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 -96
  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 -1393
  163. data/test/spec_response.rb +0 -510
  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 -320
  172. data/test/spec_session_pool.rb +0 -210
  173. data/test/spec_show_exceptions.rb +0 -80
  174. data/test/spec_show_status.rb +0 -104
  175. data/test/spec_static.rb +0 -184
  176. data/test/spec_tempfile_reaper.rb +0 -64
  177. data/test/spec_thin.rb +0 -96
  178. data/test/spec_urlmap.rb +0 -237
  179. data/test/spec_utils.rb +0 -742
  180. data/test/spec_version.rb +0 -11
  181. data/test/spec_webrick.rb +0 -206
  182. data/test/static/another/index.html +0 -1
  183. data/test/static/foo.html +0 -1
  184. data/test/static/index.html +0 -1
  185. data/test/testrequest.rb +0 -78
  186. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  187. 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
12
+ using ::Rack::RegexpExtensions
13
+
8
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
@@ -39,8 +47,6 @@ module Rack
39
47
  str
40
48
  end
41
49
 
42
- def eof?; @content_length == @cursor; end
43
-
44
50
  def rewind
45
51
  @io.rewind
46
52
  end
@@ -63,13 +69,14 @@ module Rack
63
69
  return EMPTY unless boundary
64
70
 
65
71
  io = BoundedIO.new(io, content_length) if content_length
72
+ outbuf = String.new
66
73
 
67
74
  parser = new(boundary, tmpfile, bufsize, qp)
68
- parser.on_read io.read(bufsize), io.eof?
75
+ parser.on_read io.read(bufsize, outbuf)
69
76
 
70
77
  loop do
71
78
  break if parser.state == :DONE
72
- parser.on_read io.read(bufsize), io.eof?
79
+ parser.on_read io.read(bufsize, outbuf)
73
80
  end
74
81
 
75
82
  io.rewind
@@ -92,14 +99,14 @@ module Rack
92
99
  # those which give the lone filename.
93
100
  fn = filename.split(/[\/\\]/).last
94
101
 
95
- data = {:filename => fn, :type => content_type,
96
- :name => name, :tempfile => body, :head => head}
102
+ data = { filename: fn, type: content_type,
103
+ name: name, tempfile: body, head: head }
97
104
  elsif !filename && content_type && body.is_a?(IO)
98
105
  body.rewind
99
106
 
100
107
  # Generic multipart cases, not coming from a form
101
- data = {:type => content_type,
102
- :name => name, :tempfile => body, :head => head}
108
+ data = { type: content_type,
109
+ name: name, tempfile: body, head: head }
103
110
  end
104
111
 
105
112
  yield data
@@ -140,6 +147,7 @@ module Rack
140
147
  end
141
148
 
142
149
  @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
150
+
143
151
  check_open_files
144
152
  end
145
153
 
@@ -165,25 +173,26 @@ module Rack
165
173
  attr_reader :state
166
174
 
167
175
  def initialize(boundary, tempfile, bufsize, query_parser)
168
- @buf = String.new
169
-
170
176
  @query_parser = query_parser
171
177
  @params = query_parser.make_params
172
178
  @boundary = "--#{boundary}"
173
179
  @bufsize = bufsize
174
180
 
175
- @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
176
- @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
177
181
  @full_boundary = @boundary
178
182
  @end_boundary = @boundary + '--'
179
183
  @state = :FAST_FORWARD
180
184
  @mime_index = 0
181
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
182
191
  end
183
192
 
184
- def on_read content, eof
185
- handle_empty_content!(content, eof)
186
- @buf << content
193
+ def on_read content
194
+ handle_empty_content!(content)
195
+ @sbuf.concat content
187
196
  run_parser
188
197
  end
189
198
 
@@ -194,7 +203,6 @@ module Rack
194
203
  @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
195
204
  end
196
205
  end
197
-
198
206
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
199
207
  end
200
208
 
@@ -221,7 +229,7 @@ module Rack
221
229
  if consume_boundary
222
230
  @state = :MIME_HEAD
223
231
  else
224
- raise EOFError, "bad content body" if @buf.bytesize >= @bufsize
232
+ raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
225
233
  :want_read
226
234
  end
227
235
  end
@@ -229,19 +237,16 @@ module Rack
229
237
  def handle_consume_token
230
238
  tok = consume_boundary
231
239
  # break if we're at the end of a buffer, but not if it is the end of a field
232
- if tok == :END_BOUNDARY || (@buf.empty? && tok != :BOUNDARY)
233
- @state = :DONE
240
+ @state = if tok == :END_BOUNDARY || (@sbuf.eos? && tok != :BOUNDARY)
241
+ :DONE
234
242
  else
235
- @state = :MIME_HEAD
243
+ :MIME_HEAD
236
244
  end
237
245
  end
238
246
 
239
247
  def handle_mime_head
240
- if @buf.index(EOL + EOL)
241
- i = @buf.index(EOL+EOL)
242
- head = @buf.slice!(0, i+2) # First \r\n
243
- @buf.slice!(0, 2) # Second \r\n
244
-
248
+ if @sbuf.scan_until(@head_regex)
249
+ head = @sbuf[1]
245
250
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
246
251
  if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
247
252
  name = Rack::Auth::Digest::Params::dequote(name)
@@ -252,7 +257,7 @@ module Rack
252
257
  filename = get_filename(head)
253
258
 
254
259
  if name.nil? || name.empty?
255
- name = filename || "#{content_type || TEXT_PLAIN}[]"
260
+ name = filename || "#{content_type || TEXT_PLAIN}[]".dup
256
261
  end
257
262
 
258
263
  @collector.on_mime_head @mime_index, head, filename, content_type, name
@@ -263,16 +268,19 @@ module Rack
263
268
  end
264
269
 
265
270
  def handle_mime_body
266
- if i = @buf.index(rx)
267
- # Save the rest.
268
- @collector.on_mime_body @mime_index, @buf.slice!(0, i)
269
- @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
270
275
  @state = :CONSUME_TOKEN
271
276
  @mime_index += 1
272
277
  else
273
- # Save the read body part.
274
- if @rx_max_size < @buf.size
275
- @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
276
284
  end
277
285
  :want_read
278
286
  end
@@ -280,16 +288,13 @@ module Rack
280
288
 
281
289
  def full_boundary; @full_boundary; end
282
290
 
283
- def rx; @rx; end
284
-
285
291
  def consume_boundary
286
- while @buf.gsub!(/\A([^\n]*(?:\n|\Z))/, '')
287
- read_buffer = $1
292
+ while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
288
293
  case read_buffer.strip
289
294
  when full_boundary then return :BOUNDARY
290
295
  when @end_boundary then return :END_BOUNDARY
291
296
  end
292
- return if @buf.empty?
297
+ return if @sbuf.eos?
293
298
  end
294
299
  end
295
300
 
@@ -310,8 +315,8 @@ module Rack
310
315
 
311
316
  return unless filename
312
317
 
313
- if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
314
- filename = Utils.unescape(filename)
318
+ if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
319
+ filename = Utils.unescape_path(filename)
315
320
  end
316
321
 
317
322
  filename.scrub!
@@ -327,7 +332,7 @@ module Rack
327
332
  filename
328
333
  end
329
334
 
330
- CHARSET = "charset"
335
+ CHARSET = "charset"
331
336
 
332
337
  def tag_multipart_encoding(filename, content_type, name, body)
333
338
  name = name.to_s
@@ -344,10 +349,10 @@ module Rack
344
349
  if TEXT_PLAIN == type_subtype
345
350
  rest = list.drop 1
346
351
  rest.each do |param|
347
- k,v = param.split('=', 2)
352
+ k, v = param.split('=', 2)
348
353
  k.strip!
349
354
  v.strip!
350
- v = v[1..-2] if v[0] == '"' && v[-1] == '"'
355
+ v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
351
356
  encoding = Encoding.find v if k == CHARSET
352
357
  end
353
358
  end
@@ -357,11 +362,9 @@ module Rack
357
362
  body.force_encoding(encoding)
358
363
  end
359
364
 
360
-
361
- def handle_empty_content!(content, eof)
365
+ def handle_empty_content!(content)
362
366
  if content.nil? || content.empty?
363
- raise EOFError if eof
364
- return true
367
+ raise EOFError
365
368
  end
366
369
  end
367
370
  end
@@ -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