# coding: binary
# frozen_string_literal: true
#--
#= Info
#  'OpenSSL for Ruby 2' project
#  Copyright (C) 2001 GOTOU YUUZOU <gotoyuzo@notwork.org>
#  All rights reserved.
#
#= Licence
#  This program is licensed under the same licence as Ruby.
#  (See the file 'COPYING'.)
#++

##
# OpenSSL IO buffering mix-in module.
#
# This module allows an OpenSSL::SSL::SSLSocket to behave like an IO.
#
# You typically won't use this module directly, you can see it implemented in
# OpenSSL::SSL::SSLSocket.

module OpenSSL::Buffering
  include Enumerable

  # A buffer which will retain binary encoding.
  class Buffer < String
    unless String.method_defined?(:append_as_bytes)
      alias_method :_append, :<<
      def append_as_bytes(string)
        if string.encoding == Encoding::BINARY
          _append(string)
        else
          _append(string.b)
        end

        self
      end
    end

    undef_method :concat
    undef_method :<<
  end

  ##
  # The "sync mode" of the SSLSocket.
  #
  # See IO#sync for full details.

  attr_accessor :sync

  ##
  # Default size to read from or write to the SSLSocket for buffer operations.

  BLOCK_SIZE = 1024*16

  ##
  # Creates an instance of OpenSSL's buffering IO module.

  def initialize(*)
    super
    @eof = false
    @rbuffer = Buffer.new
    @sync = @io.sync
  end

  #
  # for reading.
  #
  private

  ##
  # Fills the buffer from the underlying SSLSocket

  def fill_rbuff
    begin
      @rbuffer.append_as_bytes(self.sysread(BLOCK_SIZE))
    rescue Errno::EAGAIN
      retry
    rescue EOFError
      @eof = true
    end
  end

  ##
  # Consumes _size_ bytes from the buffer

  def consume_rbuff(size=nil)
    if @rbuffer.empty?
      nil
    else
      size = @rbuffer.size unless size
      @rbuffer.slice!(0, size)
    end
  end

  public

  # call-seq:
  #   ssl.getbyte => 81
  #
  # Get the next 8bit byte from `ssl`.  Returns `nil` on EOF
  def getbyte
    read(1)&.ord
  end

  # Get the next 8bit byte. Raises EOFError on EOF
  def readbyte
    raise EOFError if eof?
    getbyte
  end

  ##
  # Reads _size_ bytes from the stream.  If _buf_ is provided it must
  # reference a string which will receive the data.
  #
  # See IO#read for full details.

  def read(size=nil, buf=nil)
    if size == 0
      if buf
        buf.clear
        return buf
      else
        return ""
      end
    end
    until @eof
      break if size && size <= @rbuffer.size
      fill_rbuff
    end
    ret = consume_rbuff(size) || ""
    if buf
      buf.replace(ret)
      ret = buf
    end
    (size && ret.empty?) ? nil : ret
  end

  ##
  # Reads at most _maxlen_ bytes from the stream.  If _buf_ is provided it
  # must reference a string which will receive the data.
  #
  # See IO#readpartial for full details.

  def readpartial(maxlen, buf=nil)
    if maxlen == 0
      if buf
        buf.clear
        return buf
      else
        return ""
      end
    end
    if @rbuffer.empty?
      begin
        return sysread(maxlen, buf)
      rescue Errno::EAGAIN
        retry
      end
    end
    ret = consume_rbuff(maxlen)
    if buf
      buf.replace(ret)
      ret = buf
    end
    ret
  end

  ##
  # Reads at most _maxlen_ bytes in the non-blocking manner.
  #
  # When no data can be read without blocking it raises
  # OpenSSL::SSL::SSLError extended by IO::WaitReadable or IO::WaitWritable.
  #
  # IO::WaitReadable means SSL needs to read internally so read_nonblock
  # should be called again when the underlying IO is readable.
  #
  # IO::WaitWritable means SSL needs to write internally so read_nonblock
  # should be called again after the underlying IO is writable.
  #
  # OpenSSL::Buffering#read_nonblock needs two rescue clause as follows:
  #
  #   # emulates blocking read (readpartial).
  #   begin
  #     result = ssl.read_nonblock(maxlen)
  #   rescue IO::WaitReadable
  #     IO.select([io])
  #     retry
  #   rescue IO::WaitWritable
  #     IO.select(nil, [io])
  #     retry
  #   end
  #
  # Note that one reason that read_nonblock writes to the underlying IO is
  # when the peer requests a new TLS/SSL handshake.  See openssl the FAQ for
  # more details.  http://www.openssl.org/support/faq.html
  #
  # By specifying a keyword argument _exception_ to +false+, you can indicate
  # that read_nonblock should not raise an IO::Wait*able exception, but
  # return the symbol +:wait_writable+ or +:wait_readable+ instead. At EOF,
  # it will return +nil+ instead of raising EOFError.

  def read_nonblock(maxlen, buf=nil, exception: true)
    if maxlen == 0
      if buf
        buf.clear
        return buf
      else
        return ""
      end
    end
    if @rbuffer.empty?
      return sysread_nonblock(maxlen, buf, exception: exception)
    end
    ret = consume_rbuff(maxlen)
    if buf
      buf.replace(ret)
      ret = buf
    end
    ret
  end

  ##
  # Reads the next "line" from the stream.  Lines are separated by _eol_.  If
  # _limit_ is provided the result will not be longer than the given number of
  # bytes.
  #
  # _eol_ may be a String or Regexp.
  #
  # Unlike IO#gets the line read will not be assigned to +$_+.
  #
  # Unlike IO#gets the separator must be provided if a limit is provided.

  def gets(eol=$/, limit=nil, chomp: false)
    idx = @rbuffer.index(eol)
    until @eof
      break if idx
      fill_rbuff
      idx = @rbuffer.index(eol)
    end
    if eol.is_a?(Regexp)
      size = idx ? idx+$&.size : nil
    else
      size = idx ? idx+eol.size : nil
    end
    if size && limit && limit >= 0
      size = [size, limit].min
    end
    line = consume_rbuff(size)
    if chomp && line
      line.chomp!(eol)
    end
    line
  end

  ##
  # Executes the block for every line in the stream where lines are separated
  # by _eol_.
  #
  # See also #gets

  def each(eol=$/)
    while line = self.gets(eol)
      yield line
    end
  end
  alias each_line each

  ##
  # Reads lines from the stream which are separated by _eol_.
  #
  # See also #gets

  def readlines(eol=$/)
    ary = []
    while line = self.gets(eol)
      ary << line
    end
    ary
  end

  ##
  # Reads a line from the stream which is separated by _eol_.
  #
  # Raises EOFError if at end of file.

  def readline(eol=$/)
    raise EOFError if eof?
    gets(eol)
  end

  ##
  # Reads one character from the stream.  Returns nil if called at end of
  # file.

  def getc
    read(1)
  end

  ##
  # Calls the given block once for each byte in the stream.

  def each_byte # :yields: byte
    while c = getc
      yield(c.ord)
    end
  end

  ##
  # Reads a one-character string from the stream.  Raises an EOFError at end
  # of file.

  def readchar
    raise EOFError if eof?
    getc
  end

  ##
  # Pushes character _c_ back onto the stream such that a subsequent buffered
  # character read will return it.
  #
  # Unlike IO#getc multiple bytes may be pushed back onto the stream.
  #
  # Has no effect on unbuffered reads (such as #sysread).

  def ungetc(c)
    @rbuffer[0,0] = c.chr
  end

  ##
  # Returns true if the stream is at file which means there is no more data to
  # be read.

  def eof?
    fill_rbuff if !@eof && @rbuffer.empty?
    @eof && @rbuffer.empty?
  end
  alias eof eof?

  #
  # for writing.
  #
  private

  ##
  # Writes _s_ to the buffer.  When the buffer is full or #sync is true the
  # buffer is flushed to the underlying socket.

  def do_write(s)
    @wbuffer = Buffer.new unless defined? @wbuffer
    @wbuffer.append_as_bytes(s)

    @sync ||= false
    buffer_size = @wbuffer.bytesize
    if @sync or buffer_size > BLOCK_SIZE
      nwrote = 0
      begin
        while nwrote < buffer_size do
          begin
            chunk = if nwrote > 0
              @wbuffer.byteslice(nwrote, @wbuffer.bytesize)
            else
              @wbuffer
            end

            nwrote += syswrite(chunk)
          rescue Errno::EAGAIN
            retry
          end
        end
      ensure
        if nwrote < @wbuffer.bytesize
          @wbuffer[0, nwrote] = ""
        else
          @wbuffer.clear
        end
      end
    end
  end

  public

  ##
  # Writes _s_ to the stream.  If the argument is not a String it will be
  # converted using +.to_s+ method.  Returns the number of bytes written.

  def write(*s)
    s.inject(0) do |written, str|
      do_write(str)
      written + str.bytesize
    end
  end

  ##
  # Writes _s_ in the non-blocking manner.
  #
  # If there is buffered data, it is flushed first.  This may block.
  #
  # write_nonblock returns number of bytes written to the SSL connection.
  #
  # When no data can be written without blocking it raises
  # OpenSSL::SSL::SSLError extended by IO::WaitReadable or IO::WaitWritable.
  #
  # IO::WaitReadable means SSL needs to read internally so write_nonblock
  # should be called again after the underlying IO is readable.
  #
  # IO::WaitWritable means SSL needs to write internally so write_nonblock
  # should be called again after underlying IO is writable.
  #
  # So OpenSSL::Buffering#write_nonblock needs two rescue clause as follows.
  #
  #   # emulates blocking write.
  #   begin
  #     result = ssl.write_nonblock(str)
  #   rescue IO::WaitReadable
  #     IO.select([io])
  #     retry
  #   rescue IO::WaitWritable
  #     IO.select(nil, [io])
  #     retry
  #   end
  #
  # Note that one reason that write_nonblock reads from the underlying IO
  # is when the peer requests a new TLS/SSL handshake.  See the openssl FAQ
  # for more details.  http://www.openssl.org/support/faq.html
  #
  # By specifying a keyword argument _exception_ to +false+, you can indicate
  # that write_nonblock should not raise an IO::Wait*able exception, but
  # return the symbol +:wait_writable+ or +:wait_readable+ instead.

  def write_nonblock(s, exception: true)
    flush
    syswrite_nonblock(s, exception: exception)
  end

  ##
  # Writes _s_ to the stream.  _s_ will be converted to a String using
  # +.to_s+ method.

  def <<(s)
    do_write(s)
    self
  end

  ##
  # Writes _args_ to the stream along with a record separator.
  #
  # See IO#puts for full details.

  def puts(*args)
    s = Buffer.new
    if args.empty?
      s.append_as_bytes("\n")
    end
    args.each{|arg|
      s.append_as_bytes(arg.to_s)
      s.sub!(/(?<!\n)\z/, "\n")
    }
    do_write(s)
    nil
  end

  ##
  # Writes _args_ to the stream.
  #
  # See IO#print for full details.

  def print(*args)
    s = Buffer.new
    args.each{ |arg| s.append_as_bytes(arg.to_s) }
    do_write(s)
    nil
  end

  ##
  # Formats and writes to the stream converting parameters under control of
  # the format string.
  #
  # See Kernel#sprintf for format string details.

  def printf(s, *args)
    do_write(s % args)
    nil
  end

  ##
  # Flushes buffered data to the SSLSocket.

  def flush
    osync = @sync
    @sync = true
    do_write ""
    return self
  ensure
    @sync = osync
  end

  ##
  # Closes the SSLSocket and flushes any unwritten data.

  def close
    flush rescue nil
    sysclose
  end
end
