# encoding: ascii-8bit
# frozen_string_literal: true
require 'memory_io/types/types'
module MemoryIO
# Main class to use {MemoryIO}.
class IO
attr_reader :stream # @return [#pos, #pos=, #read, #write]
# Instantiate an {IO} object.
#
# @param [#pos, #pos=, #read, #write] stream
# The file-like object to be read/written.
# +file+ can be un-writable if you will not invoke any write-related method.
#
# If +stream.read(*)+ returns empty string or +nil+, it would be seen as reaching EOF.
def initialize(stream)
@stream = stream
end
# Read and convert result into custom type/structure.
#
# @param [Integer] num_elements
# Number of elements to be read.
# This parameter must be positive and larger than zero.
#
# This parameter may effect the return type,
# see documents of return value.
# @param [Integer?] from
# Invoke +stream.pos = from+ before starting to read.
# +nil+ for not changing current position of stream.
# @param [nil, Symbol, Proc] as
# Decide the type/structure when reading.
# See {MemoryIO::Types} for all supported types.
#
# A +Proc+ is allowed, which should accept +stream+ as the first argument.
# The return value of the proc would be the return objects of this method.
#
# If +nil+ is given, this method returns a string and has same behavior as +::IO#read+.
# @param [Boolean] force_array
# When +num_elements+ equals to 1, the read +Object+ would be returned.
# Pass +true+ to this parameter to force this method returning an array.
#
# @return [String, Object, Array<Object>]
# There're multiple possible return types,
# which depends on the value of parameter +num_elements+, +as+, and +force_array+.
#
# See examples for clear usage. The rule of return type is listed as following:
#
# * +as = nil+:
# A +String+ with length +num_elements+ is returned.
# * +as != nil+ and +num_elements = 1+ and +force_array = false+:
# An +Object+ is returned. The type of +Object+ depends on parameter +as+.
# * +as != nil+ and +num_elements = 1+ and +force_array = true+:
# An array with one element is returned.
# * +as != nil+ and +num_elements > 1+:
# An array with length +num_elements+ is returned.
#
# If EOF is occured, object(s) read will be returned.
#
# @example
# stream = StringIO.new('A' * 8 + 'B' * 8)
# io = MemoryIO::IO.new(stream)
# io.read(9)
# #=> "AAAAAAAAB"
# io.read(100)
# #=> "BBBBBBB"
#
# # read two unsigned 32-bit integers starts from posistion 4
# io.read(2, from: 4, as: :u32)
# #=> [1094795585, 1111638594] # [0x41414141, 0x42424242]
#
# io.read(1, as: :u16)
# #=> 16962 # 0x4242
# io.read(1, as: :u16, force_array: true)
# #=> [16962]
# @example
# stream = StringIO.new("\xef\xbe\xad\xde")
# io = MemoryIO::IO.new(stream)
# io.read(1, as: :u32)
# #=> 3735928559
# io.rewind
# io.read(1, as: :s32)
# #=> -559038737
# @example
# stream = StringIO.new("123\x0045678\x00")
# io = MemoryIO::IO.new(stream)
# io.read(2, as: :c_str)
# #=> ["123", "45678"]
# @example
# # pass lambda to `as`
# stream = StringIO.new("\x03123\x044567")
# io = MemoryIO::IO.new(stream)
# io.read(2, as: lambda { |stream| stream.read(stream.read(1).ord) })
# #=> ["123", "4567"]
#
# @note
# This method's arguments and return value are different with +::IO#read+.
# Check documents and examples.
#
# @see Types
def read(num_elements, from: nil, as: nil, force_array: false)
stream.pos = from if from
return stream.read(num_elements) if as.nil?
conv = to_proc(as, :read)
# TODO: handle eof
ret = Array.new(num_elements) { conv.call(stream) }
ret = ret.first if num_elements == 1 && !force_array
ret
end
# Write to stream.
#
# @param [Object, Array<Object>] objects
# Objects to be written.
#
# @param [Integer] from
# The position to start to write.
#
# @param [nil, Symbol, Proc] as
# Decide the method to process writing procedure.
# See {MemoryIO::Types} for all supported types.
#
# A +Proc+ is allowed, which should accept +stream+ and one object as arguments.
#
# If +objects+ is a descendant instance of {Types::Type} and +as+ is +nil,
# +objects.class+ will be used for +as+.
# Otherwise, when +as = nil+, this method will simply call +stream.write(objects)+.
#
# @return [void]
#
# @example
# stream = StringIO.new
# io = MemoryIO::IO.new(stream)
# io.write('abcd')
# stream.string
# #=> "abcd"
#
# io.write([1, 2, 3, 4], from: 2, as: :u16)
# stream.string
# #=> "ab\x01\x00\x02\x00\x03\x00\x04\x00"
#
# io.write(['A', 'BB', 'CCC'], from: 0, as: :c_str)
# stream.string
# #=> "A\x00BB\x00CCC\x00\x00"
# @example
# stream = StringIO.new
# io = MemoryIO::IO.new(stream)
# io.write(%w[123 4567], as: ->(s, str) { s.write(str.size.chr + str) })
# stream.string
# #=> "\x03123\x044567"
#
# @example
# stream = StringIO.new
# io = MemoryIO::IO.new(stream)
# cpp_string = CPP::String.new('A' * 4, 15, 16)
# # equivalent to io.write(cpp_string, as: :'cpp/string')
# io.write(cpp_string)
# stream.string
# #=> "\x10\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00AAAA\x00"
# @see Types
def write(objects, from: nil, as: nil)
stream.pos = from if from
as ||= objects.class if objects.class.ancestors.include?(MemoryIO::Types::Type)
return stream.write(objects) if as.nil?
conv = to_proc(as, :write)
Array(objects).map { |o| conv.call(stream, o) }
end
# Set +stream+ to the beginning.
# i.e. invoke +stream.pos = 0+.
#
# @return [0]
def rewind
stream.pos = 0
end
private
# @api private
def to_proc(as, rw)
ret = as.respond_to?(rw) ? as.method(rw) : as
ret = ret.respond_to?(:call) ? ret : MemoryIO::Types.get_proc(ret, rw)
raise ArgumentError, <<-EOERR.strip unless ret.respond_to?(:call)
Invalid argument `as`: #{as.inspect}. It should be either a Proc or a supported type of MemoryIO::Types.
EOERR
ret
end
end
end