rack 3.0.18 → 3.1.18

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.
@@ -107,22 +107,20 @@ module Rack
107
107
 
108
108
  def parse_cookies_from_header
109
109
  cookies = Hash.new
110
- if headers.has_key? 'set-cookie'
111
- set_cookie_header = headers.fetch('set-cookie')
112
- Array(set_cookie_header).each do |header_value|
113
- header_value.split("\n").each do |cookie|
114
- cookie_name, cookie_filling = cookie.split('=', 2)
115
- cookie_attributes = identify_cookie_attributes cookie_filling
116
- parsed_cookie = Cookie.new(
117
- 'name' => cookie_name.strip,
118
- 'value' => cookie_attributes.fetch('value'),
119
- 'path' => cookie_attributes.fetch('path', nil),
120
- 'domain' => cookie_attributes.fetch('domain', nil),
121
- 'expires' => cookie_attributes.fetch('expires', nil),
122
- 'secure' => cookie_attributes.fetch('secure', false)
123
- )
124
- cookies.store(cookie_name, parsed_cookie)
125
- end
110
+ set_cookie_header = headers['set-cookie']
111
+ if set_cookie_header && !set_cookie_header.empty?
112
+ Array(set_cookie_header).each do |cookie|
113
+ cookie_name, cookie_filling = cookie.split('=', 2)
114
+ cookie_attributes = identify_cookie_attributes cookie_filling
115
+ parsed_cookie = Cookie.new(
116
+ 'name' => cookie_name.strip,
117
+ 'value' => cookie_attributes.fetch('value'),
118
+ 'path' => cookie_attributes.fetch('path', nil),
119
+ 'domain' => cookie_attributes.fetch('domain', nil),
120
+ 'expires' => cookie_attributes.fetch('expires', nil),
121
+ 'secure' => cookie_attributes.fetch('secure', false)
122
+ )
123
+ cookies.store(cookie_name, parsed_cookie)
126
124
  end
127
125
  end
128
126
  cookies
@@ -3,53 +3,71 @@
3
3
  require 'strscan'
4
4
 
5
5
  require_relative '../utils'
6
+ require_relative '../bad_request'
6
7
 
7
8
  module Rack
8
9
  module Multipart
9
- class MultipartPartLimitError < Errno::EMFILE; end
10
+ class MultipartPartLimitError < Errno::EMFILE
11
+ include BadRequest
12
+ end
10
13
 
11
- class MultipartTotalPartLimitError < StandardError; end
14
+ class MultipartTotalPartLimitError < StandardError
15
+ include BadRequest
16
+ end
12
17
 
13
18
  # Use specific error class when parsing multipart request
14
19
  # that ends early.
15
- class EmptyContentError < ::EOFError; end
20
+ class EmptyContentError < ::EOFError
21
+ include BadRequest
22
+ end
16
23
 
17
24
  # Base class for multipart exceptions that do not subclass from
18
25
  # other exception classes for backwards compatibility.
19
- class Error < StandardError; end
26
+ class BoundaryTooLongError < StandardError
27
+ include BadRequest
28
+ end
29
+
30
+ # Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
31
+ Error = BoundaryTooLongError
20
32
 
21
33
  EOL = "\r\n"
34
+ FWS = /[ \t]+(?:\r\n[ \t]+)?/ # whitespace with optional folding
35
+ HEADER_VALUE = "(?:[^\r\n]|\r\n[ \t])*" # anything but a non-folding CRLF
22
36
  MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
23
- TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
24
- CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
25
- VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
26
- BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
27
- MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
28
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni
29
- MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
30
- # Updated definitions from RFC 2231
31
- ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
32
- ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
33
- SECTION = /\*[0-9]+/
34
- REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
35
- REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
36
- EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
37
- EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
38
- EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
39
- EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
40
- EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
41
- EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
42
- EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
43
- DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
44
- RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
37
+ MULTIPART_CONTENT_TYPE = /^Content-Type:#{FWS}?(#{HEADER_VALUE})/ni
38
+ MULTIPART_CONTENT_DISPOSITION = /^Content-Disposition:#{FWS}?(#{HEADER_VALUE})/ni
39
+ MULTIPART_CONTENT_ID = /^Content-ID:#{FWS}?(#{HEADER_VALUE})/ni
45
40
 
46
41
  class Parser
47
42
  BUFSIZE = 1_048_576
48
43
  TEXT_PLAIN = "text/plain"
49
44
  TEMPFILE_FACTORY = lambda { |filename, content_type|
50
- Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
45
+ extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
46
+
47
+ Tempfile.new(["RackMultipart", extension])
51
48
  }
52
49
 
50
+ BOUNDARY_START_LIMIT = 16 * 1024
51
+ private_constant :BOUNDARY_START_LIMIT
52
+
53
+ MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024
54
+ private_constant :MIME_HEADER_BYTESIZE_LIMIT
55
+
56
+ env_int = lambda do |key, val|
57
+ if str_val = ENV[key]
58
+ begin
59
+ val = Integer(str_val, 10)
60
+ rescue ArgumentError
61
+ raise ArgumentError, "non-integer value provided for environment variable #{key}"
62
+ end
63
+ end
64
+
65
+ val
66
+ end
67
+
68
+ BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
69
+ private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
70
+
53
71
  class BoundedIO # :nodoc:
54
72
  def initialize(io, content_length)
55
73
  @io = io
@@ -98,7 +116,7 @@ module Rack
98
116
  if boundary.length > 70
99
117
  # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
100
118
  # Most clients use no more than 55 characters.
101
- raise Error, "multipart boundary size too large (#{boundary.length} characters)"
119
+ raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
102
120
  end
103
121
 
104
122
  io = BoundedIO.new(io, content_length) if content_length
@@ -209,10 +227,13 @@ module Rack
209
227
 
210
228
  @state = :FAST_FORWARD
211
229
  @mime_index = 0
230
+ @body_retained = nil
231
+ @retained_size = 0
212
232
  @collector = Collector.new tempfile
213
233
 
214
234
  @sbuf = StringScanner.new("".dup)
215
235
  @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
236
+ @body_regex_at_end = /#{@body_regex}\z/m
216
237
  @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
217
238
  @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
218
239
  @head_regex = /(.*?#{EOL})#{EOL}/m
@@ -289,6 +310,10 @@ module Rack
289
310
 
290
311
  # retry for opening boundary
291
312
  else
313
+ # We raise if we don't find the multipart boundary, to avoid unbounded memory
314
+ # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
315
+ raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
316
+
292
317
  # no boundary found, keep reading data
293
318
  return :want_read
294
319
  end
@@ -305,32 +330,130 @@ module Rack
305
330
  end
306
331
  end
307
332
 
333
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
334
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
308
335
  def handle_mime_head
309
336
  if @sbuf.scan_until(@head_regex)
310
337
  head = @sbuf[1]
311
338
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
312
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
313
- name = dequote(name)
339
+ if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
340
+ disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
341
+
342
+ # ignore actual content-disposition value (should always be form-data)
343
+ i = disposition.index(';')
344
+ disposition.slice!(0, i+1)
345
+ param = nil
346
+ num_params = 0
347
+
348
+ # Parse parameter list
349
+ while i = disposition.index('=')
350
+ # Only parse up to max parameters, to avoid potential denial of service
351
+ num_params += 1
352
+ break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
353
+
354
+ # Found end of parameter name, ensure forward progress in loop
355
+ param = disposition.slice!(0, i+1)
356
+
357
+ # Remove ending equals and preceding whitespace from parameter name
358
+ param.chomp!('=')
359
+ param.lstrip!
360
+
361
+ if disposition[0] == '"'
362
+ # Parameter value is quoted, parse it, handling backslash escapes
363
+ disposition.slice!(0, 1)
364
+ value = String.new
365
+
366
+ while i = disposition.index(/(["\\])/)
367
+ c = $1
368
+
369
+ # Append all content until ending quote or escape
370
+ value << disposition.slice!(0, i)
371
+
372
+ # Remove either backslash or ending quote,
373
+ # ensures forward progress in loop
374
+ disposition.slice!(0, 1)
375
+
376
+ # stop parsing parameter value if found ending quote
377
+ break if c == '"'
378
+
379
+ escaped_char = disposition.slice!(0, 1)
380
+ if param == 'filename' && escaped_char != '"'
381
+ # Possible IE uploaded filename, append both escape backslash and value
382
+ value << c << escaped_char
383
+ else
384
+ # Other only append escaped value
385
+ value << escaped_char
386
+ end
387
+ end
388
+ else
389
+ if i = disposition.index(';')
390
+ # Parameter value unquoted (which may be invalid), value ends at semicolon
391
+ value = disposition.slice!(0, i)
392
+ else
393
+ # If no ending semicolon, assume remainder of line is value and stop
394
+ # parsing
395
+ disposition.strip!
396
+ value = disposition
397
+ disposition = ''
398
+ end
399
+ end
400
+
401
+ case param
402
+ when 'name'
403
+ name = value
404
+ when 'filename'
405
+ filename = value
406
+ when 'filename*'
407
+ filename_star = value
408
+ # else
409
+ # ignore other parameters
410
+ end
411
+
412
+ # skip trailing semicolon, to proceed to next parameter
413
+ if i = disposition.index(';')
414
+ disposition.slice!(0, i+1)
415
+ end
416
+ end
314
417
  else
315
418
  name = head[MULTIPART_CONTENT_ID, 1]
316
419
  end
317
420
 
318
- filename = get_filename(head)
421
+ if filename_star
422
+ encoding, _, filename = filename_star.split("'", 3)
423
+ filename = normalize_filename(filename || '')
424
+ filename.force_encoding(find_encoding(encoding))
425
+ elsif filename
426
+ filename = normalize_filename(filename)
427
+ end
319
428
 
320
429
  if name.nil? || name.empty?
321
430
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
322
431
  end
323
432
 
433
+ # Mime part head data is retained for both TempfilePart and BufferPart
434
+ # for the entireity of the parse, even though it isn't used for BufferPart.
435
+ update_retained_size(head.bytesize)
436
+
437
+ # If a filename is given, a TempfilePart will be used, so the body will
438
+ # not be buffered in memory. However, if a filename is not given, a BufferPart
439
+ # will be used, and the body will be buffered in memory.
440
+ @body_retained = !filename
441
+
324
442
  @collector.on_mime_head @mime_index, head, filename, content_type, name
325
443
  @state = :MIME_BODY
326
444
  else
327
- :want_read
445
+ # We raise if the mime part header is too large, to avoid unbounded memory
446
+ # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
447
+ raise Error, "multipart mime part header too large" if @sbuf.string.bytesize > MIME_HEADER_BYTESIZE_LIMIT
448
+
449
+ return :want_read
328
450
  end
329
451
  end
330
452
 
331
453
  def handle_mime_body
332
454
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
333
- body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
455
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
456
+ update_retained_size(body.bytesize) if @body_retained
334
457
  @collector.on_mime_body @mime_index, body
335
458
  @sbuf.pos += body.length + 2 # skip \r\n after the content
336
459
  @state = :CONSUME_TOKEN
@@ -339,7 +462,9 @@ module Rack
339
462
  # Save what we have so far
340
463
  if @rx_max_size < @sbuf.rest_size
341
464
  delta = @sbuf.rest_size - @rx_max_size
342
- @collector.on_mime_body @mime_index, @sbuf.peek(delta)
465
+ body = @sbuf.peek(delta)
466
+ update_retained_size(body.bytesize) if @body_retained
467
+ @collector.on_mime_body @mime_index, body
343
468
  @sbuf.pos += delta
344
469
  @sbuf.string = @sbuf.rest
345
470
  end
@@ -347,6 +472,13 @@ module Rack
347
472
  end
348
473
  end
349
474
 
475
+ def update_retained_size(size)
476
+ @retained_size += size
477
+ if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
478
+ raise Error, "multipart data over retained size limit"
479
+ end
480
+ end
481
+
350
482
  # Scan until the we find the start or end of the boundary.
351
483
  # If we find it, return the appropriate symbol for the start or
352
484
  # end of the boundary. If we don't find the start or end of the
@@ -360,39 +492,14 @@ module Rack
360
492
  end
361
493
  end
362
494
 
363
- def get_filename(head)
364
- filename = nil
365
- case head
366
- when RFC2183
367
- params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
368
-
369
- if filename = params['filename*']
370
- encoding, _, filename = filename.split("'", 3)
371
- elsif filename = params['filename']
372
- filename = $1 if filename =~ /^"(.*)"$/
373
- end
374
- when BROKEN
375
- filename = $1
376
- filename = $1 if filename =~ /^"(.*)"$/
377
- end
378
-
379
- return unless filename
380
-
495
+ def normalize_filename(filename)
381
496
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
382
497
  filename = Utils.unescape_path(filename)
383
498
  end
384
499
 
385
500
  filename.scrub!
386
501
 
387
- if filename !~ /\\[^\\"]/
388
- filename = filename.gsub(/\\(.)/, '\1')
389
- end
390
-
391
- if encoding
392
- filename.force_encoding ::Encoding.find(encoding)
393
- end
394
-
395
- filename
502
+ filename.split(/[\/\\]/).last || String.new
396
503
  end
397
504
 
398
505
  CHARSET = "charset"
@@ -418,11 +525,7 @@ module Rack
418
525
  v.strip!
419
526
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
420
527
  if k == "charset"
421
- encoding = begin
422
- Encoding.find v
423
- rescue ArgumentError
424
- Encoding::BINARY
425
- end
528
+ encoding = find_encoding(v)
426
529
  end
427
530
  end
428
531
  end
@@ -432,6 +535,15 @@ module Rack
432
535
  body.force_encoding(encoding)
433
536
  end
434
537
 
538
+ # Return the related Encoding object. However, because
539
+ # enc is submitted by the user, it may be invalid, so
540
+ # use a binary encoding in that case.
541
+ def find_encoding(enc)
542
+ Encoding.find enc
543
+ rescue ArgumentError
544
+ Encoding::BINARY
545
+ end
546
+
435
547
  def handle_empty_content!(content)
436
548
  if content.nil? || content.empty?
437
549
  raise EmptyContentError
@@ -6,6 +6,8 @@ require_relative 'utils'
6
6
  require_relative 'multipart/parser'
7
7
  require_relative 'multipart/generator'
8
8
 
9
+ require_relative 'bad_request'
10
+
9
11
  module Rack
10
12
  # A multipart form data parser, adapted from IOWA.
11
13
  #
@@ -13,9 +15,40 @@ module Rack
13
15
  module Multipart
14
16
  MULTIPART_BOUNDARY = "AaB03x"
15
17
 
18
+ class MissingInputError < StandardError
19
+ include BadRequest
20
+ end
21
+
22
+ # Accumulator for multipart form data, conforming to the QueryParser API.
23
+ # In future, the Parser could return the pair list directly, but that would
24
+ # change its API.
25
+ class ParamList # :nodoc:
26
+ def self.make_params
27
+ new
28
+ end
29
+
30
+ def self.normalize_params(params, key, value)
31
+ params << [key, value]
32
+ end
33
+
34
+ def initialize
35
+ @pairs = []
36
+ end
37
+
38
+ def <<(pair)
39
+ @pairs << pair
40
+ end
41
+
42
+ def to_params_hash
43
+ @pairs
44
+ end
45
+ end
46
+
16
47
  class << self
17
48
  def parse_multipart(env, params = Rack::Utils.default_query_parser)
18
- io = env[RACK_INPUT]
49
+ unless io = env[RACK_INPUT]
50
+ raise MissingInputError, "Missing input stream!"
51
+ end
19
52
 
20
53
  if content_length = env['CONTENT_LENGTH']
21
54
  content_length = content_length.to_i
@@ -1,24 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'bad_request'
3
4
  require 'uri'
4
5
 
5
6
  module Rack
6
7
  class QueryParser
7
- DEFAULT_SEP = /[&] */n
8
- COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
8
+ DEFAULT_SEP = /& */n
9
+ COMMON_SEP = { ";" => /; */n, ";," => /[;,] */n, "&" => /& */n }
9
10
 
10
11
  # ParameterTypeError is the error that is raised when incoming structural
11
12
  # parameters (parsed by parse_nested_query) contain conflicting types.
12
- class ParameterTypeError < TypeError; end
13
+ class ParameterTypeError < TypeError
14
+ include BadRequest
15
+ end
13
16
 
14
17
  # InvalidParameterError is the error that is raised when incoming structural
15
18
  # parameters (parsed by parse_nested_query) contain invalid format or byte
16
19
  # sequence.
17
- class InvalidParameterError < ArgumentError; end
20
+ class InvalidParameterError < ArgumentError
21
+ include BadRequest
22
+ end
18
23
 
19
24
  # QueryLimitError is for errors raised when the query provided exceeds one
20
25
  # of the query parser limits.
21
26
  class QueryLimitError < RangeError
27
+ include BadRequest
22
28
  end
23
29
 
24
30
  # ParamsTooDeepError is the old name for the error that is raised when params
@@ -27,12 +33,8 @@ module Rack
27
33
  # to handle bad query strings also now handles other limits.
28
34
  ParamsTooDeepError = QueryLimitError
29
35
 
30
- def self.make_default(_key_space_limit=(not_deprecated = true; nil), param_depth_limit, **options)
31
- unless not_deprecated
32
- warn("`first argument `key_space limit` is deprecated and no longer has an effect. Please call with only one argument, which will be required in a future version of Rack", uplevel: 1)
33
- end
34
-
35
- new Params, param_depth_limit, **options
36
+ def self.make_default(param_depth_limit, **options)
37
+ new(Params, param_depth_limit, **options)
36
38
  end
37
39
 
38
40
  attr_reader :param_depth_limit
@@ -55,11 +57,9 @@ module Rack
55
57
  PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
56
58
  private_constant :PARAMS_LIMIT
57
59
 
58
- def initialize(params_class, _key_space_limit=(not_deprecated = true; nil), param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
59
- unless not_deprecated
60
- warn("`second argument `key_space limit` is deprecated and no longer has an effect. Please call with only two arguments, which will be required in a future version of Rack", uplevel: 1)
61
- end
60
+ attr_reader :bytesize_limit
62
61
 
62
+ def initialize(params_class, param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
63
63
  @params_class = params_class
64
64
  @param_depth_limit = param_depth_limit
65
65
  @bytesize_limit = bytesize_limit
@@ -220,7 +220,7 @@ module Rack
220
220
  def check_query_string(qs, sep)
221
221
  if qs
222
222
  if qs.bytesize > @bytesize_limit
223
- raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{@bytesize_limit})"
223
+ raise QueryLimitError, "total query size exceeds limit (#{@bytesize_limit})"
224
224
  end
225
225
 
226
226
  if (param_count = qs.count(sep.is_a?(String) ? sep : '&')) >= @params_limit
@@ -237,59 +237,7 @@ module Rack
237
237
  URI.decode_www_form_component(string, encoding)
238
238
  end
239
239
 
240
- class Params
241
- def initialize
242
- @size = 0
243
- @params = {}
244
- end
245
-
246
- def [](key)
247
- @params[key]
248
- end
249
-
250
- def []=(key, value)
251
- @params[key] = value
252
- end
253
-
254
- def key?(key)
255
- @params.key?(key)
256
- end
257
-
258
- # Recursively unwraps nested `Params` objects and constructs an object
259
- # of the same shape, but using the objects' internal representations
260
- # (Ruby hashes) in place of the objects. The result is a hash consisting
261
- # purely of Ruby primitives.
262
- #
263
- # Mutation warning!
264
- #
265
- # 1. This method mutates the internal representation of the `Params`
266
- # objects in order to save object allocations.
267
- #
268
- # 2. The value you get back is a reference to the internal hash
269
- # representation, not a copy.
270
- #
271
- # 3. Because the `Params` object's internal representation is mutable
272
- # through the `#[]=` method, it is not thread safe. The result of
273
- # getting the hash representation while another thread is adding a
274
- # key to it is non-deterministic.
275
- #
276
- def to_h
277
- @params.each do |key, value|
278
- case value
279
- when self
280
- # Handle circular references gracefully.
281
- @params[key] = @params
282
- when Params
283
- @params[key] = value.to_h
284
- when Array
285
- value.map! { |v| v.kind_of?(Params) ? v.to_h : v }
286
- else
287
- # Ignore anything that is not a `Params` object or
288
- # a collection that can contain one.
289
- end
290
- end
291
- @params
292
- end
240
+ class Params < Hash
293
241
  alias_method :to_params_hash, :to_h
294
242
  end
295
243
  end