rack 1.6.13 → 2.0.0.alpha

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 (138) hide show
  1. checksums.yaml +5 -5
  2. data/HISTORY.md +139 -18
  3. data/README.rdoc +17 -25
  4. data/Rakefile +6 -14
  5. data/SPEC +8 -9
  6. data/contrib/rack_logo.svg +164 -111
  7. data/lib/rack.rb +70 -21
  8. data/lib/rack/auth/digest/request.rb +1 -1
  9. data/lib/rack/body_proxy.rb +14 -9
  10. data/lib/rack/builder.rb +3 -3
  11. data/lib/rack/chunked.rb +5 -5
  12. data/lib/rack/{commonlogger.rb → common_logger.rb} +2 -2
  13. data/lib/rack/{conditionalget.rb → conditional_get.rb} +0 -0
  14. data/lib/rack/content_length.rb +2 -2
  15. data/lib/rack/deflater.rb +4 -4
  16. data/lib/rack/directory.rb +49 -55
  17. data/lib/rack/etag.rb +2 -1
  18. data/lib/rack/events.rb +154 -0
  19. data/lib/rack/file.rb +55 -40
  20. data/lib/rack/handler.rb +2 -24
  21. data/lib/rack/handler/cgi.rb +15 -16
  22. data/lib/rack/handler/fastcgi.rb +13 -14
  23. data/lib/rack/handler/lsws.rb +11 -11
  24. data/lib/rack/handler/scgi.rb +15 -15
  25. data/lib/rack/handler/thin.rb +3 -0
  26. data/lib/rack/handler/webrick.rb +22 -24
  27. data/lib/rack/head.rb +15 -17
  28. data/lib/rack/lint.rb +38 -38
  29. data/lib/rack/lobster.rb +1 -1
  30. data/lib/rack/lock.rb +6 -10
  31. data/lib/rack/logger.rb +2 -2
  32. data/lib/rack/media_type.rb +38 -0
  33. data/lib/rack/{methodoverride.rb → method_override.rb} +4 -11
  34. data/lib/rack/mime.rb +18 -5
  35. data/lib/rack/mock.rb +35 -52
  36. data/lib/rack/multipart.rb +35 -6
  37. data/lib/rack/multipart/generator.rb +4 -4
  38. data/lib/rack/multipart/parser.rb +273 -158
  39. data/lib/rack/multipart/uploaded_file.rb +1 -2
  40. data/lib/rack/{nulllogger.rb → null_logger.rb} +1 -1
  41. data/lib/rack/query_parser.rb +174 -0
  42. data/lib/rack/recursive.rb +8 -8
  43. data/lib/rack/reloader.rb +1 -2
  44. data/lib/rack/request.rb +370 -304
  45. data/lib/rack/response.rb +129 -56
  46. data/lib/rack/rewindable_input.rb +1 -12
  47. data/lib/rack/runtime.rb +10 -18
  48. data/lib/rack/sendfile.rb +5 -7
  49. data/lib/rack/server.rb +31 -25
  50. data/lib/rack/session/abstract/id.rb +93 -135
  51. data/lib/rack/session/cookie.rb +26 -28
  52. data/lib/rack/session/memcache.rb +8 -14
  53. data/lib/rack/session/pool.rb +14 -21
  54. data/lib/rack/show_exceptions.rb +386 -0
  55. data/lib/rack/{showstatus.rb → show_status.rb} +3 -3
  56. data/lib/rack/static.rb +30 -5
  57. data/lib/rack/tempfile_reaper.rb +2 -2
  58. data/lib/rack/urlmap.rb +13 -14
  59. data/lib/rack/utils.rb +128 -221
  60. data/rack.gemspec +9 -5
  61. data/test/builder/an_underscore_app.rb +5 -0
  62. data/test/builder/options.ru +1 -1
  63. data/test/cgi/test.fcgi +1 -0
  64. data/test/cgi/test.gz +0 -0
  65. data/test/helper.rb +31 -0
  66. data/test/multipart/filename_with_encoded_words +7 -0
  67. data/test/multipart/{filename_with_null_byte → filename_with_single_quote} +1 -1
  68. data/test/multipart/quoted +15 -0
  69. data/test/multipart/rack-logo.png +0 -0
  70. data/test/registering_handler/rack/handler/registering_myself.rb +1 -1
  71. data/test/spec_auth_basic.rb +20 -19
  72. data/test/spec_auth_digest.rb +47 -46
  73. data/test/spec_body_proxy.rb +27 -27
  74. data/test/spec_builder.rb +51 -41
  75. data/test/spec_cascade.rb +24 -22
  76. data/test/spec_cgi.rb +49 -67
  77. data/test/spec_chunked.rb +36 -34
  78. data/test/{spec_commonlogger.rb → spec_common_logger.rb} +23 -21
  79. data/test/{spec_conditionalget.rb → spec_conditional_get.rb} +29 -28
  80. data/test/spec_config.rb +3 -2
  81. data/test/spec_content_length.rb +18 -17
  82. data/test/spec_content_type.rb +13 -12
  83. data/test/spec_deflater.rb +66 -40
  84. data/test/spec_directory.rb +72 -27
  85. data/test/spec_etag.rb +32 -31
  86. data/test/spec_events.rb +133 -0
  87. data/test/spec_fastcgi.rb +50 -72
  88. data/test/spec_file.rb +96 -77
  89. data/test/spec_handler.rb +19 -34
  90. data/test/spec_head.rb +15 -14
  91. data/test/spec_lint.rb +162 -197
  92. data/test/spec_lobster.rb +24 -23
  93. data/test/spec_lock.rb +69 -39
  94. data/test/spec_logger.rb +4 -3
  95. data/test/spec_media_type.rb +42 -0
  96. data/test/spec_method_override.rb +83 -0
  97. data/test/spec_mime.rb +19 -19
  98. data/test/spec_mock.rb +196 -151
  99. data/test/spec_multipart.rb +310 -202
  100. data/test/{spec_nulllogger.rb → spec_null_logger.rb} +5 -4
  101. data/test/spec_recursive.rb +17 -14
  102. data/test/spec_request.rb +763 -607
  103. data/test/spec_response.rb +209 -156
  104. data/test/spec_rewindable_input.rb +50 -40
  105. data/test/spec_runtime.rb +11 -10
  106. data/test/spec_sendfile.rb +30 -35
  107. data/test/spec_server.rb +78 -52
  108. data/test/spec_session_abstract_id.rb +11 -33
  109. data/test/spec_session_cookie.rb +97 -65
  110. data/test/spec_session_memcache.rb +63 -101
  111. data/test/spec_session_pool.rb +48 -84
  112. data/test/spec_show_exceptions.rb +80 -0
  113. data/test/{spec_showstatus.rb → spec_show_status.rb} +36 -35
  114. data/test/spec_static.rb +71 -32
  115. data/test/spec_tempfile_reaper.rb +11 -10
  116. data/test/spec_thin.rb +55 -50
  117. data/test/spec_urlmap.rb +79 -78
  118. data/test/spec_utils.rb +417 -345
  119. data/test/spec_version.rb +2 -8
  120. data/test/spec_webrick.rb +77 -67
  121. data/test/static/foo.html +1 -0
  122. data/test/testrequest.rb +1 -1
  123. data/test/unregistered_handler/rack/handler/unregistered.rb +1 -1
  124. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +1 -1
  125. metadata +116 -71
  126. data/KNOWN-ISSUES +0 -44
  127. data/lib/rack/backports/uri/common_18.rb +0 -56
  128. data/lib/rack/backports/uri/common_192.rb +0 -52
  129. data/lib/rack/backports/uri/common_193.rb +0 -29
  130. data/lib/rack/handler/evented_mongrel.rb +0 -8
  131. data/lib/rack/handler/mongrel.rb +0 -106
  132. data/lib/rack/handler/swiftiplied_mongrel.rb +0 -8
  133. data/lib/rack/showexceptions.rb +0 -387
  134. data/lib/rack/utils/okjson.rb +0 -600
  135. data/test/spec_methodoverride.rb +0 -111
  136. data/test/spec_mongrel.rb +0 -182
  137. data/test/spec_session_persisted_secure_secure_session_hash.rb +0 -73
  138. data/test/spec_showexceptions.rb +0 -98
@@ -63,7 +63,7 @@ end
63
63
 
64
64
  if $0 == __FILE__
65
65
  require 'rack'
66
- require 'rack/showexceptions'
66
+ require 'rack/show_exceptions'
67
67
  Rack::Server.start(
68
68
  :app => Rack::ShowExceptions.new(Rack::Lint.new(Rack::Lobster.new)), :Port => 9292
69
69
  )
@@ -5,22 +5,18 @@ module Rack
5
5
  # Rack::Lock locks every request inside a mutex, so that every request
6
6
  # will effectively be executed synchronously.
7
7
  class Lock
8
- FLAG = 'rack.multithread'.freeze
9
-
10
8
  def initialize(app, mutex = Mutex.new)
11
9
  @app, @mutex = app, mutex
12
10
  end
13
11
 
14
12
  def call(env)
15
- old, env[FLAG] = env[FLAG], false
16
13
  @mutex.lock
17
- response = @app.call(env)
18
- body = BodyProxy.new(response[2]) { @mutex.unlock }
19
- response[2] = body
20
- response
21
- ensure
22
- @mutex.unlock unless body
23
- env[FLAG] = old
14
+ begin
15
+ response = @app.call(env.merge(RACK_MULTITHREAD => false))
16
+ returned = response << BodyProxy.new(response.pop) { @mutex.unlock }
17
+ ensure
18
+ @mutex.unlock unless returned
19
+ end
24
20
  end
25
21
  end
26
22
  end
@@ -8,10 +8,10 @@ module Rack
8
8
  end
9
9
 
10
10
  def call(env)
11
- logger = ::Logger.new(env['rack.errors'])
11
+ logger = ::Logger.new(env[RACK_ERRORS])
12
12
  logger.level = @level
13
13
 
14
- env['rack.logger'] = logger
14
+ env[RACK_LOGGER] = logger
15
15
  @app.call(env)
16
16
  end
17
17
  end
@@ -0,0 +1,38 @@
1
+ module Rack
2
+ # Rack::MediaType parse media type and parameters out of content_type string
3
+
4
+ class MediaType
5
+ SPLIT_PATTERN = %r{\s*[;,]\s*}
6
+
7
+ class << self
8
+ # The media type (type/subtype) portion of the CONTENT_TYPE header
9
+ # without any media type parameters. e.g., when CONTENT_TYPE is
10
+ # "text/plain;charset=utf-8", the media-type is "text/plain".
11
+ #
12
+ # For more information on the use of media types in HTTP, see:
13
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
14
+ def type(content_type)
15
+ return nil unless content_type
16
+ content_type.split(SPLIT_PATTERN, 2).first.downcase
17
+ end
18
+
19
+ # The media type parameters provided in CONTENT_TYPE as a Hash, or
20
+ # an empty Hash if no CONTENT_TYPE or media-type parameters were
21
+ # provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
22
+ # this method responds with the following Hash:
23
+ # { 'charset' => 'utf-8' }
24
+ def params(content_type)
25
+ return {} if content_type.nil?
26
+ Hash[*content_type.split(SPLIT_PATTERN)[1..-1].
27
+ collect { |s| s.split('=', 2) }.
28
+ map { |k,v| [k.downcase, strip_doublequotes(v)] }.flatten]
29
+ end
30
+
31
+ private
32
+
33
+ def strip_doublequotes(str)
34
+ (str[0] == ?" && str[-1] == ?") ? str[1..-2] : str
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,10 +1,10 @@
1
1
  module Rack
2
2
  class MethodOverride
3
- HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK)
3
+ HTTP_METHODS = %w[GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK]
4
4
 
5
5
  METHOD_OVERRIDE_PARAM_KEY = "_method".freeze
6
6
  HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE".freeze
7
- ALLOWED_METHODS = ["POST"]
7
+ ALLOWED_METHODS = %w[POST]
8
8
 
9
9
  def initialize(app)
10
10
  @app = app
@@ -14,7 +14,7 @@ module Rack
14
14
  if allowed_methods.include?(env[REQUEST_METHOD])
15
15
  method = method_override(env)
16
16
  if HTTP_METHODS.include?(method)
17
- env["rack.methodoverride.original_method"] = env[REQUEST_METHOD]
17
+ env[RACK_METHODOVERRIDE_ORIGINAL_METHOD] = env[REQUEST_METHOD]
18
18
  env[REQUEST_METHOD] = method
19
19
  end
20
20
  end
@@ -26,11 +26,7 @@ module Rack
26
26
  req = Request.new(env)
27
27
  method = method_override_param(req) ||
28
28
  env[HTTP_METHOD_OVERRIDE_HEADER]
29
- begin
30
- method.to_s.upcase
31
- rescue ArgumentError
32
- env["rack.errors"].puts "Invalid string for method"
33
- end
29
+ method.to_s.upcase
34
30
  end
35
31
 
36
32
  private
@@ -42,9 +38,6 @@ module Rack
42
38
  def method_override_param(req)
43
39
  req.POST[METHOD_OVERRIDE_PARAM_KEY]
44
40
  rescue Utils::InvalidParameterError, Utils::ParameterTypeError
45
- req.env["rack.errors"].puts "Invalid or incomplete POST params"
46
- rescue EOFError
47
- req.env["rack.errors"].puts "Bad request content body"
48
41
  end
49
42
  end
50
43
  end
@@ -45,11 +45,6 @@ module Rack
45
45
  #
46
46
  # N.B. On Ubuntu the mime.types file does not include the leading period, so
47
47
  # users may need to modify the data before merging into the hash.
48
- #
49
- # To add the list mongrel provides, use:
50
- #
51
- # require 'mongrel/handlers'
52
- # Rack::Mime::MIME_TYPES.merge!(Mongrel::DirHandler::MIME_TYPES)
53
48
 
54
49
  MIME_TYPES = {
55
50
  ".123" => "application/vnd.lotus-1-2-3",
@@ -154,8 +149,11 @@ module Rack
154
149
  ".dmg" => "application/octet-stream",
155
150
  ".dna" => "application/vnd.dna",
156
151
  ".doc" => "application/msword",
152
+ ".docm" => "application/vnd.ms-word.document.macroEnabled.12",
157
153
  ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
158
154
  ".dot" => "application/msword",
155
+ ".dotm" => "application/vnd.ms-word.template.macroEnabled.12",
156
+ ".dotx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
159
157
  ".dp" => "application/vnd.osgi.dp",
160
158
  ".dpg" => "application/vnd.dpgraph",
161
159
  ".dsc" => "text/prs.lines.tag",
@@ -444,10 +442,19 @@ module Rack
444
442
  ".pnm" => "image/x-portable-anymap",
445
443
  ".pntg" => "image/x-macpaint",
446
444
  ".portpkg" => "application/vnd.macports.portpkg",
445
+ ".pot" => "application/vnd.ms-powerpoint",
446
+ ".potm" => "application/vnd.ms-powerpoint.template.macroEnabled.12",
447
+ ".potx" => "application/vnd.openxmlformats-officedocument.presentationml.template",
448
+ ".ppa" => "application/vnd.ms-powerpoint",
449
+ ".ppam" => "application/vnd.ms-powerpoint.addin.macroEnabled.12",
447
450
  ".ppd" => "application/vnd.cups-ppd",
448
451
  ".ppm" => "image/x-portable-pixmap",
449
452
  ".pps" => "application/vnd.ms-powerpoint",
453
+ ".ppsm" => "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
454
+ ".ppsx" => "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
450
455
  ".ppt" => "application/vnd.ms-powerpoint",
456
+ ".pptm" => "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
457
+ ".pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
451
458
  ".prc" => "application/vnd.palm",
452
459
  ".pre" => "application/vnd.lotus-freelance",
453
460
  ".prf" => "application/pics-rules",
@@ -638,8 +645,14 @@ module Rack
638
645
  ".xfdl" => "application/vnd.xfdl",
639
646
  ".xhtml" => "application/xhtml+xml",
640
647
  ".xif" => "image/vnd.xiff",
648
+ ".xla" => "application/vnd.ms-excel",
649
+ ".xlam" => "application/vnd.ms-excel.addin.macroEnabled.12",
641
650
  ".xls" => "application/vnd.ms-excel",
651
+ ".xlsb" => "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
642
652
  ".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
653
+ ".xlsm" => "application/vnd.ms-excel.sheet.macroEnabled.12",
654
+ ".xlt" => "application/vnd.ms-excel",
655
+ ".xltx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
643
656
  ".xml" => "application/xml",
644
657
  ".xo" => "application/vnd.olpc-sugar",
645
658
  ".xop" => "application/xop+xml",
@@ -41,27 +41,27 @@ module Rack
41
41
  end
42
42
 
43
43
  DEFAULT_ENV = {
44
- "rack.version" => Rack::VERSION,
45
- "rack.input" => StringIO.new,
46
- "rack.errors" => StringIO.new,
47
- "rack.multithread" => true,
48
- "rack.multiprocess" => true,
49
- "rack.run_once" => false,
50
- }
44
+ RACK_VERSION => Rack::VERSION,
45
+ RACK_INPUT => StringIO.new,
46
+ RACK_ERRORS => StringIO.new,
47
+ RACK_MULTITHREAD => true,
48
+ RACK_MULTIPROCESS => true,
49
+ RACK_RUNONCE => false,
50
+ }.freeze
51
51
 
52
52
  def initialize(app)
53
53
  @app = app
54
54
  end
55
55
 
56
- def get(uri, opts={}) request("GET", uri, opts) end
57
- def post(uri, opts={}) request("POST", uri, opts) end
58
- def put(uri, opts={}) request("PUT", uri, opts) end
59
- def patch(uri, opts={}) request("PATCH", uri, opts) end
60
- def delete(uri, opts={}) request("DELETE", uri, opts) end
61
- def head(uri, opts={}) request("HEAD", uri, opts) end
62
- def options(uri, opts={}) request("OPTIONS", uri, opts) end
56
+ def get(uri, opts={}) request(GET, uri, opts) end
57
+ def post(uri, opts={}) request(POST, uri, opts) end
58
+ def put(uri, opts={}) request(PUT, uri, opts) end
59
+ def patch(uri, opts={}) request(PATCH, uri, opts) end
60
+ def delete(uri, opts={}) request(DELETE, uri, opts) end
61
+ def head(uri, opts={}) request(HEAD, uri, opts) end
62
+ def options(uri, opts={}) request(OPTIONS, uri, opts) end
63
63
 
64
- def request(method="GET", uri="", opts={})
64
+ def request(method=GET, uri="", opts={})
65
65
  env = self.class.env_for(uri, opts.merge(:method => method))
66
66
 
67
67
  if opts[:lint]
@@ -70,17 +70,17 @@ module Rack
70
70
  app = @app
71
71
  end
72
72
 
73
- errors = env["rack.errors"]
73
+ errors = env[RACK_ERRORS]
74
74
  status, headers, body = app.call(env)
75
75
  MockResponse.new(status, headers, body, errors)
76
76
  ensure
77
77
  body.close if body.respond_to?(:close)
78
78
  end
79
79
 
80
- # For historical reasons, we're pinning to RFC 2396. It's easier for users
81
- # and we get support from ruby 1.8 to 2.2 using this method.
80
+ # For historical reasons, we're pinning to RFC 2396.
81
+ # URI::Parser = URI::RFC2396_Parser
82
82
  def self.parse_uri_rfc2396(uri)
83
- @parser ||= defined?(URI::RFC2396_Parser) ? URI::RFC2396_Parser.new : URI
83
+ @parser ||= URI::Parser.new
84
84
  @parser.parse(uri)
85
85
  end
86
86
 
@@ -91,28 +91,34 @@ module Rack
91
91
 
92
92
  env = DEFAULT_ENV.dup
93
93
 
94
- env_with_encoding(env, opts, uri)
94
+ env[REQUEST_METHOD] = opts[:method] ? opts[:method].to_s.upcase : GET
95
+ env[SERVER_NAME] = uri.host || "example.org"
96
+ env[SERVER_PORT] = uri.port ? uri.port.to_s : "80"
97
+ env[QUERY_STRING] = uri.query.to_s
98
+ env[PATH_INFO] = (!uri.path || uri.path.empty?) ? "/" : uri.path
99
+ env[RACK_URL_SCHEME] = uri.scheme || "http"
100
+ env[HTTPS] = env[RACK_URL_SCHEME] == "https" ? "on" : "off"
95
101
 
96
102
  env[SCRIPT_NAME] = opts[:script_name] || ""
97
103
 
98
104
  if opts[:fatal]
99
- env["rack.errors"] = FatalWarner.new
105
+ env[RACK_ERRORS] = FatalWarner.new
100
106
  else
101
- env["rack.errors"] = StringIO.new
107
+ env[RACK_ERRORS] = StringIO.new
102
108
  end
103
109
 
104
110
  if params = opts[:params]
105
- if env[REQUEST_METHOD] == "GET"
111
+ if env[REQUEST_METHOD] == GET
106
112
  params = Utils.parse_nested_query(params) if params.is_a?(String)
107
113
  params.update(Utils.parse_nested_query(env[QUERY_STRING]))
108
114
  env[QUERY_STRING] = Utils.build_nested_query(params)
109
115
  elsif !opts.has_key?(:input)
110
116
  opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
111
117
  if params.is_a?(Hash)
112
- if data = Utils::Multipart.build_multipart(params)
118
+ if data = Rack::Multipart.build_multipart(params)
113
119
  opts[:input] = data
114
120
  opts["CONTENT_LENGTH"] ||= data.length.to_s
115
- opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Utils::Multipart::MULTIPART_BOUNDARY}"
121
+ opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}"
116
122
  else
117
123
  opts[:input] = Utils.build_nested_query(params)
118
124
  end
@@ -122,8 +128,7 @@ module Rack
122
128
  end
123
129
  end
124
130
 
125
- empty_str = ""
126
- empty_str.force_encoding("ASCII-8BIT") if empty_str.respond_to? :force_encoding
131
+ empty_str = ''.force_encoding(Encoding::ASCII_8BIT)
127
132
  opts[:input] ||= empty_str
128
133
  if String === opts[:input]
129
134
  rack_input = StringIO.new(opts[:input])
@@ -131,10 +136,10 @@ module Rack
131
136
  rack_input = opts[:input]
132
137
  end
133
138
 
134
- rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
135
- env['rack.input'] = rack_input
139
+ rack_input.set_encoding(Encoding::BINARY)
140
+ env[RACK_INPUT] = rack_input
136
141
 
137
- env["CONTENT_LENGTH"] ||= env["rack.input"].length.to_s
142
+ env["CONTENT_LENGTH"] ||= env[RACK_INPUT].length.to_s
138
143
 
139
144
  opts.each { |field, value|
140
145
  env[field] = value if String === field
@@ -142,28 +147,6 @@ module Rack
142
147
 
143
148
  env
144
149
  end
145
-
146
- if "<3".respond_to? :b
147
- def self.env_with_encoding(env, opts, uri)
148
- env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : "GET").b
149
- env["SERVER_NAME"] = (uri.host || "example.org").b
150
- env["SERVER_PORT"] = (uri.port ? uri.port.to_s : "80").b
151
- env[QUERY_STRING] = (uri.query.to_s).b
152
- env[PATH_INFO] = ((!uri.path || uri.path.empty?) ? "/" : uri.path).b
153
- env["rack.url_scheme"] = (uri.scheme || "http").b
154
- env["HTTPS"] = (env["rack.url_scheme"] == "https" ? "on" : "off").b
155
- end
156
- else
157
- def self.env_with_encoding(env, opts, uri)
158
- env[REQUEST_METHOD] = opts[:method] ? opts[:method].to_s.upcase : "GET"
159
- env["SERVER_NAME"] = uri.host || "example.org"
160
- env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
161
- env[QUERY_STRING] = uri.query.to_s
162
- env[PATH_INFO] = (!uri.path || uri.path.empty?) ? "/" : uri.path
163
- env["rack.url_scheme"] = uri.scheme || "http"
164
- env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off"
165
- end
166
- end
167
150
  end
168
151
 
169
152
  # Rack::MockResponse provides useful helpers for testing your apps.
@@ -1,10 +1,11 @@
1
+ require 'rack/multipart/parser'
2
+
1
3
  module Rack
2
4
  # A multipart form data parser, adapted from IOWA.
3
5
  #
4
6
  # Usually, Rack::Request#POST takes care of calling this.
5
7
  module Multipart
6
8
  autoload :UploadedFile, 'rack/multipart/uploaded_file'
7
- autoload :Parser, 'rack/multipart/parser'
8
9
  autoload :Generator, 'rack/multipart/generator'
9
10
 
10
11
  EOL = "\r\n"
@@ -12,17 +13,45 @@ module Rack
12
13
  MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
13
14
  TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
14
15
  CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
15
- DISPPARM = /;\s*(#{TOKEN})=("(?:\\"|[^"])*"|#{TOKEN})/
16
- RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
16
+ VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
17
17
  BROKEN_QUOTED = /^#{CONDISP}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
18
18
  BROKEN_UNQUOTED = /^#{CONDISP}.*;\sfilename=(#{TOKEN})/i
19
19
  MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
20
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*\s+name="?([^\";]*)"?/ni
20
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*\s+name=(#{VALUE})/ni
21
21
  MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
22
+ # Updated definitions from RFC 2231
23
+ ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}
24
+ ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
25
+ SECTION = /\*[0-9]+/
26
+ REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
27
+ REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
28
+ EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
29
+ EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
30
+ EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
31
+ EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
32
+ EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
33
+ EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
34
+ EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
35
+ DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
36
+ RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
22
37
 
23
38
  class << self
24
- def parse_multipart(env)
25
- Parser.create(env).parse
39
+ def parse_multipart(env, params = Rack::Utils.default_query_parser)
40
+ extract_multipart Rack::Request.new(env), params
41
+ end
42
+
43
+ def extract_multipart(req, params = Rack::Utils.default_query_parser)
44
+ io = req.get_header(RACK_INPUT)
45
+ io.rewind
46
+ content_length = req.content_length
47
+ content_length = content_length.to_i if content_length
48
+
49
+ tempfile = req.get_header(RACK_MULTIPART_TEMPFILE_FACTORY) || Parser::TEMPFILE_FACTORY
50
+ bufsize = req.get_header(RACK_MULTIPART_BUFFER_SIZE) || Parser::BUFSIZE
51
+
52
+ info = Parser.parse io, content_length, req.get_header('CONTENT_TYPE'), tempfile, bufsize, params
53
+ req.set_header(RACK_TEMPFILES, info.tmp_files)
54
+ info.params
26
55
  end
27
56
 
28
57
  def build_multipart(params, first = true)
@@ -11,12 +11,12 @@ module Rack
11
11
 
12
12
  def dump
13
13
  return nil if @first && !multipart?
14
- return flattened_params if !@first
14
+ return flattened_params unless @first
15
15
 
16
16
  flattened_params.map do |name, file|
17
17
  if file.respond_to?(:original_filename)
18
- ::File.open(file.path, "rb") do |f|
19
- f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
18
+ ::File.open(file.path, 'rb') do |f|
19
+ f.set_encoding(Encoding::BINARY)
20
20
  content_for_tempfile(f, file, name)
21
21
  end
22
22
  else
@@ -90,4 +90,4 @@ EOF
90
90
  end
91
91
  end
92
92
  end
93
- end
93
+ end
@@ -6,159 +6,304 @@ module Rack
6
6
 
7
7
  class Parser
8
8
  BUFSIZE = 16384
9
- DUMMY = Struct.new(:parse).new
10
-
11
- def self.create(env)
12
- return DUMMY unless env['CONTENT_TYPE'] =~ MULTIPART
13
-
14
- io = env['rack.input']
15
- io.rewind
9
+ TEXT_PLAIN = "text/plain"
10
+ TEMPFILE_FACTORY = lambda { |filename, content_type|
11
+ Tempfile.new(["RackMultipart", ::File.extname(filename)])
12
+ }
13
+
14
+ class BoundedIO # :nodoc:
15
+ def initialize(io, content_length)
16
+ @io = io
17
+ @content_length = content_length
18
+ @cursor = 0
19
+ end
16
20
 
17
- content_length = env['CONTENT_LENGTH']
18
- content_length = content_length.to_i if content_length
21
+ def read(size)
22
+ return if @cursor >= @content_length
19
23
 
20
- tempfile = env['rack.multipart.tempfile_factory'] ||
21
- lambda { |filename, content_type| Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0".freeze, '%00'.freeze))]) }
22
- bufsize = env['rack.multipart.buffer_size'] || BUFSIZE
24
+ left = @content_length - @cursor
23
25
 
24
- new($1, io, content_length, env, tempfile, bufsize)
25
- end
26
+ str = if left < size
27
+ @io.read left
28
+ else
29
+ @io.read size
30
+ end
26
31
 
27
- def initialize(boundary, io, content_length, env, tempfile, bufsize)
28
- @buf = ""
32
+ if str
33
+ @cursor += str.bytesize
34
+ else
35
+ # Raise an error for mismatching Content-Length and actual contents
36
+ raise EOFError, "bad content body"
37
+ end
29
38
 
30
- if @buf.respond_to? :force_encoding
31
- @buf.force_encoding Encoding::ASCII_8BIT
39
+ str
32
40
  end
33
41
 
34
- @params = Utils::KeySpaceConstrainedParams.new
35
- @boundary = "--#{boundary}"
36
- @io = io
37
- @content_length = content_length
38
- @boundary_size = Utils.bytesize(@boundary) + EOL.size
39
- @env = env
40
- @tempfile = tempfile
41
- @bufsize = bufsize
42
+ def eof?; @content_length == @cursor; end
42
43
 
43
- if @content_length
44
- @content_length -= @boundary_size
44
+ def rewind
45
+ @io.rewind
45
46
  end
47
+ end
46
48
 
47
- @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
48
- @full_boundary = @boundary + EOL
49
+ MultipartInfo = Struct.new :params, :tmp_files
50
+ EMPTY = MultipartInfo.new(nil, [])
51
+
52
+ def self.parse_boundary(content_type)
53
+ return unless content_type
54
+ data = content_type.match(MULTIPART)
55
+ return unless data
56
+ data[1]
49
57
  end
50
58
 
51
- def parse
52
- fast_forward_to_first_boundary
59
+ def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
60
+ return EMPTY if 0 == content_length
61
+
62
+ boundary = parse_boundary content_type
63
+ return EMPTY unless boundary
64
+
65
+ io = BoundedIO.new(io, content_length) if content_length
66
+
67
+ parser = new(boundary, tmpfile, bufsize, qp)
68
+ parser.on_read io.read(bufsize), io.eof?
53
69
 
54
- opened_files = 0
55
70
  loop do
71
+ break if parser.state == :DONE
72
+ parser.on_read io.read(bufsize), io.eof?
73
+ end
56
74
 
57
- head, filename, content_type, name, body =
58
- get_current_head_and_filename_and_content_type_and_name_and_body
75
+ io.rewind
76
+ parser.result
77
+ end
59
78
 
60
- if Utils.multipart_part_limit > 0
61
- opened_files += 1 if filename
62
- raise MultipartPartLimitError, 'Maximum file multiparts in content reached' if opened_files >= Utils.multipart_part_limit
79
+ class Collector
80
+ class MimePart < Struct.new(:body, :head, :filename, :content_type, :name)
81
+ def get_data
82
+ data = body
83
+ if filename == ""
84
+ # filename is blank which means no file has been selected
85
+ return
86
+ elsif filename
87
+ body.rewind if body.respond_to?(:rewind)
88
+
89
+ # Take the basename of the upload's original filename.
90
+ # This handles the full Windows paths given by Internet Explorer
91
+ # (and perhaps other broken user agents) without affecting
92
+ # those which give the lone filename.
93
+ fn = filename.split(/[\/\\]/).last
94
+
95
+ data = {:filename => fn, :type => content_type,
96
+ :name => name, :tempfile => body, :head => head}
97
+ elsif !filename && content_type && body.is_a?(IO)
98
+ body.rewind
99
+
100
+ # Generic multipart cases, not coming from a form
101
+ data = {:type => content_type,
102
+ :name => name, :tempfile => body, :head => head}
103
+ elsif !filename && data.empty?
104
+ return
105
+ end
106
+
107
+ yield data
63
108
  end
109
+ end
64
110
 
65
- # Save the rest.
66
- if i = @buf.index(rx)
67
- body << @buf.slice!(0, i)
68
- @buf.slice!(0, @boundary_size+2)
111
+ class BufferPart < MimePart
112
+ def file?; false; end
113
+ def close; end
114
+ end
69
115
 
70
- @content_length = -1 if $1 == "--"
71
- end
116
+ class TempfilePart < MimePart
117
+ def file?; true; end
118
+ def close; body.close; end
119
+ end
72
120
 
73
- get_data(filename, body, content_type, name, head) do |data|
74
- tag_multipart_encoding(filename, content_type, name, data)
121
+ include Enumerable
75
122
 
76
- Utils.normalize_params(@params, name, data)
77
- end
123
+ def initialize tempfile
124
+ @tempfile = tempfile
125
+ @mime_parts = []
126
+ @open_files = 0
127
+ end
78
128
 
79
- # break if we're at the end of a buffer, but not if it is the end of a field
80
- break if (@buf.empty? && $1 != EOL) || @content_length == -1
129
+ def each
130
+ @mime_parts.each { |part| yield part }
81
131
  end
82
132
 
83
- @io.rewind
133
+ def on_mime_head mime_index, head, filename, content_type, name
134
+ if filename
135
+ body = @tempfile.call(filename, content_type)
136
+ body.binmode if body.respond_to?(:binmode)
137
+ klass = TempfilePart
138
+ @open_files += 1
139
+ else
140
+ body = ''.force_encoding(Encoding::ASCII_8BIT)
141
+ klass = BufferPart
142
+ end
84
143
 
85
- @params.to_params_hash
86
- end
144
+ @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
145
+ check_open_files
146
+ end
87
147
 
88
- private
89
- def full_boundary; @full_boundary; end
148
+ def on_mime_body mime_index, content
149
+ @mime_parts[mime_index].body << content
150
+ end
90
151
 
91
- def rx; @rx; end
152
+ def on_mime_finish mime_index
153
+ end
92
154
 
93
- def fast_forward_to_first_boundary
94
- loop do
95
- content = @io.read(@bufsize)
96
- raise EOFError, "bad content body" unless content
97
- @buf << content
155
+ private
98
156
 
99
- while @buf.gsub!(/\A([^\n]*\n)/, '')
100
- read_buffer = $1
101
- return if read_buffer == full_boundary
157
+ def check_open_files
158
+ if Utils.multipart_part_limit > 0
159
+ if @open_files >= Utils.multipart_part_limit
160
+ @mime_parts.each(&:close)
161
+ raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
162
+ end
102
163
  end
103
-
104
- raise EOFError, "bad content body" if Utils.bytesize(@buf) >= @bufsize
105
164
  end
106
165
  end
107
166
 
108
- def get_current_head_and_filename_and_content_type_and_name_and_body
109
- head = nil
110
- body = ''
167
+ attr_reader :state
111
168
 
112
- if body.respond_to? :force_encoding
113
- body.force_encoding Encoding::ASCII_8BIT
114
- end
169
+ def initialize(boundary, tempfile, bufsize, query_parser)
170
+ @buf = "".force_encoding(Encoding::ASCII_8BIT)
171
+
172
+ @query_parser = query_parser
173
+ @params = query_parser.make_params
174
+ @boundary = "--#{boundary}"
175
+ @boundary_size = @boundary.bytesize + EOL.size
176
+ @bufsize = bufsize
177
+
178
+ @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
179
+ @full_boundary = @boundary
180
+ @end_boundary = @boundary + '--'
181
+ @state = :FAST_FORWARD
182
+ @mime_index = 0
183
+ @collector = Collector.new tempfile
184
+ end
115
185
 
116
- filename = content_type = name = nil
186
+ def on_read content, eof
187
+ handle_empty_content!(content, eof)
188
+ @buf << content
189
+ run_parser
190
+ end
117
191
 
118
- until head && @buf =~ rx
119
- if !head && i = @buf.index(EOL+EOL)
120
- head = @buf.slice!(0, i+2) # First \r\n
192
+ def result
193
+ @collector.each do |part|
194
+ part.get_data do |data|
195
+ tag_multipart_encoding(part.filename, part.content_type, part.name, data)
196
+ @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
197
+ end
198
+ end
121
199
 
122
- @buf.slice!(0, 2) # Second \r\n
200
+ MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
201
+ end
123
202
 
124
- content_type = head[MULTIPART_CONTENT_TYPE, 1]
125
- name = head[MULTIPART_CONTENT_DISPOSITION, 1] || head[MULTIPART_CONTENT_ID, 1]
203
+ private
126
204
 
127
- filename = get_filename(head)
205
+ def run_parser
206
+ loop do
207
+ case @state
208
+ when :FAST_FORWARD
209
+ break if handle_fast_forward == :want_read
210
+ when :CONSUME_TOKEN
211
+ break if handle_consume_token == :want_read
212
+ when :MIME_HEAD
213
+ break if handle_mime_head == :want_read
214
+ when :MIME_BODY
215
+ break if handle_mime_body == :want_read
216
+ when :DONE
217
+ break
218
+ end
219
+ end
220
+ end
128
221
 
129
- if name.nil? || name.empty? && filename
130
- name = filename
131
- end
222
+ def handle_fast_forward
223
+ if consume_boundary
224
+ @state = :MIME_HEAD
225
+ else
226
+ raise EOFError, "bad content body" if @buf.bytesize >= @bufsize
227
+ :want_read
228
+ end
229
+ end
132
230
 
133
- if filename
134
- (@env['rack.tempfiles'] ||= []) << body = @tempfile.call(filename, content_type)
135
- body.binmode if body.respond_to?(:binmode)
136
- end
231
+ def handle_consume_token
232
+ tok = consume_boundary
233
+ # break if we're at the end of a buffer, but not if it is the end of a field
234
+ if tok == :END_BOUNDARY || (@buf.empty? && tok != :BOUNDARY)
235
+ @state = :DONE
236
+ else
237
+ @state = :MIME_HEAD
238
+ end
239
+ end
137
240
 
138
- next
241
+ def handle_mime_head
242
+ if @buf.index(EOL + EOL)
243
+ i = @buf.index(EOL+EOL)
244
+ head = @buf.slice!(0, i+2) # First \r\n
245
+ @buf.slice!(0, 2) # Second \r\n
246
+
247
+ content_type = head[MULTIPART_CONTENT_TYPE, 1]
248
+ if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
249
+ name = Rack::Auth::Digest::Params::dequote(name)
250
+ else
251
+ name = head[MULTIPART_CONTENT_ID, 1]
139
252
  end
140
253
 
141
- # Save the read body part.
142
- if head && (@boundary_size+4 < @buf.size)
143
- body << @buf.slice!(0, @buf.size - (@boundary_size+4))
254
+ filename = get_filename(head)
255
+
256
+ if name.nil? || name.empty?
257
+ name = filename || "#{content_type || TEXT_PLAIN}[]"
144
258
  end
145
259
 
146
- content = @io.read(@content_length && @bufsize >= @content_length ? @content_length : @bufsize)
147
- raise EOFError, "bad content body" if content.nil? || content.empty?
260
+ @collector.on_mime_head @mime_index, head, filename, content_type, name
261
+ @state = :MIME_BODY
262
+ else
263
+ :want_read
264
+ end
265
+ end
148
266
 
149
- @buf << content
150
- @content_length -= content.size if @content_length
267
+ def handle_mime_body
268
+ if @buf =~ rx
269
+ # Save the rest.
270
+ if i = @buf.index(rx)
271
+ @collector.on_mime_body @mime_index, @buf.slice!(0, i)
272
+ @buf.slice!(0, 2) # Remove \r\n after the content
273
+ end
274
+ @state = :CONSUME_TOKEN
275
+ @mime_index += 1
276
+ else
277
+ :want_read
151
278
  end
279
+ end
280
+
281
+ def full_boundary; @full_boundary; end
152
282
 
153
- [head, filename, content_type, name, body]
283
+ def rx; @rx; end
284
+
285
+ def consume_boundary
286
+ while @buf.gsub!(/\A([^\n]*(?:\n|\Z))/, '')
287
+ read_buffer = $1
288
+ case read_buffer.strip
289
+ when full_boundary then return :BOUNDARY
290
+ when @end_boundary then return :END_BOUNDARY
291
+ end
292
+ return if @buf.empty?
293
+ end
154
294
  end
155
295
 
156
296
  def get_filename(head)
157
297
  filename = nil
158
298
  case head
159
299
  when RFC2183
160
- filename = Hash[head.scan(DISPPARM)]['filename']
161
- filename = $1 if filename and filename =~ /^"(.*)"$/
300
+ params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
301
+
302
+ if filename = params['filename']
303
+ filename = $1 if filename =~ /^"(.*)"$/
304
+ elsif filename = params['filename*']
305
+ encoding, _, filename = filename.split("'", 3)
306
+ end
162
307
  when BROKEN_QUOTED, BROKEN_UNQUOTED
163
308
  filename = $1
164
309
  end
@@ -169,84 +314,54 @@ module Rack
169
314
  filename = Utils.unescape(filename)
170
315
  end
171
316
 
172
- scrub_filename filename
317
+ filename.scrub!
173
318
 
174
319
  if filename !~ /\\[^\\"]/
175
320
  filename = filename.gsub(/\\(.)/, '\1')
176
321
  end
177
- filename
178
- end
179
322
 
180
- if "<3".respond_to? :valid_encoding?
181
- def scrub_filename(filename)
182
- unless filename.valid_encoding?
183
- # FIXME: this force_encoding is for Ruby 2.0 and 1.9 support.
184
- # We can remove it after they are dropped
185
- filename.force_encoding(Encoding::ASCII_8BIT)
186
- filename.encode!(:invalid => :replace, :undef => :replace)
187
- end
323
+ if encoding
324
+ filename.force_encoding ::Encoding.find(encoding)
188
325
  end
189
326
 
190
- CHARSET = "charset"
191
- TEXT_PLAIN = "text/plain"
327
+ filename
328
+ end
329
+
330
+ CHARSET = "charset"
192
331
 
193
- def tag_multipart_encoding(filename, content_type, name, body)
194
- name.force_encoding Encoding::UTF_8
332
+ def tag_multipart_encoding(filename, content_type, name, body)
333
+ name = name.to_s
334
+ encoding = Encoding::UTF_8
195
335
 
196
- return if filename
336
+ name.force_encoding(encoding)
197
337
 
198
- encoding = Encoding::UTF_8
338
+ return if filename
199
339
 
200
- if content_type
201
- list = content_type.split(';')
202
- type_subtype = list.first
203
- type_subtype.strip!
204
- if TEXT_PLAIN == type_subtype
205
- rest = list.drop 1
206
- rest.each do |param|
207
- k,v = param.split('=', 2)
208
- k.strip!
209
- v.strip!
210
- encoding = Encoding.find v if k == CHARSET
211
- end
340
+ if content_type
341
+ list = content_type.split(';')
342
+ type_subtype = list.first
343
+ type_subtype.strip!
344
+ if TEXT_PLAIN == type_subtype
345
+ rest = list.drop 1
346
+ rest.each do |param|
347
+ k,v = param.split('=', 2)
348
+ k.strip!
349
+ v.strip!
350
+ encoding = Encoding.find v if k == CHARSET
212
351
  end
213
352
  end
214
-
215
- name.force_encoding encoding
216
- body.force_encoding encoding
217
- end
218
- else
219
- def scrub_filename(filename)
220
353
  end
221
- def tag_multipart_encoding(filename, content_type, name, body)
222
- end
223
- end
224
-
225
- def get_data(filename, body, content_type, name, head)
226
- data = body
227
- if filename == ""
228
- # filename is blank which means no file has been selected
229
- return
230
- elsif filename
231
- body.rewind if body.respond_to?(:rewind)
232
354
 
233
- # Take the basename of the upload's original filename.
234
- # This handles the full Windows paths given by Internet Explorer
235
- # (and perhaps other broken user agents) without affecting
236
- # those which give the lone filename.
237
- filename = filename.split(/[\/\\]/).last
355
+ name.force_encoding(encoding)
356
+ body.force_encoding(encoding)
357
+ end
238
358
 
239
- data = {:filename => filename, :type => content_type,
240
- :name => name, :tempfile => body, :head => head}
241
- elsif !filename && content_type && body.is_a?(IO)
242
- body.rewind
243
359
 
244
- # Generic multipart cases, not coming from a form
245
- data = {:type => content_type,
246
- :name => name, :tempfile => body, :head => head}
360
+ def handle_empty_content!(content, eof)
361
+ if content.nil? || content.empty?
362
+ raise EOFError if eof
363
+ return true
247
364
  end
248
-
249
- yield data
250
365
  end
251
366
  end
252
367
  end