A multipart form data parser, adapted from IOWA.
Usually, Rack::Request#POST takes care of calling this.
# File lib/rack/utils.rb, line 596 596: def self.build_multipart(params, first = true) 597: if first 598: unless params.is_a?(Hash) 599: raise ArgumentError, "value must be a Hash" 600: end 601: 602: multipart = false 603: query = lambda { |value| 604: case value 605: when Array 606: value.each(&query) 607: when Hash 608: value.values.each(&query) 609: when UploadedFile 610: multipart = true 611: end 612: } 613: params.values.each(&query) 614: return nil unless multipart 615: end 616: 617: flattened_params = Hash.new 618: 619: params.each do |key, value| 620: k = first ? key.to_s : "[#{key}]" 621: 622: case value 623: when Array 624: value.map { |v| 625: build_multipart(v, false).each { |subkey, subvalue| 626: flattened_params["#{k}[]#{subkey}"] = subvalue 627: } 628: } 629: when Hash 630: build_multipart(value, false).each { |subkey, subvalue| 631: flattened_params[k + subkey] = subvalue 632: } 633: else 634: flattened_params[k] = value 635: end 636: end 637: 638: if first 639: flattened_params.map { |name, file| 640: if file.respond_to?(:original_filename) 641: ::File.open(file.path, "rb") do |f| 642: f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding) 643: --#{MULTIPART_BOUNDARY}\rContent-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\rContent-Type: #{file.content_type}\rContent-Length: #{::File.stat(file.path).size}\r\r#{f.read}\r 644: end 645: else 646: --#{MULTIPART_BOUNDARY}\rContent-Disposition: form-data; name="#{name}"\r\r#{file}\r 647: end 648: }.join + "--#{MULTIPART_BOUNDARY}--\r" 649: else 650: flattened_params 651: end 652: end
# File lib/rack/utils.rb, line 474 474: def self.parse_multipart(env) 475: unless env['CONTENT_TYPE'] =~ 476: %\Amultipart/.*boundary=\"?([^\";,]+)\"?| 477: nil 478: else 479: boundary = "--#{$1}" 480: 481: params = {} 482: buf = "" 483: content_length = env['CONTENT_LENGTH'].to_i 484: input = env['rack.input'] 485: input.rewind 486: 487: boundary_size = Utils.bytesize(boundary) + EOL.size 488: bufsize = 16384 489: 490: content_length -= boundary_size 491: 492: read_buffer = '' 493: 494: status = input.read(boundary_size, read_buffer) 495: raise EOFError, "bad content body" unless status == boundary + EOL 496: 497: rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/ 498: 499: loop { 500: head = nil 501: body = '' 502: filename = content_type = name = nil 503: 504: until head && buf =~ rx 505: if !head && i = buf.index(EOL+EOL) 506: head = buf.slice!(0, i+2) # First \r\n 507: buf.slice!(0, 2) # Second \r\n 508: 509: token = /[^\s()<>,;:\\"\/\[\]?=]+/ 510: condisp = /Content-Disposition:\s*#{token}\s*/ 511: dispparm = /;\s*(#{token})=("(?:\\"|[^"])*"|#{token})*/ 512: 513: rfc2183 = /^#{condisp}(#{dispparm})+$/ 514: broken_quoted = /^#{condisp}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{token}=)/ 515: broken_unquoted = /^#{condisp}.*;\sfilename=(#{token})/ 516: 517: if head =~ rfc2183 518: filename = Hash[head.scan(dispparm)]['filename'] 519: filename = $1 if filename and filename =~ /^"(.*)"$/ 520: elsif head =~ broken_quoted 521: filename = $1 522: elsif head =~ broken_unquoted 523: filename = $1 524: end 525: 526: if filename && filename !~ /\\[^\\"]/ 527: filename = Utils.unescape(filename).gsub(/\\(.)/, '\1') 528: end 529: 530: content_type = head[/Content-Type: (.*)#{EOL}/i, 1] 531: name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/i, 1] || head[/Content-ID:\s*([^#{EOL}]*)/i, 1] 532: 533: if filename 534: body = Tempfile.new("RackMultipart") 535: body.binmode if body.respond_to?(:binmode) 536: end 537: 538: next 539: end 540: 541: # Save the read body part. 542: if head && (boundary_size+4 < buf.size) 543: body << buf.slice!(0, buf.size - (boundary_size+4)) 544: end 545: 546: c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer) 547: raise EOFError, "bad content body" if c.nil? || c.empty? 548: buf << c 549: content_length -= c.size 550: end 551: 552: # Save the rest. 553: if i = buf.index(rx) 554: body << buf.slice!(0, i) 555: buf.slice!(0, boundary_size+2) 556: 557: content_length = 1 if $1 == "--" 558: end 559: 560: if filename == "" 561: # filename is blank which means no file has been selected 562: data = nil 563: elsif filename 564: body.rewind 565: 566: # Take the basename of the upload's original filename. 567: # This handles the full Windows paths given by Internet Explorer 568: # (and perhaps other broken user agents) without affecting 569: # those which give the lone filename. 570: filename = filename.split(/[\/\\]/).last 571: 572: data = {:filename => filename, :type => content_type, 573: :name => name, :tempfile => body, :head => head} 574: elsif !filename && content_type 575: body.rewind 576: 577: # Generic multipart cases, not coming from a form 578: data = {:type => content_type, 579: :name => name, :tempfile => body, :head => head} 580: else 581: data = body 582: end 583: 584: Utils.normalize_params(params, name, data) unless data.nil? 585: 586: # break if we're at the end of a buffer, but not if it is the end of a field 587: break if (buf.empty? && $1 != EOL) || content_length == 1 588: } 589: 590: input.rewind 591: 592: params 593: end 594: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.