rack 2.2.17 → 3.2.0

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.

Potentially problematic release.


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

Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +501 -70
  3. data/CONTRIBUTING.md +63 -55
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +376 -0
  6. data/SPEC.rdoc +243 -277
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +5 -1
  9. data/lib/rack/auth/basic.rb +1 -3
  10. data/lib/rack/bad_request.rb +8 -0
  11. data/lib/rack/body_proxy.rb +21 -3
  12. data/lib/rack/builder.rb +108 -69
  13. data/lib/rack/cascade.rb +2 -3
  14. data/lib/rack/common_logger.rb +22 -17
  15. data/lib/rack/conditional_get.rb +20 -16
  16. data/lib/rack/constants.rb +68 -0
  17. data/lib/rack/content_length.rb +12 -16
  18. data/lib/rack/content_type.rb +8 -5
  19. data/lib/rack/deflater.rb +40 -26
  20. data/lib/rack/directory.rb +9 -3
  21. data/lib/rack/etag.rb +17 -23
  22. data/lib/rack/events.rb +4 -0
  23. data/lib/rack/files.rb +15 -17
  24. data/lib/rack/head.rb +8 -8
  25. data/lib/rack/headers.rb +238 -0
  26. data/lib/rack/lint.rb +817 -648
  27. data/lib/rack/lock.rb +2 -5
  28. data/lib/rack/media_type.rb +6 -7
  29. data/lib/rack/method_override.rb +5 -1
  30. data/lib/rack/mime.rb +14 -5
  31. data/lib/rack/mock.rb +1 -300
  32. data/lib/rack/mock_request.rb +161 -0
  33. data/lib/rack/mock_response.rb +147 -0
  34. data/lib/rack/multipart/generator.rb +7 -5
  35. data/lib/rack/multipart/parser.rb +241 -95
  36. data/lib/rack/multipart/uploaded_file.rb +45 -4
  37. data/lib/rack/multipart.rb +53 -40
  38. data/lib/rack/null_logger.rb +9 -0
  39. data/lib/rack/query_parser.rb +116 -121
  40. data/lib/rack/recursive.rb +2 -0
  41. data/lib/rack/reloader.rb +0 -2
  42. data/lib/rack/request.rb +269 -141
  43. data/lib/rack/response.rb +151 -66
  44. data/lib/rack/rewindable_input.rb +27 -5
  45. data/lib/rack/runtime.rb +7 -6
  46. data/lib/rack/sendfile.rb +30 -25
  47. data/lib/rack/show_exceptions.rb +25 -6
  48. data/lib/rack/show_status.rb +17 -9
  49. data/lib/rack/static.rb +8 -8
  50. data/lib/rack/tempfile_reaper.rb +15 -4
  51. data/lib/rack/urlmap.rb +3 -1
  52. data/lib/rack/utils.rb +228 -238
  53. data/lib/rack/version.rb +3 -15
  54. data/lib/rack.rb +13 -90
  55. metadata +15 -41
  56. data/README.rdoc +0 -347
  57. data/Rakefile +0 -130
  58. data/bin/rackup +0 -5
  59. data/contrib/rack.png +0 -0
  60. data/contrib/rack.svg +0 -150
  61. data/contrib/rack_logo.svg +0 -164
  62. data/contrib/rdoc.css +0 -412
  63. data/example/lobster.ru +0 -6
  64. data/example/protectedlobster.rb +0 -16
  65. data/example/protectedlobster.ru +0 -10
  66. data/lib/rack/auth/digest/md5.rb +0 -131
  67. data/lib/rack/auth/digest/nonce.rb +0 -53
  68. data/lib/rack/auth/digest/params.rb +0 -54
  69. data/lib/rack/auth/digest/request.rb +0 -43
  70. data/lib/rack/chunked.rb +0 -117
  71. data/lib/rack/core_ext/regexp.rb +0 -14
  72. data/lib/rack/file.rb +0 -7
  73. data/lib/rack/handler/cgi.rb +0 -59
  74. data/lib/rack/handler/fastcgi.rb +0 -100
  75. data/lib/rack/handler/lsws.rb +0 -61
  76. data/lib/rack/handler/scgi.rb +0 -71
  77. data/lib/rack/handler/thin.rb +0 -36
  78. data/lib/rack/handler/webrick.rb +0 -129
  79. data/lib/rack/handler.rb +0 -104
  80. data/lib/rack/lobster.rb +0 -70
  81. data/lib/rack/logger.rb +0 -20
  82. data/lib/rack/server.rb +0 -466
  83. data/lib/rack/session/abstract/id.rb +0 -523
  84. data/lib/rack/session/cookie.rb +0 -203
  85. data/lib/rack/session/memcache.rb +0 -10
  86. data/lib/rack/session/pool.rb +0 -90
  87. data/rack.gemspec +0 -46
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ require_relative 'response'
6
+
7
+ module Rack
8
+ # Rack::MockResponse provides useful helpers for testing your apps.
9
+ # Usually, you don't create the MockResponse on your own, but use
10
+ # MockRequest.
11
+
12
+ class MockResponse < Rack::Response
13
+ class Cookie
14
+ attr_reader :name, :value, :path, :domain, :expires, :secure
15
+
16
+ def initialize(args)
17
+ @name = args["name"]
18
+ @value = args["value"]
19
+ @path = args["path"]
20
+ @domain = args["domain"]
21
+ @expires = args["expires"]
22
+ @secure = args["secure"]
23
+ end
24
+
25
+ def method_missing(method_name, *args, &block)
26
+ @value.send(method_name, *args, &block)
27
+ end
28
+ # :nocov:
29
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
30
+ # :nocov:
31
+
32
+ def respond_to_missing?(method_name, include_all = false)
33
+ @value.respond_to?(method_name, include_all) || super
34
+ end
35
+ end
36
+
37
+ class << self
38
+ alias [] new
39
+ end
40
+
41
+ # Headers
42
+ attr_reader :original_headers, :cookies
43
+
44
+ # Errors
45
+ attr_accessor :errors
46
+
47
+ def initialize(status, headers, body, errors = nil)
48
+ @original_headers = headers
49
+
50
+ if errors
51
+ @errors = errors.string if errors.respond_to?(:string)
52
+ else
53
+ @errors = ""
54
+ end
55
+
56
+ super(body, status, headers)
57
+
58
+ @cookies = parse_cookies_from_header
59
+ buffered_body!
60
+ end
61
+
62
+ def =~(other)
63
+ body =~ other
64
+ end
65
+
66
+ def match(other)
67
+ body.match other
68
+ end
69
+
70
+ def body
71
+ return @buffered_body if defined?(@buffered_body)
72
+
73
+ # FIXME: apparently users of MockResponse expect the return value of
74
+ # MockResponse#body to be a string. However, the real response object
75
+ # returns the body as a list.
76
+ #
77
+ # See spec_showstatus.rb:
78
+ #
79
+ # should "not replace existing messages" do
80
+ # ...
81
+ # res.body.should == "foo!"
82
+ # end
83
+ buffer = @buffered_body = String.new
84
+
85
+ @body.each do |chunk|
86
+ buffer << chunk
87
+ end
88
+
89
+ return buffer
90
+ end
91
+
92
+ def empty?
93
+ [201, 204, 304].include? status
94
+ end
95
+
96
+ def cookie(name)
97
+ cookies.fetch(name, nil)
98
+ end
99
+
100
+ private
101
+
102
+ def parse_cookies_from_header
103
+ cookies = Hash.new
104
+ set_cookie_header = headers['set-cookie']
105
+ if set_cookie_header && !set_cookie_header.empty?
106
+ Array(set_cookie_header).each do |cookie|
107
+ cookie_name, cookie_filling = cookie.split('=', 2)
108
+ cookie_attributes = identify_cookie_attributes cookie_filling
109
+ parsed_cookie = Cookie.new(
110
+ 'name' => cookie_name.strip,
111
+ 'value' => cookie_attributes.fetch('value'),
112
+ 'path' => cookie_attributes.fetch('path', nil),
113
+ 'domain' => cookie_attributes.fetch('domain', nil),
114
+ 'expires' => cookie_attributes.fetch('expires', nil),
115
+ 'secure' => cookie_attributes.fetch('secure', false)
116
+ )
117
+ cookies.store(cookie_name, parsed_cookie)
118
+ end
119
+ end
120
+ cookies
121
+ end
122
+
123
+ def identify_cookie_attributes(cookie_filling)
124
+ cookie_bits = cookie_filling.split(';')
125
+ cookie_attributes = Hash.new
126
+ cookie_attributes.store('value', Array(cookie_bits[0].strip))
127
+ cookie_bits.drop(1).each do |bit|
128
+ if bit.include? '='
129
+ cookie_attribute, attribute_value = bit.split('=', 2)
130
+ cookie_attributes.store(cookie_attribute.strip.downcase, attribute_value.strip)
131
+ end
132
+ if bit.include? 'secure'
133
+ cookie_attributes.store('secure', true)
134
+ end
135
+ end
136
+
137
+ if cookie_attributes.key? 'max-age'
138
+ cookie_attributes.store('expires', Time.now + cookie_attributes['max-age'].to_i)
139
+ elsif cookie_attributes.key? 'expires'
140
+ cookie_attributes.store('expires', Time.httpdate(cookie_attributes['expires']))
141
+ end
142
+
143
+ cookie_attributes
144
+ end
145
+
146
+ end
147
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'uploaded_file'
4
+
3
5
  module Rack
4
6
  module Multipart
5
7
  class Generator
@@ -74,12 +76,12 @@ module Rack
74
76
 
75
77
  def content_for_tempfile(io, file, name)
76
78
  length = ::File.stat(file.path).size if file.path
77
- filename = "; filename=\"#{Utils.escape(file.original_filename)}\"" if file.original_filename
79
+ filename = "; filename=\"#{Utils.escape_path(file.original_filename)}\""
78
80
  <<-EOF
79
81
  --#{MULTIPART_BOUNDARY}\r
80
- Content-Disposition: form-data; name="#{name}"#{filename}\r
81
- Content-Type: #{file.content_type}\r
82
- #{"Content-Length: #{length}\r\n" if length}\r
82
+ content-disposition: form-data; name="#{name}"#{filename}\r
83
+ content-type: #{file.content_type}\r
84
+ #{"content-length: #{length}\r\n" if length}\r
83
85
  #{io.read}\r
84
86
  EOF
85
87
  end
@@ -87,7 +89,7 @@ EOF
87
89
  def content_for_other(file, name)
88
90
  <<-EOF
89
91
  --#{MULTIPART_BOUNDARY}\r
90
- Content-Disposition: form-data; name="#{name}"\r
92
+ content-disposition: form-data; name="#{name}"\r
91
93
  \r
92
94
  #{file}\r
93
95
  EOF
@@ -2,14 +2,55 @@
2
2
 
3
3
  require 'strscan'
4
4
 
5
+ require_relative '../utils'
6
+ require_relative '../bad_request'
7
+
5
8
  module Rack
6
9
  module Multipart
7
- class MultipartPartLimitError < Errno::EMFILE; end
8
- class MultipartTotalPartLimitError < StandardError; end
10
+ class MultipartPartLimitError < Errno::EMFILE
11
+ include BadRequest
12
+ end
9
13
 
10
- class Parser
11
- (require_relative '../core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
14
+ class MultipartTotalPartLimitError < StandardError
15
+ include BadRequest
16
+ end
17
+
18
+ # Use specific error class when parsing multipart request
19
+ # that ends early.
20
+ class EmptyContentError < ::EOFError
21
+ include BadRequest
22
+ end
23
+
24
+ # Base class for multipart exceptions that do not subclass from
25
+ # other exception classes for backwards compatibility.
26
+ class BoundaryTooLongError < StandardError
27
+ include BadRequest
28
+ end
12
29
 
30
+ # Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
31
+ Error = BoundaryTooLongError
32
+
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
36
+ MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
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
40
+
41
+ # Rack::Multipart::Parser handles parsing of multipart/form-data requests.
42
+ #
43
+ # File Parameter Contents
44
+ #
45
+ # When processing file uploads, the parser returns a hash containing
46
+ # information about uploaded files. For +file+ parameters, the hash includes:
47
+ #
48
+ # * +:filename+ - The original filename, already URL decoded by the parser
49
+ # * +:type+ - The content type of the uploaded file
50
+ # * +:name+ - The parameter name from the form
51
+ # * +:tempfile+ - A Tempfile object containing the uploaded data
52
+ # * +:head+ - The raw header content for this part
53
+ class Parser
13
54
  BUFSIZE = 1_048_576
14
55
  TEXT_PLAIN = "text/plain"
15
56
  TEMPFILE_FACTORY = lambda { |filename, content_type|
@@ -18,8 +59,6 @@ module Rack
18
59
  Tempfile.new(["RackMultipart", extension])
19
60
  }
20
61
 
21
- BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
22
-
23
62
  class BoundedIO # :nodoc:
24
63
  def initialize(io, content_length)
25
64
  @io = io
@@ -41,16 +80,12 @@ module Rack
41
80
  if str
42
81
  @cursor += str.bytesize
43
82
  else
44
- # Raise an error for mismatching Content-Length and actual contents
83
+ # Raise an error for mismatching content-length and actual contents
45
84
  raise EOFError, "bad content body"
46
85
  end
47
86
 
48
87
  str
49
88
  end
50
-
51
- def rewind
52
- @io.rewind
53
- end
54
89
  end
55
90
 
56
91
  MultipartInfo = Struct.new :params, :tmp_files
@@ -69,18 +104,17 @@ module Rack
69
104
  boundary = parse_boundary content_type
70
105
  return EMPTY unless boundary
71
106
 
107
+ if boundary.length > 70
108
+ # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
109
+ # Most clients use no more than 55 characters.
110
+ raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
111
+ end
112
+
72
113
  io = BoundedIO.new(io, content_length) if content_length
73
- outbuf = String.new
74
114
 
75
115
  parser = new(boundary, tmpfile, bufsize, qp)
76
- parser.on_read io.read(bufsize, outbuf)
116
+ parser.parse(io)
77
117
 
78
- loop do
79
- break if parser.state == :DONE
80
- parser.on_read io.read(bufsize, outbuf)
81
- end
82
-
83
- io.rewind
84
118
  parser.result
85
119
  end
86
120
 
@@ -180,33 +214,49 @@ module Rack
180
214
  def initialize(boundary, tempfile, bufsize, query_parser)
181
215
  @query_parser = query_parser
182
216
  @params = query_parser.make_params
183
- @boundary = "--#{boundary}"
184
217
  @bufsize = bufsize
185
218
 
186
- @full_boundary = @boundary
187
- @end_boundary = @boundary + '--'
188
219
  @state = :FAST_FORWARD
189
220
  @mime_index = 0
190
221
  @collector = Collector.new tempfile
191
222
 
192
223
  @sbuf = StringScanner.new("".dup)
193
- @body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
194
- @end_boundary_size = boundary.bytesize + 6 # (-- at start, -- at finish, EOL at end)
195
- @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
224
+ @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
225
+ @body_regex_at_end = /#{@body_regex}\z/m
226
+ @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
227
+ @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
196
228
  @head_regex = /(.*?#{EOL})#{EOL}/m
197
229
  end
198
230
 
199
- def on_read(content)
200
- handle_empty_content!(content)
201
- @sbuf.concat content
202
- run_parser
231
+ def parse(io)
232
+ outbuf = String.new
233
+ read_data(io, outbuf)
234
+
235
+ loop do
236
+ status =
237
+ case @state
238
+ when :FAST_FORWARD
239
+ handle_fast_forward
240
+ when :CONSUME_TOKEN
241
+ handle_consume_token
242
+ when :MIME_HEAD
243
+ handle_mime_head
244
+ when :MIME_BODY
245
+ handle_mime_body
246
+ else # when :DONE
247
+ return
248
+ end
249
+
250
+ read_data(io, outbuf) if status == :want_read
251
+ end
203
252
  end
204
253
 
205
254
  def result
206
255
  @collector.each do |part|
207
256
  part.get_data do |data|
208
257
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
209
- @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
258
+ name, data = handle_dummy_encoding(part.name, data)
259
+ @query_parser.normalize_params(@params, name, data)
210
260
  end
211
261
  end
212
262
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -214,34 +264,39 @@ module Rack
214
264
 
215
265
  private
216
266
 
217
- def run_parser
218
- loop do
219
- case @state
220
- when :FAST_FORWARD
221
- break if handle_fast_forward == :want_read
222
- when :CONSUME_TOKEN
223
- break if handle_consume_token == :want_read
224
- when :MIME_HEAD
225
- break if handle_mime_head == :want_read
226
- when :MIME_BODY
227
- break if handle_mime_body == :want_read
228
- when :DONE
229
- break
230
- end
231
- end
267
+ def read_data(io, outbuf)
268
+ content = io.read(@bufsize, outbuf)
269
+ handle_empty_content!(content)
270
+ @sbuf.concat(content)
232
271
  end
233
272
 
273
+ # This handles the initial parser state. We read until we find the starting
274
+ # boundary, then we can transition to the next state. If we find the ending
275
+ # boundary, this is an invalid multipart upload, but keep scanning for opening
276
+ # boundary in that case. If no boundary found, we need to keep reading data
277
+ # and retry. It's highly unlikely the initial read will not consume the
278
+ # boundary. The client would have to deliberately craft a response
279
+ # with the opening boundary beyond the buffer size for that to happen.
234
280
  def handle_fast_forward
235
- tok = consume_boundary
281
+ while true
282
+ case consume_boundary
283
+ when :BOUNDARY
284
+ # found opening boundary, transition to next state
285
+ @state = :MIME_HEAD
286
+ return
287
+ when :END_BOUNDARY
288
+ # invalid multipart upload
289
+ if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
290
+ # stop parsing a buffer if a buffer is only an end boundary.
291
+ @state = :DONE
292
+ return
293
+ end
236
294
 
237
- if tok == :END_BOUNDARY && @sbuf.pos == @end_boundary_size && @sbuf.eos?
238
- # stop parsing a buffer if a buffer is only an end boundary.
239
- @state = :DONE
240
- elsif tok
241
- @state = :MIME_HEAD
242
- else
243
- raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
244
- :want_read
295
+ # retry for opening boundary
296
+ else
297
+ # no boundary found, keep reading data
298
+ return :want_read
299
+ end
245
300
  end
246
301
  end
247
302
 
@@ -255,17 +310,101 @@ module Rack
255
310
  end
256
311
  end
257
312
 
313
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
314
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
258
315
  def handle_mime_head
259
316
  if @sbuf.scan_until(@head_regex)
260
317
  head = @sbuf[1]
261
318
  content_type = head[MULTIPART_CONTENT_TYPE, 1]
262
- if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
263
- name = Rack::Auth::Digest::Params::dequote(name)
319
+ if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
320
+ disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
321
+
322
+ # ignore actual content-disposition value (should always be form-data)
323
+ i = disposition.index(';')
324
+ disposition.slice!(0, i+1)
325
+ param = nil
326
+ num_params = 0
327
+
328
+ # Parse parameter list
329
+ while i = disposition.index('=')
330
+ # Only parse up to max parameters, to avoid potential denial of service
331
+ num_params += 1
332
+ break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
333
+
334
+ # Found end of parameter name, ensure forward progress in loop
335
+ param = disposition.slice!(0, i+1)
336
+
337
+ # Remove ending equals and preceding whitespace from parameter name
338
+ param.chomp!('=')
339
+ param.lstrip!
340
+
341
+ if disposition[0] == '"'
342
+ # Parameter value is quoted, parse it, handling backslash escapes
343
+ disposition.slice!(0, 1)
344
+ value = String.new
345
+
346
+ while i = disposition.index(/(["\\])/)
347
+ c = $1
348
+
349
+ # Append all content until ending quote or escape
350
+ value << disposition.slice!(0, i)
351
+
352
+ # Remove either backslash or ending quote,
353
+ # ensures forward progress in loop
354
+ disposition.slice!(0, 1)
355
+
356
+ # stop parsing parameter value if found ending quote
357
+ break if c == '"'
358
+
359
+ escaped_char = disposition.slice!(0, 1)
360
+ if param == 'filename' && escaped_char != '"'
361
+ # Possible IE uploaded filename, append both escape backslash and value
362
+ value << c << escaped_char
363
+ else
364
+ # Other only append escaped value
365
+ value << escaped_char
366
+ end
367
+ end
368
+ else
369
+ if i = disposition.index(';')
370
+ # Parameter value unquoted (which may be invalid), value ends at semicolon
371
+ value = disposition.slice!(0, i)
372
+ else
373
+ # If no ending semicolon, assume remainder of line is value and stop
374
+ # parsing
375
+ disposition.strip!
376
+ value = disposition
377
+ disposition = ''
378
+ end
379
+ end
380
+
381
+ case param
382
+ when 'name'
383
+ name = value
384
+ when 'filename'
385
+ filename = value
386
+ when 'filename*'
387
+ filename_star = value
388
+ # else
389
+ # ignore other parameters
390
+ end
391
+
392
+ # skip trailing semicolon, to proceed to next parameter
393
+ if i = disposition.index(';')
394
+ disposition.slice!(0, i+1)
395
+ end
396
+ end
264
397
  else
265
398
  name = head[MULTIPART_CONTENT_ID, 1]
266
399
  end
267
400
 
268
- filename = get_filename(head)
401
+ if filename_star
402
+ encoding, _, filename = filename_star.split("'", 3)
403
+ filename = normalize_filename(filename || '')
404
+ filename.force_encoding(find_encoding(encoding))
405
+ elsif filename
406
+ filename = normalize_filename(filename)
407
+ end
269
408
 
270
409
  if name.nil? || name.empty?
271
410
  name = filename || "#{content_type || TEXT_PLAIN}[]".dup
@@ -280,7 +419,7 @@ module Rack
280
419
 
281
420
  def handle_mime_body
282
421
  if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
283
- body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
422
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
284
423
  @collector.on_mime_body @mime_index, body
285
424
  @sbuf.pos += body.length + 2 # skip \r\n after the content
286
425
  @state = :CONSUME_TOKEN
@@ -297,54 +436,31 @@ module Rack
297
436
  end
298
437
  end
299
438
 
300
- def full_boundary; @full_boundary; end
301
-
439
+ # Scan until the we find the start or end of the boundary.
440
+ # If we find it, return the appropriate symbol for the start or
441
+ # end of the boundary. If we don't find the start or end of the
442
+ # boundary, clear the buffer and return nil.
302
443
  def consume_boundary
303
- while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
304
- case read_buffer.strip
305
- when full_boundary then return :BOUNDARY
306
- when @end_boundary then return :END_BOUNDARY
307
- end
308
- return if @sbuf.eos?
444
+ if read_buffer = @sbuf.scan_until(@body_regex)
445
+ read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
446
+ else
447
+ @sbuf.terminate
448
+ nil
309
449
  end
310
450
  end
311
451
 
312
- def get_filename(head)
313
- filename = nil
314
- case head
315
- when RFC2183
316
- params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
317
-
318
- if filename = params['filename']
319
- filename = $1 if filename =~ /^"(.*)"$/
320
- elsif filename = params['filename*']
321
- encoding, _, filename = filename.split("'", 3)
322
- end
323
- when BROKEN
324
- filename = $1
325
- filename = $1 if filename =~ /^"(.*)"$/
326
- end
327
-
328
- return unless filename
329
-
452
+ def normalize_filename(filename)
330
453
  if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
331
454
  filename = Utils.unescape_path(filename)
332
455
  end
333
456
 
334
457
  filename.scrub!
335
458
 
336
- if filename !~ /\\[^\\"]/
337
- filename = filename.gsub(/\\(.)/, '\1')
338
- end
339
-
340
- if encoding
341
- filename.force_encoding ::Encoding.find(encoding)
342
- end
343
-
344
- filename
459
+ filename.split(/[\/\\]/).last || String.new
345
460
  end
346
461
 
347
462
  CHARSET = "charset"
463
+ deprecate_constant :CHARSET
348
464
 
349
465
  def tag_multipart_encoding(filename, content_type, name, body)
350
466
  name = name.to_s
@@ -365,7 +481,9 @@ module Rack
365
481
  k.strip!
366
482
  v.strip!
367
483
  v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
368
- encoding = Encoding.find v if k == CHARSET
484
+ if k == "charset"
485
+ encoding = find_encoding(v)
486
+ end
369
487
  end
370
488
  end
371
489
  end
@@ -374,9 +492,37 @@ module Rack
374
492
  body.force_encoding(encoding)
375
493
  end
376
494
 
495
+ # Return the related Encoding object. However, because
496
+ # enc is submitted by the user, it may be invalid, so
497
+ # use a binary encoding in that case.
498
+ def find_encoding(enc)
499
+ Encoding.find enc
500
+ rescue ArgumentError
501
+ Encoding::BINARY
502
+ end
503
+
504
+ REENCODE_DUMMY_ENCODINGS = {
505
+ # ISO-2022-JP is a legacy but still widely used encoding in Japan
506
+ # Here we convert ISO-2022-JP to UTF-8 so that it can be handled.
507
+ Encoding::ISO_2022_JP => true
508
+
509
+ # Other dummy encodings are rarely used and have not been supported yet.
510
+ # Adding support for them will require careful considerations.
511
+ }
512
+
513
+ def handle_dummy_encoding(name, body)
514
+ # A string object with a 'dummy' encoding does not have full functionality and can cause errors.
515
+ # So here we covert it to UTF-8 so that it can be handled properly.
516
+ if name.encoding.dummy? && REENCODE_DUMMY_ENCODINGS[name.encoding]
517
+ name = name.encode(Encoding::UTF_8)
518
+ body = body.encode(Encoding::UTF_8)
519
+ end
520
+ return name, body
521
+ end
522
+
377
523
  def handle_empty_content!(content)
378
524
  if content.nil? || content.empty?
379
- raise EOFError
525
+ raise EmptyContentError
380
526
  end
381
527
  end
382
528
  end