core/io.rb
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