#!mkcmd
# $Id: installus.m,v 2.17 2009/01/05 15:55:12 ksb Exp $
# mkcmd script for installus options, fakes most of install's options.

from '<sys/param.h>'
from '<sys/types.h>'
from '<sys/stat.h>'
from '<sysexits.h>'
from '<pwd.h>'
from '<grp.h>'
from '<fcntl.h>'
from '<errno.h>'
from '<sys/wait.h>'

from '"argvutil.h"'
from '"control.h"'
from '"machine.h"'
from '"sendfd.h"'

require "util_getlogingr.m" "util_username.m"
require "std_help.m" "std_version.m"
basename "installus" ""

%i
static char *rcsid =
	"$Id: installus.m,v 2.17 2009/01/05 15:55:12 ksb Exp $";

static int Install(int, char **, char *);
%%

augment action 'V' {
	user "Version();"
}

%h
extern char *pcGuilty, *pcGuiltyPasswd;
%%

%c
char **ppcInstallArgs;
int iInstallArgs;

#if !defined(INSTALLPATH)
#define INSTALLPATH "/usr/local/bin/install"
#endif
%%

#
# recognize all the install options we'll pass by
# accumulating them into an argv
#

%c
static void
AccumAction(cThis)
	int cThis;
{
	char *apcBuf[1], acBuf[3];

	sprintf(acBuf, "-%c", cThis);
	apcBuf[0] = strdup(acBuf);
	ArgvAppend(&iInstallArgs, &ppcInstallArgs, 1, apcBuf);
}

char *pcGuilty = (char *)0,
     *pcGuiltyPasswd = (char *)0;
extern char **environ;

#if HAVE_GETSPNAM
#include <shadow.h>
#endif

/* we need to put 'progname' at the beginning of our InstallArgs, and	(bj)
 * allocate so machines with st00pid realloc don't coredump trying to
 * realloc(0, etc).  this is also a good time to get the passwd entry
 * of the user.
 */
void
FindGuilty()
{
	extern char *getenv(), *getlogin(), *crypt();
	register char *pcUser;
	register struct passwd *pwd;
#if HAVE_GETSPNAM
	register struct spwd *spwd;
#endif

	ppcInstallArgs = (char **) calloc(1, sizeof(char *));
	ppcInstallArgs[0] = progname;
	iInstallArgs = 1;

	pcUser = getlogin();
	if ((char *)0 == pcUser || '\000' == *pcUser) {
		pcUser = getenv("USER");
	}
	if ((char *)0 == pcUser || '\000' == *pcUser) {
		pcUser = getenv("LOGNAME");
	}
	setpwent();
	if ((char *)0 != pcUser && (struct passwd *)0 != (pwd = getpwnam(pcUser)) && pwd->pw_uid == getuid()) {
		pcGuilty = strdup(pcUser);
	} else if ((struct passwd *)0 != (pwd = getpwuid(getuid()))) {
		pcGuilty = strdup(pwd->pw_name);
	} else {
		fprintf(stderr, "%s: getpwuid: %d: cannot locate your passwd entry\n", progname, getuid());
		exit(EX_NOUSER);
	}
#if HAVE_GETSPNAM
	setspent();
	if ((struct spwd *)0 == (spwd = getspnam(pcGuilty))) {
		fprintf(stderr, "%s: getspnam: cannot locate your shadow password\n", progname);
		exit(EX_UNAVAILABLE);
	}
	pcGuiltyPasswd = strdup(spwd->sp_pwdp);
	endspent();
#else
	pcGuiltyPasswd = strdup(pwd->pw_passwd);
#endif
	if (strlen(crypt("xx", "xx")) != strlen(pcGuiltyPasswd) && !('$' == pcGuiltyPasswd[0] || '*' == pcGuiltyPasswd[0])) {
		fprintf(stderr, "%s: warning, crypted%s password looks bogus\n", progname,
#if HAVE_GETSPNAM
			" shadow"
#else
			""
#endif
		);
	}
	endpwent();
}
%%

init 3 "FindGuilty();"

###############################################
# these are clones of all the install options #
###############################################

action '1' {
	named "AccumAction"
	help "keep exactly one backup of the installed file"
}

action 'D' {
	named "AccumAction"
	help "no backup of target file"
}

boolean 'R' {
	excludes "1Drls"
	named "fRemove"
	user "AccumAction(%w);"
	help "remove the given target"
}

action 'c' {
	update "/* no op -- we always copy */"
	help "a no op for compatibility with install"
}

boolean 'd' {
	excludes "1Dls"
	named "fMustDir"
	user "AccumAction(%w);"
	left "pcDest" { }
	help "install a directory (mkdir)"
	exit {
		named "Install"
		update "exit(%n(0, (char **)0, pcDest));"
	}
	boolean 'r' {
		named "fRecurse"
		user "AccumAction(%w);"
		help "build all intervening directories"
	}
}

boolean 'n' {
	named "fTrace"
	user "AccumAction(%w);"
	help "trace execution but do not do anything"
}

action 'q' {
	named "AccumAction"
	help "if install can recover from the error be quiet"
}

action 's' {
	named "AccumAction"
	help "run strip(1) on file"
}

action 'N' {
	named "AccumAction"
	help "this must be the first installation of the file"
}

action 'l' {
	named "AccumAction"
	help "run ranlib(1) on file (for libraries)"
}

boolean 'v' {
	named "fVerbose"
	user "AccumAction(%w);"
	help "run in verbose mode"
}


#################################
# end of cloned install options #
#################################

# split into two processes.  The child will open files relative
# to the directory from which installus was run and with the
# user's UID.  The parent can then chdir() to the destination
# to avoid spoofing and race conditions
#
after {
	named "RopenSplit"
	update "%n(DropEUID);"
}

# wow, this one is black magic.  I almost ran out of chickens. -- bj
#
action 'L' {
	excludes "1dDnqrRlsW"
	update "/* none */"

	boolean 'x' {
		local named "fExclusive"
		help "list the programs that ONLY user owns"
	}
	local char* variable "pcOwnedUser" {
		param "user"
		help "user whose programs to list, default is yours"
	}
	left [ "pcOwnedUser" ] { }
	zero {
		from '"control.h"'
		named "ShowOwned"
		update "%n(pcOwnedUser, \".\", fExclusive);"
	}
	every {
		from '"control.h"'
		named "ShowOwned"
		update "%n(pcOwnedUser, %N, fExclusive);"
		param "dirs"
		help "where to check for owned programs, default is ."
	}
	help "list the programs that user owns"
}

action 'W' {
	excludes "1DnqrRlsL"
	update "/* none */"

	zero {
		from '"control.h"'
		named "ShowOwner"
		update "%n(pcGuilty);"
	}
	every {
		from '"control.h"'
		named "ShowOwner"
		update "%n(%N);"
		param "files"
		help "list maintainers for products"
	}
	help "list owners"
}

zero {
	aborts 'if (!fRemove) {fprintf(stderr, "%%s: no files to install?\\n", %b);exit(EX_DATAERR);}'
	update "/* aborts */"
}

list {
	named "Install"
	update "exit(%n(%#, %@, pcDest));"
	param "files"
	help "list of files to install"
}

char* variable "pcDest" {
	local param "destination"
	help "destination filename or directory"
}

right "pcDest" {
}


%c
/* fork/exec a program to do some work, return exit status		(bj)
 */
int
Run(path, av, envp)
char *path;
char **av;
char **envp;
{
	register int iPid;
	auto int iReturn;

	switch (iPid = fork()) {
	case -1:
		fprintf(stderr, "%s: fork: %s\n", progname, strerror(errno));
		exit(EX_OSERR);
	case 0:
		environ = envp;
		if (-1 == execvp(path, av)) {
			fprintf(stderr, "%s: execvp: %s: %s\n", progname, path, strerror(errno));
			exit(EX_UNAVAILABLE);
		}
		/* NOTREACHED we hope */
	default:
		break;
	}
#if HAS_WAITPID
	waitpid(iPid, &iReturn, 0);
#else
	{	register int i;
		while (-1 != (i = wait(& iReturn)) && i != iPid)
			/* loop */;
	}
#endif
	return (iReturn >> 8) & 0xff;
}

/* in the modes we can use `-o ~' to force Joe to own the file		(ksb)
 */
void
ReplaceTilde(argc, argv, pcWho)
int argc;
char **argv, *pcWho;
{
	register int i;
	for (i = 0; i < argc; ++i) {
		if ('~' != argv[i][0] || '\000' != argv[i][1])
			continue;
		argv[i] = pcWho;
	}
}

static int iSaveArgs;

/* install a single file from pcSource into pcDestDir/pcDestFile	(bj)
 * note that we assume that the proper ParseOwnersFor() call has
 * already been made.  we look up its entry in the
 * .owners file and find out what options to give install for it.
 * we append those to our current InstallArgs and then add the
 * file and destination before forking off an install to do the
 * work.  return boolean success.
 */
int
InstallFile(pcSource, pcDestDir, pcDestFile)
char *pcSource, *pcDestDir, *pcDestFile;
{
	register int i, fd;
	register char *pcArgs;
	auto char *apcTemp[2];

	if ((char *)0 != pcSource && 0 != strcmp("-", pcSource)) {
		/* replace file descriptor 0 with the file we're installing.
		 */
		if (-1 == (fd = ropen(pcSource, O_RDONLY, 0))) {
			fprintf(stderr, "%s: open: %s: %s\n", progname, pcSource, strerror(errno));
			exit(EX_NOPERM);
		}
		close(0);
		if (0 != dup(fd)) {
			fprintf(stderr, "%s: dup(%d) != 0: %s\n", progname, fd, strerror(errno));
			exit(EX_OSERR);
		}
		close(fd);
	}
	/* now we should have the file on stdin, ready to install
	 */

	iInstallArgs = iSaveArgs;

	/* we have the file (if any) on stdin, we know what args
	 * they want, it's time to make sure they can do it and get
	 * the appropriate install options from the .owners file
	 */
	if ((char *)0 == (pcArgs = GetInstallOpts(pcDestFile, fMustDir))) {
		/* we've already told them why it failed,
		 * now we just get out.
		 */
		return EX_USAGE;
	}
	Argvize(&iInstallArgs, &ppcInstallArgs, pcArgs);
	ReplaceTilde(iInstallArgs, ppcInstallArgs, pcGuilty);

	i = 0;
	if ((char *)0 != pcSource) {
		apcTemp[i] = "-";
		++i;
	}
	apcTemp[i] = pcDestFile;
	++i;
	ArgvAppend(&iInstallArgs, &ppcInstallArgs, i, apcTemp);
	if (fVerbose) {
		register int fChdir;

		/* print a cd command unless the dir is "" or "."
		 * then output the install command line, and
		 * the redirection for the ropen call
		 */
		fChdir = !('\000' == pcDestDir[0] || ('.' == pcDestDir[0] && '\000' == pcDestDir[1]));
		printf("%s:", progname);
		if (fChdir) {
			printf(" (cd %s;", pcDestDir);
		}
		printf(" %s", INSTALLPATH);
		for (i = 1; i < iInstallArgs; ++i) {
			printf(" %s", ppcInstallArgs[i]);
		}
		if (fChdir) {
			printf(")");
		}
		if ((char *)0 != pcSource && 0 != strcmp("-", pcSource)) {
			printf(" <%s", pcSource);
		}
		printf("\n");
	}
	return Run(INSTALLPATH, ppcInstallArgs, (char **)0);
}

/* split the destination path into a path and file component             (bj)
 * if pcDest is a dir, then the acDestFile[0] == '\000'.
 */
static int
SplitDest(pcDest, pcDestDir, pcDestFile)
char *pcDest, *pcDestDir, *pcDestFile;
{
	register int iLen;
	register char *pcTemp, cTemp;
	auto struct stat statbuf;

	if ('\000' == *pcDest) {
		fprintf(stderr, "%s: Empty destination?\n", progname);
		return 0;
	}
	iLen = strlen(pcDest);
	if (iLen+2 > MAXPATHLEN) {
		fprintf(stderr, "%s: %s: Directory name too long\n", progname, pcDest);
		return 0;
	}
	if (-1 != stat(pcDest, &statbuf) && S_ISDIR(statbuf.st_mode) && ! fRemove) {
		strcpy(pcDestDir, pcDest);
		/* it's an invariant that iLen > 0 */
		if ('/' != pcDestDir[iLen - 1]) {
			pcDestDir[iLen++] = '/';
			pcDestDir[iLen] = '\000';
		}
		pcDestFile[0] = '\000';
	} else {
		/* either stat failed (file doesn't exist?) or
		 * it wasn't a directory
		 */
		if ((char *)0 == (pcTemp = strrchr(pcDest, '/'))) {
			pcTemp = pcDest;
		} else {
			++pcTemp;
		}
		strcpy(pcDestFile, pcTemp);
		cTemp = *pcTemp;
		*pcTemp = '\000';
		strcpy(pcDestDir, pcDest);
		*pcTemp = cTemp;
	}
	return 1;
}

/* make -rd work for real						(ksb)
 * if the distination doesn't exist we have to recursively make it
 * else the .owners code can't chdir there to find the .owners...
 * This gets really messy as we have to _follow_ the path down with
 * chdir... and look for symbolic link traps.
 * (If "/" is a symbolic link we are in trouble from the get-go.)
 */
static void
BackBeat(pcIn)
char *pcIn;
{
	register char *pcSlash, *pcTop;
	auto struct stat stLink;
	auto int iRet;

	pcTop = pcIn;
	if ('/' == *pcIn) {
		++pcIn;
		if (fVerbose) {
			printf("%s: chdir /\n", progname);
		}
		if (-1 == chdir("/")) {
			fprintf(stderr, "%s: chdir: /: %s\n", progname, strerror(errno));
			exit(EX_OSFILE);
		}
	}
	while ('\000' != *pcIn) {
		if ((char *)0 != (pcSlash = strchr(pcIn, '/'))) {
			*pcSlash = '\000';
		}
		if (0 == chdir(pcIn)) {
			/* we see one already */
		} else if (-1 != lstat(pcIn, & stLink) || ENOENT != errno) {
			/* we lost */
			fprintf(stderr, "%s: %s: lstat: %s\n", progname, pcTop, strerror(errno));
			exit(EX_DATAERR);
		} else {
			if (0 != Install(0, (char **)0, pcIn) || 0 != chdir(pcIn)) {
				exit(EX_DATAERR);
			}
		}
		if (fVerbose) {
			printf("%s: chdir %s\n", progname, pcIn);
		}
		if ((char *)0 != pcSlash) {
			*pcSlash = '/';
			pcIn = pcSlash + 1;
		} else {
			break;
		}
	}
}

/* drop effective UID							(bj)
 */
void
DropEUID()
{
#if HAVE_SETREUID
	setreuid(getuid(), getuid());
#else
	setuid(getuid());
#endif
}

/* do the install, of course!						(bj)
 * put the list of files in the target directory (or file)
 */
static int
Install(ac, av, pcDest)
int ac;
char **av, *pcDest;
{
	register int iFile, iRet;
	auto char acDestDir[MAXPATHLEN+4], acDestFile[MAXPATHLEN+4];
	auto struct stat stOwners;

	/* we currently have progname and the passed options in
	 * InstallArgs.  we save the current Argc so we can go back
	 * right before each file
	 */
	iSaveArgs = iInstallArgs;

	if (!SplitDest(pcDest, acDestDir, acDestFile)) {
		exit(EX_USAGE);
	}
	/* When -d is set we for an existing directory to look like it
	 * doesn't exist so we'll update the modes/owners from .owners.
	 * Strip training slashes as a side-effect of the loop.
	 */
	while (fMustDir && '\000' == acDestFile[0]) {
		register char *pcSlash;

		pcSlash = strrchr(acDestDir, '/');
		if ((char *)0 == pcSlash) {
			(void)strcpy(acDestFile, acDestDir);
			(void)strcpy(acDestDir, ".");
		} else {
			*pcSlash++ = '\000';
			(void)strcpy(acDestFile, pcSlash);
		}
	}
	if (fRecurse && !fRemove) {
		fRecurse = 0;
		BackBeat(acDestDir);
		fRecurse = 1;
	}
	if (!ParseOwnersFor(acDestDir, & stOwners, (char *)0)) {
		/* we told them why, just get out
		 */
		exit(EX_USAGE);
	}

	/* install -R */
	if (fRemove) {
		iRet = InstallFile((char *)0, acDestDir, acDestFile);
		iInstallArgs = iSaveArgs;
		return iRet;
	}
	if ('\000' != acDestFile[0]) {
		/* we have a specific filename
		 */
		switch (ac) {
		case 0:
			/* install -d
			 */
			iRet = InstallFile((char *)0, acDestDir, acDestFile);
			break;

		case 1:
			/* install the one file as the given name
			 */
			iRet = InstallFile(av[0], acDestDir, acDestFile);
			break;

		default:
			/* pcDest was a specific filename, and they're trying
			 * to install multiple files.  oops!
			 */
			fprintf(stderr, "%s: destination is not a directory\n", progname);
			iRet = 1;
			break;
		}
		iInstallArgs = iSaveArgs;
		return iRet;
	}

	/* at this point we know we have a dest dir (acDestDir) and
	 * we want to install all the things in the files vector
         * using the same name as source and destination
	 */
	for (iFile = 0; iFile < ac; ++iFile) {
		register char *pcTail;

		if ((char *)0 == (pcTail = strrchr(av[iFile], '/'))) {
			pcTail = av[iFile];
		} else {
			++pcTail;
		}
		if (0 != (iRet = InstallFile(av[iFile], acDestDir, pcTail))) {
			iInstallArgs = iSaveArgs;
			return iRet;
		}
	}
	iInstallArgs = iSaveArgs;
	return 0;
}

/* detail version info							(ksb)
 */
static void
Version()
{
	auto gid_t awGids[NGROUPS+2];
	register int iCc;

	printf("%s: owners data from \"%s\"\n", progname, acOwners);
	printf("%s: username is %s\n", progname, username);
#if HAVE_GETSPNAM
	printf("%s: using shadow passwords\n", progname);
#endif
	if (fVerbose && -1 != (iCc = getgroups(sizeof(awGids)/sizeof(awGids[0]), awGids))) {
		register gid_t wPrimary;
		register struct group *grp;


		wPrimary = getgid();
		printf("%s: groups: ", progname);
		setgrent();
		if ((struct group *)0 != (grp = getgrgid(wPrimary)))
			printf("%s", grp->gr_name);
		else
			printf("%d", wPrimary);
		while (0 != iCc) {
			if (wPrimary == awGids[--iCc])
				continue;
			if ((struct group *)0 != (grp = getgrgid(awGids[iCc])))
				printf(" %s", grp->gr_name);
			else
				printf(" %d", awGids[iCc]);
		}
		endgrent();
		printf("\n");
	}
}
%%
