rack 3.0.11 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,51 +3,46 @@
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"
22
34
  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
35
  MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
28
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni
36
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:(.*)(?=#{EOL}(\S|\z))/ni
29
37
  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
45
38
 
46
39
  class Parser
47
40
  BUFSIZE = 1_048_576
48
41
  TEXT_PLAIN = "text/plain"
49
42
  TEMPFILE_FACTORY = lambda { |filename, content_type|
50
- Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
43
+ extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
44
+
45
+ Tempfile.new(["RackMultipart", extension])
51
46
  }
52
47
 
53
48
  class BoundedIO # :nodoc:
@@ -98,7 +93,7 @@ module Rack
98
93
  if boundary.length > 70
99
94
  # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
100
95
  # Most clients use no more than 55 characters.
101
- raise Error, "multipart boundary size too large (#{boundary.length} characters)"
96
+ raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
102
97
  end
103
98
 
104
99
  io = BoundedIO.new(io, content_length) if content_length
@@ -213,6 +208,7 @@ module Rack
213
208
 
214
209
  @sbuf = StringScanner.new("".dup)
215
210
  @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
211
+ @body_regex_at_end = /#{@body_regex}\z/m
216
212
  @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
217
213
  @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
218
214
  @head_regex = /(.*?#{EOL})#{EOL}/m
@@ -305,17 +301,102 @@ module Rack
305
301
  end
306
302
  end
307
303
 
304
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
305
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
308
306
  def handle_mime_head
309
307
  if @sbuf.scan_until(@head_regex)
310
308
  head = @sbuf[1]
311
309
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
312
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
313
- name = dequote(name)
310
+ if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
311
+ disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
312
+
313
+ # ignore actual content-disposition value (should always be form-data)
314
+ i = disposition.index(';')
315
+ disposition.slice!(0, i+1)
316
+ param = nil
317
+ num_params = 0
318
+
319
+ # Parse parameter list
320
+ while i = disposition.index('=')
321
+ # Only parse up to max parameters, to avoid potential denial of service
322
+ num_params += 1
323
+ break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
324
+
325
+ # Found end of parameter name, ensure forward progress in loop
326
+ param = disposition.slice!(0, i+1)
327
+
328
+ # Remove ending equals and preceding whitespace from parameter name
329
+ param.chomp!('=')
330
+ param.lstrip!
331
+
332
+ if disposition[0] == '"'
333
+ # Parameter value is quoted, parse it, handling backslash escapes
334
+ disposition.slice!(0, 1)
335
+ value = String.new
336
+
337
+ while i = disposition.index(/(["\\])/)
338
+ c = $1
339
+
340
+ # Append all content until ending quote or escape
341
+ value << disposition.slice!(0, i)
342
+
343
+ # Remove either backslash or ending quote,
344
+ # ensures forward progress in loop
345
+ disposition.slice!(0, 1)
346
+
347
+ # stop parsing parameter value if found ending quote
348
+ break if c == '"'
349
+
350
+ escaped_char = disposition.slice!(0, 1)
351
+ if param == 'filename' && escaped_char != '"'
352
+ # Possible IE uploaded filename, append both escape backslash and value
353
+ value << c << escaped_char
354
+ else
355
+ # Other only append escaped value
356
+ value << escaped_char
357
+ end
358
+ end
359
+ else
360
+ if i = disposition.index(';')
361
+ # Parameter value unquoted (which may be invalid), value ends at semicolon
362
+ value = disposition.slice!(0, i)
363
+ else
364
+ # If no ending semicolon, assume remainder of line is value and stop
365
+ # parsing
366
+ disposition.strip!
367
+ value = disposition
368
+ disposition = ''
369
+ end
370
+ end
371
+
372
+ case param
373
+ when 'name'
374
+ name = value
375
+ when 'filename'
376
+ filename = value
377
+ when 'filename*'
378
+ filename_star = value
379
+ # else
380
+ # ignore other parameters
381
+ end
382
+
383
+ # skip trailing semicolon, to proceed to next parameter
384
+ if i = disposition.index(';')
385
+ disposition.slice!(0, i+1)
386
+ end
387
+ end
314
388
  else
315
389
  name = head[MULTIPART_CONTENT_ID, 1]
316
390
  end
317
391
 
318
- filename = get_filename(head)
392
+ if filename_star
393
+ encoding, _, filename = filename_star.split("'", 3)
394
+ filename = normalize_filename(filename || '')
395
+ filename.force_encoding(find_encoding(encoding))
396
+ elsif filename
397
+ filename = $1 if filename =~ /^"(.*)"$/
398
+ filename = normalize_filename(filename)
399
+ end
319
400
 
320
401
  if name.nil? || name.empty?
321
402
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -330,7 +411,7 @@ module Rack
330
411
 
331
412
  def handle_mime_body
332
413
  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
414
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
334
415
  @collector.on_mime_body @mime_index, body
335
416
  @sbuf.pos += body.length + 2 # skip \r\n after the content
336
417
  @state = :CONSUME_TOKEN
@@ -360,39 +441,14 @@ module Rack
360
441
  end
361
442
  end
362
443
 
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
-
444
+ def normalize_filename(filename)
381
445
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
382
446
  filename = Utils.unescape_path(filename)
383
447
  end
384
448
 
385
449
  filename.scrub!
386
450
 
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
451
+ filename.split(/[\/\\]/).last || String.new
396
452
  end
397
453
 
398
454
  CHARSET = "charset"
@@ -418,11 +474,7 @@ module Rack
418
474
  v.strip!
419
475
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
420
476
  if k == "charset"
421
- encoding = begin
422
- Encoding.find v
423
- rescue ArgumentError
424
- Encoding::BINARY
425
- end
477
+ encoding = find_encoding(v)
426
478
  end
427
479
  end
428
480
  end
@@ -432,6 +484,15 @@ module Rack
432
484
  body.force_encoding(encoding)
433
485
  end
434
486
 
487
+ # Return the related Encoding object. However, because
488
+ # enc is submitted by the user, it may be invalid, so
489
+ # use a binary encoding in that case.
490
+ def find_encoding(enc)
491
+ Encoding.find enc
492
+ rescue ArgumentError
493
+ Encoding::BINARY
494
+ end
495
+
435
496
  def handle_empty_content!(content)
436
497
  if content.nil? || content.empty?
437
498
  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,40 +1,39 @@
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
  # ParamsTooDeepError is the error that is raised when params are recursively
20
25
  # nested over the specified limit.
21
- class ParamsTooDeepError < RangeError; end
22
-
23
- def self.make_default(_key_space_limit=(not_deprecated = true; nil), param_depth_limit)
24
- unless not_deprecated
25
- 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)
26
- end
26
+ class ParamsTooDeepError < RangeError
27
+ include BadRequest
28
+ end
27
29
 
30
+ def self.make_default(param_depth_limit)
28
31
  new Params, param_depth_limit
29
32
  end
30
33
 
31
34
  attr_reader :param_depth_limit
32
35
 
33
- def initialize(params_class, _key_space_limit=(not_deprecated = true; nil), param_depth_limit)
34
- unless not_deprecated
35
- 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)
36
- end
37
-
36
+ def initialize(params_class, param_depth_limit)
38
37
  @params_class = params_class
39
38
  @param_depth_limit = param_depth_limit
40
39
  end
@@ -194,59 +193,7 @@ module Rack
194
193
  URI.decode_www_form_component(string, encoding)
195
194
  end
196
195
 
197
- class Params
198
- def initialize
199
- @size = 0
200
- @params = {}
201
- end
202
-
203
- def [](key)
204
- @params[key]
205
- end
206
-
207
- def []=(key, value)
208
- @params[key] = value
209
- end
210
-
211
- def key?(key)
212
- @params.key?(key)
213
- end
214
-
215
- # Recursively unwraps nested `Params` objects and constructs an object
216
- # of the same shape, but using the objects' internal representations
217
- # (Ruby hashes) in place of the objects. The result is a hash consisting
218
- # purely of Ruby primitives.
219
- #
220
- # Mutation warning!
221
- #
222
- # 1. This method mutates the internal representation of the `Params`
223
- # objects in order to save object allocations.
224
- #
225
- # 2. The value you get back is a reference to the internal hash
226
- # representation, not a copy.
227
- #
228
- # 3. Because the `Params` object's internal representation is mutable
229
- # through the `#[]=` method, it is not thread safe. The result of
230
- # getting the hash representation while another thread is adding a
231
- # key to it is non-deterministic.
232
- #
233
- def to_h
234
- @params.each do |key, value|
235
- case value
236
- when self
237
- # Handle circular references gracefully.
238
- @params[key] = @params
239
- when Params
240
- @params[key] = value.to_h
241
- when Array
242
- value.map! { |v| v.kind_of?(Params) ? v.to_h : v }
243
- else
244
- # Ignore anything that is not a `Params` object or
245
- # a collection that can contain one.
246
- end
247
- end
248
- @params
249
- end
196
+ class Params < Hash
250
197
  alias_method :to_params_hash, :to_h
251
198
  end
252
199
  end
data/lib/rack/request.rb CHANGED
@@ -482,9 +482,14 @@ module Rack
482
482
 
483
483
  # Returns the data received in the query string.
484
484
  def GET
485
- if get_header(RACK_REQUEST_QUERY_STRING) == query_string
485
+ rr_query_string = get_header(RACK_REQUEST_QUERY_STRING)
486
+ query_string = self.query_string
487
+ if rr_query_string == query_string
486
488
  get_header(RACK_REQUEST_QUERY_HASH)
487
489
  else
490
+ if rr_query_string
491
+ warn "query string used for GET parsing different from current query string. Starting in Rack 3.2, Rack will used the cached GET value instead of parsing the current query string.", uplevel: 1
492
+ end
488
493
  query_hash = parse_query(query_string, '&')
489
494
  set_header(RACK_REQUEST_QUERY_STRING, query_string)
490
495
  set_header(RACK_REQUEST_QUERY_HASH, query_hash)
@@ -505,9 +510,12 @@ module Rack
505
510
 
506
511
  # If the form hash was already memoized:
507
512
  if form_hash = get_header(RACK_REQUEST_FORM_HASH)
513
+ form_input = get_header(RACK_REQUEST_FORM_INPUT)
508
514
  # And it was memoized from the same input:
509
- if get_header(RACK_REQUEST_FORM_INPUT).equal?(rack_input)
515
+ if form_input.equal?(rack_input)
510
516
  return form_hash
517
+ elsif form_input
518
+ warn "input stream used for POST parsing different from current input stream. Starting in Rack 3.2, Rack will used the cached POST value instead of parsing the current input stream.", uplevel: 1
511
519
  end
512
520
  end
513
521
 
@@ -516,7 +524,10 @@ module Rack
516
524
  set_header RACK_REQUEST_FORM_INPUT, nil
517
525
  set_header(RACK_REQUEST_FORM_HASH, {})
518
526
  elsif form_data? || parseable_data?
519
- unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
527
+ if pairs = Rack::Multipart.parse_multipart(env, Rack::Multipart::ParamList)
528
+ set_header RACK_REQUEST_FORM_PAIRS, pairs
529
+ set_header RACK_REQUEST_FORM_HASH, expand_param_pairs(pairs)
530
+ else
520
531
  form_vars = get_header(RACK_INPUT).read
521
532
 
522
533
  # Fix for Safari Ajax postings that always append \0
@@ -605,24 +616,10 @@ module Rack
605
616
  Rack::Request.ip_filter.call(ip)
606
617
  end
607
618
 
608
- # shortcut for <tt>request.params[key]</tt>
609
- def [](key)
610
- warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead", uplevel: 1)
611
-
612
- params[key.to_s]
613
- end
614
-
615
- # shortcut for <tt>request.params[key] = value</tt>
616
- #
617
- # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
618
- def []=(key, value)
619
- warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead", uplevel: 1)
620
-
621
- params[key.to_s] = value
622
- end
623
-
624
619
  # like Hash#values_at
625
620
  def values_at(*keys)
621
+ warn("Request#values_at is deprecated and will be removed in a future version of Rack. Please use request.params.values_at instead", uplevel: 1)
622
+
626
623
  keys.map { |key| params[key] }
627
624
  end
628
625
 
@@ -645,8 +642,8 @@ module Rack
645
642
  end
646
643
 
647
644
  def parse_http_accept_header(header)
648
- header.to_s.split(",").each(&:strip!).map do |part|
649
- attribute, parameters = part.split(";", 2).each(&:strip!)
645
+ header.to_s.split(/\s*,\s*/).map do |part|
646
+ attribute, parameters = part.split(/\s*;\s*/, 2)
650
647
  quality = 1.0
651
648
  if parameters and /\Aq=([\d.]+)/ =~ parameters
652
649
  quality = $1.to_f
@@ -672,6 +669,16 @@ module Rack
672
669
  Rack::Multipart.extract_multipart(self, query_parser)
673
670
  end
674
671
 
672
+ def expand_param_pairs(pairs, query_parser = query_parser())
673
+ params = query_parser.make_params
674
+
675
+ pairs.each do |k, v|
676
+ query_parser.normalize_params(params, k, v)
677
+ end
678
+
679
+ params.to_params_hash
680
+ end
681
+
675
682
  def split_header(value)
676
683
  value ? value.strip.split(/[,\s]+/) : []
677
684
  end
data/lib/rack/response.rb CHANGED
@@ -25,19 +25,11 @@ module Rack
25
25
  self.new(body, status, headers)
26
26
  end
27
27
 
28
- CHUNKED = 'chunked'
29
28
  STATUS_WITH_NO_ENTITY_BODY = Utils::STATUS_WITH_NO_ENTITY_BODY
30
29
 
31
30
  attr_accessor :length, :status, :body
32
31
  attr_reader :headers
33
32
 
34
- # Deprecated, use headers instead.
35
- def header
36
- warn 'Rack::Response#header is deprecated and will be removed in Rack 3.1, use #headers instead', uplevel: 1
37
-
38
- headers
39
- end
40
-
41
33
  # Initialize the response object with the specified +body+, +status+
42
34
  # and +headers+.
43
35
  #
@@ -62,7 +54,7 @@ module Rack
62
54
  @status = status.to_i
63
55
 
64
56
  unless headers.is_a?(Hash)
65
- warn "Providing non-hash headers to Rack::Response is deprecated and will be removed in Rack 3.1", uplevel: 1
57
+ raise ArgumentError, "Headers must be a Hash!"
66
58
  end
67
59
 
68
60
  @headers = Headers.new
@@ -97,16 +89,12 @@ module Rack
97
89
  self.status = status
98
90
  self.location = target
99
91
  end
100
-
101
- def chunked?
102
- CHUNKED == get_header(TRANSFER_ENCODING)
103
- end
104
-
92
+
105
93
  def no_entity_body?
106
94
  # The response body is an enumerable body and it is not allowed to have an entity body.
107
95
  @body.respond_to?(:each) && STATUS_WITH_NO_ENTITY_BODY[@status]
108
96
  end
109
-
97
+
110
98
  # Generate a response array consistent with the requirements of the SPEC.
111
99
  # @return [Array] a 3-tuple suitable of `[status, headers, body]`
112
100
  # which is suitable to be returned from the middleware `#call(env)` method.
@@ -117,6 +105,10 @@ module Rack
117
105
  close
118
106
  return [@status, @headers, []]
119
107
  else
108
+ if @length && @length > 0
109
+ set_header CONTENT_LENGTH, @length.to_s
110
+ end
111
+
120
112
  if block_given?
121
113
  @block = block
122
114
  return [@status, @headers, self]
@@ -320,20 +312,26 @@ module Rack
320
312
 
321
313
  protected
322
314
 
315
+ # Convert the body of this response into an internally buffered Array if possible.
316
+ #
317
+ # `@buffered` is a ternary value which indicates whether the body is buffered. It can be:
318
+ # * `nil` - The body has not been buffered yet.
319
+ # * `true` - The body is buffered as an Array instance.
320
+ # * `false` - The body is not buffered and cannot be buffered.
321
+ #
322
+ # @return [Boolean] whether the body is buffered as an Array instance.
323
323
  def buffered_body!
324
324
  if @buffered.nil?
325
325
  if @body.is_a?(Array)
326
326
  # The user supplied body was an array:
327
327
  @body = @body.compact
328
- @body.each do |part|
329
- @length += part.to_s.bytesize
330
- end
331
-
328
+ @length = @body.sum{|part| part.bytesize}
332
329
  @buffered = true
333
330
  elsif @body.respond_to?(:each)
334
331
  # Turn the user supplied body into a buffered array:
335
332
  body = @body
336
333
  @body = Array.new
334
+ @length = 0
337
335
 
338
336
  body.each do |part|
339
337
  @writer.call(part.to_s)
@@ -341,8 +339,10 @@ module Rack
341
339
 
342
340
  body.close if body.respond_to?(:close)
343
341
 
342
+ # We have converted the body into an Array:
344
343
  @buffered = true
345
344
  else
345
+ # We don't know how to buffer the user-supplied body:
346
346
  @buffered = false
347
347
  end
348
348
  end
@@ -351,12 +351,10 @@ module Rack
351
351
  end
352
352
 
353
353
  def append(chunk)
354
+ chunk = chunk.dup unless chunk.frozen?
354
355
  @body << chunk
355
356
 
356
- unless chunked?
357
- @length += chunk.bytesize
358
- set_header(CONTENT_LENGTH, @length.to_s)
359
- end
357
+ @length += chunk.bytesize
360
358
 
361
359
  return chunk
362
360
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ostruct'
4
3
  require 'erb'
5
4
 
6
5
  require_relative 'constants'
@@ -19,6 +18,11 @@ module Rack
19
18
  class ShowExceptions
20
19
  CONTEXT = 7
21
20
 
21
+ Frame = Struct.new(:filename, :lineno, :function,
22
+ :pre_context_lineno, :pre_context,
23
+ :context_line, :post_context_lineno,
24
+ :post_context)
25
+
22
26
  def initialize(app)
23
27
  @app = app
24
28
  end
@@ -79,7 +83,7 @@ module Rack
79
83
  # This double assignment is to prevent an "unused variable" warning.
80
84
  # Yes, it is dumb, but I don't like Ruby yelling at me.
81
85
  frames = frames = exception.backtrace.map { |line|
82
- frame = OpenStruct.new
86
+ frame = Frame.new
83
87
  if line =~ /(.*?):(\d+)(:in `(.*)')?/
84
88
  frame.filename = $1
85
89
  frame.lineno = $2.to_i