root/libexec/ftpd/cmds.c
/*      $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;
}