/*
 * Copyright (c) 1995 by Sun Microsystems, Inc.
 * All rights reserved.
 *
 * This module contains all the code necessary to establish the key base
 * directories to which the actual components of the package will be
 * installed or removed. -- JST
 */

#ident	"@(#)fixpath.c	1.20	95/11/29 SMI"

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>	/* mkdir() declaration */
#include <libintl.h>
#include <pkglib.h>
#include "install.h"
#include "libadm.h"
#include "libinst.h"

static char *install_root = NULL;
static int install_root_exists = 0;	/* An install root was specified */
static int install_root_len;		/* strlen(install_root) */
static char *orig_basedir = NULL;	/* The unadjusted basedir */
static char *basedir = NULL;		/* basedir (cmb w/ inst rt if req) */
static int basedir_exists = 0;		/* There are relocatable paths */
static char *client_basedir = NULL;
static int client_basedir_exists = 0;	/* Installing from a host */
static char *env_cl_bdir = NULL;	/* CLIENT_BASEDIR from environment */
static int ir_accessed = 0;		/* install_root has been used */
static int relocatable;			/* set_basedir() assumed this */

static int	ask_basedir(char *path, int nointeract);
static char	*expand_path(char *path);
static int	set_client_basedir(void);
static char 	*fixpath_dup(char *path);
static int	orig_offset_rel;

/*
 * base_sepr and rel_fmt support construction of absolute paths from
 * relative paths.
 */
static int	base_sepr = 1;	/* separator length btwn basedir & path */
static char	*rel_fmt[] = { "%s%s", "%s/%s" };

static int	eval_valid = 0;	/* everything set up to do an eval_path() */

extern void	quit(int retcode);

/* libpkg/gpkgmap.c */
extern int	getmapmode();

#define	MSG_IR_REPL	"Replacing current install root with %s."
#define	ERR_IRSET	"Install_root has already been set and used."
#define	ERR_IRNOTABS	"Install_root(-R option) requires an absolute pathname."
#define	ERR_ALLOCFAILED	"insuffient memory in %s"
#define	ERR_ADMIN_INVAL	"Invalid basedir entry in admin file."
#define	ERR_PATHNAME 	"Path name is invalid"
#define	ERR_RELINABS	"Relative path <%s> found in absolute package."
#define	ERR_CL_MIS	"Constructed CLIENT_BASEDIR <%s> and " \
			"environment CLIENT_BASEDIR <%s> do not match."
#define	ERR_ASKBD	"%s is already installed at %s. Cannot create a " \
			    "duplicate installation at %s."
#define	ERR_NO_CL_BD	"Cannot resolve CLIENT_BASEDIR conflicts."
#define	ERR_AMBDIRS	"Cannot evaluate path due to ambiguous " \
			"base directories."
#define	ERR_NODELETE	"Unable to delete <%s>."
#define	ERR_MKBASE	"Unable to make directory <%s>."
#define	MSG_REQBASEDIR	"Installation of this package requires a base " \
			"directory."

#define	MSG_MUSTEXIST	"\\nThe selected base directory <%s> must exist " \
			"before installation is attempted."
#define	MSG_YORNPRMPT	"Do you want this directory created now"

#define	MSG_ISAFILE	"\\nThe selected base directory <%s> must exist " \
			"before installation is attempted, but a file " \
			"already exists in it's place."
#define	MSG_YORNFILE	"Do you want the file deleted and the directory " \
			"created now"

#define	MSG_PROMPT	"Enter path to package base directory"

#define	MSG_HELP	"Installation of this package requires that a UNIX " \
			"directory be available for installation of " \
			"appropriate software.  This directory may be part " \
			"of any mounted filesystem, or may itself be a " \
			"mount point.  In general, it is unwise to select a " \
			"base directory which already contains other files " \
			"and/or directories."

/*
 * Set the install root (-R option).
 */

int
set_inst_root(char *path)
{
	static	char	tmp_path[PATH_MAX];

	/*
	 * If we've already set the install_root but no one has used it
	 * yet, we'll complain and allow the change. If it's been used
	 * then we'll deny the switch & return failed.
	 */
	if (install_root_exists)
		/* If the two install_roots are different - problem */
		if (strcmp(install_root, path))
			/* We are trying to *change* the install_root */
			if (ir_accessed) {
				ptext(stderr, gettext(ERR_IRSET), path);
				return (0);
			} else { /* !ir_accessed */
				ptext(stderr, gettext(MSG_IR_REPL), path);
				install_root_exists = 0;	/* reset */
				install_root = NULL;
			}

	if (path && *path) {
		if (*path != '/') {
			ptext(stderr, gettext(ERR_IRNOTABS), path);
			return (0);
		}

		strcpy(tmp_path, path);

		canonize(tmp_path);

		install_root = tmp_path;

		install_root_exists = 1;

		install_root_len = strlen(install_root);

		/* If install_root is '/' then it's trivial. */
		if (install_root_len == 1)
			install_root_len = 0;
	} else
		install_root_exists = 0;

	return (1);
}

/*
 * This routine returns a path with the correct install_root prepended.
 * if the install_root has been set. NOTE : this allocates memory
 * which will need to be freed at some point.
 */
char *
fixpath(char *path)
{
	register char *npath_ptr, *ir_ptr;
	char *npath = NULL;

	if (path && *path) {
		if (install_root_exists) {
			if ((npath =
			    calloc(1, strlen(path) + install_root_len +
			    1)) == NULL) {
				progerr(gettext(ERR_ALLOCFAILED), "fixpath()");
				quit(99);
			}

			npath_ptr = npath;
			ir_ptr = get_inst_root();

			while (*ir_ptr)	/* for every char in install_root */
				*npath_ptr++ = *ir_ptr++;	/* copy it */

			/*
			 * If install_root == "/", a concatenation will
			 * result in a return value of "//...", same goes
			 * for an install_root ending in '/'. So we back
			 * over a trailing '/' if it's there.
			 */
			if (*(npath_ptr - 1) == '/')
				npath_ptr--;

			if (strcmp(path, "/"))
				(void) strcpy(npath_ptr, path);
		} else
			/*
			 * If there's no install root & no client_basedir,
			 * then return the path
			 */
			npath = strdup(path);
	} else
		/*
		 * If there's no path specified, return the install root
		 * since no matter what happens, this is where the
		 * path will have to start.
		 */
		if (install_root_exists)
			npath = strdup(get_inst_root());

	return (npath);
}

/*
 * This routine does what fixpath() does except it's for high-volume
 * stuff restricted to the instvol() function. By using
 * pathdup() and pathalloc() memory fragmentation is reduced. Also, the
 * memory allocated by pathdup() and pathalloc() gets freed at the end
 * of each volume installed.
 */
char *
fixpath_dup(char *path)
{
	register char *npath_ptr, *ir_ptr;
	char *npath = NULL;

	if (path && *path) {
		if (install_root_exists) {
			npath = pathalloc(strlen(path) + install_root_len + 1);

			npath_ptr = npath;
			ir_ptr = get_inst_root();

			while (*ir_ptr)	/* for every char in install_root */
				*npath_ptr++ = *ir_ptr++;	/* copy it */

			/*
			 * If install_root == "/", a concatenation will
			 * result in a return value of "//...", same goes
			 * for an install_root ending in '/'. So we back
			 * over a trailing '/' if it's there.
			 */
			if (*(npath_ptr - 1) == '/')
				npath_ptr--;

			if (strcmp(path, "/"))
				(void) strcpy(npath_ptr, path);
		} else
			/*
			 * If there's no install root & no client_basedir,
			 * then return the path
			 */
			npath = pathdup(path);
	} else
		/*
		 * If there's no path specified, return the install root
		 * since no matter what happens, this is where the
		 * path will have to start.
		 */
		if (install_root_exists)
			npath = pathdup(get_inst_root());

	return (npath);
}

/*
 * This returns a pointer to a static name. This could be abused.
 * -- JST (1993-07-21)
 */
char *
get_inst_root(void)
{
	ir_accessed = 1;	/* we can't change it now */
	return (install_root);
}

/*
 * This routine takes path and removes install_root from the path
 * if it has already been prepended. If install_root is not prepended to
 * path or install_root is '/' or path == NULL then path is returned
 * as is. If the resulting path is somehow relative, a corrupt
 * package name error is raised and the program quits. NOTE : This
 * function usually returns a pointer into the original path
 * argument. It doesn't allocate new memory. This is possible,
 * of course, because the path being returned is guaranteed to
 * be a subset of the original argument unless basedir = '/' in
 * which case a pointer to a static "/" is returned. See
 * orig_path() below if you want to be handed a new copy of the
 * return value.
 */
char *
orig_path_ptr(char *path)
{
	char *retv = NULL;

	if (path && *path) {	/* as long as we got an argument */
		if (!install_root_exists)	/* if no install_root */
			retv = path;		/*   path unchanged */

		/*
		 * Otherwise, if install_root is really prepended to the path
		 * then remove it dealing appropriately with special cases.
		 */
		else if (strncmp(path, install_root, install_root_len) == 0) {
			retv = path + install_root_len;
			if (*retv == NULL)
				retv = "/";

			/*
			 * The result will be relative if install_root = '/'.
			 * If the basedir path was built legally, then moving
			 * the pointer back one character will make it
			 * absolute. If that fails then the path we got was
			 * incorrectly constructed in the first place.
			 */
			else if (*retv != '/') {
				retv--;
				if (*retv != '/') {
					progerr(gettext(ERR_PATHNAME));
					quit(99);
				}
			}
		} else
			retv = path;	/* All else failing, return path. */

		canonize(retv);
	}

	return (retv);
}

/*
 * This function does the same as orig_path_ptr() except that it mallocs
 * new space and provides a new copy of the original basedir path which
 * needs to be free()'d one way or another later.
 */
char *
orig_path(char *path)
{
	char *retv;

	retv = orig_path_ptr(path);

	return ((retv == NULL) ? retv : strdup(retv));
}

/*
 * This function lets us hold onto the environment's version of
 * CLIENT_BASEDIR for later review by set_client_basedir().
 */
void
set_env_cbdir()
{
	register char *cb_ptr;

	cb_ptr = getenv("CLIENT_BASEDIR");

	if (cb_ptr && *cb_ptr) {
		env_cl_bdir = strdup(cb_ptr);
		canonize(env_cl_bdir);
	}
}

/* ask for the basedir */
static int
ask_basedir(char *path, int nointeract)
{
	int n;

	if (nointeract) {
		progerr(gettext(MSG_REQBASEDIR));
		return (5);
	} else {
		path[0] = '\0';
		if (n = ckpath(path, P_ABSOLUTE|P_DIR|P_WRITE,
		    basedir, NULL, gettext(MSG_HELP),
		    gettext(MSG_PROMPT)))
			return (n);	/* FAIL */
		orig_basedir =
		    expand_path(path);
	}
	return (0);
}

/*
 * Set the basedir and client_basedir based on install root and config
 * files. It returns 0 if all OK otherwise returns the error code base
 * appropriate to the problem.
 */
int
set_basedirs(int reloc, char *adm_basedir, char *pkginst, int nointeract)
{
	char	path[PATH_MAX];
	int	n;

	relocatable = reloc;

	/*
	 * If there are no relocatable files basedir is probably meaningless
	 * so we skip ahead to the simple tests. Otherwise we do the twisted
	 * stuff below. The BASEDIR is set based on the following heirarchy :
	 *	1. The entry in the admin file
	 *	2. The entry in the pkginfo file delivered on the medium
	 *	3. The entry in the already installed pkginfo file
	 *	4. ask
	 * If it's not a relocatable package, we go with whatever seems
	 * reasonable; if it's relocatable and we've exhausted our
	 * options, we ask.
	 */
	if (reloc) {
		int is_adm_basedir = (adm_basedir && *adm_basedir);
		int is_update = 0;
		int is_ask = 0;

		if (is_adm_basedir) {
			if (strcmp(adm_basedir, "update") == 0) {
				is_update = 1;
				is_ask = 1;
			} else if (strcmp(adm_basedir, "ask") == 0)
				is_ask = 1;
		}

		/*
		 * If there's a BASEDIR in the admin file & it's a valid
		 * absolute pathname, use it.
		 */
		if (is_adm_basedir && strchr("/$", *adm_basedir))
			orig_basedir = expand_path(adm_basedir);

		/* If admin says 'ask regardless', ask and continue */
		else if (is_adm_basedir && is_ask) {
			if (n = ask_basedir(path, nointeract))
				return (n);
			if (is_update &&
			    strcmp(orig_basedir,
			    (basedir = getenv("BASEDIR"))) != 0) {
				progerr(gettext(ERR_ASKBD),
				    getenv("PKG"), basedir, orig_basedir);
				quit(4);
			}
		}
		/*
		 * If it isn't the only other valid option,
		 * namely 'default', quit FAIL.
		 */
		else if (is_adm_basedir &&
		    strcmp(adm_basedir, "default") != 0) {
			progerr(gettext(ERR_ADMIN_INVAL));
			return (1);

		/*
		 * OK, the admin file has no preference, so we go to the
		 * other sources.
		 */
		} else {
			/*
			 * Check to see if BASEDIR is set in the environment
			 * (probably from the pkginfo file on the installation
			 * medium).
			 */
			basedir = getenv("BASEDIR");
			if (basedir && *basedir)
				orig_basedir = expand_path(basedir);
			else {
				/*
				 * Check to see if the package BASEDIR was
				 * already defined during a previous
				 * installation of this package instance. The
				 * function below looks for an installed
				 * pkginfo file and scans it.
				 */
				basedir = pkgparam(pkginst, "BASEDIR");
				if (basedir && *basedir)
					orig_basedir = expand_path(basedir);
				else if (n = ask_basedir(path, nointeract))
					return (n);
			}
		}
	} else {	/* not relocatable */
		/*
		 * Since all paths are absolute the only reason to have a
		 * basedir is if there's an install root meaning there's
		 * really a basedir relative to this host or this package is
		 * absolute only because it's sparse in which case we're
		 * interested in the prior basedir. So we next check for a
		 * prior basedir and then an install root.
		 */
		basedir = pkgparam(pkginst, "BASEDIR");
		if (basedir && *basedir)
			orig_basedir = expand_path(basedir);

		else if (install_root_exists)
			/*
			 * If we have a basedir *only because*
			 * we have an install_root, we need to
			 * set orig_basedir to '/' to simplify
			 * later attempts to force
			 * client_basedir.
			 */
			orig_basedir = "/";
		else {
			eval_valid++;	/* we can run eval_path() now */
			return (0);	/* fixpath below unnecessary */
		}
	}

	basedir_exists = 1;

	basedir = fixpath(orig_basedir);

	/*
	 * If basedir == "/" then there's no need for a "/" between
	 * it and the rest of the path.
	 */
	if (strcmp(basedir, "/") == 0)
		base_sepr = 0;

	if (set_client_basedir() == 0) {
		progerr(gettext(ERR_NO_CL_BD));
		return (1);
	}

	eval_valid++;	/* we've confirmed the validity of everything */

	return (0);
}

/*
 * Make a directory from a path and all necessary directories above it as
 * needed.
 */
int
mkpath(char *p)
{
	char	*pt;

	pt = (*p == '/') ? p+1 : p;
	do {
		if (pt = strchr(pt, '/'))
			*pt = '\0';
		if (access(p, 0) && mkdir(p, 0755))
			return (-1);
		if (pt)
			*pt++ = '/';
	} while (pt);
	return (0);
}

/* This makes the required base directory if necessary */
void
mkbasedir(int flag, char *basedir)
{
	char	ans[MAX_INPUT];
	int	n;

	/*
	 * If a base directory is called for but there's no such directory on
	 * the system, deal with that issue.
	 */
	if (is_a_basedir() && isdir(basedir)) {
		if (flag) {	/* Interaction is OK. */
			/*
			 * If there's a non-directory object in the way, ask.
			 */
			if (access(basedir, F_OK) == 0) {
				ptext(stderr, gettext(MSG_ISAFILE), basedir);

				if (n = ckyorn(ans, NULL, NULL, NULL,
				    gettext(MSG_YORNFILE)))
					quit(n);
				if (strchr("yY", *ans) == NULL)
					quit(3);

				/*
				 * It isn't a directory, so we'll just unlink
				 * it.
				 */
				if (unlink(basedir) == -1) {
					progerr(gettext(ERR_NODELETE),
					    basedir);
					quit(99);
				}

			} else {
				ptext(stderr, gettext(MSG_MUSTEXIST), basedir);

				if (n = ckyorn(ans, NULL, NULL, NULL,
				    gettext(MSG_YORNPRMPT)))
					quit(n);
				if (strchr("yY", *ans) == NULL)
					quit(3);
			}
		}

		if (access(basedir, F_OK) == 0 || mkpath(basedir)) {
			progerr(gettext(ERR_MKBASE), basedir);
			quit(99);
		}
	}
}

/*
 * Create a client_basedir if it is appropriate. If all goes well, resulting
 * in either a valid client_basedir or a valid lack thereof, it returns 1.
 * If there is an irreconcileable conflict, it returns 0.
 */
static int
set_client_basedir(void)
{
	if (install_root_exists) {
		if (basedir_exists)
			client_basedir = strdup(orig_basedir);
		else
			client_basedir = "/";
		client_basedir_exists = 1;
	}

	/*
	 * In response to an agreement associated with bug report #1133956,
	 * CLIENT_BASEDIR will be defined in all cases where BASEDIR is
	 * defined until the on1094 release. For on1094 delete the else if
	 * and associated expressions below. -- JST (6/25/1993)
	 */
	else if (basedir_exists) {
		client_basedir = strdup(basedir);
		client_basedir_exists = 1;
	}

	/*
	 * At this point we may or may not have a client_basedir defined. Now
	 * we need to check for one in the environment & make sure it syncs
	 * up with prior findings. If there's no other client_basedir defined,
	 * the environment defines it.
	 */
	if (env_cl_bdir && *env_cl_bdir) {
		if (client_basedir_exists) {
			/* If the two client basedirs mismatch, return fail */
			if (strcmp(client_basedir, env_cl_bdir)) {
				ptext(stderr, gettext(ERR_CL_MIS),
				    client_basedir, env_cl_bdir);
				return (0);
			}
		} else {
			client_basedir = env_cl_bdir;
			client_basedir_exists = 1;
		}
	}

	return (1);
}

static char *
expand_path(char *path)
{
	char	path_buf[PATH_MAX];

	if (!path || !*path)
		return (path);

	(void) strcpy(path_buf, path);
	mappath(getmapmode(), path_buf);
	canonize(path_buf);

	return (qstrdup(path_buf));
}

char *
get_basedir(void)
{
	return (basedir);
}

char *
get_client_basedir(void)
{
	return (client_basedir);
}

/*
 * This function returns the basedir that is appropriate for this package's
 * pkginfo file.
 */
char *
get_info_basedir(void)
{
	if (install_root_exists)
		return (client_basedir);
	else if (basedir_exists)
		return (basedir);
	else
		return (NULL);
}

int
is_an_inst_root(void)
{
	return (install_root_exists);
}

int
is_a_basedir(void)
{
	return (basedir_exists);
}

int
is_relocatable(void)
{
	return (relocatable);
}

int
is_a_cl_basedir(void)
{
	return (client_basedir_exists);
}

/*
 * Since calls to putparam() become valid long after much of the above
 * code has run, this routine allows the insertion of these key
 * environment variables without passing a bunch of pointers.
 */
void
put_path_params(void)
{
	if (install_root_exists)
		putparam("PKG_INSTALL_ROOT", get_inst_root());

	if (basedir_exists)
		putparam("BASEDIR", basedir);

	if (client_basedir_exists)
		putparam("CLIENT_BASEDIR", client_basedir);
}

/*
 * This fills three pointers and a buffer which contains the longest
 * possible path (with install_root and basedir prepended. The pointers
 * are to the subpaths within the string. This was added so that the
 * eptlist could be produced with all relevant paths defined without
 * repeated calls and string scans. For example, given a path of
 * haberdasher/crute we may return
 *
 *	server_ptr -----> /export/root/client1/opt/SUNWhab/haberdasher/crute
 *                                            |            |
 *	client_ptr ---------------------------             |
 *	map_ptr -------------------------------------------
 *
 * We construct the new path based upon the established environment
 * and the type of path that was passed. Here are the possibilities:
 *
 *   |					| relative path	| absolute path	|
 *   |	--------------------------------|---------------|---------------|
 *   |	is_an_inst_root			|	1	|	2	|
 *   V	! an_inst_root && is_a_basedir	|	1	|	3	|
 *	! an_inst_root && ! a_basedir	|	X	|	3	|
 *
 * METHOD
 * 1. Prepend the basedir to the path (the basedir is guaranteed to exist
 *	whenever there's an install_root).
 *
 * 2. Prepend the install_root (not the basedir) to the path
 *
 * 3. Return the path as unchanged.
 *
 * X. THIS CAN'T HAPPEN
 */
eval_path(char **server_ptr, char **client_ptr, char **map_ptr, char *path)
{
	static int client_offset;
	static int offsets_valid, retcode;
	char *format;
	int path_size;

	if (!offsets_valid) {
		/*
		 * This is the offset from the beginning of the evaluated
		 * path to the start of the relative path. Note that we
		 * are accounting for the '/' inserted between the
		 * basedir and the path with the '+ 1'. If there is a
		 * relative path, then there is always a basedir. The
		 * only way this will come up '0' is if this is an
		 * absolute package.
		 */
		orig_offset_rel = (is_a_basedir()) ? (strlen(basedir) +
		    base_sepr) : 0;

		/*
		 * This is the position of the client-relative path
		 * in that it points to the '/' beginning the base
		 * directory or the absolute path. Once the basedir has
		 * been afixed, the path is absolute. For that reason,
		 * the client path is the same thing as the original path
		 * if it were absolute.
		 */
		client_offset = (is_an_inst_root) ? install_root_len : 0;

		offsets_valid = 1;
	}

	/*
	 * If we've evaluated the base directory and come up trumps,
	 * then we can procede with this operation, otherwise, the
	 * available data is too ambiguous to resolve the issue.
	 */
	if (eval_valid) {
		if (RELATIVE(path)) {
			if (relocatable) {
				/*
				 * Figure out how long our buffer will
				 * have to be.
				 */
				path_size = orig_offset_rel + strlen(path);

				(*server_ptr) = pathalloc(path_size);

				*client_ptr = *server_ptr + client_offset;

				if (map_ptr)
					*map_ptr = *server_ptr +
					    orig_offset_rel;

				sprintf(*server_ptr, rel_fmt[base_sepr],
				    basedir, path);
			} else {
				ptext(stderr, gettext(ERR_RELINABS), path);
				retcode = 0;
			}
		} else {	/* NOT RELATIVE */
			*server_ptr = fixpath_dup(path);

			if ((*client_ptr = *server_ptr + client_offset) == NULL)
				*client_ptr = "/";

			if (map_ptr)
				*map_ptr = *client_ptr;
		}

		retcode = 1;
	} else {
		ptext(stderr, gettext(ERR_AMBDIRS));
		retcode = 0;
	}

	return (retcode);
}
