/*
 * lib.c - etsh library routines
 */
/*-
 * Copyright (c) 2004-2019
 *	Jeffrey Allen Neitzel <jan (at) etsh (dot) nl>.
 *	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.
 *
 * THIS SOFTWARE IS PROVIDED BY JEFFREY ALLEN NEITZEL ``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 JEFFREY ALLEN NEITZEL 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.
 *
 *	@(#)$Id: lib.c,v 1.7 2019/03/18 06:54:50 jneitzel Exp $
 */
/*-
 * Copyright (C) Caldera International Inc.  2001-2002.  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 and documentation 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. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *      This product includes software developed or owned by Caldera
 *      International, Inc.
 * 4. Neither the name of Caldera International, Inc. nor the names of other
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * USE OF THE SOFTWARE PROVIDED FOR UNDER THIS LICENSE BY CALDERA
 * INTERNATIONAL, 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 CALDERA INTERNATIONAL, INC. 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.
 */

#define	ETSH_SHELL

#include "defs.h"
#include "sh.h"
#include "sh_err.h"
#include "lib.h"

/*
 * **** Global Variables ****
 */
static	const char	**gavp;	 /* points to current gav position     */
static	const char	**gave;	 /* points to current gav end          */
static	size_t		gavtot;	 /* total bytes used for all arguments */

/*
 * **** Function Prototypes ****
 */
static	void		prompt_debug(const char *);
/*@null@*/
static	char		*gtrim(/*@returned@*/ u_char *);
static	const char	**gnew(/*@only@*/ const char **);
/*@null@*/
static	char		*gcat(/*@null@*/ const char *,
			      /*@null@*/ const char *, bool);
static	const char	**glob1(enum sbikey, /*@only@*/ const char **,
				char *, int *, bool *);
static	bool		glob2(const u_char *, const u_char *);
static	void		gsort(const char **);
/*@null@*/
static	DIR		*gopendir(/*@out@*/ char *, const char *);

/*
 * Print diagnostic w/ $0, line number, and so forth if possible.
 */
void
error(int s, const char *m)
{
	long int ln;

	if (m == NULL) {
		sh_err(ESTATUS, FMT3S, getmyname(), __func__, strerror(EINVAL));
		/*NOTREACHED*/
	}

	ln = (is_noexec || no_lnum) ? -1 : get_lnum();
	if (name != NULL) {
		if (ln != -1)
			sh_err(s, FMT3LS, getmyname(), name, ln, m);
		else
			sh_err(s, FMT3S, getmyname(), name, m);
	} else {
		if (ln != -1)
			sh_err(s, FMT2LS, getmyname(), ln, m);
		else
			sh_err(s, FMT2S, getmyname(), m);
	}
}

/*
 * Print diagnostic w/ $0, line number, and so forth if possible.
 */
void
error1(int s, const char *f, const char *m)
{
	long int ln;

	if (f == NULL || m == NULL) {
		sh_err(ESTATUS, FMT3S, getmyname(), __func__, strerror(EINVAL));
		/*NOTREACHED*/
	}

	ln = (is_noexec || no_lnum) ? -1 : get_lnum();
	if (name != NULL) {
		if (ln != -1)
			sh_err(s, FMT4LS, getmyname(), name, ln, f, m);
		else
			sh_err(s, FMT4S, getmyname(), name, f, m);
	} else {
		if (ln != -1)
			sh_err(s, FMT3LFS, getmyname(), ln, f, m);
		else
			sh_err(s, FMT3S, getmyname(), f, m);
	}
}

/*
 * Print diagnostic w/ $0, line number, and so forth if possible.
 */
void
error2(int s, const char *c, const char *a, const char *m)
{
	long int ln;

	if (c == NULL || a == NULL || m == NULL) {
		sh_err(ESTATUS, FMT3S, getmyname(), __func__, strerror(EINVAL));
		/*NOTREACHED*/
	}

	ln = (is_noexec || no_lnum) ? -1 : get_lnum();
	if (name != NULL) {
		if (ln != -1)
			sh_err(s, FMT5LS, getmyname(), name, ln, c, a, m);
		else
			sh_err(s, FMT5S, getmyname(), name, c, a, m);
	} else {
		if (ln != -1)
			sh_err(s, FMT4LFS, getmyname(), ln, c, a, m);
		else
			sh_err(s, FMT4S, getmyname(), c, a, m);
	}
}

/*
 * Deallocate the memory allocation pointed to by p.
 */
void
xfree(void *p)
{

	if (p != NULL) {
		free(p);
		p = NULL;
	}
}

/*
 * Allocate memory, and check for error.
 * Return a pointer to the allocated space on success.
 * Do not return on ENOMEM error.
 */
void *
xmalloc(size_t s)
{
	void *mp;

	if ((mp = malloc(s)) == NULL) {
		error(ESTATUS, ERR_NOMEM);
		/*NOTREACHED*/
	}
	return mp;
}

/*
 * Deallocate the argument vector pointed to by vp.
 */
void
vfree(char **vp)
{
	char **p;

	if (vp != NULL) {
		for (p = vp; *p != NULL; p++) {
			free(*p);
			*p = NULL;
		}
		free(vp);
		vp = NULL;
	}
}

/*
 * Reallocate memory, and check for error.
 * Return a pointer to the reallocated space on success.
 * Do not return on ENOMEM error.
 */
void *
xrealloc(void *p, size_t s)
{
	void *rp;

	if ((rp = realloc(p, s)) == NULL) {
		error(ESTATUS, ERR_NOMEM);
		/*NOTREACHED*/
	}
	return rp;
}

/*
 * Allocate memory for a copy of the string src, and copy it to dst.
 * Return a pointer to dst on success.  Do not return on ENOMEM error
 * or if src is NULL.  Print an EINVAL error message for NULL src.
 */
char *
xstrdup(const char *src)
{
	size_t siz;
	char *dst;

	if (src == NULL) {
		/* appease splint(1) - should never (but can) be true */
		error1(ESTATUS, __func__, strerror(EINVAL));
		/*NOTREACHED*/
	}

	siz = strlen(src) + 1;
	dst = xmalloc(siz);
	(void)memcpy(dst, src, siz);
	return dst;
}

/*
 * Write the appropriate command prompt to standard error (FD2),
 * where appropriate is the default !root "% " prompt, the
 * default root "# " prompt, or the prompt set by the user
 * with `set P value'.
 *
 * The length of the value of P should be < 256 (byte|char)s,
 * but it is not enforced; it is just a personal preference.
 *
 * See the libexec.etsh/SetP script and/or the etsh(1) manual
 * page, the "Prompt (+)" subsection, for full details.
 */
void
prompt_write(void)
{
	char buf[_LINESIZ];
	const char *p;	   /* value of P, if any     */
	char *pcp;	   /* prompt copied  pointer */
	char *ptp;	   /* prompt trimmed pointer */

	if ((p = varget('P')) == NULL) /* no splint(1), we cannot free(p); */
		fd_print(FD2, "%s", PROMPT_DEFAULT);
	else {
		if (!prompt_on)
			fd_print(FD2, "%s", PROMPT_DEFAULT);
		else {
			(void)memset(&buf, 0, sizeof(buf));
			pcp = memcpy(buf, p, strlen(p) + 1);
			ptp = atrim(U_CPTR(pcp), true);
			if (prompt_dbg) /* dbg == debug in spirit =^) */
				prompt_debug(ptp);
			fd_print(FD2, "%s", ptp);
		}
	}
}

/*
 * Debug resulting prompt ($P) string, writing it to standard ouput (FD1).
 */
static void
prompt_debug(const char *ptp)
{
	const char *pdp;
	u_int cnt;

	fd_print(FD1, "\n\"quoted\" printable $P string:\n");
	fd_print(FD1, "\t\"");
	for (pdp = ptp, cnt = 0; *pdp != EOS; pdp++) {
		if (*pdp == BEL) {
			fd_print(FD1, "%s", "\\a");
			cnt += 1;
		} else if (*pdp == EOL) {
			fd_print(FD1, "%s", "\\n");
			cnt += 1;
		} else if (*pdp == TAB) {
			fd_print(FD1, "%s", "\\t");
			cnt += 1;
		} else
			fd_print(FD1, "%c", *pdp);
	}
	fd_print(FD1, "\";\n");
	fd_print(FD1, "\tescaped: %u; $P length: %zu;\n",
		 cnt, strlen(ptp));
}

/*
 * Remove (trim) any unquoted quote characters from argument
 * pointed to by ap, make copy of, and return pointer to it.
 * Return original argument pointed to by ap on error.
 *
 * Do the following if/when pe == true || !false .
 *
 * Escape / replace (trim) \a \n \t special characters from string
 * pointed to by ap (the prompt string in $P) with '\a' '\n' '\t'
 * equivalent literals.
 */
char *
atrim(u_char *ap, bool pe /* prompt escape */)
{
	size_t siz;
	u_char *a, *b;
	u_char buf[_LINESIZ], c;
	const char *em;
	bool d;

	if (ap == NULL)
		goto aterr;

	*buf = U_CHAR(EOS);
	for (a = ap, b = buf; b < &buf[_LINESIZ]; a++, b++) {
		switch (*a) {
		case EOS:
			*b = U_CHAR(EOS);
			siz = (b - buf) + 1;
			(void)memcpy(ap, buf, siz);
			return (char *)ap;
		case DQUOT:
		case SQUOT:
			c = *a++;
			d = (c == DQUOT) ? true : false;
			while (*a != c && b < &buf[_LINESIZ]) {
				if (d && *a == BQUOT && *(a + 1) == DOLLAR)
					a++;
				*b++ = *a++;
			}
			b--;
			continue;
		case BQUOT:
			/* this needs changes if we want more escapes */
			switch (*++a) {
			case EOS:
				a--, b--;
				continue;
			case BQUOT:
				break;
			case 'a':
				if (!pe) break;
				*b = U_CHAR(BEL);
				continue;
			case 'n':
				if (!pe) break;
				*b = U_CHAR(EOL);
				continue;
			case 't':
				if (!pe) break;
				*b = U_CHAR(TAB);
				continue;
			}
			break;
		}
		*b = *a;
	}

aterr:
	if (ap == NULL) {
		em = strerror(EINVAL);
		ap = U_CPTR("");
	} else
		em = ERR_TRIM;
	error(-1, em);
	return (char *)ap;
}

/*
 * Prepare possible glob() pattern pointed to by ap.
 *
 *	1) Remove (trim) any unquoted quote characters;
 *	2) Escape (w/ backslash `\') any previously quoted
 *	   glob or quote characters as needed;
 *	3) Reallocate memory for (if needed), make copy of,
 *	   and return pointer to new glob() pattern, nap.
 *
 * This function returns NULL on error.
 */
static char *
gtrim(u_char *ap)
{
	size_t siz;
	u_char *a, *b, *nap;
	u_char buf[PATHMAX], c;
	bool d;

	*buf = U_CHAR(EOS);
	for (a = ap, b = buf; b < &buf[PATHMAX]; a++, b++) {
		switch (*a) {
		case EOS:
			*b = U_CHAR(EOS);
			siz = (b - buf) + 1;
			nap = ap;
			if (siz > strlen((const char *)ap) + 1) {
				xfree(nap);
				nap = xmalloc(siz);
			}
			(void)memcpy(nap, buf, siz);
			return (char *)nap;
		case DQUOT:
		case SQUOT:
			c = *a++;
			d = (c == DQUOT) ? true : false;
			while (*a != c && b < &buf[PATHMAX]) {
				switch (*a) {
				case BQUOT:
					if (d && *(a + 1) == DOLLAR)
						a++;
					/*FALLTHROUGH*/
				case ASTERISK: case QUESTION:
				case LBRACKET: case RBRACKET: case HYPHEN:
				case DQUOT:    case SQUOT:
					*b = U_CHAR(BQUOT);
					if (++b >= &buf[PATHMAX])
						goto gterr;
					break;
				}
				*b++ = *a++;
			}
			b--;
			continue;
		case BQUOT:
			switch (*++a) {
			case EOS:
				a--, b--;
				continue;
			case ASTERISK: case QUESTION:
			case LBRACKET: case RBRACKET: case HYPHEN:
			case DQUOT:    case SQUOT:    case BQUOT:
				*b = U_CHAR(BQUOT);
				if (++b >= &buf[PATHMAX])
					goto gterr;
				break;
			}
			break;
		}
		*b = *a;
	}

gterr:
	error(-2, (gchar((const char *)ap) != NULL) ?
	      ERR_PATTOOLONG : strerror(ENAMETOOLONG));
	return NULL;
}

/*
 * Return pointer to first unquoted glob character (`*', `?', `[')
 * in argument pointed to by ap.  Otherwise, return NULL pointer
 * on error or if argument contains no glob characters.
 */
char *
gchar(const char *ap)
{
	char c;
	const char *a;

	for (a = ap; *a != EOS; a++)
		switch (*a) {
		case DQUOT:
		case SQUOT:
			for (c = *a++; *a != c; a++)
				if (*a == EOS)
					return NULL;
			continue;
		case BQUOT:
			if (*++a == EOS)
				return NULL;
			continue;
		case ASTERISK:
		case QUESTION:
		case LBRACKET:
			return (char *)a;
		}
	return NULL;
}

/*
 * Attempt to generate file-name arguments which match the given
 * pattern arguments in av.  Return pointer to newly allocated
 * argument vector, gav, on success.  Return NULL on error.
 */
const char **
glob(enum sbikey key, char **av)
{
	char *tpap;		/* temporary pattern argument pointer  */
	char **oav;		/* points to original argument vector  */
	const char **gav;	/* points to generated argument vector */
	int pmc = 0;		/* pattern match count                 */
	bool gerr = false;	/* glob error flag                     */

	gavtot = 0;

	oav  = av;
	gav  = xmalloc(GAVNEW * sizeof(char *));
	*gav = NULL;
	gavp = gav;
	gave = &gav[GAVNEW - 1];
	while (*av != NULL) {
		if ((tpap = gtrim(U_CPTR(*av))) == NULL) {
			*gavp = NULL;
			gerr  = true;
			break;
		}
		*av = tpap;/* for successful vfree(oav); */
		gav = glob1(key, gav, tpap, &pmc, &gerr);
		if (gerr)
			break;
		av++;
	}
	gavp = NULL;

	if (pmc == 0 && !gerr) {
		error(-2, ERR_NOMATCH);
		gerr = true;
	}
	if (gerr) {
		vfree((char **)gav);
		gav = NULL;
	}
	vfree(oav);
	oav = NULL;
	return gav;
}

static const char **
gnew(const char **gav)
{
	size_t siz;
	ptrdiff_t gidx;
	const char **nav;
	static unsigned int mult = 1; /* GAVNEW reallocation multiplier */

	if (gavp == gave) {
		mult *= GAVMULT;
#ifdef	DEBUG_GLOB
		fd_print(FD2, "%s: mult == %u;\n", __func__, mult);
#endif
		gidx = (ptrdiff_t)(gavp - gav);
		siz  = (size_t)((gidx + (GAVNEW * mult)) * sizeof(char *));
#ifdef	DEBUG_GLOB
		fd_print(FD2, "    : (GAVNEW * mult) == %u, siz == %zu;\n",
		    (GAVNEW * mult), siz);
#endif
		nav  = xrealloc(gav, siz);
		gav  = nav;
		gavp = gav + gidx;
		gave = &gav[gidx + (GAVNEW * mult) - 1];
	}
	return gav;
}

static char *
gcat(const char *src1, const char *src2, bool slash)
{
	size_t siz;
	char *b, buf[PATHMAX], c, *dst;
	const char *s;

	if (src1 == NULL || src2 == NULL) {
		/* never true, but appease splint(1) */
		error1(-2, __func__, strerror(EINVAL));
		return NULL;
	}

	*buf = EOS;
	b = buf;
	s = src1;
	while ((c = *s++) != EOS) {
		if (b >= &buf[PATHMAX - 1]) {
			error(-2, strerror(ENAMETOOLONG));
			return NULL;
		}
		*b++ = c;
	}
	if (slash)
		*b++ = SLASH;
	s = src2;
	do {
		if (b >= &buf[PATHMAX]) {
			error(-2, strerror(ENAMETOOLONG));
			return NULL;
		}
		*b++ = c = *s++;
	} while (c != EOS);
	b--;

	siz = (b - buf) + 1;
	gavtot += siz;
	if (gavtot > GAVMAX) {
		error(-2, ERR_E2BIG);
		return NULL;
	}
#ifdef	DEBUG_GLOB
	fd_print(FD2, "%s: siz == %zu, (%p < %p) == %s;\n", __func__,
	    siz, (void *)b, (void *)&buf[PATHMAX],
	    (b < &buf[PATHMAX]) ? "true" : "false");
	fd_print(FD2, "    : strlen(buf) == %zu;\n", strlen(buf));
#endif
	dst = xmalloc(siz);

	(void)memcpy(dst, buf, siz);
	return dst;
}

static const char **
glob1(enum sbikey key, const char **gav, char *as, int *pmc, bool *gerr)
{
	DIR *dirp;
	struct dirent *entry;
	ptrdiff_t gidx;
	bool slash;
	char dirbuf[PATHMAX], *p, *ps;
	const char *ds;

	ds = as;
	slash = false;
	if ((ps = gchar(as)) == NULL) {
		gav = gnew(gav);
		if (DO_TRIM(key))
			(void)atrim(U_CPTR(as), false);
		if ((p = gcat(as, "", slash)) == NULL) {
			*gavp = NULL;
			*gerr = true;
			return gav;
		}
		*gavp++ = p;
		*gavp = NULL;
		return gav;
	}
	for (;;) {
		if (ps == ds) {
			ds = "";
			break;
		}
		if (*--ps == SLASH) {
			*ps = EOS;
			if (ds == ps)
				ds = "/";
			else
				slash = true;
			ps++;
			break;
		}
	}
	if ((dirp = gopendir(dirbuf, *ds != EOS ? ds : ".")) == NULL) {
		error(-2, ERR_NODIR);
		*gavp = NULL;
		*gerr = true;
		return gav;
	}
	if (*ds != EOS)
		ds = dirbuf;
	gidx = (ptrdiff_t)(gavp - gav);
	while ((entry = readdir(dirp)) != NULL) {
		if (entry->d_name[0] == DOT && *ps != DOT)
			continue;
		if (glob2(U_CPTR(entry->d_name), U_CPTR(ps))) {
			gav = gnew(gav);
			if ((p = gcat(ds, entry->d_name, slash)) == NULL) {
				(void)closedir(dirp);
				*gavp = NULL;
				*gerr = true;
				return gav;
			}
			*gavp++ = p;
			(*pmc)++;
		}
	}
	(void)closedir(dirp);
	gsort(gav + gidx);
	*gavp = NULL;
	*gerr = false;
	return gav;
}

static bool
glob2(const u_char *ename, const u_char *pattern)
{
	int cok, rok;		/* `[...]' - cok (class), rok (range) */
	u_char c, ec, pc;
	const u_char *e, *n, *p;

	e = ename;
	p = pattern;

	ec = *e++;
	switch (pc = *p++) {
	case EOS:
		return ec == EOS;

	case ASTERISK:
		/*
		 * Ignore all but the last `*' in a group of consecutive
		 * `*' characters to avoid unnecessary glob2() recursion.
		 */
		while (*p++ == ASTERISK)
			;	/* nothing */
		if (*--p == EOS)
			return true;
		e--;
		while (*e != EOS)
			if (glob2(e++, p))
				return true;
		break;

	case QUESTION:
		if (ec != EOS)
			return glob2(e, p);
		break;

	case LBRACKET:
		if (*p == EOS)
			break;
		for (c = U_CHAR(EOS), cok = rok = 0, n = p + 1; ; ) {
			pc = *p++;
			if (pc == RBRACKET && p > n) {
				if (cok > 0 || rok > 0)
					return glob2(e, p);
				break;
			}
			if (*p == EOS)
				break;
			if (pc == HYPHEN && c != EOS && *p != RBRACKET) {
				if ((pc = *p++) == BQUOT)
					pc = *p++;
				if (*p == EOS)
					break;
				if (c <= ec && ec <= pc)
					rok++;
				else if (c == ec)
					cok--;
				c = U_CHAR(EOS);
			} else {
				if (pc == BQUOT) {
					pc = *p++;
					if (*p == EOS)
						break;
				}
				c = pc;
				if (ec == c)
					cok++;
			}
		}
		break;

	case BQUOT:
		if (*p != EOS)
			pc = *p++;
		/*FALLTHROUGH*/

	default:
		if (pc == ec)
			return glob2(e, p);
	}
	return false;
}

static void
gsort(const char **ogavp)
{
	const char **p1, **p2, *sap;

	p1 = ogavp;
	while (gavp - p1 > 1) {
		p2 = p1;
		while (++p2 < gavp)
			if (strcmp(*p1, *p2) > 0) {
				sap = *p1;
				*p1 = *p2;
				*p2 = sap;
			}
		p1++;
	}
}

static DIR *
gopendir(char *buf, const char *dname)
{
	char *b;
	const char *d;

	for (*buf = EOS, b = buf, d = dname; b < &buf[PATHMAX]; b++, d++)
		if ((*b = *d) == EOS) {
			(void)atrim(U_CPTR(buf), false);
			break;
		}
	return (b >= &buf[PATHMAX]) ? NULL : opendir(buf);
}
