diff --git a/samples/conway.cr b/samples/conway.cr index 118185de2bfe..873e0bf51878 100644 --- a/samples/conway.cr +++ b/samples/conway.cr @@ -13,7 +13,7 @@ struct ANSI end end -module IO +class IO def ansi ANSI.new self end diff --git a/spec/compiler/codegen/module_spec.cr b/spec/compiler/codegen/module_spec.cr index 1907bafbafdd..49946f8ac8c6 100644 --- a/spec/compiler/codegen/module_spec.cr +++ b/spec/compiler/codegen/module_spec.cr @@ -318,19 +318,19 @@ describe "Code gen: module" do end end - module IO2 + module Moo end - module IO2::Sub - include IO2 + module Moo::Sub + include Moo end class File2 - include IO2::Sub + include Moo::Sub end file = File2.new - file2 = file.as(IO2) + file2 = file.as(Moo) file.method(file2) )).to_i.should eq(1) diff --git a/spec/std/file_utils_spec.cr b/spec/std/file_utils_spec.cr index 8ff578c32873..674b1748e92e 100644 --- a/spec/std/file_utils_spec.cr +++ b/spec/std/file_utils_spec.cr @@ -1,9 +1,7 @@ require "spec" require "file_utils" -private class OneByOneIO - include IO - +private class OneByOneIO < IO @bytes : Bytes def initialize(string) diff --git a/spec/std/http/server/server_spec.cr b/spec/std/http/server/server_spec.cr index fef20485d253..e43f3b159f53 100644 --- a/spec/std/http/server/server_spec.cr +++ b/spec/std/http/server/server_spec.cr @@ -1,12 +1,10 @@ require "spec" require "http/server" -private class RaiseErrno +private class RaiseErrno < IO def initialize(@value : Int32) end - include IO - def read(slice : Bytes) Errno.value = @value raise Errno.new "..." @@ -17,9 +15,7 @@ private class RaiseErrno end end -private class ReverseResponseOutput - include IO - +private class ReverseResponseOutput < IO @output : IO def initialize(@output : IO) diff --git a/spec/std/io/buffered_spec.cr b/spec/std/io/buffered_spec.cr index a7abbebb7f14..1e4d4a473fec 100644 --- a/spec/std/io/buffered_spec.cr +++ b/spec/std/io/buffered_spec.cr @@ -1,6 +1,6 @@ require "spec" -private class BufferedWrapper +private class BufferedWrapper < IO include IO::Buffered getter called_unbuffered_read diff --git a/spec/std/io/delimited_spec.cr b/spec/std/io/delimited_spec.cr index 3ef0a0b20088..25884852f9a9 100644 --- a/spec/std/io/delimited_spec.cr +++ b/spec/std/io/delimited_spec.cr @@ -1,8 +1,6 @@ require "spec" -private class PartialReaderIO - include IO - +private class PartialReaderIO < IO @slice : Bytes def initialize(data : String) diff --git a/spec/std/io/io_spec.cr b/spec/std/io/io_spec.cr index e02f9cce4e79..81f26c63db64 100644 --- a/spec/std/io/io_spec.cr +++ b/spec/std/io/io_spec.cr @@ -5,9 +5,7 @@ require "base64" # This is a non-optimized version of IO::Memory so we can test # raw IO. Optimizations for specific IOs are tested separately # (for example in buffered_io_spec) -private class SimpleIOMemory - include IO - +private class SimpleIOMemory < IO getter buffer : UInt8* getter bytesize : Int32 @capacity : Int32 diff --git a/spec/std/io/sized_spec.cr b/spec/std/io/sized_spec.cr index 531b6cb15df5..b5a9d7ee792c 100644 --- a/spec/std/io/sized_spec.cr +++ b/spec/std/io/sized_spec.cr @@ -1,8 +1,6 @@ require "spec" -private class NoPeekIO - include IO - +private class NoPeekIO < IO def read(bytes : Bytes) 0 end diff --git a/src/file/preader.cr b/src/file/preader.cr index e539c80a0759..8416c9b35ff6 100644 --- a/src/file/preader.cr +++ b/src/file/preader.cr @@ -1,5 +1,5 @@ # :nodoc: -class File::PReader +class File::PReader < IO include IO::Buffered getter? closed = false diff --git a/src/flate/reader.cr b/src/flate/reader.cr index 3e75af4c181c..f27c2fb1eabc 100644 --- a/src/flate/reader.cr +++ b/src/flate/reader.cr @@ -3,9 +3,7 @@ # Instances of this class wrap another IO object. When you read from this instance # instance, it reads data from the underlying IO, decompresses it, and returns # it to the caller. -class Flate::Reader - include IO - +class Flate::Reader < IO # If `#sync_close?` is `true`, closing this IO will close the underlying IO. property? sync_close : Bool diff --git a/src/flate/writer.cr b/src/flate/writer.cr index 4d6b5057fc24..ab504842c38b 100644 --- a/src/flate/writer.cr +++ b/src/flate/writer.cr @@ -5,9 +5,7 @@ # # NOTE: unless created with a block, `close` must be invoked after all # data has been written to a Flate::Writer instance. -class Flate::Writer - include IO - +class Flate::Writer < IO # If `#sync_close?` is `true`, closing this IO will close the underlying IO. property? sync_close : Bool diff --git a/src/gzip/reader.cr b/src/gzip/reader.cr index d2ff243e26ac..fbbf6fcc6803 100644 --- a/src/gzip/reader.cr +++ b/src/gzip/reader.cr @@ -26,9 +26,7 @@ # end # string # => "abc" # ``` -class Gzip::Reader - include IO - +class Gzip::Reader < IO # Whether to close the enclosed `IO` when closing this reader. property? sync_close = false diff --git a/src/gzip/writer.cr b/src/gzip/writer.cr index 5102d325179c..eb40b0c43f4d 100644 --- a/src/gzip/writer.cr +++ b/src/gzip/writer.cr @@ -21,9 +21,7 @@ # end # end # ``` -class Gzip::Writer - include IO - +class Gzip::Writer < IO # Whether to close the enclosed `IO` when closing this writer. property? sync_close = false diff --git a/src/http/content.cr b/src/http/content.cr index 3edd46b984ed..6c72924aaf69 100644 --- a/src/http/content.cr +++ b/src/http/content.cr @@ -17,8 +17,7 @@ module HTTP end # :nodoc: - class UnknownLengthContent - include IO + class UnknownLengthContent < IO include Content def initialize(@io : IO) @@ -46,8 +45,7 @@ module HTTP end # :nodoc: - class ChunkedContent - include IO + class ChunkedContent < IO include Content @chunk_remaining : Int32 diff --git a/src/http/server/response.cr b/src/http/server/response.cr index 0d5932d2b60a..ec3ebd85947a 100644 --- a/src/http/server/response.cr +++ b/src/http/server/response.cr @@ -12,9 +12,7 @@ class HTTP::Server # are written and the connection `IO` (a socket) is yielded to the given block. # The block must invoke `close` afterwards, the server won't do it in this case. # This is useful to implement protocol upgrades, such as websockets. - class Response - include IO - + class Response < IO # The response headers (`HTTP::Headers`). These must be set before writing to the response. getter headers : HTTP::Headers @@ -136,7 +134,7 @@ class HTTP::Server end # :nodoc: - class Output + class Output < IO include IO::Buffered property! response : Response diff --git a/src/http/web_socket/protocol.cr b/src/http/web_socket/protocol.cr index 2ff33edfc0b1..4a4dfb9a7ebb 100644 --- a/src/http/web_socket/protocol.cr +++ b/src/http/web_socket/protocol.cr @@ -42,9 +42,7 @@ class HTTP::WebSocket::Protocol @masked = !!masked end - class StreamIO - include IO - + class StreamIO < IO def initialize(@websocket : Protocol, binary, frame_size) @opcode = binary ? Opcode::BINARY : Opcode::TEXT @buffer = Bytes.new(frame_size) diff --git a/src/io.cr b/src/io.cr index 2f0a67ec0f1d..c63f6fd0fa2a 100644 --- a/src/io.cr +++ b/src/io.cr @@ -5,9 +5,9 @@ require "c/sys/wait" require "c/errno" require "c/unistd" -# The `IO` module is the basis for all input and output in Crystal. +# The `IO` class is the basis for all input and output in Crystal. # -# This module is included by types like `File`, `Socket` and `IO::Memory` and +# This class is inherited by types like `File`, `Socket` and `IO::Memory` and # provide many useful methods for reading to and writing from an IO, like `print`, `puts`, # `gets` and `printf`. # @@ -20,9 +20,7 @@ require "c/unistd" # For example, this is a simple `IO` on top of a `Bytes`: # # ``` -# class SimpleSliceIO -# include IO -# +# class SimpleSliceIO < IO # def initialize(@slice : Bytes) # end # @@ -61,7 +59,7 @@ require "c/unistd" # Mixing string and byte operations might not give correct results and should be # avoided, as string operations might need to read extra bytes in order to get characters # in the given encoding. -module IO +abstract class IO # Argument to a `seek` operation. enum Seek # Seeks to an absolute location @@ -1048,6 +1046,75 @@ module IO @encoding.try(&.name) || "UTF-8" end + # Seeks to a given *offset* (in bytes) according to the *whence* argument. + # + # The `IO` class raises on this method, but some subclasses, notable + # `IO::FileDescriptor` and `IO::Memory` implement it. + # + # Returns `self`. + # + # ``` + # File.write("testfile", "abc") + # + # file = File.new("testfile") + # file.gets(3) # => "abc" + # file.seek(1, IO::Seek::Set) + # file.gets(2) # => "bc" + # file.seek(-1, IO::Seek::Current) + # file.gets(1) # => "c" + # ``` + def seek(offset, whence : Seek = Seek::Set) + raise Error.new "Unable to seek" + end + + # Returns the current position (in bytes) in this `IO`. + # + # The `IO` class raises on this method, but some subclasses, notable + # `IO::FileDescriptor` and `IO::Memory` implement it. + # + # ``` + # File.write("testfile", "hello") + # + # file = File.new("testfile") + # file.pos # => 0 + # file.gets(2) # => "he" + # file.pos # => 2 + # ``` + def pos + raise Error.new "Unable to pos" + end + + # Sets the current position (in bytes) in this `IO`. + # + # The `IO` class raises on this method, but some subclasses, notable + # `IO::FileDescriptor` and `IO::Memory` implement it. + # + # ``` + # File.write("testfile", "hello") + # + # file = File.new("testfile") + # file.pos = 3 + # file.gets_to_end # => "lo" + # ``` + def pos=(value) + raise Error.new "Unable to pos=" + end + + # Same as `pos`. + def tell + pos + end + + # Yields an `IO` to read a section inside this IO. + # + # The `IO` class raises on this method, but some subclasses, notable + # `File` and `IO::Memory` implement it. + # + # Mutliple sections can be read concurrently. + def read_at(offset, bytesize, &block) + raise Error.new "Unable to read_at" + end + # Copy all contents from *src* to *dst*. # # ``` diff --git a/src/io/argf.cr b/src/io/argf.cr index 47a877057d30..f422ccd1a33f 100644 --- a/src/io/argf.cr +++ b/src/io/argf.cr @@ -1,7 +1,5 @@ # :nodoc: -class IO::ARGF - include IO - +class IO::ARGF < IO @path : String? @current_io : IO? diff --git a/src/io/buffered.cr b/src/io/buffered.cr index 422c66702574..1a4320b68eb0 100644 --- a/src/io/buffered.cr +++ b/src/io/buffered.cr @@ -1,12 +1,10 @@ -# The `IO::Buffered` mixin enhances the `IO` module with input/output buffering. +# The `IO::Buffered` mixin enhances an `IO` with input/output buffering. # # The buffering behaviour can be turned on/off with the `#sync=` method. # # Additionally, several methods, like `#gets`, are implemented in a more # efficient way. module IO::Buffered - include IO - BUFFER_SIZE = 8192 @in_buffer_rem = Bytes.empty diff --git a/src/io/console.cr b/src/io/console.cr index e138c61bb999..236c0a47c6f5 100644 --- a/src/io/console.cr +++ b/src/io/console.cr @@ -1,6 +1,6 @@ require "termios" -class IO::FileDescriptor +class IO::FileDescriptor < IO # Turn off character echoing for the duration of the given block. # This will prevent displaying back to the user what they enter on the terminal. # Only call this when this IO is a TTY, such as a not redirected stdin. diff --git a/src/io/delimited.cr b/src/io/delimited.cr index 8718aa86e5ce..da0e0ca06fea 100644 --- a/src/io/delimited.cr +++ b/src/io/delimited.cr @@ -1,124 +1,120 @@ -module IO - # An `IO` that wraps another `IO`, and only reads up to the beginning of a - # specified delimiter. - # - # This is useful for exposing part of an underlying stream to a client. - # - # ``` - # io = IO::Memory.new "abc||123" - # delimited = IO::Delimited.new(io, read_delimiter: "||") - # - # delimited.gets_to_end # => "abc" - # delimited.gets_to_end # => "" - # io.gets_to_end # => "123" - # ``` - class Delimited - include IO - - # If `#sync_close?` is `true`, closing this `IO` will close the underlying `IO`. - property? sync_close - - getter read_delimiter - getter? closed : Bool - - @delimiter_buffer : Bytes - @active_delimiter_buffer : Bytes - - # Creates a new `IO::Delimited` which wraps *io*, and can read until the - # byte sequence *read_delimiter* (interpreted as UTF-8) is found. If - # *sync_close* is set, calling `#close` calls `#close` on the underlying - # `IO`. - def self.new(io : IO, read_delimiter : String, sync_close : Bool = false) - new(io, read_delimiter.to_slice, sync_close) - end +# An `IO` that wraps another `IO`, and only reads up to the beginning of a +# specified delimiter. +# +# This is useful for exposing part of an underlying stream to a client. +# +# ``` +# io = IO::Memory.new "abc||123" +# delimited = IO::Delimited.new(io, read_delimiter: "||") +# +# delimited.gets_to_end # => "abc" +# delimited.gets_to_end # => "" +# io.gets_to_end # => "123" +# ``` +class IO::Delimited < IO + # If `#sync_close?` is `true`, closing this `IO` will close the underlying `IO`. + property? sync_close + + getter read_delimiter + getter? closed : Bool + + @delimiter_buffer : Bytes + @active_delimiter_buffer : Bytes + + # Creates a new `IO::Delimited` which wraps *io*, and can read until the + # byte sequence *read_delimiter* (interpreted as UTF-8) is found. If + # *sync_close* is set, calling `#close` calls `#close` on the underlying + # `IO`. + def self.new(io : IO, read_delimiter : String, sync_close : Bool = false) + new(io, read_delimiter.to_slice, sync_close) + end - # Creates a new `IO::Delimited` which wraps *io*, and can read until the - # byte sequence *read_delimiter* is found. If *sync_close* is set, calling - # `#close` calls `#close` on the underlying `IO`. - def initialize(@io : IO, @read_delimiter : Bytes, @sync_close : Bool = false) - @closed = false - @finished = false - - # The buffer where we do all our work. - @delimiter_buffer = Bytes.new(@read_delimiter.size) - # Slice inside delimiter buffer where bytes waiting to be read are stored. - @active_delimiter_buffer = Bytes.empty - end + # Creates a new `IO::Delimited` which wraps *io*, and can read until the + # byte sequence *read_delimiter* is found. If *sync_close* is set, calling + # `#close` calls `#close` on the underlying `IO`. + def initialize(@io : IO, @read_delimiter : Bytes, @sync_close : Bool = false) + @closed = false + @finished = false + + # The buffer where we do all our work. + @delimiter_buffer = Bytes.new(@read_delimiter.size) + # Slice inside delimiter buffer where bytes waiting to be read are stored. + @active_delimiter_buffer = Bytes.empty + end - def read(slice : Bytes) - check_open - return 0 if @finished + def read(slice : Bytes) + check_open + return 0 if @finished + + first_byte = @read_delimiter[0] + read_bytes = 0 + + while read_bytes < slice.size + # Select the next byte as the head of the active delimiter buffer, + # or the next byte from the io if the buffer is not in use. + if @active_delimiter_buffer.size > 0 + byte = @active_delimiter_buffer[0] + @active_delimiter_buffer += 1 + else + byte = @io.read_byte + end + + break if byte.nil? - first_byte = @read_delimiter[0] - read_bytes = 0 + # We know we don't need to check if the delimiter matches when the buffer + # has been resized, because this signals we are coming to the end of the IO. + if byte == first_byte && @delimiter_buffer.size == @read_delimiter.size + buffer = @delimiter_buffer + buffer[0] = byte + read_start = 1 - while read_bytes < slice.size - # Select the next byte as the head of the active delimiter buffer, - # or the next byte from the io if the buffer is not in use. + # If we have an active delimiter buffer copy it in after the current + # character, and update where we should start our read operation. if @active_delimiter_buffer.size > 0 - byte = @active_delimiter_buffer[0] - @active_delimiter_buffer += 1 - else - byte = @io.read_byte + (buffer + 1).move_from(@active_delimiter_buffer) + read_start += @active_delimiter_buffer.size + end + + read_buffer = buffer + read_start + bytes = 0 + while read_buffer.size > 0 + partial_bytes = @io.read(read_buffer) + break if partial_bytes == 0 + + read_buffer += partial_bytes + bytes += partial_bytes + end + + # If read didn't read as many bytes as we asked it to, resize the buffer + # to remove garbage bytes. + if bytes != buffer.size - read_start + buffer = buffer[0, read_start + bytes] end - break if byte.nil? - - # We know we don't need to check if the delimiter matches when the buffer - # has been resized, because this signals we are coming to the end of the IO. - if byte == first_byte && @delimiter_buffer.size == @read_delimiter.size - buffer = @delimiter_buffer - buffer[0] = byte - read_start = 1 - - # If we have an active delimiter buffer copy it in after the current - # character, and update where we should start our read operation. - if @active_delimiter_buffer.size > 0 - (buffer + 1).move_from(@active_delimiter_buffer) - read_start += @active_delimiter_buffer.size - end - - read_buffer = buffer + read_start - bytes = 0 - while read_buffer.size > 0 - partial_bytes = @io.read(read_buffer) - break if partial_bytes == 0 - - read_buffer += partial_bytes - bytes += partial_bytes - end - - # If read didn't read as many bytes as we asked it to, resize the buffer - # to remove garbage bytes. - if bytes != buffer.size - read_start - buffer = buffer[0, read_start + bytes] - end - - if buffer == @read_delimiter - @finished = true - return read_bytes - end - - @delimiter_buffer = buffer - @active_delimiter_buffer = buffer + 1 + if buffer == @read_delimiter + @finished = true + return read_bytes end - slice[read_bytes] = byte - read_bytes += 1 + @delimiter_buffer = buffer + @active_delimiter_buffer = buffer + 1 end - read_bytes + slice[read_bytes] = byte + read_bytes += 1 end - def write(slice : Bytes) - raise IO::Error.new "Can't write to IO::Delimited" - end + read_bytes + end + + def write(slice : Bytes) + raise IO::Error.new "Can't write to IO::Delimited" + end - def close - return if @closed - @closed = true + def close + return if @closed + @closed = true - @io.close if @sync_close - end + @io.close if @sync_close end end diff --git a/src/io/encoding.cr b/src/io/encoding.cr index d9a785a49f36..b4c90fa471c4 100644 --- a/src/io/encoding.cr +++ b/src/io/encoding.cr @@ -1,4 +1,4 @@ -module IO +class IO # Has the `name` and the `invalid` option. struct EncodingOptions getter name : String diff --git a/src/io/error.cr b/src/io/error.cr index 105ec92a7d4d..b625207544aa 100644 --- a/src/io/error.cr +++ b/src/io/error.cr @@ -1,4 +1,4 @@ -module IO +class IO class Error < Exception end diff --git a/src/io/file_descriptor.cr b/src/io/file_descriptor.cr index 7f061e8b65b4..5ffcf75e3cd8 100644 --- a/src/io/file_descriptor.cr +++ b/src/io/file_descriptor.cr @@ -2,7 +2,7 @@ require "./syscall" require "c/fcntl" # An `IO` over a file descriptor. -class IO::FileDescriptor +class IO::FileDescriptor < IO include IO::Buffered include IO::Syscall @@ -99,11 +99,6 @@ class IO::FileDescriptor end end - # Same as `pos`. - def tell - pos - end - # Returns the current position (in bytes) in this `IO`. # # ``` diff --git a/src/io/hexdump.cr b/src/io/hexdump.cr index 8deca0115167..f383c0c694bd 100644 --- a/src/io/hexdump.cr +++ b/src/io/hexdump.cr @@ -22,43 +22,39 @@ # 00000000 00 . # 00000000 00 00 00 00 .... # ``` -module IO - class Hexdump - include IO - - def initialize(@io : IO, @output : IO = STDERR, @read = false, @write = false) - end +class IO::Hexdump < IO + def initialize(@io : IO, @output : IO = STDERR, @read = false, @write = false) + end - def read(buf : Bytes) - @io.read(buf).tap do |read_bytes| - @output.puts buf[0, read_bytes].hexdump if @read && read_bytes - end + def read(buf : Bytes) + @io.read(buf).tap do |read_bytes| + @output.puts buf[0, read_bytes].hexdump if @read && read_bytes end + end - def write(buf : Bytes) - @io.write(buf).tap do - @output.puts buf.hexdump if @write - end + def write(buf : Bytes) + @io.write(buf).tap do + @output.puts buf.hexdump if @write end + end - def peek - @io.peek - end + def peek + @io.peek + end - def closed? - @io.closed? - end + def closed? + @io.closed? + end - def close - @io.close - end + def close + @io.close + end - def flush - @io.flush - end + def flush + @io.flush + end - def tty? - @io.tty? - end + def tty? + @io.tty? end end diff --git a/src/io/memory.cr b/src/io/memory.cr index 5d332edba603..e5375cdc6fa4 100644 --- a/src/io/memory.cr +++ b/src/io/memory.cr @@ -2,9 +2,7 @@ # # The internal buffer can be resizeable and/or writeable depending # on how an `IO::Memory` is constructed. -class IO::Memory - include IO - +class IO::Memory < IO # Returns the internal buffer as a `Pointer(UInt8)`. getter buffer : Pointer(UInt8) @@ -288,11 +286,6 @@ class IO::Memory @bytesize end - # Same as `pos`. - def tell - @pos - end - # Seeks to a given *offset* (in bytes) according to the *whence* argument. # # ``` @@ -327,7 +320,7 @@ class IO::Memory # io.pos # => 2 # ``` def pos - tell + @pos end # Sets the current position (in bytes) of this `IO`. diff --git a/src/io/multi_writer.cr b/src/io/multi_writer.cr index a52f332f2e34..9bcfb8125e11 100644 --- a/src/io/multi_writer.cr +++ b/src/io/multi_writer.cr @@ -1,52 +1,48 @@ -module IO - # An `IO` which writes to a number of underlying writer IOs. - # - # ``` - # io1 = IO::Memory.new - # io2 = IO::Memory.new - # writer = IO::MultiWriter.new(io1, io2) - # writer.puts "foo bar" - # io1.to_s # => "foo bar\n" - # io2.to_s # => "foo bar\n" - # ``` - class MultiWriter - include IO - - # If `#sync_close?` is `true`, closing this `IO` will close all of the underlying - # IOs. - property? sync_close - getter? closed = false - - @writers : Array(IO) - - # Creates a new `IO::MultiWriter` which writes to *writers*. If - # *sync_close* is set, calling `#close` calls `#close` on all underlying - # writers. - def initialize(@writers : Array(IO), @sync_close = false) - end - - # Creates a new `IO::MultiWriter` which writes to *writers*. If - # *sync_close* is set, calling `#close` calls `#close` on all underlying - # writers. - def initialize(*writers : IO, @sync_close = false) - @writers = writers.map(&.as(IO)).to_a - end - - def write(slice : Bytes) - check_open - - @writers.each { |writer| writer.write(slice) } - end - - def read(slice : Bytes) - raise IO::Error.new("Can't read from IO::MultiWriter") - end - - def close - return if @closed - @closed = true - - @writers.each { |writer| writer.close } if sync_close? - end +# An `IO` which writes to a number of underlying writer IOs. +# +# ``` +# io1 = IO::Memory.new +# io2 = IO::Memory.new +# writer = IO::MultiWriter.new(io1, io2) +# writer.puts "foo bar" +# io1.to_s # => "foo bar\n" +# io2.to_s # => "foo bar\n" +# ``` +class IO::MultiWriter < IO + # If `#sync_close?` is `true`, closing this `IO` will close all of the underlying + # IOs. + property? sync_close + getter? closed = false + + @writers : Array(IO) + + # Creates a new `IO::MultiWriter` which writes to *writers*. If + # *sync_close* is set, calling `#close` calls `#close` on all underlying + # writers. + def initialize(@writers : Array(IO), @sync_close = false) + end + + # Creates a new `IO::MultiWriter` which writes to *writers*. If + # *sync_close* is set, calling `#close` calls `#close` on all underlying + # writers. + def initialize(*writers : IO, @sync_close = false) + @writers = writers.map(&.as(IO)).to_a + end + + def write(slice : Bytes) + check_open + + @writers.each { |writer| writer.write(slice) } + end + + def read(slice : Bytes) + raise IO::Error.new("Can't read from IO::MultiWriter") + end + + def close + return if @closed + @closed = true + + @writers.each { |writer| writer.close } if sync_close? end end diff --git a/src/io/sized.cr b/src/io/sized.cr index 68d35a8bdfe3..72d98804ac06 100644 --- a/src/io/sized.cr +++ b/src/io/sized.cr @@ -1,89 +1,85 @@ -module IO - # An `IO` that wraps another `IO`, setting a limit for the number of bytes that can be read. - # - # ``` - # io = IO::Memory.new "abcde" - # sized = IO::Sized.new(io, read_size: 3) - # - # sized.gets_to_end # => "abc" - # sized.gets_to_end # => "" - # io.gets_to_end # => "de" - # ``` - class Sized - include IO - - # If `#sync_close?` is `true`, closing this `IO` will close the underlying `IO`. - property? sync_close : Bool - - # The number of remaining bytes to be read. - getter read_remaining : UInt64 - getter? closed : Bool - - # Creates a new `IO::Sized` which wraps *io*, and can read a maximum of - # *read_size* bytes. If *sync_close* is set, calling `#close` calls - # `#close` on the underlying `IO`. - def initialize(@io : IO, read_size : Int, @sync_close = false) - raise ArgumentError.new "Negative read_size" if read_size < 0 - @closed = false - @read_remaining = read_size.to_u64 - end +# An `IO` that wraps another `IO`, setting a limit for the number of bytes that can be read. +# +# ``` +# io = IO::Memory.new "abcde" +# sized = IO::Sized.new(io, read_size: 3) +# +# sized.gets_to_end # => "abc" +# sized.gets_to_end # => "" +# io.gets_to_end # => "de" +# ``` +class IO::Sized < IO + # If `#sync_close?` is `true`, closing this `IO` will close the underlying `IO`. + property? sync_close : Bool + + # The number of remaining bytes to be read. + getter read_remaining : UInt64 + getter? closed : Bool + + # Creates a new `IO::Sized` which wraps *io*, and can read a maximum of + # *read_size* bytes. If *sync_close* is set, calling `#close` calls + # `#close` on the underlying `IO`. + def initialize(@io : IO, read_size : Int, @sync_close = false) + raise ArgumentError.new "Negative read_size" if read_size < 0 + @closed = false + @read_remaining = read_size.to_u64 + end - def read(slice : Bytes) - check_open + def read(slice : Bytes) + check_open - count = {slice.size.to_u64, @read_remaining}.min - bytes_read = @io.read slice[0, count] - @read_remaining -= bytes_read - bytes_read - end + count = {slice.size.to_u64, @read_remaining}.min + bytes_read = @io.read slice[0, count] + @read_remaining -= bytes_read + bytes_read + end - def read_byte - check_open + def read_byte + check_open - if @read_remaining > 0 - byte = @io.read_byte - @read_remaining -= 1 if byte - byte - else - nil - end + if @read_remaining > 0 + byte = @io.read_byte + @read_remaining -= 1 if byte + byte + else + nil end + end - def peek - check_open - - return Bytes.empty if @read_remaining == 0 # EOF + def peek + check_open - peek = @io.peek - return nil unless peek + return Bytes.empty if @read_remaining == 0 # EOF - if @read_remaining < peek.size - peek = peek[0, @read_remaining] - end + peek = @io.peek + return nil unless peek - peek + if @read_remaining < peek.size + peek = peek[0, @read_remaining] end - def skip(bytes_count) : Nil - check_open + peek + end - if bytes_count <= @read_remaining - @io.skip(bytes_count) - @read_remaining -= bytes_count - else - raise IO::EOFError.new - end - end + def skip(bytes_count) : Nil + check_open - def write(slice : Bytes) - raise IO::Error.new "Can't write to IO::Sized" + if bytes_count <= @read_remaining + @io.skip(bytes_count) + @read_remaining -= bytes_count + else + raise IO::EOFError.new end + end - def close - return if @closed - @closed = true + def write(slice : Bytes) + raise IO::Error.new "Can't write to IO::Sized" + end - @io.close if @sync_close - end + def close + return if @closed + @closed = true + + @io.close if @sync_close end end diff --git a/src/openssl/digest/digest_io.cr b/src/openssl/digest/digest_io.cr index d63db8d07ce1..047bbddc985b 100644 --- a/src/openssl/digest/digest_io.cr +++ b/src/openssl/digest/digest_io.cr @@ -14,9 +14,7 @@ module OpenSSL # io.read(buffer) # io.digest # => 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae # ``` - class DigestIO - include IO - + class DigestIO < IO getter io : IO getter digest_algorithm : OpenSSL::Digest getter mode : DigestMode diff --git a/src/openssl/ssl/socket.cr b/src/openssl/ssl/socket.cr index e2b131d556de..10fc100966d4 100644 --- a/src/openssl/ssl/socket.cr +++ b/src/openssl/ssl/socket.cr @@ -1,4 +1,4 @@ -abstract class OpenSSL::SSL::Socket +abstract class OpenSSL::SSL::Socket < IO class Client < Socket def initialize(io, context : Context::Client = Context::Client.new, sync_close : Bool = false, hostname : String? = nil) super(io, context, sync_close) diff --git a/src/socket.cr b/src/socket.cr index b95eedbeccbc..6a2dce73e701 100644 --- a/src/socket.cr +++ b/src/socket.cr @@ -5,7 +5,7 @@ require "c/netinet/tcp" require "c/sys/socket" require "c/sys/un" -class Socket +class Socket < IO include IO::Buffered include IO::Syscall diff --git a/src/string/builder.cr b/src/string/builder.cr index 0dc2149209cc..e3e7b580df71 100644 --- a/src/string/builder.cr +++ b/src/string/builder.cr @@ -3,9 +3,7 @@ require "io" # Similar to `IO::Memory`, but optimized for building a single string. # # You should never have to deal with this class. Instead, use `String.build`. -class String::Builder - include IO - +class String::Builder < IO getter bytesize : Int32 getter capacity : Int32 getter buffer : Pointer(UInt8) diff --git a/src/zip/checksum_reader.cr b/src/zip/checksum_reader.cr index aa10157cd741..bd191fefebb4 100644 --- a/src/zip/checksum_reader.cr +++ b/src/zip/checksum_reader.cr @@ -2,9 +2,7 @@ module Zip # Computes a CRC32 while reading from an underlying IO, # optionally verifying the computed value against an # expected one. - private class ChecksumReader - include IO - + private class ChecksumReader < IO getter crc32 = CRC32.initial def initialize(@io : IO, @filename : String, verify @expected_crc32 : UInt32? = nil) diff --git a/src/zip/checksum_writer.cr b/src/zip/checksum_writer.cr index 0755db9055e3..2158f2ff4952 100644 --- a/src/zip/checksum_writer.cr +++ b/src/zip/checksum_writer.cr @@ -1,9 +1,7 @@ module Zip # Counts written bytes and optionally computes a CRC32 # checksum while writing to an underlying IO. - private class ChecksumWriter - include IO - + private class ChecksumWriter < IO getter count = 0_u32 getter crc32 = CRC32.initial getter! io : IO diff --git a/src/zip/file.cr b/src/zip/file.cr index 51e22bc4a6dc..e1123e594c30 100644 --- a/src/zip/file.cr +++ b/src/zip/file.cr @@ -33,7 +33,7 @@ class Zip::File getter comment = "" # Opens a `Zip::File` for reading from the given *io*. - def initialize(@io : ::File | ::IO::Memory, @sync_close = false) + def initialize(@io : IO, @sync_close = false) directory_end_offset = find_directory_end_offset entries_size, directory_offset = read_directory_end(directory_end_offset) @entries = Array(Entry).new(entries_size) @@ -158,7 +158,7 @@ class Zip::File include FileInfo # :nodoc: - def initialize(@io : ::File | ::IO::Memory) + def initialize(@io : IO) super(at_central_directory_header: io) end diff --git a/src/zlib/reader.cr b/src/zlib/reader.cr index 34e837520f74..846058ab33e4 100644 --- a/src/zlib/reader.cr +++ b/src/zlib/reader.cr @@ -3,9 +3,7 @@ # Instances of this class wrap another IO object. When you read from this instance # instance, it reads data from the underlying IO, decompresses it, and returns # it to the caller. -class Zlib::Reader - include IO - +class Zlib::Reader < IO # Whether to close the enclosed `IO` when closing this reader. property? sync_close = false diff --git a/src/zlib/writer.cr b/src/zlib/writer.cr index 3d1e1a123209..d57025c72949 100644 --- a/src/zlib/writer.cr +++ b/src/zlib/writer.cr @@ -5,9 +5,7 @@ # # NOTE: unless created with a block, `close` must be invoked after all # data has been written to a Zlib::Writer instance. -class Zlib::Writer - include IO - +class Zlib::Writer < IO # Whether to close the enclosed `IO` when closing this writer. property? sync_close = false