#  fsh - fast remote execution
#  Copyright (C) 1999-2001 by Per Cederqvist.
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */

import os
import errno
import string
import sys
import fcntl

import fshcompat

# The number of characters that a session is always granted to send at
# startup.
QUOTA = 128 * 1024

class cursor_eof(Exception):
    """Used by cursor to signal that the list of strings is exhausted.
    """
    pass

class cursor:
    """Helper class for parse_line.  Extract strings from a buffer
    that is implemented as a list of strings.
    """
    def __init__(self, q):
	"""Set up a cursor for the list of strings Q.
	"""
	self.q = q
	self.l = -1
	self.__next_line()

    def incr(self):
	"""Step the cursor forward.  May raise cursor_eof.
	"""
	if self.l >= len(self.q):
	    raise cursor_eof
	self.c = self.c + 1
	if self.c >= len(self.q[self.l]):
	    self.__next_line()

    def __next_line(self):
	self.c = 0
	while 1:
	    self.l = self.l + 1
	    if self.l >= len(self.q):
		return
	    if len(self.q[self.l]) > 0:
		return

    def peek(self):
	"""Return the character pointed to by this cursor.
	May raise cursor_eof.
	"""
	if self.l >= len(self.q):
	    raise cursor_eof
	
	return self.q[self.l][self.c]

    def mark(self):
	"""Remember the current position.  See extract().
	"""
	self.mark_l = self.l
	self.mark_c = self.c

    def extract(self):
	"""Return the contents between the mark and the current position.
	The mark is set using mark().  The return value is a string.
	May raise cursor_eof.
	"""
	if self.l >= len(self.q):
	    raise cursor_eof

	if self.mark_l == self.l:
	    return self.q[self.l][self.mark_c:self.c]
	elif self.mark_l == self.l - 1:
	    return (self.q[self.mark_l][self.mark_c:]
		    + self.q[self.l][:self.c])
	else:
	    return (self.q[self.mark_l][self.mark_c:]
		    + string.join(self.q[self.mark_l + 1 : self.l], "")
		    + self.q[self.l][:self.c])

    def incr_many(self, sz):
	"""Moves the current position SZ characters forward.
	May raise cursor_eof.  
	"""
	while sz > 0:
	    if self.l >= len(self.q):
		raise cursor_eof
	    left = len(self.q[self.l]) - self.c
	    if sz < left:
		self.c = self.c + sz
		sz = 0
	    else:
		self.__next_line()
		sz = sz - left

    def discard_handled(self):
	"""Remove everything up to the current position.
	This destructively alters the list supplied to the constructor
	of this cursor.  The cursor cannot be used once this method
	has been called.
	"""
	del self.q[0:self.l]
	if self.c != 0:
	    self.q[0] = self.q[0][self.c:]
	self.q = None
	self.c = None
	self.l = None

def parse_line(queue, want_session):
    """Parse one command line from the QUEUE of input.
    The QUEUE should be a list of strings.  Any parsed data will be
    destructively removed from it.  If a syntax error occurs the QUEUE
    will be cleared.

    A line is expected to contain:
       A command (consisting of any character but space)
       A single space.
       A session number (if, and only if, want_session is true).
       An optional space and hollerith-encoded string.
       A newline.
    Note that the hollerith-encoded string, if present, may contain
    any character (including newline).

    The return value is a list containing:
       The command (as a string).
       The session number (if, and only if, want_session is true).
       The string, or None if no hollerith-encoded string was present.
    """
    if want_session:
        nothing = [None, None, None]
    else:
        nothing = [None, None]

    try:
	c = cursor(queue)

	# Find the command.
	c.mark()
	while not c.peek() in " \n":
	    c.incr()
	cmd = c.extract()

	# Find the session number, if wanted.
	if want_session:
	    c.incr()
	    c.mark()
	    while c.peek() not in " \n":
		c.incr()
	    session = string.atoi(c.extract())

	# Find the string length, if any.
	if c.peek() == " ":
	    c.incr()
	    c.mark()
	    while c.peek() != "H":
		if c.peek() not in string.digits:
		    del queue[:]
		    return nothing
		c.incr()
	    sz = string.atoi(c.extract())
	    c.incr()
	    c.mark()
	    c.incr_many(sz)
	    data = c.extract()
	else:
	    data = None
	if c.peek() != "\n":
	    del queue[:]
	    return nothing
	c.incr()
	c.discard_handled()

	if want_session:
	    return [cmd, session, data]
	else:
	    return [cmd, data]

    except cursor_eof:
	return nothing


def hollerith(n):
    """Return a hollerith-encoding of N.  N may be a string of integer.
    """
    if type(n) == type(0):
	s = str(n)
    else:
	s = n
    return "%dH%s" % (len(s), s)

def set_nonblocking(fd):
    """Set file descriptor FD nonblocking.
    """
    oldflags = fcntl.fcntl(fd, fshcompat.F_GETFD)
    fcntl.fcntl(fd, fshcompat.F_SETFD, oldflags | fshcompat.O_NONBLOCK)

def read(fd, queue, sz):
    """Read at most SZ bytes from FD and store the result in QUEUE.
    Returns -1 on end of file, or 0 on success.  QUEUE should be a
    list of strings.  If anything is read it will be appended to
    QUEUE.
    """
    try:
	data = os.read(fd, sz)
    except os.error, (e, emsg):
	if e == errno.EINTR or e == errno.EAGAIN or e == errno.EWOULDBLOCK:
	    return 0
	else:
	    raise
    if data == "":
	return -1
    queue.append(data)
    return 0

def write(fd, queue):
    """Write the contents of QUEUE to FD.
    FD is assumed to be in non-blocking mode.  This function returns
    when there is nothing more to write, or when a write would block.
    Returns the number of bytes written, or -1 if the file descriptor
    was closed.

    This function may issue several os.write calls.  If a few of them
    succeeds and the file descriptor is closed -1 will be returned.

    All data that was written will be destructively removed from QUEUE.
    """
    ret = 0
    while 1: # Loop as long as we can write anything.
	# Make sure there is some data to write at the start of the queue.
	# Return if the queue is empty.
	while 1:
	    if len(queue) == 0:
		return ret
	    if len(queue[0]) == 0:
		del queue[0]
	    else:
		break

	# Avoid writing small chunks.
	while len(queue) > 1 and len(queue[0]) + len(queue[1]) < 4096:
	    queue[0] = queue[0] + queue[1]
	    del queue[1]

	# Write a chunk of data.  Return if failure.
	try:
	    sz = os.write(fd, queue[0])
	except os.error, (e, emsg):
	    if e == errno.EINTR or e == errno.EAGAIN or e == errno.EWOULDBLOCK:
		return ret
	    elif e == errno.EPIPE:
		return -1
	    else:
		raise
	if sz == 0:
	    raise "huh? zero-write?"

	# Remove the chunk just written.
	if sz == len(queue[0]):
	    del queue[0]
	else:
	    queue[0] = queue[0][sz:]
	ret = ret + sz

def print_version(prog):
    import fshversion
    print prog, "from fsh version", fshversion.version
    print "Copyright (C) 1999-2001 Per Cederqvist"
    sys.exit(0)

def fshd_socket(server, method, login):
    """Return the name of the fshd socket, and the containing directory.
    """

    # The shell-quote encoding is good enough for this purpose as well.
    # It would be enough to only quote "/" and "\0" here, as long as
    # we are using a traditional Unix-like filesystem.
    server = shell_quote(server)
    method = shell_quote(method)
    login = shell_quote(login)

    d =  "/tmp/fshd-%d" % (os.getuid(), )
    f = "%s.%s.%s" % (server, method, login)
    return (os.path.join(d, f), d)

__safe_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

def shell_quote(s):
    """Quote s so that it is safe for all common shells.
    """
    res = []
    for c in s:
        if c in __safe_chars:
            res.append(c)
        else:
            res.append("=%02x" % ord(c))
    return string.join(res, '')
    
def shell_unquote(s):
    """Unquote a string quoted by shell_quote.
    """
    if s == "":
        return ""

    frags = string.split(s, "=")
    res = [frags[0]]
    for f in frags[1:]:
        res.append(chr(string.atoi(f[:2], 0x10)))
        res.append(f[2:])
    return string.join(res, '')
