# encoding: binary
#
# This file is taken from Unicorn. The following license applies to this file
# (and this file only, not to the rest of Phusion Passenger):
#
# 1. You may make and give away verbatim copies of the source form of the
#    software without restriction, provided that you duplicate all of the
#    original copyright notices and associated disclaimers.
#
# 2. You may modify your copy of the software in any way, provided that
#    you do at least ONE of the following:
#
#      a) place your modifications in the Public Domain or otherwise make them
#      Freely Available, such as by posting said modifications to Usenet or an
#      equivalent medium, or by allowing the author to include your
#      modifications in the software.
#
#      b) use the modified software only within your corporation or
#         organization.
#
#      c) rename any non-standard executables so the names do not conflict with
#      standard executables, which must also be provided.
#
#      d) make other distribution arrangements with the author.
#
# 3. You may distribute the software in object code or executable
#    form, provided that you do at least ONE of the following:
#
#      a) distribute the executables and library files of the software,
#      together with instructions (in the manual page or equivalent) on where
#      to get the original distribution.
#
#      b) accompany the distribution with the machine-readable source of the
#      software.
#
#      c) give non-standard executables non-standard names, with
#         instructions on where to get the original software distribution.
#
#      d) make other distribution arrangements with the author.
#
# 4. You may modify and include the part of the software into any other
#    software (possibly commercial).  But some files in the distribution
#    are not written by the author, so that they are not under this terms.
#
# 5. The scripts and library files supplied as input to or produced as
#    output from the software do not automatically fall under the
#    copyright of the software, but belong to whomever generated them,
#    and may be sold commercially, and may be aggregated with this
#    software.
#
# 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
#    IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
#    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
#    PURPOSE.

require 'stringio'
PhusionPassenger.require_passenger_lib 'utils/tmpio'

module PhusionPassenger
module Utils

# acts like tee(1) on an input input to provide a input-like stream
# while providing rewindable semantics through a File/StringIO backing
# store.  On the first pass, the input is only read on demand so your
# Rack application can use input notification (upload progress and
# like).  This should fully conform to the Rack::Lint::InputWrapper
# specification on the public API.  This class is intended to be a
# strict interpretation of Rack::Lint::InputWrapper functionality and
# will not support any deviations from it.
#
# When processing uploads, Unicorn exposes a TeeInput object under
# "rack.input" of the Rack environment.
class TeeInput
  CONTENT_LENGTH = "CONTENT_LENGTH".freeze
  HTTP_TRANSFER_ENCODING = "HTTP_TRANSFER_ENCODING".freeze
  CHUNKED = "chunked".freeze

  # The maximum size (in +bytes+) to buffer in memory before
  # resorting to a temporary file.  Default is 112 kilobytes.
  @@client_body_buffer_size = 112 * 1024

  # sets the maximum size of request bodies to buffer in memory,
  # amounts larger than this are buffered to the filesystem
  def self.client_body_buffer_size=(bytes)
    @@client_body_buffer_size = bytes
  end

  # returns the maximum size of request bodies to buffer in memory,
  # amounts larger than this are buffered to the filesystem
  def self.client_body_buffer_size
    @@client_body_buffer_size
  end

  # Initializes a new TeeInput object.  You normally do not have to call
  # this unless you are writing an HTTP server.
  def initialize(socket, env)
    if @len = env[CONTENT_LENGTH]
      @len = @len.to_i
    elsif env[HTTP_TRANSFER_ENCODING] != CHUNKED
      @len = 0
    end
    @socket = socket
    @bytes_read = 0
    if @len && @len <= @@client_body_buffer_size
      @tmp = StringIO.new("")
    else
      @tmp = TmpIO.new("PassengerTeeInput")
    end
    @tmp.binmode
  end

  def close
    @tmp.close
  end

  def size
    if @len
      @len
    else
      pos = @tmp.pos
      consume!
      @tmp.pos = pos
      @len = @tmp.size
    end
  end

  def read(len = nil, buf = "")
    buf ||= ""
    if len
      if len < 0
        raise ArgumentError, "negative length #{len} given"
      elsif len == 0
        buf.replace('')
        buf
      else
        if socket_drained?
          @tmp.read(len, buf)
        else
          tee(read_exact(len, buf))
        end
      end
    else
      if socket_drained?
        @tmp.read(nil, buf)
      else
        tee(read_all(buf))
      end
    end
  end

  def gets
    if socket_drained?
      @tmp.gets
    else
      if @bytes_read == @len
        nil
      elsif line = @socket.gets
        if @len
          max_len = @len - @bytes_read
          line.slice!(max_len, line.size - max_len)
        end
        @bytes_read += line.size
        tee(line)
      else
        nil
      end
    end
  end

  def seek(*args)
    if !socket_drained?
      # seek may be forward, or relative to the end, so we need to consume the socket fully into tmp
      pos = @tmp.pos # save/restore tmp.pos, to not break relative seeks
      consume!
      @tmp.pos = pos
    end
    @tmp.seek(*args)
  end

  def rewind
    return 0 if 0 == @tmp.size
    consume! if !socket_drained?
    @tmp.rewind # Rack does not specify what the return value is here
  end

  def each
    while line = gets
      yield line
    end

    self # Rack does not specify what the return value is here
  end

private

  def socket_drained?
    if @socket
      if @socket.eof?
        @socket = nil
        true
      else
        false
      end
    else
      true
    end
  end

  # consumes the stream of the socket
  def consume!
    junk = ""
    nil while read(16 * 1024, junk)
    @socket = nil
  end

  def tee(buffer)
    if buffer && buffer.size > 0
      @tmp.write(buffer)
    end
    buffer
  end

  def read_exact(len, buf)
    if @len
      max_len = @len - @bytes_read
      len = max_len if len > max_len
      return nil if len == 0
    end
    ret = @socket.read(len, buf)
    @bytes_read += ret.size if ret
    ret
  end

  def read_all(buf)
    if @len
      ret = @socket.read(@len - @bytes_read, buf)
      if ret
        @bytes_read += ret.size
        ret
      else
        buf.replace("")
        buf
      end
    else
      ret = @socket.read(nil, buf)
      @bytes_read += ret.size
      ret
    end
  end
end

end # module Utils
end # module PhusionPassenger
