/*	$NetBSD: cmds.c,v 1.35 2016/01/17 14:46:07 christos Exp $	*/

/*
 * Copyright (c) 1999-2009 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Luke Mewburn.
 *
 * 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.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. 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 FOUNDATION 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.
 */

/*
 * Copyright (c) 1985, 1988, 1990, 1992, 1993, 1994
 *	The 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.
 */

/*
 * Copyright (C) 1997 and 1998 WIDE Project.
 * 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 project 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 PROJECT 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 PROJECT 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 <sys/cdefs.h>
#ifndef lint
__RCSID("$NetBSD: cmds.c,v 1.35 2016/01/17 14:46:07 christos Exp $");
#endif /* not lint */

#include <sys/param.h>
#include <sys/stat.h>

#include <arpa/ftp.h>

#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tzfile.h>
#include <unistd.h>
#include <ctype.h>

#ifdef KERBEROS5
#include <krb5/krb5.h>
#endif

#include "extern.h"

typedef enum {
	FE_MLSD		= 1<<0,		/* if op is MLSD (MLST otherwise ) */
	FE_ISCURDIR	= 1<<1,		/* if name is the current directory */
} factflag_t;

typedef struct {
	const char	*path;		/* full pathname */
	const char	*display;	/* name to display */
	struct stat	*stat;		/* stat of path */
	struct stat	*pdirstat;	/* stat of path's parent dir */
	factflag_t	 flags;		/* flags */
} factelem;

static void	ack(const char *);
static void	base64_encode(const char *, size_t, char *, int);
static void	fact_type(const char *, FILE *, factelem *);
static void	fact_size(const char *, FILE *, factelem *);
static void	fact_modify(const char *, FILE *, factelem *);
static void	fact_perm(const char *, FILE *, factelem *);
static void	fact_unique(const char *, FILE *, factelem *);
static int	matchgroup(gid_t);
static void	mlsname(FILE *, factelem *);
static void	replydirname(const char *, const char *);

struct ftpfact {
	const char	 *name;		/* name of fact */
	int		  enabled;	/* if fact is enabled */
	void		(*display)(const char *, FILE *, factelem *);
					/* function to display fact */
};

struct ftpfact facttab[] = {
	{ "Type",	1, fact_type },
#define	FACT_TYPE 0
	{ "Size",	1, fact_size },
	{ "Modify",	1, fact_modify },
	{ "Perm",	1, fact_perm },
	{ "Unique",	1, fact_unique },
	/* "Create" */
	/* "Lang" */
	/* "Media-Type" */
	/* "CharSet" */
};

#define FACTTABSIZE	(sizeof(facttab) / sizeof(struct ftpfact))

static char cached_path[MAXPATHLEN + 1] = "/";
static void discover_path(char *, const char *);

void
cwd(const char *path)
{

	if (chdir(path) < 0)
		perror_reply(550, path);
	else {
		show_chdir_messages(250);
		ack("CWD");
		if (getcwd(cached_path, MAXPATHLEN) == NULL) {
			discover_path(cached_path, path);
		}
	}
}

void
delete(const char *name)
{
	char *p = NULL;

	if (remove(name) < 0) {
		p = strerror(errno);
		perror_reply(550, name);
	} else
		ack("DELE");
	logxfer("delete", -1, name, NULL, NULL, p);
}

void
feat(void)
{
	size_t i;

	reply(-211, "Features supported");
	cprintf(stdout, " MDTM\r\n");
	cprintf(stdout, " MLST ");
	for (i = 0; i < FACTTABSIZE; i++)
		cprintf(stdout, "%s%s;", facttab[i].name,
		    facttab[i].enabled ? "*" : "");
	cprintf(stdout, "\r\n");
	cprintf(stdout, " REST STREAM\r\n");
	cprintf(stdout, " SIZE\r\n");
	cprintf(stdout, " TVFS\r\n");
	reply(211,  "End");
}

void
makedir(const char *name)
{
	char *p = NULL;

	if (mkdir(name, 0777) < 0) {
		p = strerror(errno);
		perror_reply(550, name);
	} else
		replydirname(name, "directory created.");
	logxfer("mkdir", -1, name, NULL, NULL, p);
}

void
mlsd(const char *path)
{
	struct dirent	*dp;
	struct stat	 sb, pdirstat;
	factelem f;
	FILE	*dout;
	DIR	*dirp;
	char	name[MAXPATHLEN];
	int	hastypefact;

	hastypefact = facttab[FACT_TYPE].enabled;
	if (path == NULL)
		path = ".";
	if (stat(path, &pdirstat) == -1) {
 mlsdperror:
		perror_reply(550, path);
		return;
	}
	if (! S_ISDIR(pdirstat.st_mode)) {
		errno = ENOTDIR;
		perror_reply(501, path);
		return;
	}
	if ((dirp = opendir(path)) == NULL)
		goto mlsdperror;

	dout = dataconn("MLSD", (off_t)-1, "w");
	if (dout == NULL) {
		(void) closedir(dirp);
		return;
	}

	memset(&f, 0, sizeof(f));
	f.stat = &sb;
	f.flags |= FE_MLSD;
	while ((dp = readdir(dirp)) != NULL) {
		snprintf(name, sizeof(name), "%s/%s", path, dp->d_name);
		if (ISDOTDIR(dp->d_name)) {	/* special case curdir: */
			if (! hastypefact)
				continue;
			f.pdirstat = NULL;	/*   require stat of parent */
			f.display = path;	/*   set name to real name */
			f.flags |= FE_ISCURDIR; /*   flag name is curdir */
		} else {
			if (ISDOTDOTDIR(dp->d_name)) {
				if (! hastypefact)
					continue;
				f.pdirstat = NULL;
			} else
				f.pdirstat = &pdirstat;	/* cache parent stat */
			f.display = dp->d_name;
			f.flags &= ~FE_ISCURDIR;
		}
		if (stat(name, &sb) == -1)
			continue;
		f.path = name;
		mlsname(dout, &f);
	}
	(void)closedir(dirp);

	if (ferror(dout) != 0)
		perror_reply(550, "Data connection");
	else
		reply(226, "MLSD complete.");
	closedataconn(dout);
	total_xfers_out++;
	total_xfers++;
}

void
mlst(const char *path)
{
	struct stat sb;
	factelem f;

	if (path == NULL)
		path = ".";
	if (stat(path, &sb) == -1) {
		perror_reply(550, path);
		return;
	}
	reply(-250, "MLST %s", path);
	memset(&f, 0, sizeof(f));
	f.path = path;
	f.display = path;
	f.stat = &sb;
	f.pdirstat = NULL;
	CPUTC(' ', stdout);
	mlsname(stdout, &f);
	reply(250, "End");
}


void
opts(const char *command)
{
	struct tab *c;
	char *ep;

	if ((ep = strchr(command, ' ')) != NULL)
		*ep++ = '\0';
	c = lookup(cmdtab, command);
	if (c == NULL) {
		reply(502, "Unknown command '%s'.", command);
		return;
	}
	if (! CMD_IMPLEMENTED(c)) {
		reply(502, "%s command not implemented.", c->name);
		return;
	}
	if (! CMD_HAS_OPTIONS(c)) {
		reply(501, "%s command does not support persistent options.",
		    c->name);
		return;
	}

			/* special case: MLST */
	if (strcasecmp(command, "MLST") == 0) {
		int	 enabled[FACTTABSIZE];
		size_t	 i, onedone;
		size_t	 len;
		char	*p;

		for (i = 0; i < sizeof(enabled) / sizeof(int); i++)
			enabled[i] = 0;
		if (ep == NULL || *ep == '\0')
			goto displaymlstopts;

				/* don't like spaces, and need trailing ; */
		len = strlen(ep);
		if (strchr(ep, ' ') != NULL || ep[len - 1] != ';') {
 badmlstopt:
			reply(501, "Invalid MLST options");
			return;
		}
		ep[len - 1] = '\0';
		while ((p = strsep(&ep, ";")) != NULL) {
			if (*p == '\0')
				goto badmlstopt;
			for (i = 0; i < FACTTABSIZE; i++)
				if (strcasecmp(p, facttab[i].name) == 0) {
					enabled[i] = 1;
					break;
				}
		}

 displaymlstopts:
		for (i = 0; i < FACTTABSIZE; i++)
			facttab[i].enabled = enabled[i];
		cprintf(stdout, "200 MLST OPTS");
		for (i = onedone = 0; i < FACTTABSIZE; i++) {
			if (facttab[i].enabled) {
				cprintf(stdout, "%s%s;", onedone ? "" : " ",
				    facttab[i].name);
				onedone++;
			}
		}
		cprintf(stdout, "\r\n");
		fflush(stdout);
		return;
	}

			/* default cases */
	if (ep != NULL && *ep != '\0')
		REASSIGN(c->options, ftpd_strdup(ep));
	if (c->options != NULL)
		reply(200, "Options for %s are '%s'.", c->name,
		    c->options);
	else
		reply(200, "No options defined for %s.", c->name);
}

void
pwd(void)
{
	char path[MAXPATHLEN];

	if (getcwd(path, sizeof(path) - 1) == NULL) {
		if (chdir(cached_path) < 0) {
			reply(550, "Can't get the current directory: %s.",
			    strerror(errno));
			return;
		}
		(void)strlcpy(path, cached_path, MAXPATHLEN);
	}
	replydirname(path, "is the current directory.");
}

void
removedir(const char *name)
{
	char *p = NULL;

	if (rmdir(name) < 0) {
		p = strerror(errno);
		perror_reply(550, name);
	} else
		ack("RMD");
	logxfer("rmdir", -1, name, NULL, NULL, p);
}

char *
renamefrom(const char *name)
{
	struct stat st;

	if (stat(name, &st) < 0) {
		perror_reply(550, name);
		return (NULL);
	}
	reply(350, "File exists, ready for destination name");
	return (ftpd_strdup(name));
}

void
renamecmd(const char *from, const char *to)
{
	char *p = NULL;

	if (rename(from, to) < 0) {
		p = strerror(errno);
		perror_reply(550, "rename");
	} else
		ack("RNTO");
	logxfer("rename", -1, from, to, NULL, p);
}

void
sizecmd(const char *filename)
{
	switch (type) {
	case TYPE_L:
	case TYPE_I:
	    {
		struct stat stbuf;
		if (stat(filename, &stbuf) < 0 || !S_ISREG(stbuf.st_mode))
			reply(550, "%s: not a plain file.", filename);
		else
			reply(213, ULLF, (ULLT)stbuf.st_size);
		break;
	    }
	case TYPE_A:
	    {
		FILE *fin;
		int c;
		off_t count;
		struct stat stbuf;
		fin = fopen(filename, "r");
		if (fin == NULL) {
			perror_reply(550, filename);
			return;
		}
		if (fstat(fileno(fin), &stbuf) < 0 || !S_ISREG(stbuf.st_mode)) {
			reply(550, "%s: not a plain file.", filename);
			(void) fclose(fin);
			return;
		}
		if (stbuf.st_size > 10240) {
			reply(550, "%s: file too large for SIZE.", filename);
			(void) fclose(fin);
			return;
		}

		count = 0;
		while((c = getc(fin)) != EOF) {
			if (c == '\n')	/* will get expanded to \r\n */
				count++;
			count++;
		}
		(void) fclose(fin);

		reply(213, LLF, (LLT)count);
		break;
	    }
	default:
		reply(504, "SIZE not implemented for Type %c.", "?AEIL"[type]);
	}
}

void
statfilecmd(const char *filename)
{
	FILE *fin;
	int c;
	int atstart;
	const char *argv[] = { INTERNAL_LS, "-lgA", "", NULL };

	argv[2] = filename;
	fin = ftpd_popen(argv, "r", STDOUT_FILENO);
	reply(-211, "status of %s:", filename);
/* XXX: use fgetln() or fparseln() here? */
	atstart = 1;
	while ((c = getc(fin)) != EOF) {
		if (c == '\n') {
			if (ferror(stdout)){
				perror_reply(421, "control connection");
				(void) ftpd_pclose(fin);
				dologout(1);
				/* NOTREACHED */
			}
			if (ferror(fin)) {
				perror_reply(551, filename);
				(void) ftpd_pclose(fin);
				return;
			}
			CPUTC('\r', stdout);
		}
		if (atstart && isdigit(c))
			CPUTC(' ', stdout);
		CPUTC(c, stdout);
		atstart = (c == '\n');
	}
	(void) ftpd_pclose(fin);
	reply(211, "End of Status");
}

/* -- */

static void
ack(const char *s)
{

	reply(250, "%s command successful.", s);
}

/*
 * Encode len bytes starting at clear using base64 encoding into encoded,
 * which should be at least ((len + 2) * 4 / 3 + 1) in size.
 * If nulterm is non-zero, terminate with \0 otherwise pad to 3 byte boundary
 * with `='.
 */
static void
base64_encode(const char *clear, size_t len, char *encoded, int nulterm)
{
	static const char base64[] =
	    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
	const char *c;
	char	*e, termchar;
	int	 i;

			/* determine whether to pad with '=' or NUL terminate */
	termchar = nulterm ? '\0' : '=';
	c = clear;
	e = encoded;
			/* convert all but last 2 bytes */
	for (i = len; i > 2; i -= 3, c += 3) {
		*e++ = base64[(c[0] >> 2) & 0x3f];
		*e++ = base64[((c[0] << 4) & 0x30) | ((c[1] >> 4) & 0x0f)];
		*e++ = base64[((c[1] << 2) & 0x3c) | ((c[2] >> 6) & 0x03)];
		*e++ = base64[(c[2]) & 0x3f];
	}
			/* handle slop at end */
	if (i > 0) {
		*e++ = base64[(c[0] >> 2) & 0x3f];
		*e++ = base64[((c[0] << 4) & 0x30) |
		     (i > 1 ? ((c[1] >> 4) & 0x0f) : 0)];
		*e++ = (i > 1) ? base64[(c[1] << 2) & 0x3c] : termchar;
		*e++ = termchar;
	}
	*e = '\0';
}

static void
fact_modify(const char *fact, FILE *fd, factelem *fe)
{
	struct tm *t;

	t = gmtime(&(fe->stat->st_mtime));
	cprintf(fd, "%s=%04d%02d%02d%02d%02d%02d;", fact,
	    TM_YEAR_BASE + t->tm_year,
	    t->tm_mon+1, t->tm_mday,
	    t->tm_hour, t->tm_min, t->tm_sec);
}

static void
fact_perm(const char *fact, FILE *fd, factelem *fe)
{
	int		rok, wok, xok, pdirwok;
	struct stat	*pdir, dir;

	if (fe->stat->st_uid == geteuid()) {
		rok = ((fe->stat->st_mode & S_IRUSR) != 0);
		wok = ((fe->stat->st_mode & S_IWUSR) != 0);
		xok = ((fe->stat->st_mode & S_IXUSR) != 0);
	} else if (matchgroup(fe->stat->st_gid)) {
		rok = ((fe->stat->st_mode & S_IRGRP) != 0);
		wok = ((fe->stat->st_mode & S_IWGRP) != 0);
		xok = ((fe->stat->st_mode & S_IXGRP) != 0);
	} else {
		rok = ((fe->stat->st_mode & S_IROTH) != 0);
		wok = ((fe->stat->st_mode & S_IWOTH) != 0);
		xok = ((fe->stat->st_mode & S_IXOTH) != 0);
	}

	cprintf(fd, "%s=", fact);

			/*
			 * if parent info not provided, look it up, but
			 * only if the current class has modify rights,
			 * since we only need this info in such a case.
			 */
	pdir = fe->pdirstat;
	if (pdir == NULL && CURCLASS_FLAGS_ISSET(modify)) {
		size_t		len;
		char		realdir[MAXPATHLEN], *p;

		len = strlcpy(realdir, fe->path, sizeof(realdir));
		if (len < sizeof(realdir) - 4) {
			if (S_ISDIR(fe->stat->st_mode))
				strlcat(realdir, "/..", sizeof(realdir));
			else {
					/* if has a /, move back to it */
					/* otherwise use '..' */
				if ((p = strrchr(realdir, '/')) != NULL) {
					if (p == realdir)
						p++;
					*p = '\0';
				} else
					strlcpy(realdir, "..", sizeof(realdir));
			}
			if (stat(realdir, &dir) == 0)
				pdir = &dir;
		}
	}
	pdirwok = 0;
	if (pdir != NULL) {
		if (pdir->st_uid == geteuid())
			pdirwok = ((pdir->st_mode & S_IWUSR) != 0);
		else if (matchgroup(pdir->st_gid))
			pdirwok = ((pdir->st_mode & S_IWGRP) != 0);
		else
			pdirwok = ((pdir->st_mode & S_IWOTH) != 0);
	}

			/* 'a': can APPE to file */
	if (wok && CURCLASS_FLAGS_ISSET(upload) && S_ISREG(fe->stat->st_mode))
		CPUTC('a', fd);

			/* 'c': can create or append to files in directory */
	if (wok && CURCLASS_FLAGS_ISSET(modify) && S_ISDIR(fe->stat->st_mode))
		CPUTC('c', fd);

			/* 'd': can delete file or directory */
	if (pdirwok && CURCLASS_FLAGS_ISSET(modify)) {
		int candel;

		candel = 1;
		if (S_ISDIR(fe->stat->st_mode)) {
			DIR *dirp;
			struct dirent *dp;

			if ((dirp = opendir(fe->display)) == NULL)
				candel = 0;
			else {
				while ((dp = readdir(dirp)) != NULL) {
					if (ISDOTDIR(dp->d_name) ||
					    ISDOTDOTDIR(dp->d_name))
						continue;
					candel = 0;
					break;
				}
				closedir(dirp);
			}
		}
		if (candel)
			CPUTC('d', fd);
	}

			/* 'e': can enter directory */
	if (xok && S_ISDIR(fe->stat->st_mode))
		CPUTC('e', fd);

			/* 'f': can rename file or directory */
	if (pdirwok && CURCLASS_FLAGS_ISSET(modify))
		CPUTC('f', fd);

			/* 'l': can list directory */
	if (rok && xok && S_ISDIR(fe->stat->st_mode))
		CPUTC('l', fd);

			/* 'm': can create directory */
	if (wok && CURCLASS_FLAGS_ISSET(modify) && S_ISDIR(fe->stat->st_mode))
		CPUTC('m', fd);

			/* 'p': can remove files in directory */
	if (wok && CURCLASS_FLAGS_ISSET(modify) && S_ISDIR(fe->stat->st_mode))
		CPUTC('p', fd);

			/* 'r': can RETR file */
	if (rok && S_ISREG(fe->stat->st_mode))
		CPUTC('r', fd);

			/* 'w': can STOR file */
	if (wok && CURCLASS_FLAGS_ISSET(upload) && S_ISREG(fe->stat->st_mode))
		CPUTC('w', fd);

	CPUTC(';', fd);
}

static void
fact_size(const char *fact, FILE *fd, factelem *fe)
{

	if (S_ISREG(fe->stat->st_mode))
		cprintf(fd, "%s=" LLF ";", fact, (LLT)fe->stat->st_size);
}

static void
fact_type(const char *fact, FILE *fd, factelem *fe)
{

	cprintf(fd, "%s=", fact);
	switch (fe->stat->st_mode & S_IFMT) {
	case S_IFDIR:
		if (fe->flags & FE_MLSD) {
			if ((fe->flags & FE_ISCURDIR) || ISDOTDIR(fe->display))
				cprintf(fd, "cdir");
			else if (ISDOTDOTDIR(fe->display))
				cprintf(fd, "pdir");
			else
				cprintf(fd, "dir");
		} else {
			cprintf(fd, "dir");
		}
		break;
	case S_IFREG:
		cprintf(fd, "file");
		break;
	case S_IFIFO:
		cprintf(fd, "OS.unix=fifo");
		break;
	case S_IFLNK:		/* XXX: probably a NO-OP with stat() */
		cprintf(fd, "OS.unix=slink");
		break;
	case S_IFSOCK:
		cprintf(fd, "OS.unix=socket");
		break;
	case S_IFBLK:
	case S_IFCHR:
		cprintf(fd, "OS.unix=%s-" ULLF "/" ULLF,
		    S_ISBLK(fe->stat->st_mode) ? "blk" : "chr",
		    (ULLT)major(fe->stat->st_rdev),
		    (ULLT)minor(fe->stat->st_rdev));
		break;
	default:
		cprintf(fd, "OS.unix=UNKNOWN(0%o)", fe->stat->st_mode & S_IFMT);
		break;
	}
	CPUTC(';', fd);
}

static void
fact_unique(const char *fact, FILE *fd, factelem *fe)
{
	char obuf[(sizeof(dev_t) + sizeof(ino_t) + 2) * 4 / 3 + 2];
	char tbuf[sizeof(dev_t) + sizeof(ino_t)];

	memcpy(tbuf,
	    (char *)&(fe->stat->st_dev), sizeof(dev_t));
	memcpy(tbuf + sizeof(dev_t),
	    (char *)&(fe->stat->st_ino), sizeof(ino_t));
	base64_encode(tbuf, sizeof(dev_t) + sizeof(ino_t), obuf, 1);
	cprintf(fd, "%s=%s;", fact, obuf);
}

static int
matchgroup(gid_t gid)
{
	int	i;

	for (i = 0; i < gidcount; i++)
		if (gid == gidlist[i])
			return(1);
	return (0);
}

static void
mlsname(FILE *fp, factelem *fe)
{
	char realfile[MAXPATHLEN];
	int userf = 0;
	size_t i;

	for (i = 0; i < FACTTABSIZE; i++) {
		if (facttab[i].enabled)
			(facttab[i].display)(facttab[i].name, fp, fe);
	}
	if ((fe->flags & FE_MLSD) &&
	    !(fe->flags & FE_ISCURDIR) && !ISDOTDIR(fe->display)) {
			/* if MLSD and not "." entry, display as-is */
		userf = 0;
	} else {
			/* if MLST, or MLSD and "." entry, realpath(3) it */
		if (realpath(fe->display, realfile) != NULL)
			userf = 1;
	}
	cprintf(fp, " %s\r\n", userf ? realfile : fe->display);
}

static void
replydirname(const char *name, const char *message)
{
	char *p, *ep;
	char npath[MAXPATHLEN * 2];

	p = npath;
	ep = &npath[sizeof(npath) - 1];
	while (*name) {
		if (*name == '"') {
			if (ep - p < 2)
				break;
			*p++ = *name++;
			*p++ = '"';
		} else {
			if (ep - p < 1)
				break;
			*p++ = *name++;
		}
	}
	*p = '\0';
	reply(257, "\"%s\" %s", npath, message);
}

static void
discover_path(char *last_path, const char *new_path) 
{
	char tp[MAXPATHLEN + 1] = "";
	char tq[MAXPATHLEN + 1] = "";
	char *cp;
	char *cq; 
	int sz1, sz2;
	int nomorelink;
	struct stat st1, st2;
	
	if (new_path[0] != '/') {
		(void)strlcpy(tp, last_path, MAXPATHLEN);
		(void)strlcat(tp, "/", MAXPATHLEN);
	}
	(void)strlcat(tp, new_path, MAXPATHLEN);
	(void)strlcat(tp, "/", MAXPATHLEN);

	/* 
	 * resolve symlinks. A symlink may introduce another symlink, so we
	 * loop trying to resolve symlinks until we don't find any of them.
	 */
	do {
		/* Collapse any // into / */
		while ((cp = strstr(tp, "//")) != NULL)
			(void)memmove(cp, cp + 1, strlen(cp) - 1 + 1);

		/* Collapse any /./ into / */
		while ((cp = strstr(tp, "/./")) != NULL)
			(void)memmove(cp, cp + 2, strlen(cp) - 2 + 1);

		cp = tp;
		nomorelink = 1;
		
		while ((cp = strstr(cp + 1, "/")) != NULL) {
			sz1 = (unsigned long)cp - (unsigned long)tp;
			if (sz1 > MAXPATHLEN)
				goto bad;
			*cp = 0;
			sz2 = readlink(tp, tq, MAXPATHLEN); 
			*cp = '/';

			/* If this is not a symlink, move to next / */
			if (sz2 <= 0)
				continue;

			/*
			 * We found a symlink, so we will have to 
			 * do one more pass to check there is no 
			 * more symlink in the path
			 */
			nomorelink = 0;

			/* 
			 * Null terminate the string and remove trailing /
			 */
			tq[sz2] = 0;
			sz2 = strlen(tq);
			if (tq[sz2 - 1] == '/') 
				tq[--sz2] = 0;

			/* 
			 * Is this an absolute link or a relative link? 
			 */
			if (tq[0] == '/') {
				/* absolute link */
				if (strlen(cp) + sz2 > MAXPATHLEN)
					goto bad;
				memmove(tp + sz2, cp, strlen(cp) + 1);
				memcpy(tp, tq, sz2);
			} else {			
				/* relative link */
				for (cq = cp - 1; *cq != '/'; cq--);
				if (strlen(tp) -
				    ((unsigned long)cq - (unsigned long)cp)
				    + 1 + sz2 > MAXPATHLEN)
					goto bad;
				(void)memmove(cq + 1 + sz2, 
				    cp, strlen(cp) + 1);
				(void)memcpy(cq + 1, tq, sz2);
			}

			/* 
			 * start over, looking for new symlinks 
			 */
			break;
		}
	} while (nomorelink == 0);

	/* Collapse any /foo/../ into /foo/ */
	while ((cp = strstr(tp, "/../")) != NULL) {
		/* ^/../foo/ becomes ^/foo/ */
		if (cp == tp) {
			(void)memmove(cp, cp + 3,
			    strlen(cp) - 3 + 1);
		} else {
			for (cq = cp - 1; *cq != '/'; cq--);
			(void)memmove(cq, cp + 3,
			    strlen(cp) - 3 + 1);
		}
	}

	/* strip strailing / */
	if (strlen(tp) != 1)
		tp[strlen(tp) - 1] = '\0';

	/* check that the path is correct */
	if (stat(tp, &st1) == -1 || stat(".", &st2) == -1)
		goto bad;
	if ((st1.st_dev != st2.st_dev) || (st1.st_ino != st2.st_ino))
		goto bad;

	(void)strlcpy(last_path, tp, MAXPATHLEN);
	return;

bad:
	(void)strlcat(last_path, "/", MAXPATHLEN);
	(void)strlcat(last_path, new_path, MAXPATHLEN);
	return;
}

