/*
 * if.c - a port of the Version 6 (V6) conditional command
 */
/*-
 * 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: if.c,v 1.7 2019/02/16 18:14:56 jneitzel Exp $
 */
/*
 *	Derived from:
 *		- V6 UNIX /usr/source/s1/if.c
 *		- V7 UNIX /usr/src/cmd/test.c
 */
/*-
 * 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.
 */

#include "defs.h"
#include "sh_err.h"
#include "pexec.h"
#include "sasignal.h"
#include "strtoint.h"

const char	*name;
bool		no_lnum	= true;	/* no line number flag */

static	int	ac;
static	int	ap;
static	char	**av;
static	uid_t	ifeuid;

/*@noreturn@*/
static	void	doex(bool);
static	bool	e1(void);
static	bool	e2(void);
static	bool	e3(void);
static	bool	equal(/*@null@*/ const char *, /*@null@*/ const char *);
static	bool	expr(void);
static	bool	ifaccess(/*@null@*/ const char *, int);
static	bool	ifstat1(/*@null@*/ const char *, mode_t);
static	bool	ifstat2(/*@null@*/ const char *, /*@null@*/ const char *, int);
/*@null@*/
static	char	*nxtarg(bool);

/*
 * NAME
 *	if - conditional command
 *
 * SYNOPSIS
 *	if [expression [command [arg ...]]]
 *
 * DESCRIPTION
 *	See the if(1) manual page for full details.
 */
int
main(int argc, char **argv)
{
	bool re;		/* return value of expr() */

	setmyerrexit(&ut_errexit);
	setmyname(argv[0]);
	setmypid(getpid());

	ifeuid = geteuid();

	/*
	 * Set-ID execution is not supported.
	 */
	if (ifeuid != getuid() || getegid() != getgid())
		sh_err(FC_ERR, FMT2S, getmyname(), ERR_SETID);

	(void)sasignal(SIGCHLD, SIG_DFL);

	if (argc > 1) {
		ac = argc;
		av = argv;
		ap = 1;
		re = expr();
		if (re && ap < ac)
			doex(!FORKED);
	} else
		re = false;

	return re ? SH_TRUE : SH_FALSE;
}

/*
 * Evaluate the expression.
 * Return true (1) or false (0).
 */
static bool
expr(void)
{
	bool re;

	re = e1();
	if (equal(nxtarg(RETERR), "-o"))
		return re | expr();
	ap--;
	return re;
}

static bool
e1(void)
{
	bool re;

	re = e2();
	if (equal(nxtarg(RETERR), "-a"))
		return re & e1();
	ap--;
	return re;
}

static bool
e2(void)
{

	if (equal(nxtarg(RETERR), "!"))
		return !e3();
	ap--;
	return e3();
}

static bool
e3(void)
{
	bool re;
	pid_t cpid, tpid;
	long int ai, ci;
	int cstat, d;
	char *a, *b, *c;

	if ((a = nxtarg(RETERR)) == NULL)
		sh_err(FC_ERR, FMT3S, getmyname(), av[ap - 2], ERR_EXPR);

	/*
	 * Deal w/ parentheses for grouping.
	 */
	if (equal(a, "(")) {
		re = expr();
		if (!equal(nxtarg(RETERR), ")"))
			sh_err(FC_ERR, FMT3S, getmyname(), a, ERR_PAREN);
		return re;
	}

	/*
	 * Execute { command [arg ...] } to obtain its exit status.
	 */
	if (equal(a, "{")) {
		if ((cpid = fork()) == -1)
			sh_err(FC_ERR, FMT2S, getmyname(), ERR_FORK);
		if (cpid == 0)
			/**** Child! ****/
			doex(FORKED);
		else {
			/**** Parent! ****/
			tpid = wait(&cstat);
			while ((a = nxtarg(RETERR)) != NULL && !equal(a, "}"))
				;	/* nothing */
			if (a == NULL)
				ap--;
			return (tpid == cpid && cstat == 0) ? true : false;
		}
	}

	/*
	 * file existence/permission tests
	 */
	if (equal(a, "-e"))
		return ifaccess(nxtarg(!RETERR), F_OK);
	if (equal(a, "-r"))
		return ifaccess(nxtarg(!RETERR), R_OK);
	if (equal(a, "-w"))
		return ifaccess(nxtarg(!RETERR), W_OK);
	if (equal(a, "-x"))
		return ifaccess(nxtarg(!RETERR), X_OK);

	/*
	 * file existence/type tests
	 */
	if (equal(a, "-d"))
		return ifstat1(nxtarg(!RETERR), S_IFDIR);
	if (equal(a, "-f"))
		return ifstat1(nxtarg(!RETERR), S_IFREG);
	if (equal(a, "-h"))
		return ifstat1(nxtarg(!RETERR), S_IFLNK);
	if (equal(a, "-s"))
		return ifstat1(nxtarg(!RETERR), F_GZ);
	if (equal(a, "-t")) {
		/* Does the descriptor refer to a terminal device? */
		if ((b = nxtarg(RETERR)) == NULL || *b == EOS) {
			sh_err(FC_ERR, FMT3S, getmyname(), a, ERR_DIGIT);
			/*NOTREACHED*/
		}
		if (*b >= '0' && *b <= '9' && *(b + 1) == EOS) {
			d = *b - '0';
			if (IS_DIGIT(d, *b))
				return isatty(d) != 0;
		}
		sh_err(FC_ERR, FMT3S, getmyname(), b, ERR_BADDIGIT);
	}

	/* This is the same as "$1" != "", for example, in the shell... */
	if (equal(a, "-n"))
		return strlen(nxtarg(!RETERR)) != 0;

	/*
	 * And this is the same as "$1" = "", for example.  However, it
	 * is difficult to use in tsh because of the need for quotes; $1
	 * must be unquoted for substitution to occur.  "$1" & '$1' are
	 * string literals otherwise.  So...  Difficult to use??  Yes,
	 * but not impossible if you know your shell voodoo! ;^)
	 *
	 * See scratch/ifzyon for an illustration if you like.
	 */
	if (equal(a, "-z"))
		return strlen(nxtarg(!RETERR)) == 0;

	/*
	 * binary comparisons
	 */
	if ((b = nxtarg(RETERR)) == NULL)
		sh_err(FC_ERR, FMT3S, getmyname(), a, ERR_OPERATOR);
	if (equal(b, "!="))
		return !equal(a, nxtarg(!RETERR));
	if (equal(b, "=="))
		return  equal(a, nxtarg(!RETERR));
	if (equal(b,  "="))
		return  equal(a, nxtarg(!RETERR));
	if (equal(b, "<"))
		return strcmp(a, nxtarg(!RETERR)) < 0;
	if (equal(b, ">"))
		return strcmp(a, nxtarg(!RETERR)) > 0;
	if (equal(b, "-ef"))
		return ifstat2(a, nxtarg(!RETERR), F_EF);
	if (equal(b, "-nt"))
		return ifstat2(a, nxtarg(!RETERR), F_NT);
	if (equal(b, "-ot"))
		return ifstat2(a, nxtarg(!RETERR), F_OT);
	if (equal(b, "-eq") || equal(b, "-ne") || equal(b, "-gt") ||
	    equal(b, "-ge") || equal(b, "-lt") || equal(b, "-le")) {
		if ((c = nxtarg(RETERR)) == NULL)
			sh_err(FC_ERR, FMT3S, getmyname(), b, ERR_INTEGER);
		if (strtoint(a, &ai) && strtoint(c, &ci)) {
			if (equal(b, "-eq"))
				return ai == ci;
			if (equal(b, "-ne"))
				return ai != ci;
			if (equal(b, "-gt"))
				return ai >  ci;
			if (equal(b, "-ge"))
				return ai >= ci;
			if (equal(b, "-lt"))
				return ai <  ci;
			if (equal(b, "-le"))
				return ai <= ci;
		} else
			sh_err(FC_ERR, NULL);
	}
	sh_err(FC_ERR, FMT3S, getmyname(), b, ERR_OPUNKNOWN);
	/*NOTREACHED*/
	return false;
}

static void
doex(bool forked)
{
	char **xap, **xav;
	size_t i, s;
	const char **tav;
	const char *cmd;

	if (ap < 2 || ap > ac)	/* should never (but can) be true */
		sh_err(FC_ERR, FMT2S, getmyname(), ERR_AVIINVAL);

	xav = xap = &av[ap];
	while (*xap != NULL) {
		if (forked && equal(*xap, "}"))
			break;
		xap++;
	}
	if (forked && xap - xav > 0 && !equal(*xap, "}"))
		sh_err(FC_ERR, FMT3S, getmyname(), av[ap - 1], ERR_BRACE);
	*xap = NULL;
	if (xav[0] == NULL) {
		if (forked)
			sh_err(FC_ERR,FMT3S,getmyname(),av[ap - 1],ERR_COMMAND);
		else
			/* Currently unreachable; do not remove. */
			sh_err(FC_ERR,FMT2S,getmyname(),ERR_COMMAND);
	}

	/* Invoke the ":" or "exit" special command. */
	if (equal(xav[0], ":"))
		EXIT(SH_TRUE);
	if (equal(xav[0], "exit")) {
		(void)lseek(FD0, (off_t)0, SEEK_END);
		EXIT(SH_TRUE);
	}

	cmd = xav[0];
	if (IS_LIBEXEC(equal)) {
		for (i = 0; xav[i] != NULL; i++)
			;	/* nothing */
		/*
		 * Check for size_t overflow.
		 */
		i += 1;
		s  = sizeof(char *);
		SIZE_T_EOVERFLOW(i, s);
		if ((tav = malloc(i * s)) == NULL) {
			sh_err(FC_ERR, FMT2S, getmyname(), ERR_NOMEM);
			/*NOTREACHED*/
		}
		if (equal(cmd, "fd2"))
			tav[0] = FD2_PATH;
		else if (equal(cmd, "goto"))
			tav[0] = GOTO_PATH;
		else
			tav[0] = IF_PATH;
		cmd = tav[0];
		(void)memcpy(&tav[1], &xav[1], i * s);
		(void)pexec_err(cmd, (char *const *)tav);
	} else
		(void)pexec_err(cmd, xav);
}

/*
 * Check access permission for file according to mode while
 * dealing w/ special cases for the superuser and `-x':
 *	- Always grant search access on directories.
 *	- If not a directory, require at least one execute bit.
 * Return true  (1) if access is granted.
 * Return false (0) if access is denied.
 */
static bool
ifaccess(const char *file, int mode)
{
	struct stat sb;
	bool ra;

	if (file == NULL || *file == EOS)
		return false;

	ra = access(file, mode) == 0;

	if (ra && mode == X_OK && ifeuid == 0) {
		if (stat(file, &sb) < 0)
			ra = false;
		else if (S_ISDIR(sb.st_mode))
			ra = true;
		else
			ra = (sb.st_mode & (S_IXUSR|S_IXGRP|S_IXOTH)) != 0;
	}

	return ra;
}

static bool
ifstat1(const char *file, mode_t type)
{
	struct stat sb;
	bool rs;

	if (file == NULL || *file == EOS)
		return false;

	if (type == S_IFLNK) {
		if (lstat(file, &sb) < 0)
			rs = false;
		else
			rs = (sb.st_mode & S_IFMT) == type;
	} else if (stat(file, &sb) < 0)
		rs = false;
	else if (type == S_IFDIR || type == S_IFREG)
		rs = (sb.st_mode & S_IFMT) == type;
	else if (type == F_GZ)
		rs = sb.st_size > (off_t)0;
	else
		rs = false;

	return rs;
}

static bool
ifstat2(const char *file1, const char *file2, int act)
{
	struct stat sb1, sb2;
	bool rs;

	if (file1 == NULL || *file1 == EOS)
		return false;
	if (file2 == NULL || *file2 == EOS)
		return false;

	if (stat(file1, &sb1) < 0)
		return false;
	if (stat(file2, &sb2) < 0)
		return false;

	if (act == F_OT)
		rs = sb1.st_mtime < sb2.st_mtime;
	else if (act == F_NT)
		rs = sb1.st_mtime > sb2.st_mtime;
	else if (act == F_EF)
		rs = sb1.st_dev == sb2.st_dev && sb1.st_ino == sb2.st_ino;
	else
		rs = false;

	return rs;
}

static char *
nxtarg(bool reterr)
{
	char *nap;

	if (ap < 1 || ap > ac)	/* should never (but can) be true */
		sh_err(FC_ERR, FMT2S, getmyname(), ERR_AVIINVAL);

	if (ap == ac) {
		if (reterr) {
			ap++;
			return NULL;
		}
		sh_err(FC_ERR, FMT3S, getmyname(), av[ap - 1], ERR_ARGUMENT);
	}
	nap = av[ap];
	ap++;
	return nap;
}

static bool
equal(const char *a, const char *b)
{

	if (a == NULL || b == NULL)
		return false;
	return EQUAL(a, b);
}
