/*
 *	aegis - project change supervisor
 *	Copyright (C) 1999, 2001-2003 Peter Miller;
 *	All rights reserved.
 *
 *	This program is free software; you can redistribute it and/or modify
 *	it under the terms of the GNU General Public License as published by
 *	the Free Software Foundation; either version 2 of the License, or
 *	(at your option) any later version.
 *
 *	This program is distributed in the hope that it will be useful,
 *	but WITHOUT ANY WARRANTY; without even the implied warranty of
 *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *	GNU General Public License for more details.
 *
 *	You should have received a copy of the GNU General Public License
 *	along with this program; if not, write to the Free Software
 *	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
 *
 * MANIFEST: wrappers around operating system functions
 *
 * This file, in selected areas, DOES NOT use the glue interface.
 * This is intentional - there can be no security breach, and no
 * distortion of semantics.
 */

#include <ac/errno.h>
#include <ac/stdlib.h>
#include <ac/string.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <ac/unistd.h>
#include <ac/mntent.h>
#include <ac/sys/clu.h>

#include <error.h>
#include <glue.h>
#include <mem.h>
#include <os.h>
#include <str.h>
#include <stracc.h>
#include <str_list.h>
#include <sub.h>
#include <trace.h>


/*
 * NAME
 *	os_curdir_sub - get current directory
 *
 * SYNOPSIS
 *	string_ty *os_curdir_sub(void);
 *
 * DESCRIPTION
 *	The os_curdir_sub function is used to determine the system's idea
 *	of the current directory.
 *
 * RETURNS
 *	A pointer to a string in dynamic memory is returned.
 *	A null pointer is returned on error.
 *
 * CAVEAT
 *	DO NOT use str_free() on the value returned.
 */

static string_ty *
os_curdir_sub(void)
{
    static string_ty *s;

    os_become_must_be_active();
    if (!s)
    {
	char            buffer[2000];
	char            *pwd;

	pwd = getenv("PWD");
	if (pwd && *pwd == '/')
	{
	    struct stat     st1;
	    struct stat     st2;

	    if
	    (
		glue_stat(".", &st1) == 0
	    &&
		glue_stat(pwd, &st2) == 0
	    &&
		st1.st_dev == st2.st_dev
	    &&
		st1.st_ino == st2.st_ino
	    )
	    {
		s = str_from_c(pwd);
		return s;
	    }
	}

	if (!glue_getcwd(buffer, sizeof(buffer)))
	{
	    sub_context_ty  *scp;

	    scp = sub_context_new();
	    sub_errno_set(scp);
	    fatal_intl(scp, i18n("getcwd: $errno"));
	    /* NOTREACHED */
	}
	assert(buffer[0] == '/');
	s = str_from_c(buffer);
    }
    return s;
}


/*
 * NAME
 *	os_curdir - full current directory path
 *
 * SYNOPSIS
 *	string_ty *os_curdir(void);
 *
 * DESCRIPTION
 *	Os_curdir is used to determine the pathname
 *	of the current directory.  Automounter vaguaries will be elided.
 *
 * RETURNS
 *	A pointer to a string in dynamic memory is returned.
 *	A null pointer is returned on error.
 *
 * CAVEAT
 *	Use str_free() when you are done with the value returned.
 */

string_ty *
os_curdir(void)
{
    static string_ty *result;

    os_become_must_be_active();
    if (!result)
    {
	string_ty       *dot;

	dot = str_from_c(".");
	result = os_pathname(dot, 1);
	str_free(dot);
    }
    return str_copy(result);
}


/*
 * NAME
 *	has_prefix
 *
 * SYNOPSIS
 *	string_ty *has_prefix(string_ty *prefix, string_ty *candidate);
 *
 * DESCRIPTION
 *	The has_prefix function is used to test if the ``prefix'' string
 *	is a prefix of the ``candidate'' string.
 *
 * RETURNS
 *	On success: the remainder of the string with the prefix removed.
 *	On failure: NULL.
 */

static string_ty *
has_prefix(string_ty *pfx, string_ty *path)
{
    if (str_equal(pfx, path))
	return str_copy(path);
    if
    (
	pfx->str_length < path->str_length
    &&
	path->str_text[pfx->str_length] == '/'
    &&
	0 == memcmp(pfx->str_text, path->str_text, pfx->str_length)
    )
    {
	return
	    str_n_from_c
	    (
		path->str_text + pfx->str_length,
		path->str_length - pfx->str_length
	    );
    }
    return 0;
}


/*
 * NAME
 *	has_a_prefix
 *
 * SYNOPSIS
 *	string_ty *has-a_prefix(string_list_ty *prefix, string_ty *candidate);
 *
 * DESCRIPTION
 *	The has_a_prefix function is used to test if any one of the
 *	``prefix'' strings is a prefix of the ``candidate'' string.
 *
 * RETURNS
 *	On success: the remainder of the string with the prefix removed.
 *	On failure: NULL.
 */

static string_ty *
has_a_prefix(string_list_ty *pfx, string_ty *path)
{
    size_t          j;
    string_ty       *result;

    for (j = 0; j < pfx->nstrings; ++j)
    {
	result = has_prefix(pfx->string[j], path);
	if (result)
	    return result;
    }
    return 0;
}


/*
 * NAME
 *	get_prefix_list
 *
 * SYNOPSIS
 *	string_list_ty *get_prefix_list(void);
 *
 * DESCRIPTION
 *	The get_prefix_list function is used to get the list of possible
 *	auto mount points from the AEGIS_AUTOMOUNT_POINTS environment
 *	variable.
 *
 *	The value is colon separated.  values not starting with slash,
 *	and the root directory, are silently ignored.
 *
 * RETURNS
 *	string list of prefixes
 *	DO NOT string_list_delete is *ever* because it is cached.
 */

static string_list_ty *
get_prefix_list(void)
{
    static string_list_ty *prefix;
    const char      *cp;
    string_list_ty  tmp;
    string_ty       *s;
    size_t          j;

    if (prefix)
	return prefix;

    /*
     * Pull the value out of the relevant environment
     * variable, and break it into pieces (it's colon
     * separated).
     */
    prefix = string_list_new();
    cp = getenv("AEGIS_AUTOMOUNT_POINTS");
    if (!cp)
	cp = "/tmp_mnt:/a:/.automount";
    s = str_from_c(cp);
    string_list_constructor(&tmp);
    str2wl(&tmp, s, ":", 0);
    str_free(s);

    /*
     * Rip off any trailing slashes.
     */
    for (j = 0; j < tmp.nstrings; ++j)
    {
	size_t          max;

	s = tmp.string[j];
	if (s->str_text[0] != '/')
	    continue;
	max = s->str_length;
	while (max > 0 && s->str_text[max - 1] == '/')
	    --max;
	if (max != s->str_length)
	{
	    if (max > 0)
	    {
		s = str_n_from_c(s->str_text, max);
		string_list_append_unique(prefix, s);
		str_free(s);
	    }
	}
	else
	    string_list_append_unique(prefix, s);
    }
    string_list_destructor(&tmp);

    return prefix;
}


/*
 * NAME
 *	get_auto_mount_dirs
 *
 * SYNOPSIS
 *	string_list_ty *get_auto_mount_dirs(void);
 *
 * DESCRIPTION
 *	The get_auto_mount_dirs function is used to grab all the active
 *	mount points with an automount prefix.
 *
 *	Because we are called (indirectly) by os_pathname, all of
 *	the relevant auto mount activities have taken place, and thus
 *	the mount table (obtained through the getmntent api) will be
 *	up-to-date, and contain mount entries with auto mount point prefixes.
 *
 *	We cache, and re-read if this the MOUNTED file changes.
 *
 * RETURNS
 *	String list of actual automounted mount points.
 *	DO NOT string_list_delete is *ever* because it is cached.
 */

static string_list_ty *
get_auto_mount_dirs(string_list_ty *prefix)
{
    FILE            *fp;
    static string_list_ty *dirs;
    static struct stat mntent_st;
    static string_ty *slash;
    int             err;
    struct stat     st;
    string_ty       *p1;
    struct stat     st1;
    string_ty       *p2;
    struct stat     st2;

    fp = setmntent(MOUNTED, "r");
    if (!fp)
    {
	if (dirs && dirs->nstrings)
	{
	    string_list_delete(dirs);
	    dirs = string_list_new();
	}
	return dirs;
    }

    /*
     * The ``fp'' variable is probably not a real FILE*, so don't
     * use it like one!  E.g. Cygwin cast it to FILE*, but there's
     * actually a whole 'nother data structure behind there, and it
     * GPFs if you try to access it as if it were a FILE*.
     */
    err = stat(MOUNTED, &st);
    if (err)
	memset(&st, 0, sizeof(st));
    if (dirs && mntent_st.st_mtime == st.st_mtime)
    {
	endmntent(fp);
	return dirs;
    }
    mntent_st = st;

    if (!slash)
	slash = str_from_c("/");
    if (dirs)
	string_list_delete(dirs);
    dirs = string_list_new();
    for (;;)
    {
	struct mntent   *mep;
	string_ty       *dir;
	string_ty       *tmp;

	mep = getmntent(fp);
	if (!mep)
	    break;
	dir = str_from_c(mep->mnt_dir);

	/*
	 * Ignore ``/'' because everything is below it.
	 */
	if (str_equal(dir, slash))
	{
	  the_next_one:
	    str_free(dir);
	    continue;
	}

	/*
	 * If the mount point doesn't have an automount
	 * point prefix, skip this mount point.
	 *
	 * We are called by os_pathname, which has just
	 * exersized all of the symbolic links and automount
	 * thingies.  So, they will all be in the mount table.
	 * (Except for really weird cases of symlinks between
	 * NFS file systems, with some servers fast and some
	 * really really slow, but don't worry about that.)
	 */
	tmp = has_a_prefix(prefix, dir);
	if (!tmp)
	    goto the_next_one;

	/*
	 * Simply fiddling with the path is a security
	 * hole.  We must make sure that the auto-mounted
	 * and un-auto-mounted paths give the same answer.
	 */
	p1 = str_format("%S/.", dir);
	err = lstat(p1->str_text, &st1);
	str_free(p1);
	if (err)
	    goto the_next_one;
	p2 = str_format("%S/.", tmp);
	str_free(tmp);
	err = lstat(p2->str_text, &st2);
	str_free(p2);
	if (err)
	    goto the_next_one;
	if (st1.st_ino != st2.st_ino || st1.st_dev != st2.st_dev) /*lint !e81 */
	    goto the_next_one;

	/*
	 * Everything checks out,
	 * remember this one.
	 */
	string_list_append(dirs, dir);
	str_free(dir);
    }
    endmntent(fp);
    return dirs;
}


/*
 * NAME
 *	remove_automounter_prefix
 *
 * SYNOPSIS
 *	string_ty *remove_automounter_prefix(string_ty *path);
 *
 * DESCRIPTION
 *	The remove_automounter_prefix function is used to remove
 *	any automounter prefix that may be present on an absolute
 *	path name.  The prefixes to check for are obtained from the
 *	AEGIS_AUTOMOUNT_POINTS environment variable.
 *
 * RETURNS
 *	string_ty * - pointer dynamically allocated string.  Use str_free
 *	when done with it.
 *
 * CAVEAT
 *	This function is dangerous.  Use with extreme care.
 */

static string_ty *
remove_automounter_prefix(string_ty *path)
{
    string_list_ty *prefix;
    string_list_ty *amdl;
    string_ty      *result;

    /*
     * Get the list of possible automount prefixes.
     */
    prefix = get_prefix_list();

    /*
     * Get the list of automounted mount points.
     * It's cached, so it usually doesn't take long.
     */
    amdl = get_auto_mount_dirs(prefix);

    /*
     * Look for leading path prefixes.
     */
    result = has_a_prefix(prefix, path);
    if (result)
    {
	string_ty       *tmp;

	/*
	 * Now see if the path (known to have an auto mount
	 * prefix) is below an automounted mount point (also
	 * known to have an auto mount prefix).
	 */
	tmp = has_a_prefix(amdl, path);
	if (tmp)
	{
	    str_free(tmp);
	    return result;
	}

	/*
	 * The path is below an auto mount directory, but
	 * not in the mount table, so it's bogus in some way.
	 * Ignore it, in case it is an attempt to subvert Aegis
	 * into a security breach.
	 */
	str_free(result);
    }

    /*
     * No match, return the original.
     */
    return str_copy(path);
}


/*
 * NAME
 *	memb
 *
 * SYNOPSIS
 *	string_ty *memb(void);
 *
 * DESCRIPTION
 *	The memb function is used to determine the member name of an
 *	OSF/1 cluster member name.
 *
 * CAVEAT
 *	This is only meaningful on OSF/1
 */

#ifdef HAVE_CLU_INFO

static string_ty *
memb(void)
{
    static string_ty *result;
    if (!result)
    {
	char            name[MAXHOSTNAMELEN];
	memberid_t      my_id;

	if
	(
	    clu_info(CLU_INFO_MY_ID, &my_id) < 0
	||
	    clu_info(CLU_INFO_NODENAME_BY_ID, my_id, name, sizeof(name)) < 0
	)
	    result = str_from_c("member0");
	else
	    result = str_from_c(name);
    }
    return result;
}


/*
 * NAME
 *	magic_memb_replace
 *
 * SYNOPSIS
 *	string_ty *magic_memb_replace(string_ty *);
 *
 * DESCRIPTION
 *	The magic_memb_replace function is used to replace instances
 *	of "{memb}" in a symbolic link's value with the name of the
 *	cluster member.
 *
 * CAVEAT
 *	This is only meaningful on OSF/1
 */

static string_ty *
magic_memb_replace(string_ty *s)
{
    static stracc_t sa;
    char            *cp;
    char            *end;
    char            *ep;

    stracc_open(&sa);
    cp = s->str_text;
    end = s->str_text + s->str_length;
    while (cp < end)
    {
	ep = memchr(cp, '{', end - cp);
	if (!ep)
	{
	    stracc_chars(&sa, cp, end - cp);
	    break;
	}
	if (ep > cp)
	{
	    stracc_chars(&sa, cp, ep - cp);
	    cp = ep;
	}
	if (cp + 6 <= end && 0 == memcmp(cp, "{memb}", 6))
	{
	    string_ty       *name;

	    name = memb();
	    stracc_chars(&sa, name->str_text, name->str_length);
	    cp += 6;
	}
	else
	    stracc_char(&sa, *cp++);
    }
    return stracc_close(&sa);
}

#endif


/*
 * NAME
 *	os_pathname - determine full file name
 *
 * SYNOPSIS
 *	string_ty *os_pathname(string_ty *path, int resolve);
 *
 * DESCRIPTION
 *	Os_pathname is used to determine the full path name
 *	of a partial path given.
 *
 * ARGUMENTS
 *	path	- path to canonicalize
 *	resolve	- non-zero if should resolve symlinks, 0 if not
 *
 * RETURNS
 *	pointer to dynamically allocated string.
 *
 * CAVEAT
 *	Use str_free() when you are done with the value returned.
 */

string_ty *
os_pathname(string_ty *path, int resolve)
{
    static char     *tmp;
    static size_t   tmplen;
    static size_t   ipos;
    static size_t   opos;
    int             c;
    int             found;
#ifdef S_IFLNK
    int             loop;
#endif
    string_ty       *result;

    /*
     * Change relative pathnames to absolute
     */
    trace(("os_pathname(path = %08lX)\n{\n", (long)path));
    if (!path)
	path = os_curdir();
    if (resolve)
	os_become_must_be_active();
    trace_string(path->str_text);
    if (path->str_text[0] != '/')
	path = str_format("%S/%S", os_curdir_sub(), path);
    else
	path = str_copy(path);

    /*
     * Take kinks out of the pathname
     */
    ipos = 0;
    opos = 0;
    found = 0;
#ifdef S_IFLNK
    loop = 0;
#endif
    while (!found)
    {
	/*
	 * get the next character
	 */
	c = path->str_text[ipos];
	if (c)
	    ipos++;
	else
	{
	    found = 1;
	    c = '/';
	}

	/*
	 * remember the normal characters
	 * until get to slash
	 */
	if (c != '/')
	    goto remember;

	/*
	 * leave root alone
	 */
	if (!opos)
	    goto remember;

	/*
	 * "/.." -> "/"
	 */
	if (opos == 3 && tmp[1] == '.' && tmp[2] == '.')
	{
	    opos = 1;
	    continue;
	}

	/*
	 * "a//" -> "a/"
	 */
	if (tmp[opos - 1] == '/')
	    continue;

	/*
	 * "a/./" -> "a/"
	 */
	if (opos >= 2 && tmp[opos - 1] == '.' && tmp[opos - 2] == '/')
	{
	    opos--;
	    continue;
	}

	/*
	 * "a/b/../" -> "a/"
	 */
	if
	(
	    opos > 3
	&&
	    tmp[opos - 1] == '.'
	&&
	    tmp[opos - 2] == '.'
	&&
	    tmp[opos - 3] == '/'
	)
	{
	    opos -= 4;
	    assert(opos > 0);
	    while (tmp[opos - 1] != '/')
		opos--;
	    continue;
	}

	/*
	 * see if the path so far is a symbolic link
	 */
#ifdef S_IFLNK
	if (resolve)
	{
	    char            pointer[2000];
	    int             nbytes;
	    string_ty       *s;

	    s = str_n_from_c(tmp, opos);
	    nbytes = glue_readlink(s->str_text, pointer, sizeof(pointer) - 1);
	    if (nbytes < 0)
	    {
		/*
		 * probably not a symbolic link
		 */
		if
		(
		    /*
		     * Some broken Unixes say this when they should
		     * say EINVAL.
		     */
		    errno != ENXIO
		&&
		    /*
		     * The file definitely isn't a symbolic link.
		     */
		    errno != EINVAL
		&&
		    /*
		     * The file doesn't exist.
		     */
		    errno != ENOENT
		&&
		    /*
		     * One of the path elements isn't a directory.
		     */
		    errno != ENOTDIR
		&&
		    /*
		     * If we can't read the link (or more likely, one
		     * of the directories above it wont let us search)
		     * assume it isn't a link.
		     */
		    errno != EACCES
		)
		{
		    sub_context_ty  *scp;

		    scp = sub_context_new();
		    sub_errno_set(scp);
		    sub_var_set_string(scp, "File_Name", s);
		    fatal_intl(scp, i18n("readlink $filename: $errno"));
		    /* NOTREACHED */
		}
		str_free(s);
	    }
	    else
	    {
		string_ty       *newpath;
		string_ty       *link1;

		if (nbytes == 0)
		{
		    pointer[0] = '.';
		    nbytes = 1;
		}
		if (++loop > 1000)
		{
		    sub_context_ty  *scp;

		    scp = sub_context_new();
		    sub_errno_setx(scp, ELOOP);
		    sub_var_set_string(scp, "File_Name", s);
		    fatal_intl(scp, i18n("readlink $filename: $errno"));
		    /* NOTREACHED */
		}
		link1 = str_n_from_c(pointer, nbytes);
#ifdef HAVE_CLU_INFO
		{
		    string_ty       *link2;

		    /*
		     * OSF/1 has magic symlinks,
		     * where the string "{memb}"
		     * is replaced by the cluster
		     * member name.
		     *
		     * This is like the DG/UX "elink"
		     * concept, where environment
		     * variables can be substituted
		     * into symlinks.
		     */
		    link2 = magic_memb_replace(link1);
		    str_free(link1);
		    link1 = link2;
		}
#endif
		str_free(s);
		if (link1->str_text[0] == '/')
		{
		    newpath =
			str_format
			(
			    "%s/%s",
			    link1->str_text,
			    path->str_text + ipos
			);
		}
		else
		{
		    while (tmp[opos - 1] != '/')
			opos--;
		    tmp[opos] = 0;

		    newpath =
			str_format
			(
			    "%s/%s/%s",
			    tmp,
			    link1->str_text,
			    path->str_text + ipos
			);
		}
		str_free(link1);
		str_free(path);
		path = newpath;
		ipos = 0;
		opos = 0;
		found = 0;
		continue;
	    }
	}
#endif

	/*
	 * keep the slash
	 */
        remember:
	if (opos >= tmplen)
	{
	    tmplen = tmplen * 2 + 8;
	    tmp = (char *)mem_change_size(tmp, tmplen);
	}
	tmp[opos++] = c;
    }
    str_free(path);
    assert(opos >= 1);
    assert(tmp[0] == '/');
    assert(tmp[opos - 1] == '/');
    if (opos >= 2)
	opos--;
    path = str_n_from_c(tmp, opos);
    trace_string(path->str_text);

    /*
     * Check for automounter prefixes, and remove them if you
     * find them.  The user needs to use this sparingly, because
     * extreme chaos can result.
     */
    result = remove_automounter_prefix(path);
    str_free(path);
    trace_string(result->str_text);
    trace(("}\n"));
    return result;
}
