A multipart form data parser, adapted from IOWA.
Usually, Rack::Request#POST takes care of calling this.
# File lib/rack/utils.rb, line 642 def self.build_multipart(params, first = true) if first unless params.is_a?(Hash) raise ArgumentError, "value must be a Hash" end multipart = false query = lambda { |value| case value when Array value.each(&query) when Hash value.values.each(&query) when UploadedFile multipart = true end } params.values.each(&query) return nil unless multipart end flattened_params = Hash.new params.each do |key, value| k = first ? key.to_s : "[#{key}]" case value when Array value.map { |v| build_multipart(v, false).each { |subkey, subvalue| flattened_params["#{k}[]#{subkey}"] = subvalue } } when Hash build_multipart(value, false).each { |subkey, subvalue| flattened_params[k + subkey] = subvalue } else flattened_params[k] = value end end if first flattened_params.map { |name, file| if file.respond_to?(:original_filename) ::File.open(file.path, "rb") do |f| f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding) "--#{MULTIPART_BOUNDARY}\r Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r Content-Type: #{file.content_type}\r Content-Length: #{::File.stat(file.path).size}\r \r #{f.read}\r " end else "--#{MULTIPART_BOUNDARY}\r Content-Disposition: form-data; name="#{name}"\r \r #{file}\r " end }.join + "--#{MULTIPART_BOUNDARY}--\r" else flattened_params end end
# File lib/rack/utils.rb, line 510 def self.parse_multipart(env) unless env['CONTENT_TYPE'] =~ %r\Amultipart/.*boundary=\"?([^\";,]+)\"?| nil else boundary = "--#{$1}" params = {} buf = "" content_length = env['CONTENT_LENGTH'].to_i input = env['rack.input'] input.rewind boundary_size = Utils.bytesize(boundary) + EOL.size bufsize = 16384 content_length -= boundary_size read_buffer = '' status = input.read(boundary_size, read_buffer) raise EOFError, "bad content body" unless status == boundary + EOL rx = %r(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/ max_key_space = Utils.key_space_limit bytes = 0 loop { head = nil body = '' filename = content_type = name = nil until head && buf =~ rx if !head && i = buf.index(EOL+EOL) head = buf.slice!(0, i+2) # First \r\n buf.slice!(0, 2) # Second \r\n token = %r[^\s()<>,;:\\"\/\[\]?=]+/ condisp = %rContent-Disposition:\s*#{token}\s*/ dispparm = %r;\s*(#{token})=("(?:\\"|[^"])*"|#{token})*/ rfc2183 = %r^#{condisp}(#{dispparm})+$/ broken_quoted = %r^#{condisp}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{token}=)/ broken_unquoted = %r^#{condisp}.*;\sfilename=(#{token})/ if head =~ rfc2183 filename = Hash[head.scan(dispparm)]['filename'] filename = $1 if filename and filename =~ %r^"(.*)"$/ elsif head =~ broken_quoted filename = $1 elsif head =~ broken_unquoted filename = $1 end if filename && filename !~ %r\\[^\\"]/ filename = Utils.unescape(filename).gsub(%r\\(.)/, '\1') end content_type = head[%rContent-Type: (.*)#{EOL}/i, 1] name = head[%rContent-Disposition:.*\s+name="?([^\";]*)"?/i, 1] || head[%rContent-ID:\s*([^#{EOL}]*)/i, 1] if name bytes += name.size if bytes > max_key_space raise RangeError, "exceeded available parameter key space" end end if content_type || filename body = Tempfile.new("RackMultipart") body.binmode if body.respond_to?(:binmode) end next end # Save the read body part. if head && (boundary_size+4 < buf.size) body << buf.slice!(0, buf.size - (boundary_size+4)) end c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer) raise EOFError, "bad content body" if c.nil? || c.empty? buf << c content_length -= c.size end # Save the rest. if i = buf.index(rx) body << buf.slice!(0, i) buf.slice!(0, boundary_size+2) content_length = -1 if $1 == "--" end if filename == "" # filename is blank which means no file has been selected data = nil elsif filename body.rewind # Take the basename of the upload's original filename. # This handles the full Windows paths given by Internet Explorer # (and perhaps other broken user agents) without affecting # those which give the lone filename. filename = filename.split(%r[\/\\]/).last data = {:filename => filename, :type => content_type, :name => name, :tempfile => body, :head => head} elsif !filename && content_type body.rewind # Generic multipart cases, not coming from a form data = {:type => content_type, :name => name, :tempfile => body, :head => head} else data = body end Utils.normalize_params(params, name, data) unless data.nil? # break if we're at the end of a buffer, but not if it is the end of a field break if (buf.empty? && $1 != EOL) || content_length == -1 } input.rewind params end end