# encoding: binary
#  Phusion Passenger - https://www.phusionpassenger.com/
#  Copyright (c) 2010 Phusion
#
#  "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#  THE SOFTWARE.

module PhusionPassenger

# This class allows reading and writing structured messages over
# I/O channels. This is the Ruby implementation of ext/common/Utils/MessageIO.h;
# see that file for more information.
class MessageChannel
	HEADER_SIZE = 2                  # :nodoc:
	DELIMITER = "\0"                 # :nodoc:
	DELIMITER_NAME = "null byte"     # :nodoc:
	UINT16_PACK_FORMAT = "n"         # :nodoc:
	UINT32_PACK_FORMAT = "N"         # :nodoc:
	
	class InvalidHashError < StandardError
	end
	
	# The wrapped IO object.
	attr_accessor :io

	# Create a new MessageChannel by wrapping the given IO object.
	def initialize(io = nil)
		@io = io
		# Make it binary just in case.
		@io.binmode if @io
	end
	
	# Read an array message from the underlying file descriptor.
	# Returns the array message as an array, or nil when end-of-stream has
	# been reached.
	#
	# Might raise SystemCallError, IOError or SocketError when something
	# goes wrong.
	def read
		buffer = new_buffer
		if !@io.read(HEADER_SIZE, buffer)
			return nil
		end
		while buffer.size < HEADER_SIZE
			tmp = @io.read(HEADER_SIZE - buffer.size)
			if tmp.empty?
				return nil
			else
				buffer << tmp
			end
		end
		
		chunk_size = buffer.unpack(UINT16_PACK_FORMAT)[0]
		if !@io.read(chunk_size, buffer)
			return nil
		end
		while buffer.size < chunk_size
			tmp = @io.read(chunk_size - buffer.size)
			if tmp.empty?
				return nil
			else
				buffer << tmp
			end
		end
		
		message = []
		offset = 0
		delimiter_pos = buffer.index(DELIMITER, offset)
		while !delimiter_pos.nil?
			if delimiter_pos == 0
				message << ""
			else
				message << buffer[offset .. delimiter_pos - 1]
			end
			offset = delimiter_pos + 1
			delimiter_pos = buffer.index(DELIMITER, offset)
		end
		return message
	rescue Errno::ECONNRESET
		return nil
	end
	
	# Read an array message from the underlying file descriptor and return the
	# result as a hash instead of an array. This assumes that the array message
	# has an even number of elements.
	# Returns nil when end-of-stream has been reached.
	#
	# Might raise SystemCallError, IOError or SocketError when something
	# goes wrong.
	def read_hash
		buffer = new_buffer
		if !@io.read(HEADER_SIZE, buffer)
			return nil
		end
		while buffer.size < HEADER_SIZE
			tmp = @io.read(HEADER_SIZE - buffer.size)
			if tmp.empty?
				return nil
			else
				buffer << tmp
			end
		end
		
		chunk_size = buffer.unpack(UINT16_PACK_FORMAT)[0]
		if !@io.read(chunk_size, buffer)
			return nil
		end
		while buffer.size < chunk_size
			tmp = @io.read(chunk_size - buffer.size)
			if tmp.empty?
				return nil
			else
				buffer << tmp
			end
		end
		
		result = {}
		offset = 0
		delimiter_pos = buffer.index(DELIMITER, offset)
		while !delimiter_pos.nil?
			if delimiter_pos == 0
				name = ""
			else
				name = buffer[offset .. delimiter_pos - 1]
			end
			
			offset = delimiter_pos + 1
			delimiter_pos = buffer.index(DELIMITER, offset)
			if delimiter_pos.nil?
				raise InvalidHashError
			elsif delimiter_pos == 0
				value = ""
			else
				value = buffer[offset .. delimiter_pos - 1]
			end
			
			result[name] = value
			offset = delimiter_pos + 1
			delimiter_pos = buffer.index(DELIMITER, offset)
		end
		return result
	rescue Errno::ECONNRESET
		return nil
	end

	# Read a scalar message from the underlying IO object. Returns the
	# read message, or nil on end-of-stream.
	#
	# Might raise SystemCallError, IOError or SocketError when something
	# goes wrong.
	#
	# The +buffer+ argument specifies a buffer in which #read_scalar
	# stores the read data. It is good practice to reuse existing buffers
	# in order to minimize stress on the garbage collector.
	#
	# The +max_size+ argument allows one to specify the maximum allowed
	# size for the scalar message. If the received scalar message's size
	# is larger than +max_size+, then a SecurityError will be raised.
	def read_scalar(buffer = new_buffer, max_size = nil)
		if !@io.read(4, buffer)
			return nil
		end
		while buffer.size < 4
			tmp = @io.read(4 - buffer.size)
			if tmp.empty?
				return nil
			else
				buffer << tmp
			end
		end
		
		size = buffer.unpack(UINT32_PACK_FORMAT)[0]
		if size == 0
			buffer.replace('')
			return buffer
		else
			if !max_size.nil? && size > max_size
				raise SecurityError, "Scalar message size (#{size}) " <<
					"exceeds maximum allowed size (#{max_size})."
			end
			if !@io.read(size, buffer)
				return nil
			end
			if buffer.size < size
				tmp = ''
				while buffer.size < size
					if !@io.read(size - buffer.size, tmp)
						return nil
					else
						buffer << tmp
					end
				end
			end
			return buffer
		end
	rescue Errno::ECONNRESET
		return nil
	end
	
	# Send an array message, which consists of the given elements, over the underlying
	# file descriptor. _name_ is the first element in the message, and _args_ are the
	# other elements. These arguments will internally be converted to strings by calling
	# to_s().
	#
	# Might raise SystemCallError, IOError or SocketError when something
	# goes wrong.
	def write(name, *args)
		check_argument(name)
		args.each do |arg|
			check_argument(arg)
		end
		
		message = "#{name}#{DELIMITER}"
		args.each do |arg|
			message << arg.to_s << DELIMITER
		end
		@io.write([message.size].pack('n') << message)
		@io.flush
	end
	
	# Send a scalar message over the underlying IO object.
	#
	# Might raise SystemCallError, IOError or SocketError when something
	# goes wrong.
	def write_scalar(data)
		@io.write([data.size].pack('N') << data)
		@io.flush
	end
	
	# Receive an IO object (a file descriptor) from the channel. The other
	# side must have sent an IO object by calling send_io(). Note that
	# this only works on Unix sockets.
	#
	# Might raise SystemCallError, IOError or SocketError when something
	# goes wrong.
	def recv_io(klass = IO, negotiate = true)
		write("pass IO") if negotiate
		io = @io.recv_io(klass)
		write("got IO") if negotiate
		return io
	end
	
	# Send an IO object (a file descriptor) over the channel. The other
	# side must receive the IO object by calling recv_io(). Note that
	# this only works on Unix sockets.
	#
	# Might raise SystemCallError, IOError or SocketError when something
	# goes wrong.
	def send_io(io)
		# We read a message before actually calling #send_io
		# in order to prevent the other side from accidentally
		# read()ing past the normal data and reading our file
		# descriptor too.
		#
		# For example suppose that side A looks like this:
		#
		#   read(fd, buf, 1024)
		#   read_io(fd)
		#
		# and side B:
		#
		#   write(fd, buf, 100)
		#   send_io(fd_to_pass)
		#
		# If B completes both write() and send_io(), then A's read() call
		# reads past the 100 bytes that B sent. On some platforms, like
		# Linux, this will cause read_io() to fail. And it just so happens
		# that Ruby's IO#read method slurps more than just the given amount
		# of bytes.
		result = read
		if !result
			raise EOFError, "End of stream"
		elsif result != ["pass IO"]
			raise IOError, "IO passing pre-negotiation header expected"
		else
			@io.send_io(io)
			# Once you've sent the IO you expect to be able to close it on the
			# sender's side, even if the other side hasn't read the IO yet.
			# Not so: on some operating systems (I'm looking at you OS X) this
			# can cause the receiving side to receive a bad file descriptor.
			# The post negotiation protocol ensures that we block until the
			# other side has really received the IO.
			result = read
			if !result
				raise EOFError, "End of stream"
			elsif result != ["got IO"]
				raise IOError, "IO passing post-negotiation header expected"
			end
		end
	end
	
	# Return the file descriptor of the underlying IO object.
	def fileno
		return @io.fileno
	end
	
	# Close the underlying IO stream. Might raise SystemCallError or
	# IOError when something goes wrong.
	def close
		@io.close
	end
	
	# Checks whether the underlying IO stream is closed.
	def closed?
		return @io.closed?
	end

private
	def check_argument(arg)
		if arg.to_s.index(DELIMITER)
			raise ArgumentError, "Message name and arguments may not contain #{DELIMITER_NAME}."
		end
	end
	
	if defined?(ByteString)
		def new_buffer
			return ByteString.new
		end
	else
		def new_buffer
			return ""
		end
	end
end

end # module PhusionPassenger
