/*
 * stat.c - stat builtin interface to system call
 *
 * This file is part of zsh, the Z shell.
 *
 * Copyright (c) 1996-1997 Peter Stephenson
 * All rights reserved.
 *
 * Permission is hereby granted, without written agreement and without
 * license or royalty fees, to use, copy, modify, and distribute this
 * software and to distribute modified versions of this software for any
 * purpose, provided that the above copyright notice and the following
 * two paragraphs appear in all copies of this software.
 *
 * In no event shall Peter Stephenson or the Zsh Development Group be liable
 * to any party for direct, indirect, special, incidental, or consequential
 * damages arising out of the use of this software and its documentation,
 * even if Peter Stephenson and the Zsh Development Group have been advised of
 * the possibility of such damage.
 *
 * Peter Stephenson and the Zsh Development Group specifically disclaim any
 * warranties, including, but not limited to, the implied warranties of
 * merchantability and fitness for a particular purpose.  The software
 * provided hereunder is on an "as is" basis, and Peter Stephenson and the
 * Zsh Development Group have no obligation to provide maintenance,
 * support, updates, enhancements, or modifications.
 *
 */

#include "stat.mdh"
#include "stat.pro"

enum statnum { ST_DEV, ST_INO, ST_MODE, ST_NLINK, ST_UID, ST_GID,
		   ST_RDEV, ST_SIZE, ST_ATIM, ST_MTIM, ST_CTIM,
		   ST_BLKSIZE, ST_BLOCKS, ST_READLINK, ST_COUNT };
enum statflags { STF_NAME = 1,  STF_FILE = 2, STF_STRING = 4, STF_RAW = 8,
		     STF_PICK = 16, STF_ARRAY = 32, STF_GMT = 64,
		     STF_HASH = 128, STF_OCTAL = 256 };
static char *statelts[] = { "device", "inode", "mode", "nlink",
				"uid", "gid", "rdev", "size", "atime",
				"mtime", "ctime", "blksize", "blocks",
				"link", NULL };
#define HNAMEKEY "name"

/**/
static void
statmodeprint(mode_t mode, char *outbuf, int flags)
{
    if (flags & STF_RAW) {
	sprintf(outbuf, (flags & STF_OCTAL) ? "0%lo" : "%lu",
		(unsigned long)mode);
	if (flags & STF_STRING)
	    strcat(outbuf, " (");
    }
    if (flags & STF_STRING) {
	static const char *modes = "?rwxrwxrwx";
#ifdef __CYGWIN__
	static mode_t mflags[9] = { 0 };
#else
	static const mode_t mflags[9] = {
	    S_IRUSR, S_IWUSR, S_IXUSR,
	    S_IRGRP, S_IWGRP, S_IXGRP,
	    S_IROTH, S_IWOTH, S_IXOTH
	};
#endif
	const mode_t *mfp = mflags;
	char pm[11];
	int i;

#ifdef __CYGWIN__
	if (mflags[0] == 0) {
	    mflags[0] = S_IRUSR;
	    mflags[1] = S_IWUSR;
	    mflags[2] = S_IXUSR;
	    mflags[3] = S_IRGRP;
	    mflags[4] = S_IWGRP;
	    mflags[5] = S_IXGRP;
	    mflags[6] = S_IROTH;
	    mflags[7] = S_IWOTH;
	    mflags[8] = S_IXOTH;
	}
#endif

	if (S_ISBLK(mode))
	    *pm = 'b';
	else if (S_ISCHR(mode))
	    *pm = 'c';
	else if (S_ISDIR(mode))
	    *pm = 'd';
	else if (S_ISDOOR(mode))
	    *pm = 'D';
	else if (S_ISFIFO(mode))
	    *pm = 'p';
	else if (S_ISLNK(mode))
	    *pm = 'l';
	else if (S_ISMPC(mode))
	    *pm = 'm';
	else if (S_ISNWK(mode))
	    *pm = 'n';
	else if (S_ISOFD(mode))
	    *pm = 'M';
	else if (S_ISOFL(mode))
	    *pm = 'M';
	else if (S_ISREG(mode))
	    *pm = '-';
	else if (S_ISSOCK(mode))
	    *pm = 's';
	else
	    *pm = '?';

	for (i = 1; i <= 9; i++)
	    pm[i] = (mode & *mfp++) ? modes[i] : '-';
	pm[10] = '\0';

	if (mode & S_ISUID)
	    pm[3] = (mode & S_IXUSR) ? 's' : 'S';
	if (mode & S_ISGID)
	    pm[6] = (mode & S_IXGRP) ? 's' : 'S';
	if (mode & S_ISVTX)
	    pm[9] = (mode & S_IXOTH) ? 't' : 'T';

	pm[10] = 0;
	strcat(outbuf, pm);
	if (flags & STF_RAW)
	    strcat(outbuf, ")");
    }
}


/**/
static void
statuidprint(uid_t uid, char *outbuf, int flags)
{
    if (flags & STF_RAW) {
	sprintf(outbuf, "%lu", (unsigned long)uid);
	if (flags & STF_STRING)
	    strcat(outbuf, " (");
    }
    if (flags & STF_STRING) {
#ifdef HAVE_GETPWUID
	struct passwd *pwd;
	pwd = getpwuid(uid);
	strcat(outbuf, pwd ? pwd->pw_name : "???");
#else /* !HAVE_GETPWUID */
	strcat(outbuf, "???");
#endif /* !HAVE_GETPWUID */
	if (flags & STF_RAW)
	    strcat(outbuf, ")");
    }
}


/**/
static void
statgidprint(gid_t gid, char *outbuf, int flags)
{
    if (flags & STF_RAW) {
	sprintf(outbuf, "%lu", (unsigned long)gid);
	if (flags & STF_STRING)
	    strcat(outbuf, " (");
    }
    if (flags & STF_STRING) {
#ifdef HAVE_GETGRGID
	struct group *gr;
	gr = getgrgid(gid);
	strcat(outbuf, gr ? gr->gr_name : "???");
#else /* !HAVE_GETGRGID */
	strcat(outbuf, "???");
#endif /* !HAVE_GETGRGID */
	if (flags & STF_RAW)
	    strcat(outbuf, ")");
    }
}

static char *timefmt;

/**/
static void
stattimeprint(time_t tim, char *outbuf, int flags)
{
    if (flags & STF_RAW) {
	sprintf(outbuf, "%ld", (unsigned long)tim);
	if (flags & STF_STRING)
	    strcat(outbuf, " (");
    }
    if (flags & STF_STRING) {
	char *oend = outbuf + strlen(outbuf);
	ztrftime(oend, 40, timefmt, (flags & STF_GMT) ? gmtime(&tim) :
		 localtime(&tim));
	if (flags & STF_RAW)
	    strcat(outbuf, ")");
    }
}


/**/
static void
statulprint(unsigned long num, char *outbuf)
{
    sprintf(outbuf, "%lu", num);
}


/**/
static void
statlinkprint(struct stat *sbuf, char *outbuf, char *fname)
{
    int num;

    /* fname is NULL if we are looking at an fd */
    if (fname && S_ISLNK(sbuf->st_mode) &&
 	(num = readlink(fname, outbuf, PATH_MAX)) > 0) {
	/* readlink doesn't terminate the buffer itself */
	outbuf[num] = '\0';
    }
}


/**/
static void
statprint(struct stat *sbuf, char *outbuf, char *fname, int iwhich, int flags)
{
    char *optr = outbuf;

    if (flags & STF_NAME) {
	sprintf(outbuf, (flags & (STF_PICK|STF_ARRAY)) ?
		"%s " : "%-8s", statelts[iwhich]);
	optr += strlen(outbuf);
    }
    *optr = '\0';

    /* cast values to unsigned long as safest bet */
    switch (iwhich) {
    case ST_DEV:
	statulprint((unsigned long)sbuf->st_dev, optr);
	break;

    case ST_INO:
#ifdef INO_T_IS_64_BIT
	convbase(optr, sbuf->st_ino, 0);
#else
	DPUTS(sizeof(sbuf->st_ino) > 4,
	      "Shell compiled with wrong ino_t size");
	statulprint((unsigned long)sbuf->st_ino, optr);
#endif
	break;

    case ST_MODE:
	statmodeprint(sbuf->st_mode, optr, flags);
	break;

    case ST_NLINK:
	statulprint((unsigned long)sbuf->st_nlink, optr);
	break;

    case ST_UID:
	statuidprint(sbuf->st_uid, optr, flags);
	break;

    case ST_GID:
	statgidprint(sbuf->st_gid, optr, flags);
	break;

    case ST_RDEV:
	statulprint((unsigned long)sbuf->st_rdev, optr);
	break;

    case ST_SIZE:
#ifdef OFF_T_IS_64_BIT
	convbase(optr, sbuf->st_size, 0);
#else
	DPUTS(sizeof(sbuf->st_size) > 4,
	      "Shell compiled with wrong off_t size");
	statulprint((unsigned long)sbuf->st_size, optr);
#endif
	break;

    case ST_ATIM:
	stattimeprint(sbuf->st_atime, optr, flags);
	break;

    case ST_MTIM:
	stattimeprint(sbuf->st_mtime, optr, flags);
	break;

    case ST_CTIM:
	stattimeprint(sbuf->st_ctime, optr, flags);
	break;

    case ST_BLKSIZE:
	statulprint((unsigned long)sbuf->st_blksize, optr);
	break;

    case ST_BLOCKS:
	statulprint((unsigned long)sbuf->st_blocks, optr);
	break;

    case ST_READLINK:
	statlinkprint(sbuf, optr, fname);
	break;

    case ST_COUNT:			/* keep some compilers happy */
	break;
    }
}


/*
 *
 * Options:
 *  -f fd:   stat fd instead of file
 *  -g:   use GMT rather than local time for time strings (forces -s on).
 *  -n:   always print file name of file being statted
 *  -N:   never print file name
 *  -l:   list stat types
 *  -L:   do lstat (else links are implicitly dereferenced by stat)
 *  -t:   always print name of stat type
 *  -T:   never print name of stat type
 *  -r:   print raw alongside string data
 *  -s:   string, print mode, times, uid, gid as appropriate strings:
 *        harmless but unnecessary when combined with -r.
 *  -A array:  assign results to given array, one stat result per element.
 *        File names and type names are only added if explicitly requested:
 *        file names are returned as a separate array element, type names as
 *        prefix to element.  Note the formatting deliberately contains
 *        fewer frills when -A is used.
 *  -H hash:  as for -A array, but returns a hash with the keys being those
 *        from stat -l
 *  -F fmt: specify a $TIME-like format for printing times; the default
 *        is the (CTIME-like) "%a %b %e %k:%M:%S".  This option implies
 *        -s as it is not useful for numerical times.
 *
 *  +type selects just element type of stat buffer (-l gives list):
 *        type can be shortened to unambiguous string.  only one type is
 *        allowed.  The extra type, +link, reads the target of a symbolic
 *        link; it is empty if the stat was not an lstat or if 
 *        a file descriptor was stat'd, if the stat'd file is
 *        not a symbolic link, or if symbolic links are not
 *        supported.  If +link is explicitly requested, the -L (lstat)
 *        option is set automatically.
 */
/**/
static int
bin_stat(char *name, char **args, char *ops, int func)
{
    char **aptr, *arrnam = NULL, **array = NULL, **arrptr = NULL;
    char *hashnam = NULL, **hash = NULL, **hashptr = NULL;
    int len, iwhich = -1, ret = 0, flags = 0, arrsize = 0, fd = 0;
    struct stat statbuf;
    int found = 0, nargs;

    timefmt = "%a %b %e %k:%M:%S";

    for (; *args && (**args == '+' || **args == '-'); args++) {
	char *arg = *args+1;
	if (!*arg || *arg == '-' || *arg == '+') {
	    args++;
	    break;
	}

	if (**args == '+') {
	    if (found)
		break;
	    len = strlen(arg);
	    for (aptr = statelts; *aptr; aptr++)
		if (!strncmp(*aptr, arg, len)) {
		    found++;
		    iwhich = aptr - statelts;
		}
	    if (found > 1) {
		zwarnnam(name, "%s: ambiguous stat element", arg, 0);
		return 1;
	    } else if (found == 0) {
		zwarnnam(name, "%s: no such stat element", arg, 0);
		return 1;
	    }
	    /* if name of link requested, turn on lstat */
	    if (iwhich == ST_READLINK)
		ops['L'] = 1;
	    flags |= STF_PICK;
	} else {
	    for (; *arg; arg++) {
		if (strchr("glLnNorstT", *arg))
		    ops[STOUC(*arg)] = 1;
		else if (*arg == 'A') {
		    if (arg[1]) {
			arrnam = arg+1;
		    } else if (!(arrnam = *++args)) {
			zwarnnam(name, "missing parameter name",
				NULL, 0);
			return 1;
		    }
		    flags |= STF_ARRAY;
		    break;
		} else if (*arg == 'H') {
		    if (arg[1]) {
			hashnam = arg+1;
		    } else if (!(hashnam = *++args)) {
			zwarnnam(name, "missing parameter name",
				NULL, 0);
			return 1;
		    }
		    flags |= STF_HASH;
		    break;
		} else if (*arg == 'f') {
		    char *sfd;
		    ops['f'] = 1;
		    if (arg[1]) {
			sfd = arg+1;
		    } else if (!(sfd = *++args)) {
			zwarnnam(name, "missing file descriptor", NULL, 0);
			return 1;
		    }
		    fd = zstrtol(sfd, &sfd, 10);
		    if (*sfd) {
			zwarnnam(name, "bad file descriptor", NULL, 0);
			return 1;
		    }
		    break;
		} else if (*arg == 'F') {
		    if (arg[1]) {
			timefmt = arg+1;
		    } else if (!(timefmt = *++args)) {
			zwarnnam(name, "missing time format", NULL, 0);
			return 1;
		    }
		    /* force string format in order to use time format */
		    ops['s'] = 1;
		    break;
		} else {
		    zwarnnam(name, "bad option: -%c", NULL, *arg);
		    return 1;
		}
	    }
	}
    }

    if ((flags & STF_ARRAY) && (flags & STF_HASH)) {
    	/* We don't implement setting multiple variables at once */
	zwarnnam(name, "both array and hash requested", NULL, 0);
	return 1;
	/* Alternate method would be to make -H blank arrnam etc etc *
	 * and so get 'silent loss' of earlier choice, which would   *
	 * be similar to stat -A foo -A bar filename                 */
    }

    if (ops['l']) {
	/* list types and return:  can also list to array */
	if (arrnam) {
	    arrptr = array = (char **)zalloc((ST_COUNT+1)*sizeof(char *));
	    array[ST_COUNT] = NULL;
	}
	for (aptr = statelts; *aptr; aptr++) {
	    if (arrnam) {
		*arrptr++ = ztrdup(*aptr);
	    } else {
		printf("%s", *aptr);
		if (aptr[1])
		    putchar(' ');
	    }
	}
	if (arrnam) {
	    setaparam(arrnam, array);
	    if (errflag)
		return 1;
	} else
	    putchar('\n');
	return 0;
    }

    if (!*args && !ops['f']) {
	zwarnnam(name, "no files given", NULL, 0);
	return 1;
    } else if (*args && ops['f']) {
	zwarnnam(name, "no files allowed with -f", NULL, 0);
	return 1;
    }

    nargs = 0;
    if (ops['f'])
	nargs = 1;
    else
	for (aptr = args; *aptr; aptr++)
	    nargs++;

    if (ops['g']) {
	flags |= STF_GMT;
	ops['s'] = 1;
    }
    if (ops['s'] || ops['r'])
	flags |= STF_STRING;
    if (ops['r'] || !ops['s'])
	flags |= STF_RAW;
    if (ops['n'])
	flags |= STF_FILE;
    if (ops['o'])
	flags |= STF_OCTAL;
    if (ops['t'])
	flags |= STF_NAME;

    if (!(arrnam || hashnam)) {
	if (nargs > 1)
	    flags |= STF_FILE;
	if (!(flags & STF_PICK))
	    flags |= STF_NAME;
    }

    if (ops['N'] || ops['f'])
	flags &= ~STF_FILE;
    if (ops['T'] || ops['H'])
	flags &= ~STF_NAME;

    if (hashnam) {
    	if (nargs > 1) {
	    zwarnnam(name, "only one file allowed with -H", NULL, 0);
	    return 1;
	}
	arrsize = (flags & STF_PICK) ? 1 : ST_COUNT;
	if (flags & STF_FILE)
	    arrsize++;
	hashptr = hash = (char **)zcalloc((arrsize+1)*2*sizeof(char *));
    }

    if (arrnam) {
	arrsize = (flags & STF_PICK) ? 1 : ST_COUNT;
	if (flags & STF_FILE)
	    arrsize++;
	arrsize *= nargs;
	arrptr = array = (char **)zcalloc((arrsize+1)*sizeof(char *));
    }

    for (; ops['f'] || *args; args++) {
	char outbuf[PATH_MAX + 9]; /* "link   " + link name + NULL */
	int rval = ops['f'] ? fstat(fd, &statbuf) :
	    ops['L'] ? lstat(*args, &statbuf) : stat(*args, &statbuf);
	if (rval) {
	    if (ops['f'])
		sprintf(outbuf, "%d", fd);
	    zwarnnam(name, "%s: %e", ops['f'] ? outbuf : *args, errno);
	    ret = 1;
	    if (ops['f'] || arrnam)
		break;
	    else
		continue;
	}

	if (flags & STF_FILE) {
	    if (arrnam)
		*arrptr++ = ztrdup(*args);
	    else if (hashnam) {
	    	*hashptr++ = ztrdup(HNAMEKEY);
		*hashptr++ = ztrdup(*args);
	    } else
		printf("%s%s", *args, (flags & STF_PICK) ? " " : ":\n");
	}
	if (iwhich > -1) {
	    statprint(&statbuf, outbuf, *args, iwhich, flags);
	    if (arrnam)
		*arrptr++ = ztrdup(outbuf);
	    else if (hashnam) {
		/* STF_NAME explicitly turned off for ops['H'] above */
	    	*hashptr++ = ztrdup(statelts[iwhich]);
		*hashptr++ = ztrdup(outbuf);
	    } else
		printf("%s\n", outbuf);
	} else {
	    int i;
	    for (i = 0; i < ST_COUNT; i++) {
		statprint(&statbuf, outbuf, *args, i, flags);
		if (arrnam)
		    *arrptr++= ztrdup(outbuf);
		else if (hashnam) {
		    /* STF_NAME explicitly turned off for ops['H'] above */
		    *hashptr++ = ztrdup(statelts[i]);
		    *hashptr++ = ztrdup(outbuf);
		} else
		    printf("%s\n", outbuf);
	    }
	}
	if (ops['f'])
	    break;

	if (!arrnam && !hashnam && args[1] && !(flags & STF_PICK))
	    putchar('\n');
    }

    if (arrnam) {
	if (ret)
	    freearray(array);
	else {
	    setaparam(arrnam, array);
	    if (errflag)
		return 1;
	}
    }

    if (hashnam) {
    	if (ret)
	    freearray(hash);
	else {
	    sethparam(hashnam, hash);
	    if (errflag)
		return 1;
	}
    }

    return ret;
}

static struct builtin bintab[] = {
    BUILTIN("stat", 0, bin_stat, 0, -1, 0, NULL, NULL),
};

/**/
int
setup_(Module m)
{
    return 0;
}

/**/
int
boot_(Module m)
{
    return !addbuiltins(m->nam, bintab, sizeof(bintab)/sizeof(*bintab));
}

/**/
int
cleanup_(Module m)
{
    deletebuiltins(m->nam, bintab, sizeof(bintab)/sizeof(*bintab));
    return 0;
}

/**/
int
finish_(Module m)
{
    return 0;
}
