rubinius/rubinius

View on GitHub
core/io.rb

Summary

Maintainability
F
3 wks
Test Coverage
class IO
  include Enumerable

  def self.fnmatch(pattern, path, flags)
    Rubinius.primitive :io_fnmatch
    raise PrimitiveFailure, "IO#fnmatch primitive failed"
  end

  def socket_recv(bytes, flags, type)
    Rubinius.primitive :io_socket_read
    raise PrimitiveFailure, "io_socket_read failed"
  end

  module TransferIO
    def send_io(io)
      Rubinius.primitive :io_send_io
      raise PrimitiveFailure, "IO#send_io failed"
    end

    def recv_fd
      Rubinius.primitive :io_recv_fd
      raise PrimitiveFailure, "IO#recv_fd failed"
    end
  end

  module WaitReadable; end
  module WaitWritable; end

  class EAGAINWaitReadable < Errno::EAGAIN
    include ::IO::WaitReadable
  end

  class EAGAINWaitWritable < Errno::EAGAIN
    include ::IO::WaitWritable
  end

  class EWOULDBLOCKWaitReadable < Errno::EAGAIN
    include WaitReadable
  end

  class EWOULDBLOCKWaitWritable < Errno::EAGAIN
    include WaitWritable
  end

  class EINPROGRESSWaitReadable < Errno::EINPROGRESS
    include WaitReadable
  end

  class EINPROGRESSWaitWritable < Errno::EINPROGRESS
    include WaitWritable
  end

  class FileDescriptor
    class RIOStream
      def self.close(io, allow_exception)
        Rubinius.primitive :rio_close
        raise PrimitiveFailure, "IO::FileDescriptor::RIOStream.close primitive failed"
      end
    end

    attr_reader :offset

    def self.choose_type(fd, io)
      stat = File::Stat.fstat(fd)

      case stat.ftype
      when "file"
        BufferedFileDescriptor.new(fd, stat, io)
      when "fifo", "characterSpecial"
        FIFOFileDescriptor.new(fd, stat, io)
      when "socket"
        SocketFileDescriptor.new(fd, stat, io)
      when "directory"
        DirectoryFileDescriptor.new(fd, stat, io)
      when "blockSpecial"
        raise "cannot make block special"
      when "link"
        raise "cannot make link"
      else
        BufferedFileDescriptor.new(fd, stat, io)
      end
    end

    def self.open_with_mode(path, mode, perm)
      fd = open_with_cloexec(path, mode, perm)

      if fd < 0
        Errno.handle("failed to open file")
      end

      return fd
    end

    def self.open_with_cloexec(path, mode, perm)
      if O_CLOEXEC
        fd = FFI::Platform::POSIX.open(path, mode | O_CLOEXEC, perm)
        update_max_fd(fd)
      else
        fd = FFI::Platform::POSIX.open(path, mode, perm)
        new_open_fd(fd)
      end

      return fd
    end

    def self.new_open_fd(new_fd)
      if new_fd > 2
        flags = FFI::Platform::POSIX.fcntl(new_fd, F_GETFD, 0)
        Errno.handle("fcntl(2) failed") if FFI.call_failed?(flags)
        flags = FFI::Platform::POSIX.fcntl(new_fd, F_SETFD, flags | FD_CLOEXEC)
        Errno.handle("fcntl(2) failed") if FFI.call_failed?(flags)
      end

      update_max_fd(new_fd)
    end

    def self.pagesize
      @pagesize ||= FFI::Platform::POSIX.getpagesize
    end

    def self.truncate(name, offset)
      raise RangeError, "bignum too big to convert into `long'" if offset.kind_of?(Bignum)

      status = FFI::Platform::POSIX.truncate(name, offset)
      Errno.handle("truncate(2) failed") if FFI.call_failed?(status)
      return status
    end

    def self.update_max_fd(new_fd)
      @@max_descriptors.get_and_set(new_fd)
    end

    def self.max_fd
      @@max_descriptors.get
    end

    def self.get_flags(fd)
      if IO::F_GETFL
        if FFI.call_failed?(flags = FFI::Platform::POSIX.fcntl(fd, IO::F_GETFL, 0))
          Errno.handle("fcntl(2) failed")
        end
      else
        flags = 0
      end
      flags
    end

    def self.clear_flag(flag, fd)
      flags = get_flags(fd)
      if (flags & flag) == 0
        flags &= ~flag
        if FFI.call_failed?(flags = FFI::Platform::POSIX.fcntl(fd, IO::F_SETFL, flags))
          Errno.handle("fcntl(2) failed")
        end
      end
    end

    def self.set_flag(flag, fd)
      flags = get_flags(fd)
      if (flags & flag) == 0
        flags |= flag
        if FFI.call_failed?(flags = FFI::Platform::POSIX.fcntl(fd, IO::F_SETFL, flags))
          Errno.handle("fcntl(2) failed")
        end
      end
    end

    def self.get_empty_8bit_buffer
      "".force_encoding(Encoding::ASCII_8BIT)
    end

    def initialize(fd, stat, io)
      @descriptor, @stat, @io = fd, stat, io
      acc_mode = FileDescriptor.get_flags(@descriptor)

      if acc_mode < 0
        # Assume it's closed.
        if Errno.eql?(Errno::EBADF::Errno)
          @descriptor = -1
        end

        @mode = nil
      else
        @mode = acc_mode
      end

      @sync = true
      @autoclose = false
      reset_positioning(@stat)

      # Don't bother to add finalization for stdio
      if @descriptor >= 3
        # Sometimes a FileDescriptor class is replaced (see IO#reopen) so we need to be
        # careful we don't finalize that descriptor. Probably need a way to cancel
        # the finalization when we are transferring an FD from one instance to another.
        ObjectSpace.define_finalizer(self)
      end
    end

    private :initialize

    def autoclose=(bool)
      @autoclose = bool
    end

    def descriptor
      @descriptor
    end

    def descriptor=(value)
      @descriptor = value
    end

    def mode
      @mode
    end

    def mode=(value)
      @mode = value
    end

    def sync
      @sync
    end

    def sync=(value)
      @sync = value
    end

    CLONG_OVERFLOW = 1 << 64

    def lseek(offset, whence=SEEK_SET)
      ensure_open

      # FIXME: check +amount+ to make sure it isn't too large
      raise RangeError if offset > CLONG_OVERFLOW

      position = FFI::Platform::POSIX.lseek(descriptor, offset, whence)

      if FFI.call_failed?(position)
        Errno.handle("seek failed: fd: #{descriptor}, offset: #{offset}, from: #{whence}")
      end

      @offset = position
      determine_eof

      return position
    end

    def read(length, output_string=nil)
      length ||= FileDescriptor.pagesize

      while true
        ensure_open

        storage = FFI::MemoryPointer.new(length)
        raise IOError, "read(2) failed to malloc a buffer for read length #{length}" if storage.null?
        bytes_read = read_into_storage(length, storage)

        if bytes_read == 0
          @eof = true if length > 0
          return nil
        else
          break
        end
      end

      if output_string
        output_string.replace(storage.read_string(bytes_read))
      else
        output_string = storage.read_string(bytes_read).force_encoding(Encoding::ASCII_8BIT)
      end

      @offset += bytes_read
      determine_eof

      return output_string
    end

    def read_into_storage(count, storage)
      while true
        bytes_read = FFI::Platform::POSIX.read(descriptor, storage, count)

        if FFI.call_failed?(bytes_read)
          errno = Errno.errno

          if errno == Errno::EAGAIN::Errno || errno == Errno::EINTR::Errno
            ensure_open
            next
          else
            Errno.handle "read(2) failed"
          end
        else
          break
        end
      end

      # before returning verify file hasn't been closed in another thread
      ensure_open

      return bytes_read
    end
    private :read_into_storage

    def write(str)
      buf_size = str.bytesize
      left = buf_size

      buffer = FFI::MemoryPointer.new(left)
      buffer.write_string(str)
      error = false

      while left > 0
        bytes_written = FFI::Platform::POSIX.write(@descriptor, buffer, left)

        if FFI.call_failed?(bytes_written)
          errno = Errno.errno
          if errno == Errno::EINTR::Errno || errno == Errno::EAGAIN::Errno
            # do a #select and wait for descriptor to become writable
            if blocking?
              Select.wait_for_writable(@descriptor)
              next
            else
              break
            end
          end

          error = true
          break
        end

        break if error

        left -= bytes_written
        buffer += bytes_written
        @offset += bytes_written

        if @offset > @total_size
          @total_size = @offset
          @eof = false # only a failed read can set EOF!
        end
      end

      Errno.handle("write failed") if error

      return(buf_size - left)
    end

    def close
      ensure_open
      fd = @descriptor

      if fd != -1
        # return early if this handle was promoted to a stream by a C-ext
        return if RIOStream.close(@io, true)
        ret_code = FFI::Platform::POSIX.close(fd)

        if FFI.call_failed?(ret_code)
          Errno.handle("close failed")
        elsif ret_code == 0
          # no op
        else
          raise IOError, "::close(): Unknown error on fd #{fd}"
        end
      end

      @descriptor = -1

      return nil
    end

    def determine_eof
      if @offset >= @total_size
        @eof = true
        @total_size += (@total_size - @offset)
        @total_size = @offset if @offset > @total_size
      else
        @eof = false
      end
    end
    private :determine_eof

    def eof?
      @eof
    end

    # This is NOT the same as close().
    def shutdown(how)
      ensure_open
      fd = descriptor

      if how != IO::SHUT_RD && how != IO::SHUT_WR && how != IO::SHUT_RDWR
        raise ArgumentError, "::shutdown(): Invalid `how` #{how} for fd #{fd}"
      end

      ret_code = FFI::Platform::POSIX.shutdown(fd, how)

      if FFI.call_failed?(ret_code)
        Errno.handle("shutdown(2) failed")
      elsif ret_code == 0
        if how == IO::SHUT_RDWR
          close
          self.descriptor = -2
        end
      else
        Errno.handle("::shutdown(): Unknown error on fd #{fd}")
      end

      return how
    end

    def ensure_open(fd=nil)
      fd ||= descriptor

      if fd.nil?
        raise IOError, "uninitialized stream"
      elsif fd == -1
        raise IOError, "closed stream"
      elsif fd == -2
        raise IOError, "shutdown stream"
      end
      return nil
    end

    def force_read_only
      @mode = (@mode & ~IO::O_ACCMODE ) | O_RDONLY
    end

    def force_write_only
      @mode = (@mode & ~IO::O_ACCMODE) | O_WRONLY
    end

    def read_only?
      (@mode & O_ACCMODE) == O_RDONLY
    end

    def write_only?
      (@mode & O_ACCMODE) == O_WRONLY
    end

    def read_write?
      (@mode & O_ACCMODE) == O_RDWR
    end

    def reopen(other_fd)
      current_fd = @descriptor

      if FFI.call_failed?(FFI::Platform::POSIX.dup2(other_fd, current_fd))
        Errno.handle("reopen")
      end

      set_mode
      reset_positioning

      return true
    end

    def reopen_path(path, mode)
      current_fd = @descriptor
      other_fd = FileDescriptor.open_with_cloexec(path, mode, 0666)

      Errno.handle("could not reopen path \"#{path}\"") if other_fd < 0

      if FFI.call_failed?(FFI::Platform::POSIX.dup2(other_fd, current_fd))
        if Errno.eql?(Errno::EBADF::Errno)
          # means current_fd is closed, so set ourselves to use the new fd and continue
          @descriptor = other_fd
        else
          FFI::Platform::POSIX.close(other_fd) if other_fd > 0
          Errno.handle("could not reopen path \"#{path}\"")
        end
      else
        FFI::Platform::POSIX.close(other_fd)
      end

      set_mode
      reset_positioning

      return true
    end

    def reset_positioning(stat=nil)
      # Discover final size of file so we can set EOF properly
      stat = File::Stat.fstat(@descriptor) unless stat
      @total_size = stat.size

      # We may have reopened a file descriptor that went from "file" to a different
      # ftype which doesn't allow seeking, so catch it here.
      if stat.ftype == "file"
        seek_positioning
      else
        @offset = 0
      end

      determine_eof
    end

    def seek_positioning
      @offset = lseek(0, SEEK_CUR) # find current position if we are reopening!
    end
    private :seek_positioning

    def set_mode
      @mode = FileDescriptor.get_flags(@descriptor)
    end

    def blocking?
      (FileDescriptor.get_flags(@descriptor) & O_NONBLOCK) == 0
    end

    def clear_nonblock
      flags = FileDescriptor.get_flags(@descriptor)
      FileDescriptor.clear_flag(O_NONBLOCK, @descriptor)
    end

    def set_nonblock
      flags = FileDescriptor.get_flags(@descriptor)
      FileDescriptor.set_flag(O_NONBLOCK, @descriptor)
    end

    def ftruncate(offset)
      ensure_open

      # FIXME: fail if +offset+ is too large, see C++ code

      status = FFI::Platform::POSIX.ftruncate(descriptor, offset)
      Errno.handle("ftruncate(2) failed") if FFI.call_failed?(status)
      return status
    end

    ##
    # Returns true if ios is associated with a terminal device (tty), false otherwise.
    #
    #  File.new("testfile").isatty   #=> false
    #  File.new("/dev/tty").isatty   #=> true
    def tty?
      ensure_open
      FFI::Platform::POSIX.isatty(@descriptor) == 1
    end

    def ttyname
      # Need to protect this call with a lock since the OS copies this string information
      # to an internal static object and returns a pointer to that object. Future calls
      # to #ttyname modify that same object. Therefore, we need to protect this from
      # race conditions.
      Rubinius.lock(self)
      name = FFI::Platform::POSIX.ttyname(descriptor)
      if name
        name
      else
        Errno.handle("ttyname(3) failed")
        nil
      end
    ensure
      Rubinius.unlock(self)
    end

    def inspect
      stat = File::Stat.fstat(@descriptor)
      "fd [#{descriptor}], mode [#{@mode}], total_size [#{@total_size}], offset [#{@offset}], eof [#{@eof}], stat.size [#{stat.size}], written? [#{@written}]"
    end

    def cancel_finalizer
      ObjectSpace.undefine_finalizer(self)
    end

    def __finalize__
      return if @descriptor.nil? || @descriptor == -1

      fd = @descriptor

      # Should flush a write buffer here if one exists. Current
      # implementation does not buffer writes internally, so this
      # is a no-op.

      # Do not close stdin/out/err (0, 1, and 2)
      if @descriptor > 3
        @descriptor = -1

        # Take care of any IO cleanup for the C API here. An IO may
        # have been promoted to a low-level RIO struct using #fdopen,
        # so we MUST use #fclose to clost it.
        return if RIOStream.close(@io, false)

        # Ignore any return code... don't care if it fails
        FFI::Platform::POSIX.close(fd) if @autoclose
      end
    end
  end # class FileDescriptor

  class BufferedFileDescriptor < FileDescriptor

    def buffer_reset
      @unget_buffer.clear
    end
    private :buffer_reset

    def read(length, output_string=nil)
      length ||= FileDescriptor.pagesize

      # Preferentially read from the buffer and then from the underlying
      # FileDescriptor.
      # FIXME: make this logic clearer.
      if length > @unget_buffer.size
        @offset += @unget_buffer.size
        length -= @unget_buffer.size

        str = @unget_buffer.inject(self.class.get_empty_8bit_buffer) { |sum, val| val.chr + sum }
        str2 = super(length, output_string)

        if str.size == 0 && str2.nil?
          determine_eof
          return nil
        elsif str2
          str += str2
        end
        buffer_reset
      elsif length == @unget_buffer.size
        @offset += length
        length -= @unget_buffer.size

        str = @unget_buffer.inject(self.class.get_empty_8bit_buffer) { |sum, val| val.chr + sum }
        buffer_reset
      else
        @offset += @unget_buffer.size
        str = self.class.get_empty_8bit_buffer

        length.times do
          str << @unget_buffer.pop
        end
      end

      if output_string
        output_string.replace(str)
      else
        output_string = str.force_encoding(Encoding::ASCII_8BIT)
      end

      determine_eof
      return output_string
    end

    def read_only_buffer(length)
      unless @unget_buffer.empty?
        if length >= @unget_buffer.size
          @offset += @unget_buffer.size
          length -= @unget_buffer.size

          str = @unget_buffer.inject(self.class.get_empty_8bit_buffer) { |sum, val| val.chr + sum }
          buffer_reset
          [str, length]
        else
          @offset += @unget_buffer.size
          str = self.class.get_empty_8bit_buffer

          length.times do
            str << @unget_buffer.pop
          end
          [str, 0]
        end
      else
        [nil, length]
      end
    end

    def seek(bytes, whence)
      # @offset may not match actual file pointer if there were calls to #unget.
      if whence == SEEK_CUR
        # adjust the number of bytes to seek based on how far ahead we are with the buffer
        bytes -= @unget_buffer.size
      end

      buffer_reset
      lseek(bytes, whence)
    end

    def sysread(byte_count)
      raise_if_buffering
      read(byte_count)
    end

    def sysseek(bytes, whence)
      raise_if_buffering
      lseek(bytes, whence)
    end

    def eof?
      super && @unget_buffer.empty?
    end

    def flush
      #@unget_buffer.clear
    end

    def raise_if_buffering
      raise IOError unless @unget_buffer.empty?
    end

    def reset_positioning(*args)
      @unget_buffer = []
      super
    end

    def write_nonblock(str)
      buffer_reset
      set_nonblock

      buf_size = str.bytesize
      left = buf_size

      if left > 0
        buffer = FFI::MemoryPointer.new(left)
        buffer.write_string(str)

        if FFI.call_failed?(bytes_written = FFI::Platform::POSIX.write(@descriptor, buffer, left))
          clear_nonblock
          Errno.raise_waitwritable("write_nonblock")
        end

        left -= bytes_written
        buffer += bytes_written
        @offset += bytes_written
      end

      return(buf_size - left)
    end

    def unget(byte)
      @offset -= 1
      @unget_buffer << byte
    end
  end # class BufferedFileDescriptor

  class FIFOFileDescriptor < BufferedFileDescriptor

    def self.connect_pipe_fds
      fds = FFI::MemoryPointer.new(:int, 2)

      Errno.handle("creating pipe failed") if FFI.call_failed?(FFI::Platform::POSIX.pipe(fds))
      fd0, fd1 = fds.read_array_of_int(2)

      FileDescriptor.new_open_fd(fd0)
      FileDescriptor.new_open_fd(fd1)

      return [fd0, fd1]
    end

    def initialize(fd, stat, io, mode=nil)
      super(fd, stat, self)
      @mode = mode if mode
      @eof = false # force to false
    end

    private :initialize

    def determine_eof
      if @offset >= @total_size
        @eof = true

        # No seeking allowed on a pipe, so its size is always its offset
        @total_size = @offset
      end
    end

    def eof?
      # The only way to confirm we are EOF with a pipe is to try to read from
      # it. If we fail, then we are EOF. If we succeed, then unget the byte
      # so that EOF is false (i.e. more to read).
      str = read(1)
      unget(str) if str
      super
    end

    def seek_positioning
      # no seeking allowed for pipes
      @offset = 0
    end

    def sysseek(offset, whence=SEEK_SET)
      # lseek does not work with pipes.
      raise Errno::ESPIPE
    end
  end # class FIFOFileDescriptor

  class DirectoryFileDescriptor < BufferedFileDescriptor
  end # class DirectoryFileDescriptor

  class SocketFileDescriptor < FIFOFileDescriptor
    def initialize(fd, stat, io)
      super

      @mode &= O_ACCMODE
      @sync = true
    end

    private :initialize

    def force_read_write
      @mode &= ~(O_RDONLY | O_WRONLY)
      @mode |= O_RDWR
    end
  end # class SocketFileDescriptor

  # Encapsulates all of the logic necessary for handling #select.
  class Select
    #eval(Rubinius::Config['rbx.platform.timeval.class'])

    class FDSet
      def self.new
        Rubinius.primitive :fdset_allocate
        raise PrimitiveFailure, "FDSet.allocate failed"
      end

      def zero
        Rubinius.primitive :fdset_zero
        raise PrimitiveFailure, "FDSet.zero failed"
      end

      def set(descriptor)
        Rubinius.primitive :fdset_set
        raise PrimitiveFailure, "FDSet.set failed"
      end

      def set?(descriptor)
        Rubinius.primitive :fdset_is_set
        raise PrimitiveFailure, "FDSet.set? failed"
      end

      def to_set
        Rubinius.primitive :fdset_to_set
        raise PrimitiveFailure, "FDSet.to_set failed"
      end
    end

    def self.fd_set_from_array(array)
      highest = -1
      fd_set = FDSet.new
      fd_set.zero

      array.each do |io|
        io = io[1] if io.is_a?(Array)
        descriptor = io.descriptor

        if descriptor >= FD_SETSIZE
          raise IOError
        end

        if descriptor >= 0
          highest = descriptor > highest ? descriptor : highest
          fd_set.set(descriptor)
        end
      end

      return [fd_set, highest]
    end

    def self.collect_set_fds(array, fd_set)
      return [] unless fd_set
      array.map do |io|
        key, io = if io.is_a?(Array)
          [io[0], io[1]]
        else
          [io, io]
        end

        if fd_set.set?(io.descriptor) || io.descriptor < 0
          key
        else
          nil
        end
      end.compact
    end

    def self.timer_add(time1, time2, result)
      result[:tv_sec] = time1[:tv_sec] + time2[:tv_sec]
      result[:tv_usec] = time1[:tv_usec] + time2[:tv_usec]

      if result[:tv_usec] >= 1_000_000
        result[:tv_sec] += 1
        result[:tv_usec] -= 1_000_000
      end
    end

    def self.timer_sub(time1, time2, result)
      result[:tv_sec] = time1[:tv_sec] - time2[:tv_sec]
      result[:tv_usec] = time1[:tv_usec] - time2[:tv_usec]

      if result[:tv_usec] < 0
        result[:tv_sec] -= 1
        result[:tv_usec] += 1_000_000
      end
    end

    def self.make_timeval_timeout(timeout)
      if timeout
        limit = Rubinius::FFI::Platform::POSIX::TimeVal.new
        future = Rubinius::FFI::Platform::POSIX::TimeVal.new

        limit[:tv_sec] = (timeout / 1_000_000.0).to_i
        limit[:tv_usec] = (timeout % 1_000_000.0).to_i

        # Get current time to be used if select is interrupted and we have to recalculate the sleep time
        if FFI.call_failed?(FFI::Platform::POSIX.gettimeofday(future, nil))
          Errno.handle("gettimeofday(2) failed")
        end

        timer_add(future, limit, future)
      end

      [limit, future]
    end

    def self.reset_timeval_timeout(time_limit, future)
      now = Rubinius::FFI::Platform::POSIX::TimeVal.new

      if FFI.call_failed?(FFI::Platform::POSIX.gettimeofday(now, nil))
        Errno.handle("gettimeofday(2) failed")
      end

      timer_sub(future, now, time_limit)
    end

    def self.readable_events(read_fd)
      fd_set = FDSet.new
      fd_set.zero
      fd_set.set(read_fd)

      # sets fields to zero by default
      timer = Rubinius::FFI::Platform::POSIX::TimeVal.new

      FFI::Platform::POSIX.select(read_fd + 1, fd_set.to_set, nil, nil, timer)
    end

    def self.wait_for_writable(fd)
      fd_set = FDSet.new
      fd_set.zero
      fd_set.set(fd)

      FFI::Platform::POSIX.select(fd + 1, nil, fd_set.to_set, nil, nil)
    end

    def self.select(readables, writables, errorables, timeout)
      read_set, highest_read_fd = readables.nil? ? [nil, nil] : fd_set_from_array(readables)
      write_set, highest_write_fd = writables.nil? ? [nil, nil] : fd_set_from_array(writables)
      error_set, highest_err_fd = errorables.nil? ? [nil, nil] : fd_set_from_array(errorables)
      max_fd = [highest_read_fd, highest_write_fd, highest_err_fd].compact.max || -1

      time_limit, future = make_timeval_timeout(timeout)

      events = 0
      loop do
        events = FFI::Platform::POSIX.select(max_fd + 1,
        (read_set ? read_set.to_set : nil),
        (write_set ? write_set.to_set : nil),
        (error_set ? error_set.to_set : nil),
        time_limit)

        if FFI.call_failed?(events)

          if Errno::EAGAIN::Errno == Errno.errno || Errno::EINTR::Errno == Errno.errno
            # return nil if async_interruption?
            time_limit = reset_timeval_timeout(time_limit, future)
            continue
          end

          Errno.handle("select(2) failed")
        end

        break
      end

      return nil if events.zero?

      output_fds = []
      output_fds << collect_set_fds(readables, read_set)
      output_fds << collect_set_fds(writables, write_set)
      output_fds << collect_set_fds(errorables, error_set)
      return output_fds
    end

    def self.validate_and_convert_argument(objects)
      if objects
        raise TypeError, "Argument must be an Array" unless objects.respond_to?(:to_ary)
        objects =
          objects.to_ary.map do |obj|
          if obj.kind_of? IO
            raise IOError, "closed stream" if obj.closed?
            obj
          else
            raise TypeError unless obj.respond_to?(:to_io)
            io = obj.to_io
            raise TypeError unless io
            raise IOError, "closed stream" if io.closed?
            [obj, io]
          end
        end
      end

      objects
    end
  end # class Select

  attr_accessor :external
  attr_accessor :internal
  # intended to only be used by IO.setup to associate a new FileDescriptor object with instance of IO
  attr_accessor :fd

  def self.binread(file, length=nil, offset=0)
    raise ArgumentError, "Negative length #{length} given" if !length.nil? && length < 0

    File.open(file, "r", :encoding => "ascii-8bit:-") do |f|
      f.seek(offset)
      f.read(length)
    end
  end

  def self.binwrite(file, string, *args)
    offset, opts = args
    opts ||= {}
    if offset.is_a?(Hash)
      offset, opts = nil, offset
    end

    mode, binary, external, internal, autoclose, encoding_options = IO.normalize_options(nil, opts)
    unless mode
      mode = File::CREAT | File::RDWR | File::BINARY
      mode |= File::TRUNC unless offset
    end
    File.open(file, mode, encoding_options.merge(:encoding => (external || "ASCII-8BIT"))) do |f|
      f.seek(offset || 0)
      f.write(string)
    end
  end

  class StreamCopier
    def initialize(from, to, length, offset)
      @length = length
      @offset = offset

      @from_io, @from = to_io(from, "rb")
      @to_io, @to = to_io(to, "wb")

      @method = read_method @from
    end

    private :initialize

    def to_io(obj, mode)
      if obj.kind_of? IO
        flag = true
        io = obj
      else
        flag = false

        if obj.kind_of? String
          io = File.open obj, mode
        elsif obj.respond_to? :to_path
          path = Rubinius::Type.coerce_to obj, String, :to_path
          io = File.open path, mode
        else
          io = obj
        end
      end

      return flag, io
    end

    def read_method(obj)
      if obj.respond_to? :readpartial
        :readpartial
      else
        :read
      end
    end

    def run
      @from.ensure_open_and_readable if @from.kind_of? IO
      @to.ensure_open_and_writable if @to.kind_of? IO

      if @offset
        if @from_io && !@from.pipe?
          saved_pos = @from.pos
        else
          saved_pos = 0
        end

        @from.seek @offset, IO::SEEK_CUR
      end

      size = @length ? @length : 16384
      bytes = 0

      begin
        while data = @from.__send__(@method, size, "")
          @to.write data
          bytes += data.bytesize

          break if @length && bytes >= @length
        end
      rescue EOFError
        # done reading
      end

      @to.flush if @to.kind_of? IO
      return bytes
    ensure
      if @from_io
        @from.pos = saved_pos if @offset
      else
        @from.close if @from.kind_of? IO
      end

      @to.close if @to.kind_of? IO unless @to_io
    end
  end # class StreamCopier

  def self.copy_stream(from, to, max_length=nil, offset=nil)
    StreamCopier.new(from, to, max_length, offset).run
  end

  def self.foreach(name, separator=undefined, limit=undefined, options=undefined)
    return to_enum(:foreach, name, separator, limit, options) unless block_given?

    name = Rubinius::Type.coerce_to_path name

    case separator
    when Fixnum
      options = limit
      limit = separator
      separator = $/
    when undefined
      separator = $/
    when Hash
      options = separator
      separator = $/
    when nil
      # do nothing
    else
      separator = StringValue(separator)
    end

    case limit
    when Fixnum, nil
      # do nothing
    when undefined
      limit = nil
    when Hash
      if undefined.equal? options
        options = limit
        limit = nil
      else
        raise TypeError, "can't convert Hash into Integer"
      end
    else
      value = limit
      limit = Rubinius::Type.try_convert limit, Fixnum, :to_int

      unless limit
        options = Rubinius::Type.coerce_to value, Hash, :to_hash
      end
    end

    case options
    when Hash
      # do nothing
    when undefined, nil
      options = { }
    else
      options = Rubinius::Type.coerce_to options, Hash, :to_hash
    end

    if name[0] == ?|
      io = IO.popen(name[1..-1], "r")
      return nil unless io
    else
      options[:mode] = "r" unless options.key? :mode
      io = File.open(name, options)
    end

    begin
      while line = io.gets(separator, limit)
        yield line
      end
    ensure
      $_ = nil
      io.close
    end

    return nil
  end

  def self.readlines(name, separator=undefined, limit=undefined, options=undefined)
    lines = []
    saved_line = $_
    foreach(name, separator, limit, options) { |l| lines << l }

    $_ = saved_line
    lines
  end

  def self.read_encode(io, str, encoding_options)
    internal = io.internal_encoding
    external = io.external_encoding || Encoding.default_external

    if external.equal? Encoding::ASCII_8BIT
      str.force_encoding external
    elsif internal and external
      ec = Encoding::Converter.new(external, internal, (encoding_options || {}))
      ec.convert str
    else
      str.force_encoding external
    end
  end

  def self.write(file, string, *args)
    if args.size > 2
      raise ArgumentError, "wrong number of arguments (#{args.size + 2} for 2..3)"
    end

    offset, opts = args
    opts ||= {}
    if offset.is_a?(Hash)
      offset, opts = nil, offset
    end

    mode, binary, external, internal, autoclose, encoding_options = IO.normalize_options(nil, opts)
    unless mode
      mode = File::CREAT | File::WRONLY
      mode |= File::TRUNC unless offset
    end

    open_args = opts[:open_args] || [mode, :encoding => (external || "ASCII-8BIT")]
    open_args[1].merge!(encoding_options) if open_args[1].kind_of?(Hash)
    File.open(file, *open_args) do |f|
      f.seek(offset) if offset
      f.write(string)
    end
  end

  def self.for_fd(fd, mode=undefined, options=undefined)
    new fd, mode, options
  end

  def self.read(name, length_or_options=undefined, offset=0, options=nil)
    offset = 0 if offset.nil?
    name = Rubinius::Type.coerce_to_path name
    mode = "r"

    if undefined.equal? length_or_options
      length = undefined
    elsif Rubinius::Type.object_kind_of? length_or_options, Hash
      length = undefined
      offset = 0
      options = length_or_options
    elsif length_or_options
      offset = Rubinius::Type.coerce_to(offset || 0, Fixnum, :to_int)
      raise Errno::EINVAL, "offset must not be negative" if offset < 0

      length = Rubinius::Type.coerce_to(length_or_options, Fixnum, :to_int)
      raise ArgumentError, "length must not be negative" if length < 0
    else
      length = undefined
    end

    if options
      mode = options.delete(:mode) || "r"
    end

    # Detect pipe mode
    if name[0] == ?|
      io = IO.popen(name[1..-1], "r")
      return nil unless io # child process
    else
      io = File.new(name, mode, options)
    end

    str = nil
    begin
      io.seek(offset) unless offset == 0
      if undefined.equal?(length)
        str = io.read
      else
        str = io.read length
      end
    ensure
      io.close
    end

    return str
  end

  def self.try_convert(obj)
    Rubinius::Type.try_convert obj, IO, :to_io
  end

  def self.normalize_options(mode, options)
    mode = nil if undefined.equal?(mode)
    autoclose = true
    encoding_options = {}

    if undefined.equal?(options)
      options = Rubinius::Type.try_convert(mode, Hash, :to_hash)
      mode = nil if options
    elsif !options.nil?
      options = Rubinius::Type.try_convert(options, Hash, :to_hash)
      raise ArgumentError, "wrong number of arguments (3 for 1..2)" unless options
    end

    if mode
      mode = (Rubinius::Type.try_convert(mode, Integer, :to_int) or
              Rubinius::Type.coerce_to(mode, String, :to_str))
    end

    if options
      if optmode = options[:mode]
        optmode = (Rubinius::Type.try_convert(optmode, Integer, :to_int) or
                   Rubinius::Type.coerce_to(optmode, String, :to_str))
      end

      if mode
        raise ArgumentError, "mode specified twice" if optmode
      else
        mode = optmode
      end

      autoclose = !!options[:autoclose] if options.key?(:autoclose)
    end

    if mode.kind_of?(String)
      mode, external, internal = mode.split(":")
      raise ArgumentError, "invalid access mode" unless mode

      binary = true  if mode[1] === ?b
      binary = false if mode[1] === ?t
    elsif mode
      binary = true  if (mode & BINARY) != 0
    end

    if options
      if options[:textmode] and options[:binmode]
        raise ArgumentError, "both textmode and binmode specified"
      end

      if binary.nil?
        binary = options[:binmode]
      elsif options.key?(:textmode) or options.key?(:binmode)
        raise ArgumentError, "text/binary mode specified twice"
      end

      if !external and !internal
        external = options[:external_encoding]
        internal = options[:internal_encoding]
      elsif options[:external_encoding] or options[:internal_encoding] or options[:encoding]
        raise ArgumentError, "encoding specified twice"
      end

      if !external and !internal
        encoding = options[:encoding]

        if encoding.kind_of? Encoding
          external = encoding
        elsif !encoding.nil?
          encoding = StringValue(encoding)
          external, internal = encoding.split(':')
        end
      end

      [
       :invalid, :undef, :replace, :newline, :universal_newline, :crlf_newline,
       :cr_newline, :xml
      ].each do |options_key|
        encoding_options[options_key] = options[options_key] if options.has_key?(options_key)
      end
    end

    [mode, binary, external, internal, autoclose, encoding_options]
  end

  def self.open(*args)
    io = new(*args)

    return io unless block_given?

    begin
      yield io
    ensure
      begin
        io.close unless io.closed?
      rescue StandardError
        # nothing, just swallow them.
      end
    end
  end

  def self.parse_mode(mode)
    return mode if Rubinius::Type.object_kind_of? mode, Integer

    mode = StringValue(mode)

    ret = 0

    case mode[0]
    when ?r
      ret |= RDONLY
    when ?w
      ret |= WRONLY | CREAT | TRUNC
    when ?a
      ret |= WRONLY | CREAT | APPEND
    else
      raise ArgumentError, "invalid mode -- #{mode}"
    end

    return ret if mode.length == 1

    case mode[1]
    when ?+
        ret &= ~(RDONLY | WRONLY)
      ret |= RDWR
    when ?b
      ret |= BINARY
    when ?t
      ret &= ~BINARY
    when ?:
        warn("encoding options not supported in 1.8")
      return ret
    else
      raise ArgumentError, "invalid mode -- #{mode}"
    end

    return ret if mode.length == 2

    case mode[2]
    when ?+
        ret &= ~(RDONLY | WRONLY)
      ret |= RDWR
    when ?b
      ret |= BINARY
    when ?t
      ret &= ~BINARY
    when ?:
        warn("encoding options not supported in 1.8")
      return ret
    else
      raise ArgumentError, "invalid mode -- #{mode}"
    end

    ret
  end

  def self.pipe(external=nil, internal=nil, options=nil)
    # The use of #allocate is to make sure we create an IO obj. Would be so much
    # cleaner to just do a PipeIO class as a subclass, but that would not be
    # backward compatible. <sigh>
    fd0, fd1 = FIFOFileDescriptor.connect_pipe_fds
    lhs = allocate
    lhs.send(:new_pipe, fd0, external, internal, options, FileDescriptor::O_RDONLY, true)
    rhs = allocate
    rhs.send(:new_pipe, fd1, nil, nil, nil, FileDescriptor::O_WRONLY)

    if block_given?
      begin
        yield lhs, rhs
      ensure
        lhs.close unless lhs.closed?
        rhs.close unless rhs.closed?
      end
    else
      [lhs, rhs]
    end
  end

  def self.popen(*args)
    if env = Rubinius::Type.try_convert(args.first, Hash, :to_hash)
      args.shift
    end

    if io_options = Rubinius::Type.try_convert(args.last, Hash, :to_hash)
      args.pop
    end

    if args.size > 2
      raise ArgumentError, "#{__method__}: given #{args.size}, expected 1..2"
    end

    cmd, mode = args
    mode ||= "r"

    if cmd.kind_of? Array
      if sub_env = Rubinius::Type.try_convert(cmd.first, Hash, :to_hash)
        env = sub_env unless env
        cmd.shift
      end

      if exec_options = Rubinius::Type.try_convert(cmd.last, Hash, :to_hash)
        cmd.pop
      end
    end

    mode, binary, external, internal, autoclose, encoding_options =
      IO.normalize_options(mode, io_options || {})
    mode_int = parse_mode mode

    readable = false
    writable = false

    if mode_int & IO::RDWR != 0
      readable = true
      writable = true
    elsif mode_int & IO::WRONLY != 0
      writable = true
    else # IO::RDONLY
      readable = true
    end

    pa_read, ch_write = pipe if readable
    ch_read, pa_write = pipe if writable

    # We only need the Bidirectional pipe if we're reading and writing.
    # If we're only doing one, we can just return the IO object for
    # the proper half.
    if readable and writable
      # Transmogrify pa_read into a BidirectionalPipe object,
      # and then tell it abou it's pid and pa_write

      Rubinius::Unsafe.set_class pa_read, IO::BidirectionalPipe

      pipe = pa_read
      pipe.set_pipe_info(pa_write)
    elsif readable
      pipe = pa_read
    elsif writable
      pipe = pa_write
    else
      raise ArgumentError, "IO is neither readable nor writable"
    end

    pipe.binmode if binary
    pipe.set_encoding(external || Encoding.default_external, internal)

    if cmd == "-"
      pid = Rubinius::Mirror::Process.fork

      if !pid
        # Child
        begin
          if readable
            pa_read.close
            STDOUT.reopen ch_write
          end

          if writable
            pa_write.close
            STDIN.reopen ch_read
          end

          if block_given?
            yield nil
            exit! 0
          else
            return nil
          end
        rescue => e
          exit! 0
        end
      end
    else
      options = {}
      options[:in] = ch_read.fileno if ch_read
      options[:out] = ch_write.fileno if ch_write

      if io_options
        io_options.delete_if do |key, _|
          [:mode, :external_encoding, :internal_encoding,
           :encoding, :textmode, :binmode, :autoclose
           ].include? key
        end

        options.merge! io_options
      end

      if exec_options
        options.merge! exec_options
      end

      pid = Rubinius::Mirror::Process.spawn(env || {}, *cmd, options)
    end

    pipe.pid = pid

    ch_write.close if readable
    ch_read.close  if writable

    return pipe unless block_given?

    begin
      yield pipe
    ensure
      pipe.close unless pipe.closed?
    end
  end

  #
  # +select+ examines the IO object Arrays that are passed in
  # as +readables+, +writables+, and +errorables+ to see if any
  # of their descriptors are ready for reading, are ready for
  # writing, or have an exceptions pending respectively. An IO
  # may appear in more than one of the sets. Any of the three
  # sets may be +nil+ if you are not interested in those events.
  #
  # If +timeout+ is not nil, it specifies the number of seconds
  # to wait for events (maximum.) The number may be fractional,
  # conceptually up to a microsecond resolution.
  #
  # A +timeout+ of 0 indicates that each descriptor should be
  # checked once only, effectively polling the sets.
  #
  # Leaving the +timeout+ to +nil+ causes +select+ to block
  # infinitely until an event transpires.
  #
  # If the timeout expires without events, +nil+ is returned.
  # Otherwise, an [readable, writable, errors] Array of Arrays
  # is returned, only, with the IO objects that have events.
  #
  # @compatibility  MRI 1.8 and 1.9 require the +readables+ Array,
  #                 Rubinius does not.
  #
  def self.select(readables=nil, writables=nil, errorables=nil, timeout=nil)
    if timeout
      unless Rubinius::Type.object_kind_of? timeout, Numeric
        raise TypeError, "Timeout must be numeric"
      end

      raise ArgumentError, 'timeout must be positive' if timeout < 0

      # Microseconds, rounded down
      timeout = Integer(timeout * 1_000_000)
    end

    readables = IO::Select.validate_and_convert_argument(readables)
    writables = IO::Select.validate_and_convert_argument(writables)
    errorables = IO::Select.validate_and_convert_argument(errorables)

    IO::Select.select(readables, writables, errorables, timeout)
  end

  ##
  # Opens the given path, returning the underlying file descriptor as a Fixnum.
  #  IO.sysopen("testfile")   #=> 3
  def self.sysopen(path, mode = nil, perm = nil)
    path = Rubinius::Type.coerce_to_path path
    mode = parse_mode(mode || "r")
    perm ||= 0666

    FileDescriptor.open_with_mode path, mode, perm
  end

  #
  # Internally associate +io+ with the given descriptor.
  #
  # The +mode+ will be checked and set as the current mode if
  # the underlying descriptor allows it.
  #
  # The +sync+ attribute will also be set.
  #
  def self.setup(io, fd, mode=nil, sync=false)
    cur_mode = FileDescriptor.get_flags(fd)
    Errno.handle if cur_mode < 0

    cur_mode &= ACCMODE

    if mode
      mode = parse_mode(mode)
      mode &= ACCMODE

      if (cur_mode == RDONLY or cur_mode == WRONLY) and mode != cur_mode
        raise Errno::EINVAL, "Invalid new mode for existing descriptor #{fd}"
      end
    end

    # Check the given +io+ for a valid fd instance first. If it has one, cancel
    # the existing finalizer since we are about to allocate a new fd instance.
    if fd_obj = io.instance_variable_get(:@fd)
      fd_obj.cancel_finalizer
    end

    fd_obj = FileDescriptor.choose_type(fd, io)
    io.instance_variable_set(:@fd, fd_obj)
    raise "FD could not be allocated for fd [#{fd}]" unless fd_obj
    raise "No descriptor set for fd [#{fd}]" unless fd_obj.descriptor

    io.mode       = mode || cur_mode
    io.sync       = !!sync
    io.lineno     = 0 if io.lineno.nil?

    # FIXME - re-enable this somehow. Right now this breaks kernel/delta/io.rb when it
    # redefines STDIN/STDOUT/STDERR from the IO.open call. The new IO code has already
    # loaded so we can no longer access the object that STDIN/STDOUT/STDERR points to
    # via Ruby code, so the following code blows up.
    #    if STDOUT.respond_to?(:fileno) and not STDOUT.closed?
    #      io.sync ||= STDOUT.fileno == fd
    #    end
    #
    #    if STDERR.respond_to?(:fileno) and not STDERR.closed?
    #      io.sync ||= STDERR.fileno == fd
    #    end
  end

  #
  # Create a new IO associated with the given fd.
  #
  def initialize(fd, mode=undefined, options=undefined)
    if block_given?
      warn 'IO::new() does not take block; use IO::open() instead'
    end

    mode, binary, external, internal, @autoclose, @encoding_options =
      IO.normalize_options(mode, options)

    fd = Rubinius::Type.coerce_to fd, Integer, :to_int
    autoclose = @autoclose
    IO.setup self, fd, mode

    binmode if binary
    set_encoding external, internal

    if @external && !external
      @external = nil
    end

    if @internal
      if Encoding.default_external == Encoding.default_internal or
        (@external || Encoding.default_external) == Encoding::ASCII_8BIT
        @internal = nil
      end
    elsif !mode_read_only?
      if Encoding.default_external != Encoding.default_internal
        @internal = Encoding.default_internal
      end
    end

    unless @external
      if @binmode
        @external = Encoding::ASCII_8BIT
      elsif @internal or Encoding.default_internal
        @external = Encoding.default_external
      end
    end

    @pipe = false # FIXME
  end

  private :initialize

  ##
  # Obtains a new duplicate descriptor for the current one.
  def initialize_copy(original_io) # :nodoc:
    # Make a complete copy of the +original_io+ object including
    # the mode, binmode, path, position, lineno, and a new FD.
    dest_io = self

    fd = FFI::Platform::POSIX.dup(original_io.descriptor)

    # The system makes a shallow copy of all ivars, so this copy has
    # the same @fd as the original. That shallow copy is really only
    # relevant for primitive values (Fixnum, String, etc) and not
    # our own objects. Instantiate a new @fd.
    @fd = FileDescriptor.choose_type(fd, dest_io)
    dest_io.mode = original_io.mode
    dest_io.sync = original_io.sync
    dest_io.binmode if original_io.binmode?
    dest_io.autoclose = original_io.autoclose

    dest_io
  end
  private :initialize_copy

  def new_pipe(fd, external, internal, options, mode, do_encoding=false)
    @fd = FIFOFileDescriptor.new(fd, nil, self, mode)
    @lineno = 0
    @pipe = true

    # Why do we only set encoding for the "left hand side" pipe? Why not both?
    if do_encoding
      set_encoding((external || Encoding.default_external), (internal || Encoding.default_internal), options)
    end
  end
  private :new_pipe

  def super_inspect
    "<IO:#{object_id}> \n#{@fd.inspect}"
  end

  #  alias_method :prim_write, :write
  #  alias_method :prim_close, :close
  #  alias_method :prim_read, :read

  def descriptor
    @fd.descriptor if @fd
  end

  def descriptor=(value)
    @fd.descriptor = value if @fd
  end

  def mode
    @fd.mode if @fd
  end

  def mode=(value)
    @fd.mode = value if @fd
  end

  def sync
    @fd.sync if @fd
  end

  def sync=(value)
    @fd.sync = value if @fd
  end

  def advise(advice, offset = 0, len = 0)
    raise IOError, "stream is closed" if closed?
    raise TypeError, "advice must be a Symbol" unless advice.kind_of?(Symbol)

    if offset.kind_of?(Bignum) || len.kind_of?(Bignum)
      raise RangeError, "bignum too big to convert into `long'"
    end

    unless [:normal, :sequential, :random, :noreuse, :dontneed, :willneed].include? advice
      raise NotImplementedError, advice.inspect
    end

    advice = case advice
    when :normal; POSIX_FADV_NORMAL
    when :sequential; POSIX_FADV_SEQUENTIAL
    when :random; POSIX_FADV_RANDOM
    when :willneed; POSIX_FADV_WILLNEED
    when :dontneed; POSIX_FADV_DONTNEED
    when :noreuse; POSIX_FADV_NOREUSE
    end

    offset = Rubinius::Type.coerce_to offset, Integer, :to_int
    len = Rubinius::Type.coerce_to len, Integer, :to_int

    begin
      if FFI.call_failed?(FFI::Platform::POSIX.posix_fadvise(descriptor, offset, len, advice))
        Errno.handle("posix_fadvise(2) failed")
      end
    rescue NotImplementedError
      # MRI thinks platforms that don't support #advise should silently fail.
      # See https://bugs.ruby-lang.org/issues/11806
      nil
    end

    nil
  end

  def autoclose
    @autoclose
  end

  def autoclose?
    @autoclose
  end

  def autoclose=(bool)
    @fd.autoclose = bool
    @autoclose = !!bool
  end

  def binmode
    ensure_open

    @binmode = true
    @external = Encoding::BINARY
    @internal = nil

    # HACK what to do?
    self
  end

  def binmode?
    !@binmode.nil?
  end

  def close_on_exec=(value)
    if value
      fcntl(F_SETFD, fcntl(F_GETFD) | FD_CLOEXEC)
    else
      fcntl(F_SETFD, fcntl(F_GETFD) & ~FD_CLOEXEC)
    end
    nil
  end

  def close_on_exec?
    (fcntl(F_GETFD) & FD_CLOEXEC) != 0
  end

  def <<(obj)
    write(obj.to_s)
    return self
  end

  ##
  # Closes the read end of a duplex I/O stream (i.e., one
  # that contains both a read and a write stream, such as
  # a pipe). Will raise an IOError if the stream is not duplexed.
  #
  #  f = IO.popen("/bin/sh","r+")
  #  f.close_read
  #  f.readlines
  # produces:
  #
  #  prog.rb:3:in `readlines': not opened for reading (IOError)
  #   from prog.rb:3
  def close_read
    return if invalid_descriptor?

    if mode_write_only? || mode_read_write?
      raise IOError, 'closing non-duplex IO for reading'
    end

    @fd.close unless closed?
  end

  ##
  # Closes the write end of a duplex I/O stream (i.e., one
  # that contains both a read and a write stream, such as
  # a pipe). Will raise an IOError if the stream is not duplexed.
  #
  #  f = IO.popen("/bin/sh","r+")
  #  f.close_write
  #  f.print "nowhere"
  # produces:
  #
  #  prog.rb:3:in `write': not opened for writing (IOError)
  #   from prog.rb:3:in `print'
  #   from prog.rb:3
  def close_write
    return if invalid_descriptor?

    if mode_read_only? || mode_read_write?
      raise IOError, 'closing non-duplex IO for writing'
    end

    @fd.close unless closed?
  end

  ##
  # Returns true if ios is completely closed (for duplex
  # streams, both reader and writer), false otherwise.
  #
  #  f = File.new("testfile")
  #  f.close         #=> nil
  #  f.closed?       #=> true
  #  f = IO.popen("/bin/sh","r+")
  #  f.close_write   #=> nil
  #  f.closed?       #=> false
  #  f.close_read    #=> nil
  #  f.closed?       #=> true
  def closed?
    invalid_descriptor?
  end

  def dup
    ensure_open
    super # calls #initialize_copy
  end

  # Argument matrix for IO#gets and IO#each:
  #
  #  separator / limit | nil | >= 0 | < 0
  # ===================+=====+======+=====
  #  String (nonempty) |  A  |  B   |  C
  #                    +-----+------+-----
  #  ""                |  D  |  E   |  F
  #                    +-----+------+-----
  #  nil               |  G  |  H   |  I
  #

  class EachReader
    READ_SIZE = 512 # bytes

    def initialize(io, separator, limit, encoding_options)
      @io = io
      @separator = separator ? separator.force_encoding("ASCII-8BIT") : separator
      @limit = limit
      @encoding_options = encoding_options
      @skip = nil
    end

    private :initialize

    def each(&block)
      if @separator
        if @separator.empty?
          @separator = "\n\n"
          @skip = "\n"
        end

        if @limit
          read_to_separator_with_limit(&block)
        else
          read_to_separator(&block)
        end
      else
        if @limit
          read_to_limit(&block)
        else
          read_all(&block)
        end
      end
    end

    def read_and_yield_count_chars(str, buffer, byte_count, &block)
      str << buffer.slice!(0, byte_count)

      if @limit
        # Always read to char boundary because the +limit+ may have cut a multi-byte
        # character in the middle. Returning such a string would have an invalid encoding.
        if buffer.size < PEEK_AHEAD_LIMIT
          # Use nonblocking read since existing +buffer+ might already have a valid encoding
          # and we don't want to block forever if no more data is coming
          result = @io.get_empty_8bit_buffer

          # save and restore encodings around the read operation so that we ensure ASCII_8BIT
          internal, external = @io.internal, @io.external
          @io.external = Encoding::ASCII_8BIT
          (@io.read_nonblock(PEEK_AHEAD_LIMIT, result, exception: false) || @io.get_empty_8bit_buffer)
          @io.internal, @io.external = internal, external

          buffer << result
        end
        str, bytes_read = read_to_char_boundary(@io, str, buffer)
      else
        # We are confident that our +str+ ends on a char boundary
        str = IO.read_encode(@io, str, @encoding_options)
      end

      str.taint
      $. = @io.increment_lineno
      skip_contiguous_chars(buffer)

      # Unused bytes/chars should be saved for the next read. Since the block that we yield to
      # may +return+ we don't want to drop the bytes that are stored in +buffer+. To save,
      # unget them so the next read will fetch them again. This might be expensive and could
      # potentially use a little tuning. Maybe use an +unread(bytes)+ method which just moves
      # a pointer around. Think about this for the mmap stuff.
      @io.ungetc(buffer)
      buffer.clear

      yield str
    end

    def read_and_yield_entire_string(str, &block)
      str = IO.read_encode(@io, str, @encoding_options)
      str.taint
      $. = @io.increment_lineno
      yield str
    end

    # method A, D
    def read_to_separator(&block)
      str = @io.get_empty_8bit_buffer
      buffer = @io.get_empty_8bit_buffer
      separator_size = @separator.bytesize

      begin
        if buffer.size == 0
          buffer = @io.read(READ_SIZE)
        end

        break unless buffer.size > 0

        if count = buffer.index(@separator)
          # #index returns a 0-based location but we want a length (so +1) and it should include
          # the pattern/separator which may be >1. therefore, add the separator size.
          count += separator_size

          read_and_yield_count_chars(str, buffer, count, &block)
          str = @io.get_empty_8bit_buffer
        else
          str << buffer
          buffer.clear
        end
      end until buffer.size == 0 && @io.eof?

      str << buffer

      unless str.empty?
        read_and_yield_entire_string(str, &block)
      end
    end

    # method B, E

    def read_to_separator_with_limit(&block)
      str = @io.get_empty_8bit_buffer
      buffer = @io.get_empty_8bit_buffer
      separator_size = @separator.bytesize

      #TODO: implement ignoring encoding with negative limit
      wanted = limit = @limit.abs

      begin
        if buffer.size == 0
          buffer = @io.read(READ_SIZE)
        end

        break unless buffer && buffer.size > 0

        if count = buffer.index(@separator)
          # #index returns a 0-based location but we want a length (so +1) and it should include
          # the pattern/separator which may be >1. therefore, add the separator size.
          count += separator_size
          count = count < wanted ? count : wanted
          read_and_yield_count_chars(str, buffer, count, &block)

          str = @io.get_empty_8bit_buffer
        else
          if wanted < buffer.size
            read_and_yield_count_chars(str, buffer, wanted, &block)
            str = @io.get_empty_8bit_buffer
          else
            str << buffer
            wanted -= buffer.size
            buffer.clear
          end
        end
      end until buffer.size == 0 && @io.eof?

      unless str.empty?
        read_and_yield_entire_string(str, &block)
      end
    end

    # Method G
    def read_all(&block)
      str = @io.get_empty_8bit_buffer

      begin
        str << @io.read
      end until @io.eof?

      unless str.empty?
        read_and_yield_entire_string(str, &block)
      end
    end

    # Method H
    # Only ever called if separator or $/ is forced to nil
    def read_to_limit(&block)
      str = @io.get_empty_8bit_buffer
      wanted = limit = @limit.abs

      begin
        str << @io.read(wanted)
        read_and_yield_count_chars(str, @io.get_empty_8bit_buffer, str.bytesize, &block)
        str = @io.get_empty_8bit_buffer
      end until @io.eof?

      unless str.empty?
        read_and_yield_entire_string(str, &block)
      end
    end

    # Utility methods

    def try_to_force_encoding(io, str)
      str.force_encoding(io.external_encoding || Encoding.default_external)

      IO.read_encode(io, str, @encoding_options)
    end

    PEEK_AHEAD_LIMIT = 16

    def read_to_char_boundary(io, str, buffer)
      str.force_encoding(io.external_encoding || Encoding.default_external)
      return [IO.read_encode(io, str, @encoding_options), 0] if str.valid_encoding?

      peek_ahead = 0
      while buffer.size > 0 and peek_ahead < PEEK_AHEAD_LIMIT
        str.force_encoding Encoding::ASCII_8BIT
        substring = buffer.slice!(0, 1)
        str << substring
        peek_ahead += 1

        str.force_encoding(io.external_encoding || Encoding.default_external)
        if str.valid_encoding?
          return [IO.read_encode(io, str, @encoding_options), peek_ahead]
        end
      end

      [IO.read_encode(io, str, @encoding_options), peek_ahead]
    end

    # Advances the buffer index past any number of contiguous
    # characters == +skip+ and throws away that data. For
    # example, if +skip+ is ?\n and the buffer contents are
    # "\n\n\nAbc...", the buffer will discard all chars
    # up to 'A'.
    def skip_contiguous_chars(buffer)
      return 0 unless @skip

      skip_count = 0
      skip_count += 1 while buffer[skip_count] == @skip
      if skip_count > 0
        slice = buffer.slice!(0, skip_count)
        slice.bytesize
      else
        0
      end
    end
  end

  def increment_lineno
    @lineno += 1
  end

  ##
  # Return a string describing this IO object.
  def inspect
    if @fd
      if @fd.descriptor != -1
        "#<#{self.class}:fd #{@fd.descriptor}>"
      else
        "#<#{self.class}:(closed)>"
      end
    else
      "#<#{self.class}:fd nil>"
    end
  end

  def lines(*args, &block)
    if block_given?
      each_line(*args, &block)
    else
      to_enum :each_line, *args
    end
  end

  def each(sep_or_limit=$/, limit=nil, &block)
    return to_enum(:each, sep_or_limit, limit) unless block_given?

    ensure_open_and_readable

    if limit
      limit = Rubinius::Type.coerce_to limit, Integer, :to_int
      sep = sep_or_limit ? StringValue(sep_or_limit) : nil
    else
      case sep_or_limit
      when String
        sep = sep_or_limit
      when nil
        sep = nil
      else
        unless sep = Rubinius::Type.check_convert_type(sep_or_limit, String, :to_str)
          sep = $/
          limit = Rubinius::Type.coerce_to sep_or_limit, Integer, :to_int
        end
      end
    end

    raise ArgumentError, "limit of 0 is invalid" if limit && limit.zero?
    return if eof?

    EachReader.new(self, sep, limit, @encoding_options).each(&block)

    self
  end

  def each_line(sep_or_limit=$/, limit=nil, &block)
    if limit
      limit = Rubinius::Type.coerce_to limit, Integer, :to_int
      sep = sep_or_limit ? StringValue(sep_or_limit) : nil
      raise ArgumentError, "invalid limit: 0 for each_line" if limit.zero?
    else
      case sep_or_limit
      when String
        sep = sep_or_limit
      when nil
        sep = nil
      else
        unless sep = Rubinius::Type.check_convert_type(sep_or_limit, String, :to_str)
          sep = $/
          limit = Rubinius::Type.coerce_to sep_or_limit, Integer, :to_int
          raise ArgumentError, "invalid limit: 0 for each_line" if limit.zero?
          sep_or_limit = sep
        end
      end
    end

    each(sep_or_limit, limit, &block)
  end

  def each_byte
    return to_enum(:each_byte) unless block_given?

    yield getbyte until eof?

    self
  end

  alias_method :bytes, :each_byte

  def each_char
    return to_enum :each_char unless block_given?
    ensure_open_and_readable

    while char = getc
      yield char
    end

    self
  end

  alias_method :chars, :each_char

  def each_codepoint
    return to_enum :each_codepoint unless block_given?
    ensure_open_and_readable

    while char = getc
      yield char.ord
    end

    self
  end

  alias_method :codepoints, :each_codepoint


  ##
  # Set the pipe so it is at the end of the file
  def eof!
    @eof = true
  end

  ##
  # Returns true if ios is at end of file that means
  # there are no more data to read. The stream must be
  # opened for reading or an IOError will be raised.
  #
  #  f = File.new("testfile")
  #  dummy = f.readlines
  #  f.eof   #=> true
  # If ios is a stream such as pipe or socket, IO#eof?
  # blocks until the other end sends some data or closes it.
  #
  #  r, w = IO.pipe
  #  Thread.new { sleep 1; w.close }
  #  r.eof?  #=> true after 1 second blocking
  #
  #  r, w = IO.pipe
  #  Thread.new { sleep 1; w.puts "a" }
  #  r.eof?  #=> false after 1 second blocking
  #
  #  r, w = IO.pipe
  #  r.eof?  # blocks forever
  #
  # Note that IO#eof? reads data to a input buffer.
  # So IO#sysread doesn't work with IO#eof?.
  def eof?
    ensure_open_and_readable
    @fd.eof?
  end

  alias_method :eof, :eof?

  def ensure_open_and_readable
    ensure_open
    raise IOError, "not opened for reading" if mode_write_only?
  end

  def ensure_open_and_writable
    ensure_open
    raise IOError, "not opened for writing" if mode_read_only?
  end

  def external_encoding
    return @external if @external
    return Encoding.default_external if mode_read_only?
  end

  ##
  # Provides a mechanism for issuing low-level commands to
  # control or query file-oriented I/O streams. Arguments
  # and results are platform dependent. If arg is a number,
  # its value is passed directly. If it is a string, it is
  # interpreted as a binary sequence of bytes (Array#pack
  # might be a useful way to build this string). On Unix
  # platforms, see fcntl(2) for details. Not implemented on all platforms.
  def fcntl(command, arg=0)
    ensure_open

    if !arg
      arg = 0
    elsif arg == true
      arg = 1
    elsif arg.kind_of? String
      raise NotImplementedError, "cannot handle String"
    else
      arg = Rubinius::Type.coerce_to arg, Fixnum, :to_int
    end

    command = Rubinius::Type.coerce_to command, Fixnum, :to_int
    FFI::Platform::POSIX.fcntl descriptor, command, arg
  end

  def internal_encoding
    @internal
  end

  ##
  # Provides a mechanism for issuing low-level commands to
  # control or query file-oriented I/O streams. Arguments
  # and results are platform dependent. If arg is a number,
  # its value is passed directly. If it is a string, it is
  # interpreted as a binary sequence of bytes (Array#pack
  # might be a useful way to build this string). On Unix
  # platforms, see fcntl(2) for details. Not implemented on all platforms.
  def ioctl(command, arg=0)
    ensure_open

    if !arg
      real_arg = 0
    elsif arg == true
      real_arg = 1
    elsif arg.kind_of? String
      # This could be faster.
      buffer_size = arg.bytesize
      # On BSD and Linux, we could read the buffer size out of the ioctl value.
      # Most Linux ioctl codes predate the convention, so a fallback like this
      # is still necessary.
      buffer_size = 4096 if buffer_size < 4096
      buffer = FFI::MemoryPointer.new buffer_size
      buffer.write_string arg, arg.bytesize
      real_arg = buffer.address
    else
      real_arg = Rubinius::Type.coerce_to arg, Fixnum, :to_int
    end

    command = Rubinius::Type.coerce_to command, Fixnum, :to_int
    ret = FFI::Platform::POSIX.ioctl descriptor, command, real_arg
    Errno.handle if ret < 0
    if arg.kind_of?(String)
      arg.replace buffer.read_string(buffer_size)
      buffer.free
    end
    ret
  end

  ##
  # Returns an integer representing the numeric file descriptor for ios.
  #
  #  $stdin.fileno    #=> 0
  #  $stdout.fileno   #=> 1
  def fileno
    ensure_open
    return @fd.descriptor
  end

  alias_method :to_i, :fileno

  ##
  # Flushes any buffered data within ios to the underlying
  # operating system (note that this is Ruby internal
  # buffering only; the OS may buffer the data as well).
  #
  #  $stdout.print "no newline"
  #  $stdout.flush
  # produces:
  #
  #  no newline
  def flush
    ensure_open
    #@fd.reset_positioning
    return self
  end

  def force_read_only
    @fd.force_read_only
  end

  def force_write_only
    @fd.force_write_only
  end

  ##
  # Immediately writes all buffered data in ios to disk. Returns
  # nil if the underlying operating system does not support fsync(2).
  # Note that fsync differs from using IO#sync=. The latter ensures
  # that data is flushed from Ruby's buffers, but does not guarantee
  # that the underlying operating system actually writes it to disk.
  def fsync
    flush

    err = FFI::Platform::POSIX.fsync descriptor

    Errno.handle 'fsync(2)' if err < 0

    return err
  end

  def getbyte
    ensure_open
    byte = read(1)
    return(byte ? byte.ord : nil)
  end

  ##
  # Gets the next 8-bit byte (0..255) from ios.
  # Returns nil if called at end of file.
  #
  #  f = File.new("testfile")
  #  f.getc   #=> 84
  #  f.getc   #=> 104
  def getc
    ensure_open
    return if eof?

    char = get_empty_8bit_buffer
    begin
      char.force_encoding Encoding::ASCII_8BIT
      char << read(1)

      char.force_encoding(self.external_encoding || Encoding.default_external)
      if char.chr_at(0)
        return IO.read_encode(self, char, @encoding_options)
      end
    end until eof?

    return nil
  end

  def gets(sep_or_limit=$/, limit=nil)
    each sep_or_limit, limit do |line|
      $_ = line if line
      return line
    end

    return nil
  end

  ##
  # Returns the current line number in ios. The
  # stream must be opened for reading. lineno
  # counts the number of times gets is called,
  # rather than the number of newlines encountered.
  # The two values will differ if gets is called with
  # a separator other than newline. See also the $. variable.
  #
  #  f = File.new("testfile")
  #  f.lineno   #=> 0
  #  f.gets     #=> "This is line one\n"
  #  f.lineno   #=> 1
  #  f.gets     #=> "This is line two\n"
  #  f.lineno   #=> 2
  def lineno
    ensure_open

    return @lineno
  end

  ##
  # Manually sets the current line number to the
  # given value. $. is updated only on the next read.
  #
  #  f = File.new("testfile")
  #  f.gets                     #=> "This is line one\n"
  #  $.                         #=> 1
  #  f.lineno = 1000
  #  f.lineno                   #=> 1000
  #  $. # lineno of last read   #=> 1
  #  f.gets                     #=> "This is line two\n"
  #  $. # lineno of last read   #=> 1001
  def lineno=(line_number)
    ensure_open

    raise TypeError if line_number.nil?

    @lineno = Integer(line_number)
  end

  def mode_read_only?
    @fd.read_only?
  end
  private :mode_read_only?

  def mode_read_write?
   @fd.read_write?
  end
  private :mode_read_write?

  def mode_write_only?
    @fd.write_only?
  end
  private :mode_write_only?

  ##
  # FIXME
  # Returns the process ID of a child process
  # associated with ios. This will be set by IO::popen.
  #
  #  pipe = IO.popen("-")
  #  if pipe
  #    $stderr.puts "In parent, child pid is #{pipe.pid}"
  #  else
  #    $stderr.puts "In child, pid is #{$$}"
  #  end
  # produces:
  #
  #  In child, pid is 26209
  #  In parent, child pid is 26209
  def pid
    raise IOError, 'closed stream' if closed?
    @pid
  end

  attr_writer :pid

  def pipe=(v)
    @pipe = !!v
  end

  def pipe?
    @pipe
  end

  ##
  #
  def pos
    ensure_open
    @fd.offset
  end

  alias_method :tell, :pos

  ##
  # Seeks to the given position (in bytes) in ios.
  #
  #  f = File.new("testfile")
  #  f.pos = 17
  #  f.gets   #=> "This is line two\n"
  def pos=(offset)
    seek offset, SEEK_SET
  end

  ##
  # Writes each given argument.to_s to the stream or $_ (the result of last
  # IO#gets) if called without arguments. Appends $\.to_s to output. Returns
  # nil.
  def print(*args)
    if args.empty?
      write $_.to_s
    else
      args.each { |o| write o.to_s }
    end

    write $\.to_s
    nil
  end

  ##
  # Formats and writes to ios, converting parameters under
  # control of the format string. See Kernel#sprintf for details.
  def printf(fmt, *args)
    fmt = StringValue(fmt)
    write ::Rubinius::Sprinter.get(fmt).call(*args)
  end

  ##
  # If obj is Numeric, write the character whose code is obj,
  # otherwise write the first character of the string
  # representation of obj to ios.
  #
  #  $stdout.putc "A"
  #  $stdout.putc 65
  # produces:
  #
  #  AA
  def putc(obj)
    if Rubinius::Type.object_kind_of? obj, String
      write obj.substring(0, 1)
    else
      byte = Rubinius::Type.coerce_to(obj, Integer, :to_int) & 0xff
      write byte.chr
    end

    return obj
  end

  ##
  # Writes the given objects to ios as with IO#print.
  # Writes a record separator (typically a newline)
  # after any that do not already end with a newline
  # sequence. If called with an array argument, writes
  # each element on a new line. If called without arguments,
  # outputs a single record separator.
  #
  #  $stdout.puts("this", "is", "a", "test")
  # produces:
  #
  #  this
  #  is
  #  a
  #  test
  def puts(*args)
    if args.empty?
      write DEFAULT_RECORD_SEPARATOR
    else
      args.each do |arg|
        if arg.equal? nil
          str = ""
        elsif arg.kind_of? String
          str = arg
        elsif Thread.guarding? arg
          str = "[...]"
        else
          Thread.recursion_guard arg do
            begin
              arg.to_ary.each { |a| puts a }
            rescue NoMethodError
              unless (str = arg.to_s).kind_of? String
                str = "#<#{arg.class}:0x#{arg.object_id.to_s(16)}>"
              end
            end
          end
        end

        if str
          write str
          write DEFAULT_RECORD_SEPARATOR unless str.suffix?(DEFAULT_RECORD_SEPARATOR)
        end
      end
    end

    nil
  end

  def read(length=nil, buffer=nil)
    ensure_open_and_readable
    buffer = StringValue(buffer) if buffer

    unless length
      str = IO.read_encode(self, read_all, @encoding_options)
      return str unless buffer

      return buffer.replace(str)
    end

    str = ""
    emulate_blocking_read do
      read_nonblock(length, str)
    end

    if str.empty? && length > 0
      str = nil
    end

    if str
      if buffer
        buffer.replace str.force_encoding(buffer.encoding)
      else
        str.force_encoding Encoding::ASCII_8BIT
      end
    else
      buffer.clear if buffer
      nil
    end
  end

  ##
  # Reads all input until +#eof?+ is true. Returns the input read.
  # If the buffer is already exhausted, returns +""+.
  def read_all
    str = ""
    begin
      buffer = ""
      emulate_blocking_read do
        read_nonblock(FileDescriptor.pagesize, buffer)
      end
      str << buffer
    end until eof?

    str
  end

  private :read_all

  def read_if_available(bytes)
    return "" if bytes.zero?
    buffer, bytes = @fd.read_only_buffer(bytes)

    events = IO::Select.readable_events(descriptor)
    if events == 0 && !buffer
      Errno.raise_waitreadable("no data ready")
      return ""
    elsif events < 0 && !buffer
      Errno.handle("read(2) failed")
      return ""
    elsif events == 0 && buffer
      # we were able to read from the buffer but no more data is waiting
      return buffer
    end

    # if we get here then we have data to read from the descriptor
    str = ""
    bytes_read = @fd.read(bytes, str)

    if bytes_read.nil?
      # there's a chance the read could fail even when we have data read from the buffer
      # to return to caller
      return (nil || buffer)
    else
      # combine what we read from the buffer with what we read from the descriptor
      buffer = buffer.to_s + str
      return buffer
    end
  end
  # defined in bootstrap, used here.
  private :read_if_available

  ##
  # Reads at most maxlen bytes from ios using read(2) system
  # call after O_NONBLOCK is set for the underlying file descriptor.
  #
  # If the optional outbuf argument is present, it must reference
  # a String, which will receive the data.
  #
  # read_nonblock just calls read(2). It causes all errors read(2)
  # causes: EAGAIN, EINTR, etc. The caller should care such errors.
  #
  # read_nonblock causes EOFError on EOF.
  #
  # If the read buffer is not empty, read_nonblock reads from the
  # buffer like readpartial. In this case, read(2) is not called.
  def read_nonblock(size, buffer=nil, exception: true)
    raise ArgumentError, "illegal read size" if size < 0
    ensure_open

    buffer = StringValue buffer if buffer

    unless @fd.blocking?
      @fd.set_nonblock
      nonblock_reset = true
    end

    begin
      str = read_if_available(size)
    rescue EAGAINWaitReadable => exc
      raise exc if exception

      return :wait_readable
    end

    if str
      if buffer
        buffer.replace(str)
        IO.read_encode(self, buffer, @encoding_options)
      else
        IO.read_encode(self, str, @encoding_options)
      end
    else
      raise EOFError, "stream closed" if exception
    end
  ensure
    @fd.clear_nonblock if nonblock_reset
  end

  ##
  # Reads a character as with IO#getc, but raises an EOFError on end of file.
  def readchar
    char = getc
    raise EOFError, 'end of file reached' unless char
    char
  end

  def readbyte
    byte = getbyte
    raise EOFError, "end of file reached" unless byte
    #raise EOFError, "end of file" unless bytes # bytes/each_byte is deprecated, FIXME - is this line necessary?
    byte
  end

  ##
  # Reads a line as with IO#gets, but raises an EOFError on end of file.
  def readline(sep=$/)
    out = gets(sep)
    raise EOFError, "end of file" unless out
    return out
  end

  ##
  # Reads all of the lines in ios, and returns them in an array.
  # Lines are separated by the optional sep_string. If sep_string
  # is nil, the rest of the stream is returned as a single record.
  # The stream must be opened for reading or an IOError will be raised.
  #
  #  f = File.new("testfile")
  #  f.readlines[0]   #=> "This is line one\n"
  def readlines(sep=$/)
    sep = StringValue sep if sep

    old_line = $_
    ary = Array.new
    while line = gets(sep)
      ary << line
    end
    $_ = old_line

    ary
  end

  ##
  # Reads at most maxlen bytes from the I/O stream. It blocks
  # only if ios has no data immediately available. It doesn‘t
  # block if some data available. If the optional outbuf argument
  # is present, it must reference a String, which will receive the
  # data. It raises EOFError on end of file.
  #
  # readpartial is designed for streams such as pipe, socket, tty,
  # etc. It blocks only when no data immediately available. This
  # means that it blocks only when following all conditions hold.
  #
  # the buffer in the IO object is empty.
  # the content of the stream is empty.
  # the stream is not reached to EOF.
  # When readpartial blocks, it waits data or EOF on the stream.
  # If some data is reached, readpartial returns with the data.
  # If EOF is reached, readpartial raises EOFError.
  #
  # When readpartial doesn‘t blocks, it returns or raises immediately.
  # If the buffer is not empty, it returns the data in the buffer.
  # Otherwise if the stream has some content, it returns the data in
  # the stream. Otherwise if the stream is reached to EOF, it raises EOFError.
  #
  #  r, w = IO.pipe           #               buffer          pipe content
  #  w << "abc"               #               ""              "abc".
  #  r.readpartial(4096)      #=> "abc"       ""              ""
  #  r.readpartial(4096)      # blocks because buffer and pipe is empty.
  #
  #  r, w = IO.pipe           #               buffer          pipe content
  #  w << "abc"               #               ""              "abc"
  #  w.close                  #               ""              "abc" EOF
  #  r.readpartial(4096)      #=> "abc"       ""              EOF
  #  r.readpartial(4096)      # raises EOFError
  #
  #  r, w = IO.pipe           #               buffer          pipe content
  #  w << "abc\ndef\n"        #               ""              "abc\ndef\n"
  #  r.gets                   #=> "abc\n"     "def\n"         ""
  #  w << "ghi\n"             #               "def\n"         "ghi\n"
  #  r.readpartial(4096)      #=> "def\n"     ""              "ghi\n"
  #  r.readpartial(4096)      #=> "ghi\n"     ""              ""
  # Note that readpartial behaves similar to sysread. The differences are:
  #
  # If the buffer is not empty, read from the buffer instead
  # of "sysread for buffered IO (IOError)".
  # It doesn‘t cause Errno::EAGAIN and Errno::EINTR. When readpartial
  # meets EAGAIN and EINTR by read system call, readpartial retry the system call.
  # The later means that readpartial is nonblocking-flag insensitive. It
  # blocks on the situation IO#sysread causes Errno::EAGAIN as if the fd is blocking mode.
  def readpartial(size, buffer=nil)
    raise ArgumentError, 'negative string size' unless size >= 0
    ensure_open

    if buffer
      buffer = StringValue(buffer)

      buffer.shorten! buffer.bytesize

      return buffer if size == 0

      data = nil
      begin
        data = read_nonblock(size)
      rescue IO::WaitReadable
        IO.select([self])
        retry
      rescue IO::WaitWritable
        IO.select(nil, [self])
        retry
      end

      buffer.replace(data)

      return buffer
    else
      return "" if size == 0

      data = nil
      begin
        data = read_nonblock(size)
      rescue IO::WaitReadable
        IO.select([self])
        retry
      rescue IO::WaitWritable
        IO.select(nil, [self])
        retry
      end

      return data
    end
  end

  ##
  # Reassociates ios with the I/O stream given in other_IO or to
  # a new stream opened on path. This may dynamically change the
  # actual class of this stream.
  #
  #  f1 = File.new("testfile")
  #  f2 = File.new("testfile")
  #  f2.readlines[0]   #=> "This is line one\n"
  #  f2.reopen(f1)     #=> #<File:testfile>
  #  f2.readlines[0]   #=> "This is line one\n"
  def reopen(other, mode=undefined)
    if other.respond_to?(:to_io)
      flush

      if other.kind_of? IO
        io = other
      else
        io = other.to_io
        unless io.kind_of? IO
          raise TypeError, "#to_io must return an instance of IO"
        end
      end

      # Note: this is the whole reason that FileDescriptor#ensure_open takes an argument.
      #
      ensure_open(io.descriptor)
      #      io.reset_buffering

      @fd.reopen(io.descriptor)

      # When reopening we may be going from a Pipe to a File or vice versa. Let the
      # system figure out the proper FD class.
      @fd.cancel_finalizer # cancel soon-to-be-overwritten instance's finalizer
      @fd = FileDescriptor.choose_type(descriptor, self)
      Rubinius::Unsafe.set_class self, io.class
      if io.respond_to?(:path)
        @path = io.path
      end

      seek(other.pos, SEEK_SET) rescue Errno::ESPIPE
    else
      flush unless closed?

      # If a mode isn't passed in, use the mode that the IO is already in.
      if undefined.equal? mode
        mode = @fd.mode
        # If this IO was already opened for writing, we should
        # create the target file if it doesn't already exist.
        if mode_read_write? || mode_write_only?
          mode |= CREAT
        end
      else
        mode = IO.parse_mode(mode)
      end

      reopen_path Rubinius::Type.coerce_to_path(other), mode

      unless closed?
        seek(0, SEEK_SET) rescue Errno::ESPIPE
      end
    end

    self
  end

  def reopen_path(path, mode)
    status =  @fd.reopen_path(path, mode)
    @fd.cancel_finalizer
    @fd = FileDescriptor.choose_type(descriptor, self)
    return status
  end

  ##
  # Internal method used to reset the state of the buffer, including the
  # physical position in the stream.
  def reset_buffering
    #    ##@ibuffer.unseek! self
  end

  ##
  # Positions ios to the beginning of input, resetting lineno to zero.
  #
  #  f = File.new("testfile")
  #  f.readline   #=> "This is line one\n"
  #  f.rewind     #=> 0
  #  f.lineno     #=> 0
  #  f.readline   #=> "This is line one\n"
  def rewind
    seek 0
    @lineno = 0
    return 0
  end

  ##
  # Seeks to a given offset +amount+ in the stream according to the value of whence:
  #
  # IO::SEEK_CUR  | Seeks to _amount_ plus current position
  # --------------+----------------------------------------------------
  # IO::SEEK_END  | Seeks to _amount_ plus end of stream (you probably
  #               | want a negative value for _amount_)
  # --------------+----------------------------------------------------
  # IO::SEEK_SET  | Seeks to the absolute location given by _amount_
  # Example:
  #
  #  f = File.new("testfile")
  #  f.seek(-13, IO::SEEK_END)   #=> 0
  #  f.readline                  #=> "And so on...\n"
  def seek(amount, whence=SEEK_SET)
    flush

    @eof = false

    @fd.seek Integer(amount), whence

    return 0
  end

  def set_encoding(external, internal=nil, options=undefined)
    case external
    when Encoding
      @external = external
    when String
      @external = nil
    when nil
      if mode_read_only? || @external
        @external = nil
      else
        @external = Encoding.default_external
      end
    else
      @external = nil
      external = StringValue(external)
    end

    if @external.nil? and not external.nil?
      if index = external.index(":")
        internal = external[index+1..-1]
        external = external[0, index]
      end

      if external[3] == ?|
        if encoding = strip_bom
          external = encoding
        else
          external = external[4..-1]
        end
      end

      @external = Encoding.find external
    end

    unless undefined.equal? options
      # TODO: set the encoding options on the IO instance
      if options and not options.kind_of? Hash
        @encoding_options = Rubinius::Type.coerce_to options, Hash, :to_hash
      end
    end

    case internal
    when Encoding
      @internal = nil if @external == internal
    when String
      # do nothing
    when nil
      internal = Encoding.default_internal
    else
      internal = StringValue(internal)
    end

    if internal.kind_of? String
      return self if internal == "-"
      internal = Encoding.find internal
    end

    @internal = internal unless internal && @external == internal

    self
  end

  def read_bom_byte
    read_ios, _, _ = IO.select [self], nil, nil, 0.1
    return getbyte if read_ios
  end

  def strip_bom
    return unless File::Stat.fstat(descriptor).file?

    case b1 = getbyte
    when 0x00
      b2 = getbyte
      if b2 == 0x00
        b3 = getbyte
        if b3 == 0xFE
          b4 = getbyte
          if b4 == 0xFF
            return "UTF-32BE"
          end
          ungetbyte b4
        end
        ungetbyte b3
      end
      ungetbyte b2

    when 0xFF
      b2 = getbyte
      if b2 == 0xFE
        b3 = getbyte
        if b3 == 0x00
          b4 = getbyte
          if b4 == 0x00
            return "UTF-32LE"
          end
          ungetbyte b4
        else
          ungetbyte b3
          return "UTF-16LE"
        end
        ungetbyte b3
      end
      ungetbyte b2

    when 0xFE
      b2 = getbyte
      if b2 == 0xFF
        return "UTF-16BE"
      end
      ungetbyte b2

    when 0xEF
      b2 = getbyte
      if b2 == 0xBB
        b3 = getbyte
        if b3 == 0xBF
          return "UTF-8"
        end
        ungetbyte b3
      end
      ungetbyt b2  # FIXME: syntax error waiting to happen!
    end

    ungetbyte b1
    nil
  end

  ##
  # Returns status information for ios as an object of type File::Stat.
  #
  #  f = File.new("testfile")
  #  s = f.stat
  #  "%o" % s.mode   #=> "100644"
  #  s.blksize       #=> 4096
  #  s.atime         #=> Wed Apr 09 08:53:54 CDT 2003
  def stat
    ensure_open

    File::Stat.fstat descriptor
  end

  ##
  # Returns the current "sync mode" of ios. When sync mode is true,
  # all output is immediately flushed to the underlying operating
  # system and is not buffered by Ruby internally. See also IO#fsync.
  #
  #  f = File.new("testfile")
  #  f.sync   #=> false
  def sync
    ensure_open
    @sync == true
  end

  ##
  # Sets the "sync mode" to true or false. When sync mode is true,
  # all output is immediately flushed to the underlying operating
  # system and is not buffered internally. Returns the new state.
  # See also IO#fsync.
  def sync=(v)
    ensure_open
    @sync = !!v
  end

  ##
  # Reads integer bytes from ios using a low-level read and returns
  # them as a string. Do not mix with other methods that read from
  # ios or you may get unpredictable results. Raises SystemCallError
  # on error and EOFError at end of file.
  #
  #  f = File.new("testfile")
  #  f.sysread(16)   #=> "This is line one"
  #
  def sysread(number_of_bytes, buffer=undefined)
    str = @fd.sysread number_of_bytes
    raise EOFError if str.nil?

    unless undefined.equal? buffer
      StringValue(buffer).replace str
    end

    str
  end

  ##
  # Seeks to a given offset in the stream according to the value
  # of whence (see IO#seek for values of whence). Returns the new offset into the file.
  #
  #  f = File.new("testfile")
  #  f.sysseek(-13, IO::SEEK_END)   #=> 53
  #  f.sysread(10)                  #=> "And so on."
  def sysseek(amount, whence=SEEK_SET)
    ensure_open
    amount = Integer(amount)

    @fd.sysseek amount, whence
  end

  def to_io
    self
  end

  def ftruncate(offset)
    @fd.ftruncate offset
  end

  def truncate(name, offset)
    @fd.truncate name, offset
  end

  ##
  # Returns true if ios is associated with a terminal device (tty), false otherwise.
  #
  #  File.new("testfile").isatty   #=> false
  #  File.new("/dev/tty").isatty   #=> true
  def tty?
    @fd.tty?
  end

  alias_method :isatty, :tty?

  def ttyname
    @fd.ttyname
  end

  def syswrite(data)
    data = String data
    return 0 if data.bytesize == 0

    ensure_open_and_writable
    #    @ibuffer.unseek!(self) unless @sync

    @fd.write(data)
  end

  def ungetbyte(obj)
    ensure_open

    case obj
    when String
      str = obj
    when Integer
      @fd.unget(obj & 0xff)
      return
    when nil
      return
    else
      str = StringValue(obj)
    end

    str.bytes.reverse_each { |byte| @fd.unget byte }

    nil
  end

  def ungetc(obj)
    ensure_open

    case obj
    when String
      str = obj
    when Integer
      @fd.unget(obj)
      return
    when nil
      return
    else
      str = StringValue(obj)
    end

    str.bytes.reverse_each { |byte| @fd.unget byte }

    nil
  end

  def write(data)
    data = String data
    return 0 if data.bytesize == 0

    ensure_open_and_writable

    if !binmode? && external_encoding &&
        external_encoding != data.encoding &&
        external_encoding != Encoding::ASCII_8BIT
      unless data.ascii_only? && external_encoding.ascii_compatible?
        data.encode!(external_encoding)
      end
    end
    data.encode!(@encoding_options) unless (@encoding_options || {}).empty?

    #    if @sync
    @fd.write(data)
    #    else
    #      @ibuffer.unseek! self
    #      bytes_to_write = data.bytesize
    #
    #      while bytes_to_write > 0
    #        bytes_to_write -= @ibuffer.unshift(data, data.bytesize - bytes_to_write)
    #        @ibuffer.empty_to self if @ibuffer.full? or sync
    #      end
    #    end

    data.bytesize
  end

  def write_nonblock(data, exception: true)
    ensure_open_and_writable

    data = String data
    return 0 if data.bytesize == 0

    @fd.write_nonblock(data)
  rescue EAGAINWaitWritable => exc
    raise exc if exception

    return :wait_writable
  end

  def close
    return if closed?
    begin
      flush
    ensure
      @fd.close
    end

    if @pid and @pid != 0
      begin
        Process.wait @pid
      rescue Errno::ECHILD
        # If the child already exited
      end
      @pid = nil
    end

    return nil
  end

  def ensure_open(fd=nil)
    raise IOError, "uninitialized stream" unless @fd
    @fd.ensure_open(fd)
  end

  def ensure_open_and_readable
    ensure_open
    raise IOError, "not opened for reading" if @fd.write_only?
  end

  def ensure_open_and_writable
    ensure_open
    raise IOError, "not opened for writing" if @fd.read_only?
  end

  def get_empty_8bit_buffer
    FileDescriptor.get_empty_8bit_buffer
  end

  def invalid_descriptor?
    descriptor == -1 || descriptor.nil?
  end
  private :invalid_descriptor?

  def emulate_blocking_read
    # Simple wrapper intended to wrap a call to #read_nonblock.
    # Loops forever while waiting for data to become available
    # just like a blocking read, but avoids blocking on the
    # low-level read(2) call. Allows us to catch situations
    # where another thread has closed the FD.
    begin
      yield
    rescue IO::EAGAINWaitReadable
      sleep 0.10
      retry
    rescue EOFError
    end
  end
  private :emulate_blocking_read
end

##
# Implements the pipe returned by IO::pipe.

class IO::BidirectionalPipe < IO

  def set_pipe_info(write)
    @write = write
    @sync = true
  end

  ##
  # Closes ios and flushes any pending writes to the
  # operating system. The stream is unavailable for
  # any further data operations; an IOError is raised
  # if such an attempt is made. I/O streams are
  # automatically closed when they are claimed by
  # the garbage collector.
  #
  # If ios is opened by IO.popen, close sets $?.
  def close
    @write.close unless @write.closed?

    super unless closed?

    nil
  end

  def closed?
    super and @write.closed?
  end

  def close_read
    super
  end

  def close_write
    return if @write.closed?

    @write.close
  end

  # Expand these out rather than using some metaprogramming because it's a fixed
  # set and it's faster to have them as normal methods because then InlineCaches
  # work right.
  #
  def <<(obj)
    @write << obj
  end

  def print(*args)
    @write.print(*args)
  end

  def printf(fmt, *args)
    @write.printf(fmt, *args)
  end

  def putc(obj)
    @write.putc(obj)
  end

  def puts(*args)
    @write.puts(*args)
  end

  def syswrite(data)
    @write.syswrite(data)
  end

  def write(data)
    @write.write(data)
  end

  def write_nonblock(data)
    @write.write_nonblock(data)
  end

end

module Rubinius
  class IOUtility
    # Redefine STDIN, STDOUT & STDERR to use the new IO class. It reopened and redefined
    # all methods used in the bootstrap step. Secondly, update the $std* globals to point
    # to the new objects.

    def self.redefine_io(fd, mode)
      # Note that we use IO.open instead of IO.reopen. The reason is that we reopened the
      # IO class in common/io.rb and overwrote a lot of the methods that were defined in
      # bootstrap/io.rb. So if we try to use IO.reopen on the original IO object, it won't
      # be able to reach those original methods anymore. So, we just pass in the file
      # descriptor integer directly and wrap it up in a new object. The original object
      # will probably get garbage collected but we don't set a finalizer for FDs 0-2 which
      # correspond to STDIN, STDOUT and STDERR so we don't need to worry that they'll get
      # closed out from under us.
      # Hopefully we can find a cleaner way to do this in the future, but for now it's a
      # bit ugly.
      new_io = IO.open(fd)
      new_io.sync = true

      if mode == :read_only
        new_io.force_read_only
      elsif mode == :write_only
        new_io.force_write_only
      end

      return new_io
    end
  end
end