class Praxis::MultipartParser

Constants

BROKEN_QUOTED
BROKEN_UNQUOTED
BUFSIZE
CONDISP
DISPPARM
EOL
HEADER_KV
MULTIPART
MULTIPART_BOUNDARY
MULTIPART_CONTENT_DISPOSITION
MULTIPART_CONTENT_ID
MULTIPART_CONTENT_TYPE
PARAMS_BUF_SIZE
RFC2183
TERMINAL_CRLF
TOKEN
UNTIL_NEWLINE

Public Class Methods

new(headers, body) click to toggle source
# File lib/praxis/multipart/parser.rb, line 33
def initialize(headers, body)
  @headers = headers
  @io = StringIO.new
  Array(body).each do |chunk|
    @io << chunk
  end
  @io.rewind
  @parts = []
end
parse(headers, body) click to toggle source
# File lib/praxis/multipart/parser.rb, line 29
def self.parse(headers, body)
  new(headers, body).parse
end

Public Instance Methods

parse() click to toggle source
# File lib/praxis/multipart/parser.rb, line 43
def parse
  return nil unless setup_parse

  @preamble = fast_forward_to_first_boundary

  loop do
    head, filename, content_type, name, body =
      current_head_and_filename_and_content_type_and_name_and_body

    # Save the rest.
    if (i = @buf.index(rx))
      body << @buf.slice!(0, i)
      @buf.slice!(0, @boundary_size + 2)

      @content_length = -1 if Regexp.last_match(1) == '--'
    end

    filename, data = get_data(filename, body, content_type, name, head)

    parsed_headers = head.split(EOL).each_with_object({}) do |line, hash|
      match = HEADER_KV.match(line)
      k = match[:k]
      v = match[:v]
      hash[k] = v
    end

    part = MultipartPart.new(data, parsed_headers, name: name, filename: filename)

    @parts << part

    # break if we're at the end of a buffer, but not if it is the end of a field
    break if (@buf.empty? && Regexp.last_match(1) != EOL) || @content_length == -1
  end

  @io.rewind

  [@preamble, @parts]
end

Private Instance Methods

current_head_and_filename_and_content_type_and_name_and_body() click to toggle source
# File lib/praxis/multipart/parser.rb, line 142
def current_head_and_filename_and_content_type_and_name_and_body
  head = nil
  body = String.new
  filename = content_type = name = nil
  content = 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

      content_type = head[MULTIPART_CONTENT_TYPE, 1]
      name = head[MULTIPART_CONTENT_DISPOSITION, 1] || head[MULTIPART_CONTENT_ID, 1]
      name.strip!

      filename = get_filename(head)

      if filename
        body = Tempfile.new('RackMultipart')
        body.binmode if body.respond_to?(:binmode)
      end

      next
    end

    # Save the read body part.
    body << @buf.slice!(0, @buf.size - (@boundary_size + 4)) if head && (@boundary_size + 4 < @buf.size)

    content = @io.read(@content_length && @content_length <= BUFSIZE ? @content_length : BUFSIZE)
    raise EOFError, 'bad content body' if content.nil? || content.empty?

    @buf << content
    @content_length -= content.size if @content_length
  end

  [head, filename, content_type, name, body]
end
fast_forward_to_first_boundary() click to toggle source
# File lib/praxis/multipart/parser.rb, line 123
def fast_forward_to_first_boundary
  preamble = String.new
  loop do
    content = @io.read(BUFSIZE)
    raise EOFError, 'bad content body' unless content

    @buf << content

    while @buf.gsub!(UNTIL_NEWLINE, '')
      read_buffer = Regexp.last_match(1)
      return preamble.gsub!(TERMINAL_CRLF, '') if read_buffer == full_boundary

      preamble << read_buffer
    end

    raise EOFError, 'bad content body' if Rack::Utils.bytesize(@buf) >= BUFSIZE
  end
end
full_boundary() click to toggle source
# File lib/praxis/multipart/parser.rb, line 115
def full_boundary
  @boundary + EOL
end
get_data(filename, body, _content_type, _name, _head) click to toggle source
# File lib/praxis/multipart/parser.rb, line 197
def get_data(filename, body, _content_type, _name, _head)
  # filename is blank which means no file has been selected
  return nil if filename == ''

  # 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 if filename

  # Rewind any IO bodies so the app can read them at its leisure
  body.rewind if body.is_a?(IO)

  [filename, body]
end
get_filename(head) click to toggle source
# File lib/praxis/multipart/parser.rb, line 181
def get_filename(head)
  filename = nil
  if head =~ RFC2183
    filename = Hash[head.scan(DISPPARM)]['filename']
    filename = Regexp.last_match(1) if filename && filename =~ (/^"(.*)"$/)
  elsif head =~ BROKEN_QUOTED
    filename = Regexp.last_match(1)
  elsif head =~ BROKEN_UNQUOTED
    filename = Regexp.last_match(1)
  end

  filename = Rack::Utils.unescape(filename) if filename&.scan(/%.?.?/)&.all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
  filename = filename.gsub(/\\(.)/, '\1') if filename && filename !~ /\\[^\\"]/
  filename
end
new_params() click to toggle source

Rack 2 requires the buffer size

# File lib/praxis/multipart/parser.rb, line 106
def new_params
  Rack::Utils::KeySpaceConstrainedParams.new(PARAMS_BUF_SIZE)
end
rx() click to toggle source
# File lib/praxis/multipart/parser.rb, line 119
def rx
  @rx ||= /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
end
setup_parse() click to toggle source
# File lib/praxis/multipart/parser.rb, line 84
def setup_parse
  unless (match = MULTIPART.match @headers['Content-Type'])
    return false
  end

  @boundary = "--#{match[1]}"

  @buf = String.new

  @params = new_params

  @boundary_size = @boundary.bytesize + EOL.size

  if (@content_length = @headers['Content-Length'])
    @content_length = @content_length.to_i
    @content_length -= @boundary_size
  end
  true
end