# An `IO` that reads and writes from a buffer in memory. # # The internal buffer can be resizeable and/or writeable depending # on how an `IO::Memory` is constructed. class IO::Memory < IO # Returns the internal buffer as a `Pointer(UInt8)`. getter buffer : Pointer(UInt8) # Same as `size`. getter bytesize : Int32 @capacity : Int32 # Creates an empty, resizeable and writeable `IO::Memory` with the given # initial *capacity* for the internal buffer. # # ``` # io = IO::Memory.new # slice = Bytes.new(1) # io.pos # => 0 # io.read(slice) # => 0 # slice # => Bytes[0] # ``` def initialize(capacity : Int = 64) String.check_capacity_in_bounds(capacity) @buffer = GC.malloc_atomic(capacity.to_u32).as(UInt8*) @bytesize = 0 @capacity = capacity.to_i @pos = 0 @closed = false @resizeable = true @writeable = true end # Creates an `IO::Memory` that will read, and optionally write, from/to # the given slice. The created `IO::Memory` is non-resizeable. # # The IO starts at position zero for reading. # # ``` # slice = Slice.new(6) { |i| ('a'.ord + i).to_u8 } # io = IO::Memory.new slice, writeable: false # io.pos # => 0 # io.read(slice) # => 6 # String.new(slice) # => "abcdef" # ``` def initialize(slice : Bytes, writeable = true) @buffer = slice.to_unsafe @bytesize = @capacity = slice.size.to_i @pos = 0 @closed = false @resizeable = false @writeable = !slice.read_only? && writeable end # Creates an `IO::Memory` whose contents are the exact contents of *string*. # The created `IO::Memory` is non-resizeable and non-writeable. # # The `IO` starts at position zero for reading. # # ``` # io = IO::Memory.new "hello" # io.pos # => 0 # io.gets(2) # => "he" # io.print "hi" # raises IO::Error # ``` def self.new(string : String) : self new string.to_slice, writeable: false end # See `IO#read(slice)`. def read(slice : Bytes) : Int32 check_open count = slice.size count = Math.min(count, @bytesize - @pos) slice.copy_from(@buffer + @pos, count) @pos += count count end # See `IO#write(slice)`. Raises if this `IO::Memory` is non-writeable, # or if it's non-resizeable and a resize is needed. def write(slice : Bytes) : Nil check_writeable check_open count = slice.size return if count == 0 increase_capacity_by(count) slice.copy_to(@buffer + @pos, count) if @pos > @bytesize (@buffer + @bytesize).clear(@pos - @bytesize) end @pos += count @bytesize = @pos if @pos > @bytesize end # See `IO#write_byte`. Raises if this `IO::Memory` is non-writeable, # or if it's non-resizeable and a resize is needed. def write_byte(byte : UInt8) : Nil check_writeable check_open increase_capacity_by(1) (@buffer + @pos).value = byte if @pos > @bytesize (@buffer + @bytesize).clear(@pos - @bytesize) end @pos += 1 @bytesize = @pos if @pos > @bytesize nil end # :nodoc: def gets(delimiter : Char, limit : Int32, chomp : Bool = false) : String? return super if @encoding || delimiter.ord >= 128 check_open raise ArgumentError.new "Negative limit" if limit < 0 index = (@buffer + @pos).to_slice(@bytesize - @pos).index(delimiter.ord) if index if index >= limit index = limit else index += 1 end else index = @bytesize - @pos return nil if index == 0 if index >= limit index = limit end end advance = index if chomp && index > 0 && (@buffer + @pos + index - 1).value === delimiter index -= 1 if delimiter == '\n' && index > 0 && (@buffer + @pos + index - 1).value === '\r' index -= 1 end end string = String.new(@buffer + @pos, index) @pos += advance string end def read_byte : UInt8? check_open pos = Math.min(@pos, @bytesize) if pos == @bytesize nil else byte = @buffer[@pos] @pos += 1 byte end end def peek : Bytes check_open Slice.new(@buffer + @pos, @bytesize - @pos, read_only: !@writeable) end # :nodoc: def skip(bytes_count : Int) : Nil check_open available = @bytesize - @pos if available >= bytes_count @pos += bytes_count else raise IO::EOFError.new end end def skip_to_end : Nil check_open @pos = @bytesize end # :inherit: def gets_to_end : String return super if @encoding check_open if @pos >= @bytesize "" else str = String.new(@buffer + @pos, @bytesize - @pos) @pos = @bytesize str end end # :inherit: def getb_to_end : Bytes check_open if @pos >= @bytesize Bytes[] else bytes = Slice.new(@buffer + @pos, @bytesize - @pos).dup @pos = @bytesize bytes end end # Clears the internal buffer and resets the position to zero. # Raises if this `IO::Memory` is non-resizeable. # # ``` # io = IO::Memory.new # io << "abc" # io.rewind # io.gets(1) # => "a" # io.clear # io.pos # => 0 # io.gets_to_end # => "" # # io = IO::Memory.new "hello" # io.clear # raises IO::Error # ``` def clear : Nil check_open check_resizeable @bytesize = 0 @pos = 0 end # Returns `true` if this `IO::Memory` has no contents. # # ``` # io = IO::Memory.new # io.empty? # => true # io.print "hello" # io.empty? # => false # ``` def empty? : Bool @bytesize == 0 end # Rewinds this `IO` to the initial position (zero). # # ``` # io = IO::Memory.new "hello" # io.gets(2) # => "he" # io.rewind # io.gets(2) # => "he" # ``` def rewind : self @pos = 0 self end # Returns the total number of bytes in this `IO`. # # ``` # io = IO::Memory.new "hello" # io.size # => 5 # ``` def size : Int32 @bytesize end # Seeks to a given *offset* (in bytes) according to the *whence* argument. # # ``` # io = IO::Memory.new("abcdef") # io.gets(3) # => "abc" # io.seek(1, IO::Seek::Set) # io.gets(2) # => "bc" # io.seek(-1, IO::Seek::Current) # io.gets(1) # => "c" # ``` def seek(offset, whence : Seek = Seek::Set) check_open case whence when Seek::Set # Nothing when Seek::Current offset += @pos when Seek::End offset += @bytesize end self.pos = offset end # Returns the current position (in bytes) of this `IO`. # # ``` # io = IO::Memory.new "hello" # io.pos # => 0 # io.gets(2) # => "he" # io.pos # => 2 # ``` def pos : Int32 @pos end # Sets the current position (in bytes) of this `IO`. # # ``` # io = IO::Memory.new "hello" # io.pos = 3 # io.gets # => "lo" # ``` def pos=(value) raise ArgumentError.new("Negative pos") if value < 0 @pos = value.to_i end # Yields an `IO::Memory` to read a section of this `IO`'s buffer. # # During the block duration `self` becomes read-only, # so multiple concurrent open are allowed. def read_at(offset, bytesize, & : IO ->) unless 0 <= offset <= @bytesize raise ArgumentError.new("Offset out of bounds") end if bytesize < 0 raise ArgumentError.new("Negative bytesize") end unless 0 <= offset + bytesize <= @bytesize raise ArgumentError.new("Bytesize out of bounds") end old_writeable = @writeable old_resizeable = @resizeable io = IO::Memory.new(to_slice[offset, bytesize], writeable: false) begin @writeable = false @resizeable = false yield io ensure io.close @writeable = old_writeable @resizeable = old_resizeable end end # Closes this `IO`. Further operations on this `IO` will raise an `IO::Error`. # # ``` # io = IO::Memory.new "hello" # io.close # io.gets_to_end # raises IO::Error (closed stream) # ``` def close : Nil @closed = true end # Determines if this `IO` is closed. # # ``` # io = IO::Memory.new "hello" # io.closed? # => false # io.close # io.closed? # => true # ``` def closed? : Bool @closed end # Returns a new `String` that contains the contents of the internal buffer. # # ``` # io = IO::Memory.new # io.print 1, 2, 3 # io.to_s # => "123" # ``` def to_s : String if encoding = @encoding {% if flag?(:without_iconv) %} raise NotImplementedError.new("String.encode") {% else %} String.new to_slice, encoding: encoding.name, invalid: encoding.invalid {% end %} else String.new @buffer, @bytesize end end # Returns the underlying bytes. # # ``` # io = IO::Memory.new # io.print "hello" # # io.to_slice # => Bytes[104, 101, 108, 108, 111] # ``` def to_slice : Bytes Slice.new(@buffer, @bytesize, read_only: !@writeable) end # Appends this internal buffer to the given `IO`. def to_s(io : IO) : Nil if io == self # When appending to itself, we need to pull the resize up before taking # pointer to the buffer. It would become invalid when a resize happens during `#write`. new_bytesize = bytesize * 2 resize_to_capacity(new_bytesize) if @capacity < new_bytesize end if encoding = @encoding {% if flag?(:without_iconv) %} raise NotImplementedError.new("String.encode") {% else %} String.encode(to_slice, encoding.name, io.encoding, io, io.@encoding.try(&.invalid)) {% end %} else io.write(to_slice) end end private def check_writeable unless @writeable raise IO::Error.new "Read-only stream" end end private def check_resizeable unless @resizeable raise IO::Error.new "Non-resizeable stream" end end private def increase_capacity_by(count) raise IO::EOFError.new if count >= Int32::MAX - bytesize new_bytesize = @pos + count return if new_bytesize <= @capacity check_resizeable new_capacity = calculate_new_capacity(new_bytesize) resize_to_capacity(new_capacity) end private def calculate_new_capacity(new_bytesize : Int32) # If the new bytesize is bigger than 1 << 30, the next power of two would # be 1 << 31, which is out of range for Int32. # So we limit the capacity to Int32::MAX in order to be able to use the # range (1 << 30) < new_bytesize < Int32::MAX return Int32::MAX if new_bytesize > 1 << 30 Math.pw2ceil(new_bytesize) end private def resize_to_capacity(capacity) @capacity = capacity @buffer = GC.realloc(@buffer, @capacity) end end