/*	$OpenBSD: client.c,v 1.19 2003/06/03 02:56:14 millert Exp $	*/

/*
 * Copyright (c) 1983 Regents of the University of California.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include "defs.h"
#include "y.tab.h"

#ifndef lint
#if 0
static char RCSid[] __attribute__((__unused__)) = 
"$From: client.c,v 1.13 1999/11/01 00:22:14 christos Exp $";
#else
static char RCSid[] __attribute__((__unused__)) = 
"$OpenBSD: client.c,v 1.19 2003/06/03 02:56:14 millert Exp $";
#endif

static char sccsid[] __attribute__((__unused__)) =
"@(#)client.c";

static char copyright[] __attribute__((__unused__)) =
"@(#) Copyright (c) 1983 Regents of the University of California.\n\
 All rights reserved.\n";
#endif /* not lint */

/*
 * Routines used in client mode to communicate with remove server.
 */


/*
 * Update status
 */
#define US_NOTHING 	0	/* No update needed */
#define US_NOENT	1	/* Entry does not exist */
#define US_OUTDATE	2	/* Entry is out of date */
#define US_DOCOMP	3	/* Do a binary comparison */
#define US_CHMOG	4	/* Modes or ownership of file differ */

struct	linkbuf *ihead = NULL;	/* list of files with more than one link */
char	buf[BUFSIZ];		/* general purpose buffer */
u_char	respbuff[BUFSIZ];	/* Response buffer */
char	target[BUFSIZ];		/* target/source directory name */
char	source[BUFSIZ];		/* source directory name */
char	*ptarget;		/* pointer to end of target name */
char	*Tdest;			/* pointer to last T dest*/
struct namelist	*updfilelist = NULL; /* List of updated files */

static void runspecial(char *, opt_t, char *, int);
static void addcmdspecialfile(char *, char *, int);
static void freecmdspecialfiles(void);
static struct linkbuf *linkinfo(struct stat *);
static int sendhardlink(opt_t, struct linkbuf *, char *, int);
static int sendfile(char *, opt_t, struct stat *, char *, char *, int);
static int rmchk(opt_t);
static int senddir(char *, opt_t, struct stat *, char *, char *, int);
static int sendlink(char *, opt_t, struct stat *, char *, char *, int);
static int update(char *, opt_t, struct stat *);
static int dostat(char *, struct stat *, opt_t);
static int statupdate(int, char *, opt_t, char *, int, struct stat *, char *, char *);
static int fullupdate(int, char *, opt_t, char *, int, struct stat *, char *, char *);
static int sendit(char *, opt_t, int);

/*
 * return remote file pathname (relative from target)
 */
char *
remfilename(char *src, char *dest, char *path, char *rname, int destdir)
{
	extern struct namelist *filelist;
	char *lname, *cp;
	static char buff[BUFSIZ];
	int srclen, pathlen;
	char *p;


	debugmsg(DM_MISC, 
		 "remfilename: src=%s dest=%s path=%s rname=%s destdir=%d\n",
		A(src), A(dest), A(path), A(rname), destdir);

	if (!dest) {
		debugmsg(DM_MISC, "remfilename: remote filename=%s\n", path);
		return(path);
	}

	if (!destdir) {
		debugmsg(DM_MISC, "remfilename: remote filename=%s\n", dest);
		return(dest);
	}

	buff[0] = CNULL;
	lname = buff;
	if (path && *path) {
		cp = strrchr(path, '/');
 		if (cp == NULL)
			(void) snprintf(buff, sizeof(buff), "%s/%s", dest, path);
		else {
			srclen = strlen(src);
			pathlen = strlen(path);
			if (srclen >= pathlen)
				cp++; /* xbasename(path) */
			else {
				if (filelist && filelist->n_next == NULL)
					/* path relative to src */
					cp = path + srclen;
				else {
					if ((p = strrchr(src, '/')))
						cp = path + srclen - strlen(p);
					else
						cp = path;
				}
			}
			if ((*cp != '/') && *cp)
				(void) snprintf(buff, sizeof(buff), "%s/%s",
						dest, cp);
			else
				(void) snprintf(buff, sizeof(buff), "%s%s",
						dest, cp);
		}
	} else
		(void) strlcpy(lname, dest, buf + sizeof buff - lname);

	debugmsg(DM_MISC, "remfilename: remote filename=%s\n", lname);

	return(lname);
}

/*
 * Return true if name is in the list.
 */
int
inlist(struct namelist *list, char *file)
{
	struct namelist *nl;

	for (nl = list; nl != NULL; nl = nl->n_next)
		if (strcmp(file, nl->n_name) == 0)
			return(1);
	return(0);
}

/*
 * Run any special commands for this file
 */
static void
runspecial(char *starget, opt_t opts, char *rname, int destdir)
{
	struct subcmd *sc;
	extern struct subcmd *subcmds;
	char *rfile;

 	rfile = remfilename(source, Tdest, target, rname, destdir);

	for (sc = subcmds; sc != NULL; sc = sc->sc_next) {
		if (sc->sc_type != SPECIAL)
			continue;
		if (sc->sc_args != NULL && !inlist(sc->sc_args, starget))
			continue;
		message(MT_CHANGE, "special \"%s\"", sc->sc_name);
		if (IS_ON(opts, DO_VERIFY))
			continue;
		(void) sendcmd(C_SPECIAL,
			"%s=%s;%s=%s;%s=%s;export %s %s %s;%s",
			E_LOCFILE, starget,
			E_REMFILE, rfile,
			E_BASEFILE, xbasename(rfile),
			E_LOCFILE, E_REMFILE, E_BASEFILE,
			sc->sc_name);
		while (response() > 0)
			;
	}
}

/*
 * If we're doing a target with a "cmdspecial" in it, then
 * save the name of the file being updated for use with "cmdspecial".
 */
static void
addcmdspecialfile(char *starget, char *rname, int destdir)
{
	char *rfile;
	struct namelist *new;
	struct subcmd *sc;
	extern struct subcmd *subcmds;
	int isokay = 0;

 	rfile = remfilename(source, Tdest, target, rname, destdir);

	for (sc = subcmds; sc != NULL && !isokay; sc = sc->sc_next) {
		if (sc->sc_type != CMDSPECIAL)
			continue;
		if (sc->sc_args != NULL && !inlist(sc->sc_args, starget))
			continue;
		isokay = TRUE;
	}

	if (isokay) {
		new = (struct namelist *) xmalloc(sizeof(struct namelist));
		new->n_name = xstrdup(rfile);
		new->n_regex = NULL;
		new->n_next = updfilelist;
		updfilelist = new;
	}
}

/*
 * Free the file list
 */
static void
freecmdspecialfiles(void)
{
	struct namelist *ptr, *save;

	for (ptr = updfilelist; ptr; ) {
		if (ptr->n_name) (void) free(ptr->n_name);
		save = ptr->n_next;
		(void) free(ptr);
		if (save)
			ptr = save->n_next;
		else
			ptr = NULL;
	}
	updfilelist = NULL;
}

/*
 * Run commands for an entire cmd
 */
void
runcmdspecial(struct cmd *cmd, opt_t opts)
{
	struct subcmd *sc;
	struct namelist *f;
	int first = TRUE;

	for (sc = cmd->c_cmds; sc != NULL; sc = sc->sc_next) {
		if (sc->sc_type != CMDSPECIAL)
			continue;
		message(MT_CHANGE, "cmdspecial \"%s\"", sc->sc_name);
		if (IS_ON(opts, DO_VERIFY))
			continue;
		/* Send all the file names */
		for (f = updfilelist; f != NULL; f = f->n_next) {
			if (first) {
				(void) sendcmd(C_CMDSPECIAL, NULL);
				if (response() < 0)
					return;
				first = FALSE;
			}
			(void) sendcmd(RC_FILE, f->n_name);
			if (response() < 0)
				return;
		}
		if (first) {
			(void) sendcmd(C_CMDSPECIAL, NULL);
			if (response() < 0)
				return;
			first = FALSE;
		}
		/* Send command to run and wait for it to complete */
		(void) sendcmd(RC_COMMAND, sc->sc_name);
		while (response() > 0)
			;
		first = TRUE;	/* Reset in case there are more CMDSPECIAL's */
	}
	freecmdspecialfiles();
}

/*
 * For security, reject filenames that contains a newline
 */
int
checkfilename(char *name)
{
	char *cp;

	if (strchr(name, '\n')) {
		for (cp = name; *cp; cp++)
			if (*cp == '\n')
				*cp = '?';
		message(MT_NERROR, 
			"Refuse to handle filename containing newline: %s",
			name);
		return(-1);
	}

	return(0);
}

void
freelinkinfo(struct linkbuf *lp)
{
	if (lp->pathname)
		free(lp->pathname);
	if (lp->src)
		free(lp->src);
	if (lp->target)
		free(lp->target);
	free(lp);
}

/*
 * Save and retrieve hard link info
 */
static struct linkbuf *
linkinfo(struct stat *statp)
{
	struct linkbuf *lp;

	/* XXX - linear search doesn't scale with many links */
	for (lp = ihead; lp != NULL; lp = lp->nextp)
		if (lp->inum == statp->st_ino && lp->devnum == statp->st_dev) {
			lp->count--;
			return(lp);
		}

	lp = (struct linkbuf *) xmalloc(sizeof(*lp));
	lp->nextp = ihead;
	ihead = lp;
	lp->inum = statp->st_ino;
	lp->devnum = statp->st_dev;
	lp->count = statp->st_nlink - 1;
	lp->pathname = xstrdup(target);
	lp->src = xstrdup(source);
	if (Tdest)
		lp->target = xstrdup(Tdest);
	else
		lp->target = NULL;

	return(NULL);
}

/*
 * Send a hardlink
 */
static int
sendhardlink(opt_t opts, struct linkbuf *lp, char *rname, int destdir)
{
	static char buff[MAXPATHLEN];
	char *lname;	/* name of file to link to */
	char ername[MAXPATHLEN*4], elname[MAXPATHLEN*4];

	debugmsg(DM_MISC, 
	       "sendhardlink: rname='%s' pathname='%s' src='%s' target='%s'\n",
		rname, lp->pathname ? lp->pathname : "",
		lp->src ? lp->src : "", lp->target ? lp->target : "");
		 
	if (lp->target == NULL)
		lname = lp->pathname;
	else {
		lname = buff;
		strlcpy(lname, remfilename(lp->src, lp->target, 
					  lp->pathname, rname, 
					  destdir), sizeof(buff));
		debugmsg(DM_MISC, "sendhardlink: lname=%s\n", lname);
	}
	ENCODE(elname, lname);
	ENCODE(ername, rname);
	(void) sendcmd(C_RECVHARDLINK, "%o %s %s", 
		       opts, elname, ername);

	return(response());
}

/*
 * Send a file
 */
static int
sendfile(char *rname, opt_t opts, struct stat *stb, char *user,
	 char *group, int destdir)
{
	int goterr, f;
	off_t i;
	char ername[MAXPATHLEN*4];

	if (stb->st_nlink > 1) {
		struct linkbuf *lp;
		
		if ((lp = linkinfo(stb)) != NULL)
			return(sendhardlink(opts, lp, rname, destdir));
	}

	if ((f = open(target, O_RDONLY)) < 0) {
		error("%s: open for read failed: %s", target, SYSERR);
		return(-1);
	}

	/*
	 * Send file info
	 */
	ENCODE(ername, rname);

	(void) sendcmd(C_RECVREG, "%o %04o %ld %ld %ld %s %s %s", 
		       opts, stb->st_mode & 07777, (long) stb->st_size, 
		       stb->st_mtime, stb->st_atime,
		       user, group, ername);
	if (response() < 0) {
		(void) close(f);
		return(-1);
	}


	debugmsg(DM_MISC, "Send file '%s' %ld bytes\n", rname,
		 (long) stb->st_size);

	/*
	 * Set remote time out alarm handler.
	 */
	(void) signal(SIGALRM, sighandler);

	/*
	 * Actually transfer the file
	 */
	goterr = 0;
	for (i = 0; i < stb->st_size; i += BUFSIZ) {
		off_t amt = BUFSIZ;

		(void) alarm(rtimeout);
		if (i + amt > stb->st_size)
			amt = stb->st_size - i;
		if (read(f, buf, (size_t) amt) != (ssize_t) amt) {
			error("%s: File changed size", target);
			err();
			++goterr;
			/*
			 * XXX - We have to keep going because the
			 * server expects to receive a fixed number
			 * of bytes that we specified as the file size.
			 * We need Out Of Band communication to handle
			 * this situation gracefully.
			 */
		}
		if (xwrite(rem_w, buf, (size_t) amt) < 0) {
			error("%s: Error writing to client: %s", 
			      target, SYSERR);
			err();
			++goterr;
			break;
		}
		(void) alarm(0);
	}

	(void) alarm(0);	/* Insure alarm is off */
	(void) close(f);

	debugmsg(DM_MISC, "Send file '%s' %s.\n", 
		 (goterr) ? "failed" : "complete", rname);

	/*
	 * Check for errors and end send
	 */
	if (goterr)
		return(-1);
	else {
		ack();
		f = response();
		if (f < 0)
			return(-1);
		else if (f == 0 && IS_ON(opts, DO_COMPARE))
			return(0);

		runspecial(target, opts, rname, destdir);
		addcmdspecialfile(target, rname, destdir);

		return(0);
	}
}

/*
 * Check for files on the machine being updated that are not on the master
 * machine and remove them.
 *
 * Return < 0 on error.
 * Return 0 if nothing happened.
 * Return > 0 if anything is updated.
 */
static int
rmchk(opt_t opts)
{
	u_char *s;
	struct stat stb;
	int didupdate = 0;
	int n;
	char targ[MAXPATHLEN*4];

	debugmsg(DM_CALL, "rmchk()\n");

	/*
	 * Tell the remote to clean the files from the last directory sent.
	 */
	(void) sendcmd(C_CLEAN, "%o", IS_ON(opts, DO_VERIFY));
	if (response() < 0)
		return(-1);

	for ( ; ; ) {
		n = remline(s = respbuff, sizeof(respbuff), TRUE);
		if (n <= 0) {
			error("rmchk: unexpected control record");
			return(didupdate);
		}

		switch (*s++) {
		case CC_QUERY: /* Query if file should be removed */
			/*
			 * Return the following codes to remove query.
			 * CC_NO -- file exists - DON'T remove.
			 * CC_YES -- file doesn't exist - REMOVE.
			 */
			if (DECODE(targ, (char *) s) == -1) {
				error("rmchk: cannot decode file");
				return(-1);
			}
			(void) snprintf(ptarget,
					sizeof(target) - (ptarget - target),
					"%s%s", 
				        (ptarget[-1] == '/' ? "" : "/"),
				        targ);
			debugmsg(DM_MISC, "check %s\n", target);
			if (except(target))
				(void) sendcmd(CC_NO, NULL);
			else if (lstat(target, &stb) < 0) {
				if (sendcmd(CC_YES, NULL) == 0)
					didupdate = 1;
			} else
				(void) sendcmd(CC_NO, NULL);
			break;

		case CC_END:
			*ptarget = CNULL;
			ack();
			return(didupdate);

		case C_LOGMSG:
			if (n > 0)
				message(MT_INFO, "%s", s);
			break;

		case C_NOTEMSG:
			if (n > 0)
				message(MT_NOTICE, "%s", s);
			break;
			/* Goto top of loop */

		case C_ERRMSG:
			message(MT_NERROR, "%s", s);
			return(didupdate);

		case C_FERRMSG:
			message(MT_FERROR, "%s", s);
			finish();

		default:
			error("rmchk: unexpected response '%s'", respbuff);
			err();
		}
	}
	/*NOTREACHED*/
}

/*
 * Send a directory
 *
 * Return < 0 on error.
 * Return 0 if nothing happened.
 * Return > 0 if anything is updated.
 */
static int
senddir(char *rname, opt_t opts, struct stat *stb, char *user,
	char *group, int destdir)
{
	DIRENTRY *dp;
	DIR *d;
	char *optarget, *cp;
	int len;
	int didupdate = 0;
	char ername[MAXPATHLEN*4];

	/*
	 * Send recvdir command in recvit() format.
	 */
	ENCODE(ername, rname);
	(void) sendcmd(C_RECVDIR, "%o %04o 0 0 0 %s %s %s", 
		       opts, stb->st_mode & 07777, user, group, ername);
	if (response() < 0)
		return(-1);

	/*
	 * Don't descend into directory
	 */
	if (IS_ON(opts, DO_NODESCEND))
		return(0);

	if (IS_ON(opts, DO_REMOVE))
		if (rmchk(opts) > 0)
			++didupdate;
	
	if ((d = opendir(target)) == NULL) {
		error("%s: opendir failed: %s", target, SYSERR);
		return(-1);
	}

	optarget = ptarget;
	len = ptarget - target;
	while ((dp = readdir(d)) != NULL) {
		if (!strcmp(dp->d_name, ".") ||
		    !strcmp(dp->d_name, ".."))
			continue;
		if (len + 1 + (int) strlen(dp->d_name) >= MAXPATHLEN - 1) {
			error("%s/%s: Name too long", target,
			      dp->d_name);
			continue;
		}
		ptarget = optarget;
		if (ptarget[-1] != '/')
			*ptarget++ = '/';
		cp = dp->d_name;
		while ((*ptarget++ = *cp++) != '\0')
			continue;
		ptarget--;
		if (sendit(dp->d_name, opts, destdir) > 0)
			didupdate = 1;
	}
	(void) closedir(d);

	(void) sendcmd(C_END, NULL);
	(void) response();

	ptarget = optarget;
	*ptarget = CNULL;

	return(didupdate);
}

/*
 * Send a link
 */
static int
sendlink(char *rname, opt_t opts, struct stat *stb, char *user,
	 char *group, int destdir)
{
	int f, n;
	static char tbuf[BUFSIZ];
	char lbuf[MAXPATHLEN];
	u_char *s;
	char ername[MAXPATHLEN*4];

	debugmsg(DM_CALL, "sendlink(%s, %x, stb, %d)\n", rname, opts, destdir);

	if (stb->st_nlink > 1) {
		struct linkbuf *lp;
		
		if ((lp = linkinfo(stb)) != NULL)
			return(sendhardlink(opts, lp, rname, destdir));
	}

	/*
	 * Gather and send basic link info
	 */
	ENCODE(ername, rname);
	(void) sendcmd(C_RECVSYMLINK, "%o %04o %ld %ld %ld %s %s %s", 
		       opts, stb->st_mode & 07777, (long) stb->st_size, 
		       stb->st_mtime, stb->st_atime,
		       user, group, ername);
	if (response() < 0)
		return(-1);

	/*
	 * Gather and send additional link info
	 */
	if ((n = readlink(target, lbuf, sizeof(lbuf)-1)) != -1)
		lbuf[n] = '\0';
	else {
		error("%s: readlink failed", target);
		err();
	}
	(void) snprintf(tbuf, sizeof(tbuf), "%.*s", (int) stb->st_size, lbuf);
	ENCODE(ername, tbuf);
	(void) sendcmd(C_NONE, "%s\n", ername);

	if (n != stb->st_size) {
		error("%s: file changed size", target);
		err();
	} else
		ack();

	/*
	 * Check response
	 */
	f = response();
	if (f < 0)
		return(-1);
	else if (f == 0 && IS_ON(opts, DO_COMPARE))
		return(0);

	/*
	 * Read and process responses from server.
	 * The server may send multiple messages regarding
	 * file deletes if the remote target is a directory.
	 */
	for (;;) {
		n = remline(s = respbuff, sizeof(respbuff), TRUE);
		if (n == -1)	/* normal EOF */
			return(0);
		if (n == 0) {
			error("expected control record");
			continue;
		}
		
		switch (*s++) {
		case C_END:	/* End of send operation */
			*ptarget = CNULL;
			ack();
			runspecial(target, opts, rname, destdir);
			addcmdspecialfile(target, rname, destdir);
			return(0);
			
		case C_LOGMSG:
			if (n > 0)
				message(MT_INFO, "%s", s);
			break;

		case C_NOTEMSG:
			if (n > 0)
				message(MT_NOTICE, "%s", s);
			break;
			/* Goto top of loop */

		case C_ERRMSG:
			message(MT_NERROR, "%s", s);
			return(-1);

		case C_FERRMSG:
			message(MT_FERROR, "%s", s);
			finish();

		default:
			error("install link: unexpected response '%s'", 
			      respbuff);
			err();
		}
	}
	/*NOTREACHED*/
}

/*
 * Check to see if file needs to be updated on the remote machine.
 * Returns:
 * 	US_NOTHING	- no update
 *	US_NOENT	- remote doesn't exist
 *	US_OUTDATE	- out of date
 *	US_DOCOMP	- comparing binaries to determine if out of date
 *	US_CHMOG	- File modes or ownership do not match
 */
static int
update(char *rname, opt_t opts, struct stat *statp)
{
	off_t size;
	time_t mtime;
	unsigned short lmode;
	unsigned short rmode;
	char *owner = NULL, *group = NULL;
	int done, n;
	u_char *cp;
	char ername[MAXPATHLEN*4];

	debugmsg(DM_CALL, "update(%s, 0x%x, 0x%x)\n", rname, opts, statp);

	if (IS_ON(opts, DO_NOEXEC))
		if (isexec(target, statp)) {
			debugmsg(DM_MISC, "%s is an executable\n", target);
			return(US_NOTHING);
		}

	/*
	 * Check to see if the file exists on the remote machine.
	 */
	ENCODE(ername, rname);
	(void) sendcmd(C_QUERY, "%s", ername);

	for (done = 0; !done;) {
		n = remline(cp = respbuff, sizeof(respbuff), TRUE);
		if (n <= 0) {
			error("update: unexpected control record in response to query");
			return(US_NOTHING);
		}

		switch (*cp++) {
		case QC_ONNFS:  /* Resides on a NFS */
			debugmsg(DM_MISC,
				 "update: %s is on a NFS.  Skipping...\n", 
				 rname);
			return(US_NOTHING);

		case QC_SYM:  /* Is a symbolic link */
			debugmsg(DM_MISC,
				 "update: %s is a symlink.  Skipping...\n", 
				 rname);
			return(US_NOTHING);

		case QC_ONRO:  /* Resides on a Read-Only fs */
			debugmsg(DM_MISC,
				 "update: %s is on a RO fs.  Skipping...\n", 
				 rname);
			return(US_NOTHING);
			
		case QC_YES:
			done = 1;
			break;

		case QC_NO:  /* file doesn't exist so install it */
			return(US_NOENT);

		case C_ERRMSG:
			if (cp)
				message(MT_NERROR, "%s", cp);
			return(US_NOTHING);

		case C_FERRMSG:
			if (cp)
				message(MT_FERROR, "%s", cp);
			finish();

		case C_NOTEMSG:
			if (cp)
				message(MT_NOTICE, "%s", cp);
			break;
			/* Goto top of loop */

		default:
			error("update: unexpected response to query '%s'", respbuff);
			return(US_NOTHING);
		}
	}

	/*
	 * Target exists, but no other info passed
	 */
	if (n <= 1 || !S_ISREG(statp->st_mode))
		return(US_OUTDATE);

	if (IS_ON(opts, DO_COMPARE))
		return(US_DOCOMP);

	/*
	 * Parse size
	 */
	size = (off_t) strtol(cp, (char **)&cp, 10);
	if (*cp++ != ' ') {
		error("update: size not delimited");
		return(US_NOTHING);
	}

	/*
	 * Parse mtime
	 */
	mtime = strtol(cp, (char **)&cp, 10);
	if (*cp++ != ' ') {
		error("update: mtime not delimited");
		return(US_NOTHING);
	}

	/*
	 * Parse remote file mode
	 */
	rmode = strtol(cp, (char **)&cp, 8);
	if (cp && *cp)
		++cp;

	/*
	 * Be backwards compatible
	 */
	if (cp && *cp != CNULL) {
		/*
		 * Parse remote file owner
		 */
		owner = strtok((char *)cp, " ");
		if (owner == NULL) {
			error("update: owner not delimited");
			return(US_NOTHING);
		}

		/*
		 * Parse remote file group
		 */
		group = strtok(NULL, " ");
		if (group == NULL) {
			error("update: group not delimited");
			return(US_NOTHING);
		}
	}

	/*
	 * File needs to be updated?
	 */
	lmode = statp->st_mode & 07777;

	debugmsg(DM_MISC, "update(%s,) local mode %04o remote mode %04o\n", 
		 rname, lmode, rmode);
	debugmsg(DM_MISC, "update(%s,) size %ld mtime %d owner '%s' grp '%s'\n",
		 rname, (long) size, mtime, owner, group);

	if (statp->st_mtime != mtime) {
		if (statp->st_mtime < mtime && IS_ON(opts, DO_YOUNGER)) {
			message(MT_WARNING, 
				"%s: Warning: remote copy is newer",
				target);
			return(US_NOTHING);
		}
		return(US_OUTDATE);
	}

	if (statp->st_size != size) {
		debugmsg(DM_MISC, "size does not match (%ld != %ld).\n",
			 (long) statp->st_size, (long) size);
		return(US_OUTDATE);
	} 

	if (!IS_ON(opts, DO_NOCHKMODE) && lmode != rmode) {
		debugmsg(DM_MISC, "modes do not match (%04o != %04o).\n",
			 lmode, rmode);
		return(US_CHMOG);
	}


	/*
	 * Check ownership
	 */
	if (!IS_ON(opts, DO_NOCHKOWNER) && owner) {
		if (!IS_ON(opts, DO_NUMCHKOWNER)) {
			/* Check by string compare */
			if (strcmp(owner, getusername(statp->st_uid, 
						      target, opts)) != 0) {
				debugmsg(DM_MISC, 
					 "owner does not match (%s != %s).\n",
					 getusername(statp->st_uid, 
						     target, opts), owner);
				return(US_CHMOG);
			}
		} else {
			/* 
			 * Check numerically.
			 * Allow negative numbers.
			 */
			while (*owner && !isdigit((unsigned char)*owner) &&
			    (*owner != '-'))
				++owner;
			if (owner && (UID_T) atoi(owner) != statp->st_uid) {
				debugmsg(DM_MISC, 
					 "owner does not match (%d != %s).\n",
					 statp->st_uid, owner);
				return(US_CHMOG);
			}
		}
	} 

	if (!IS_ON(opts, DO_NOCHKGROUP) && group) {
		if (!IS_ON(opts, DO_NUMCHKGROUP)) {
			/* Check by string compare */
			if (strcmp(group, getgroupname(statp->st_gid, 
						       target, opts)) != 0) {
				debugmsg(DM_MISC, 
					 "group does not match (%s != %s).\n",
					 getgroupname(statp->st_gid, 
						      target, opts), group);
				return(US_CHMOG);
			}
		} else {	
			/* Check numerically */
			/* Allow negative gid */
			while (*group && !isdigit((unsigned char) *group) &&
			    (*group != '-'))
				++group;
			if (group && (UID_T) atoi(group) != statp->st_gid) {
				debugmsg(DM_MISC,
					 "group does not match (%d != %s).\n",
					 statp->st_gid, group);
				return(US_CHMOG);
			}
		}
	}

	return(US_NOTHING);
}

/*
 * Stat a file
 */
static int
dostat(char *file, struct stat *statbuf, opt_t opts)
{
	int s;

	if (IS_ON(opts, DO_FOLLOW))
		s = stat(file, statbuf);
	else
		s = lstat(file, statbuf);

	if (s < 0)
		error("%s: %s failed: %s", file,
		      IS_ON(opts, DO_FOLLOW) ? "stat" : "lstat", SYSERR);
	return(s);
}

/*
 * We need to just change file info.
 */
static int
statupdate(int u, char *target, opt_t opts, char *rname, int destdir,
	   struct stat *st, char *user, char *group)
{
	int rv = 0;
	char ername[MAXPATHLEN*4];
	int lmode = st->st_mode & 07777;

	if (u == US_CHMOG) {
		if (IS_ON(opts, DO_VERIFY)) {
			message(MT_INFO,
				"%s: need to change to perm %04o, owner %s, group %s",
				target, lmode, user, group);
			runspecial(target, opts, rname, destdir);
		}
		else {
			message(MT_CHANGE, "%s: change to perm %04o, owner %s, group %s", 
				target, lmode, user, group);
			ENCODE(ername, rname);
			(void) sendcmd(C_CHMOG, "%o %04o %s %s %s",
				       opts, lmode, user, group, ername);
			(void) response();
		}
		rv = 1;
	}
	return(rv);
}


/*
 * We need to install/update:
 */
static int
fullupdate(int u, char *target, opt_t opts, char *rname, int destdir,
	   struct stat *st, char *user, char *group)
{
	/*
	 * No entry - need to install
	 */
	if (u == US_NOENT) {
		if (IS_ON(opts, DO_VERIFY)) {
			message(MT_INFO, "%s: need to install", target);
			runspecial(target, opts, rname, destdir);
			return(1);
		}
		if (!IS_ON(opts, DO_QUIET))
			message(MT_CHANGE, "%s: installing", target);
		FLAG_OFF(opts, (DO_COMPARE|DO_REMOVE));
	}

	/*
	 * Handle special file types, including directories and symlinks
	 */
	if (S_ISDIR(st->st_mode)) {
		if (senddir(rname, opts, st, user, group, destdir) > 0)
			return(1);
		return(0);
	} else if (S_ISLNK(st->st_mode)) {
		if (u == US_NOENT)
			FLAG_ON(opts, DO_COMPARE);
		/*
		 * Since we always send link info to the server
		 * so the server can determine if the remote link
		 * is correct, we never get any acknowledge meant
		 * from the server whether the link was really
		 * updated or not.
		 */
		(void) sendlink(rname, opts, st, user, group, destdir);
		return(0);
	} else if (S_ISREG(st->st_mode)) {		
		if (u == US_OUTDATE) {
			if (IS_ON(opts, DO_VERIFY)) {
				message(MT_INFO, "%s: need to update", target);
				runspecial(target, opts, rname, destdir);
				return(1);
			}
			if (!IS_ON(opts, DO_QUIET))
				message(MT_CHANGE, "%s: updating", target);
		}
		return (sendfile(rname, opts, st, user, group, destdir) == 0);
	} else {
		message(MT_INFO, "%s: unknown file type 0%o", target,
			st->st_mode);
		return(0);
	}
}

/*
 * Transfer the file or directory in target[].
 * rname is the name of the file on the remote host.
 *
 * Return < 0 on error.
 * Return 0 if nothing happened.
 * Return > 0 if anything is updated.
 */
static int
sendit(char *rname, opt_t opts, int destdir)
{
	static struct stat stb;
	char *user, *group;
	int u, len;

	/*
	 * Remove possible accidental newline
	 */
	len = strlen(rname);
	if (len > 0 && rname[len-1] == '\n')
		rname[len-1] = CNULL;

	if (checkfilename(rname) != 0)
		return(-1);

	debugmsg(DM_CALL, "sendit(%s, 0x%x) called\n", rname, opts);

	if (except(target))
		return(0);

	if (dostat(target, &stb, opts) < 0)
		return(-1);

	/*
	 * Does rname need updating?
	 */
	u = update(rname, opts, &stb);
	debugmsg(DM_MISC, "sendit(%s, 0x%x): update status of %s is %d\n", 
		 rname, opts, target, u);

	/*
	 * Don't need to update the file, but we may need to save hardlink
	 * info.
	 */
	if (u == US_NOTHING) {
		if (S_ISREG(stb.st_mode) && stb.st_nlink > 1)
			(void) linkinfo(&stb);
		return(0);
	}

	user = getusername(stb.st_uid, target, opts);
	group = getgroupname(stb.st_gid, target, opts);

	if (u == US_CHMOG && IS_OFF(opts, DO_UPDATEPERM))
		u = US_OUTDATE;

	if (u == US_NOENT || u == US_OUTDATE || u == US_DOCOMP)
		return(fullupdate(u, target, opts, rname, destdir, &stb,
				  user, group));

	if (u == US_CHMOG)
		return(statupdate(u, target, opts, rname, destdir, &stb,
				  user, group));

	return(0);
}
	
/*
 * Remove temporary files and do any cleanup operations before exiting.
 */
void
cleanup(int dummy)
{
	char *file;
#ifdef USE_STATDB
	extern char statfile[];

	(void) unlink(statfile);
#endif

	if ((file = getnotifyfile()) != NULL)
		(void) unlink(file);
}

/*
 * Update the file(s) if they are different.
 * destdir = 1 if destination should be a directory
 * (i.e., more than one source is being copied to the same destination).
 *
 * Return < 0 on error.
 * Return 0 if nothing updated.
 * Return > 0 if something was updated.
 */
int
install(char *src, char *dest, int ddir, int destdir, opt_t opts)
{
	static char destcopy[MAXPATHLEN];
	char *rname;
	int didupdate = 0;
	char ername[MAXPATHLEN*4];

	debugmsg(DM_CALL,
		"install(src=%s,dest=%s,ddir=%d,destdir=%d,opts=%d) start\n",
		(src?src:"NULL"), (dest?dest:"NULL"), ddir, destdir, opts);
	/*
	 * Save source name
	 */
	if (IS_ON(opts, DO_WHOLE))
		source[0] = CNULL;
	else
		(void) strlcpy(source, src, sizeof(source));

	if (dest == NULL) {
		FLAG_OFF(opts, DO_WHOLE); /* WHOLE only useful if renaming */
		dest = src;
	}

	if (checkfilename(dest) != 0)
		return(-1);

	if (nflag || debug) {
		static char buff[BUFSIZ];
		char *cp;

		cp = getondistoptlist(opts);
		(void) snprintf(buff, sizeof(buff), "%s%s%s %s %s", 
			       IS_ON(opts, DO_VERIFY) ? "verify" : "install",
			       (cp) ? " -o" : "", (cp) ? cp : "", 
			       src, dest);
		if (nflag) {
			printf("%s\n", buff);
			return(0);
		} else
			debugmsg(DM_MISC, "%s\n", buff);
	}

	rname = exptilde(target, src, sizeof(target));
	if (rname == NULL)
		return(-1);
	ptarget = target;
	while (*ptarget)
		ptarget++;
	/*
	 * If we are renaming a directory and we want to preserve
	 * the directory heirarchy (-w), we must strip off the leading
	 * directory name and preserve the rest.
	 */
	if (IS_ON(opts, DO_WHOLE)) {
		while (*rname == '/')
			rname++;
		ddir = 1;
		destdir = 1;
	} else {
		rname = strrchr(target, '/');
		/* Check if no '/' or target ends in '/' */
		if (rname == NULL || 
		    rname+1 == NULL || 
		    *(rname+1) == CNULL)
			rname = target;
		else
			rname++;
	}

	debugmsg(DM_MISC, 
 	"install: target=%s src=%s rname=%s dest='%s' destdir=%d, ddir=%d\n", 
 		 target, source, rname, dest, destdir, ddir);

	/*
	 * Pass the destination file/directory name to remote.
	 */
	ENCODE(ername, dest);
 	if (ddir)
		(void) sendcmd(C_DIRTARGET, "%o %s", opts, ername);
	else
		(void) sendcmd(C_TARGET, "%o %s", opts, ername);
	if (response() < 0)
		return(-1);

	/*
	 * Save the name of the remote target destination if we are
	 * in WHOLE mode (destdir > 0) or if the source and destination
	 * are not the same.  This info will be used later for maintaining
	 * hardlink info.
	 */
	if (destdir || (src && dest && strcmp(src, dest))) {
		(void) strlcpy(destcopy, dest, sizeof(destcopy));
		Tdest = destcopy;
	}

	didupdate = sendit(rname, opts, destdir);
	Tdest = 0;

	return(didupdate);
}