/*
*   This file released to public domain by the copyright holders 
*   including ArrayComm, Inc.
*   Author: Ted Merrill
*/

/*-M- GetTruePath.c -- get true path based on a pathname.
*   Substitute for getcwd() or `pwd` and much more!
*
*   Functions:
*   (The AllocGet versions return allocated memory whereas the Get
*       versions return a pointer to a static buffer overwritten each call).
*   All functions return NULL if error, and emit message for unusual errors;
*       missing file is not an unusual error.
*     STRING [Alloc]GetCwd(void) -- returns current directory
*     STRING [Alloc]GetTruePath( STRING Path ) -- returns path w/ no soft links
*     STRING [Alloc]GetTruePathSoft( STRING Path ) -- returns path w/ no
*           soft links except for last component may be.
*     STRING [Alloc]GetTruePathPossible( STRING Path ) --
*           As GetTruePath as far as the path exists, then assumes
*           that additional components would be added.
*
*   General:
*   These frequently take a lot of system calls to compute.
*
*   Theory:
*   The current directory is known to the kernel only by its 
*   device and inode number.
*   If it wasn't for a little help, there might be no path to the current
*   directory, or multiple paths.
*   Fortunately, the convention universally observed and partly 
*   enforced by the kernel is the following:
*
*   The (hard) links allowed to a directory are exactly:
*       a. a real name in some higher level directory (except for root dir).
*       b. the single "." entry in the directory, pointing to self.
*       c. ".." entries in every subdirectory of the directory.
*           (in the root directory, ".." points to itself).
*
*   The general algorithm for ANY directory as follows:
*   Starting with some relative pathname that identifies the directory,
*   stat the directory and its parent; 
*   if they have the same device and inode numbers,
*   they are the same directory and thus must be the root directory
*   whose name is "/";
*   Otherwise, find the absolute pathname of the parent directory
*   using the relative pathname plus "/..", then search the parent
*   directory for the an entry which when stat'ed yields the
*   same device and inode number as the directory in question;
*   the name yield by stat (we skipped an "." and ".." entries)
*   is the real name of the directory, which can be added to the
*   abs. pathname of the parent (separated by a slash).
*
*   Faster algorithm, not just for dirs, for a path starting with '/' is 
*   to evaluate each name from left to right, and replace soft link
*   names where found.
*   The following cases require special treatment:
*   Link value begins with a slash -- start over (beware of infinite loops!)
*   embedded name == "." -- skip this.
*   Embedded name == ".." -- back up one name but not past root.
*
*   Faster algorithm, not just for dirs, for a path not starting with '/' is
*   to use the general algorithm to find the current directory,
*   then tack on components as for the fast algorithm starting with '/'.
*
*/
#include <dirent.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/param.h>

#include <allocparseX.h>
#include <AllocCopyString.c.h>
#include <FileType.c.h>
#ifdef ATOOL_TARGET_sun4
int rename(const char *old, const char *new);
int lstat(const char *path, struct stat *buf);
int readlink(const char *path, void *buf, size_t bufsiz);
#endif


#define GETTRUEPATH_BUFSIZE 1200
#define GETTRUEPATH_NAMESIZE 300
/* Output is built in OutBuf */
static char OutBuf[GETTRUEPATH_BUFSIZE+GETTRUEPATH_NAMESIZE];
/* UpBuf: Input with ../../.. added... */
static char UpBuf[GETTRUEPATH_BUFSIZE+GETTRUEPATH_NAMESIZE];

/*--- SearchUpInternal 
*/
static STRING SearchUpInternal( 
    struct stat *ChildStatP  /* stat of our child directory */
    )
{
    DIR * DirP;
    struct dirent * EntryP;
    struct stat Stat;
    int BaseLen;
    if ( lstat( UpBuf, &Stat ) )
    {
        ErrPrint("Can not stat `%s'", UpBuf );  /* unusual error! */
        return NULL;
    }
    if ( Stat.st_dev == ChildStatP->st_dev &&
        Stat.st_ino == ChildStatP->st_ino )
    {   /* Only root directory has . equal to .. */
        strcpy( OutBuf, "/" );
        return OutBuf;
    }

    /* find name so far by recursion... */
    BaseLen = strlen(UpBuf);
    if ( BaseLen > GETTRUEPATH_BUFSIZE-8 )
    {
        ErrPrint("Buffer overflow; infinite loop?");    /* unusual error! */
        return NULL;
    }
    strcpy(UpBuf+BaseLen, "/..");
    if ( ! SearchUpInternal( &Stat ) ) return NULL;

    /* Add name of child directory by finding it in parent directory */
    BaseLen = strlen(OutBuf);
    if ( BaseLen > GETTRUEPATH_BUFSIZE-GETTRUEPATH_NAMESIZE )
    {   /* leave room enough to grow... */
        ErrPrint("Buffer overflow; (not an infinite loop)");    /* unusual error! */
        return NULL;
    }
    DirP = opendir(OutBuf);
    if ( ! DirP )
    {
        ErrPrint("Cannot read directory `%s'", UpBuf ); /* unusual error! */
        return NULL;
    }
    while ( (EntryP = readdir(DirP)) != NULL )
    {   /* search parent directory for name of child */
        int TempBaseLen = BaseLen;
        struct stat EntryStat;
        int NameLen;
        #if defined(ATOOL_TARGET_sun4) || defined(ATOOL_TARGET_alpha)  /* where d_name not null terminated */
        NameLen = EntryP->d_namlen;
        #else /* no d_namlen; d_name should be null term */
        NameLen = strlen(EntryP->d_name);
        #endif

        /* skip . and .. */
        if ( EntryP->d_name[0] == '.' && (
            NameLen == 1 || (
                EntryP->d_name[1] == '.' && NameLen == 2 ) ) )
                    continue;
        /* Corrupted auto-mounters can make the following useful: */
        if ( NameLen <= 0 ) continue;
        if ( NameLen > GETTRUEPATH_NAMESIZE ) continue;

        #if 0   /* NO, this is not compatible with st_ino */
        /* We can skip this one if the inode no. is different. */
        if ( EntryP->d_fileno != ChildStatP->st_ino ) continue;
        /* If inode no. is the same, we still really need to
        *   do a stat because the device (file system) may differ.
        */
        #endif

        if ( BaseLen > 1 ) 
        {
            strcpy( OutBuf+BaseLen, "/" );
            TempBaseLen++;
        }
        strncpy( OutBuf+TempBaseLen, EntryP->d_name, NameLen );
        OutBuf[TempBaseLen+NameLen] = 0;  /* null terminate */
        /* silently ignore any errors from stat? maybe an unimportant
        *   file got deleted or something... */
        if ( lstat( OutBuf, &EntryStat ) ) continue;
        if ( EntryStat.st_dev == ChildStatP->st_dev &&
            EntryStat.st_ino == ChildStatP->st_ino )
        {   /* Here if found! */
            closedir(DirP);
            return OutBuf;
        }
        OutBuf[BaseLen] = 0;    /* chop off last addition */
    }
    /* Not found: */
    closedir(DirP);
    ErrPrint("Could not find directory in its parent directory, at `%s'",
            OutBuf );  /* unusual error! */
    return NULL;
}


/*-F- GetCwd -- get current working directory.
*   Substitute for getcwd() or `pwd`.
*   Returns a pointer to a static buffer overwritten on each call,
*   or returns NULL if error.
*   Logs a message in case of an unusual error.
*/
 STRING GetCwd(void)
{
    /* NOTE: functions further on in this file assume that OutBuf
    *   will be built! */
    struct stat Stat;
    if ( lstat( ".", &Stat ) )
    {
        ErrPrint("Can not stat `.'");   /* unusual error! */
        return NULL;
    }
#if 0  /* This works, but causes problems on ArrayComm network due to...
        **  stat'ing of various mounted directories in the root,
        **  which is a problem because some? of these file systems
        **  are strange and pull down the system when they are stat'd.
        **  It seems that getwd() works much better in this regard.
        **  Don't use getcwd() because it is inefficient (is this true?
        **  it certainly is safer...).
        */
    strcpy(UpBuf,"./..");
    if ( ! SearchUpInternal(&Stat) )
    {
        ErrPrint("Can not determine current directory.");   /* unusual error! */
        return NULL;
    }
#else
    if ( ! getcwd( OutBuf, sizeof(OutBuf) ) )
    {   /* unusual error! */
        ErrPrint("Can not determine current directory");
        return NULL;
    }
#endif
    return OutBuf;
}

/*-F- AllocGetCwd -- As with GetCwd but returns allocated memory. */
 STRING AllocGetCwd(void)
{
    return AllocCopyString(GetCwd());
}



/*--- AddNameInternal -- Assumes that OutBuf already contains
*   the name of a valid TRUE abs directory path (at least "/");
*   The Name is tentatively tacked on, but if it is a soft link,
*   the value of the softlink is expanded instead.
*   Returns OutBuf is success, NULL if error.
*   Errors include file not found.
*/
static STRING AddNameInternal( STRING Name )
{
    int BaseLen;
    struct stat Stat;
    STRING Ptr;
    char ValueBuf[GETTRUEPATH_BUFSIZE+GETTRUEPATH_NAMESIZE];
    static STRING AddPathInternal(STRING);
    int Result;

    if ( ! Name[0] ) return OutBuf; /* e.g. from two slashes in a row */
    if ( Name[0] == '.' && ! Name[1] ) return OutBuf; /* eg. from xxx/./yyy*/
    if ( Name[0] == '.' && Name[1] == '.'&& ! Name[2] )
    {   /* e.g. from xxx/../yyy */
        Ptr = strrchr(OutBuf, '/');
        /* There WILL be at least OutBuf[0] == '/' */
        if ( Ptr == OutBuf ) OutBuf[1] = 0; /* retain initial / */
        else *Ptr = 0;  /* chop off slash and name */
        return OutBuf;
    }

    BaseLen = strlen(OutBuf);
    if ( BaseLen == 1 ) strcpy(OutBuf+1, Name); /* (avoid double slash) */
    else
    {
        OutBuf[BaseLen] = '/';
        strcpy( OutBuf+BaseLen+1, Name );
    }
    if ( lstat( OutBuf, &Stat ) )
    {
        ErrPrint("Can not lstat `%s'", OutBuf); /* unusual error! */
        return NULL;
    }
    if ( ! S_ISLNK( Stat.st_mode ) )
    {   /* not a soft link -- good as is... */
        return OutBuf;      /* success so far! */
    }
    /* If it is a soft link: */
    Result = readlink(OutBuf, ValueBuf, sizeof(ValueBuf)-1);
    if ( Result < 0 )
    {
        ErrPrint("Cannot readlink `%s'", OutBuf );  /* unusual error! */
    }
    OutBuf[BaseLen] = 0;    /* chop off proposed name */
    ValueBuf[Result] = 0;   /* readlink doesn't null terminate */
    return AddPathInternal(ValueBuf);
}


/*--- AddPathInternal -- Assumes that OutBuf already contains
*   the name of a valid TRUE abs directory path (at least "/");
*   The Path is tacked on (after expansion), or overrides OutBuf
*   contents if begins with '/'.
*   Returns OutBuf is success, NULL if error.
*   Errors include file not found.
*
*   NOTE: PathBuf is temporarily mangled during use, but restored
*   to original value on return; so it must be writeable.
*/
static STRING AddPathInternal( STRING PathBuf )
{
    STRING Ptr;
    STRING Ptr1 = NULL;
    if ( PathBuf[0] == '/' )
    {   /* start over! */
        strcpy( OutBuf, "/" );
        Ptr = PathBuf+1;
    }
    else Ptr = PathBuf;
    while ( *Ptr )
    {   /* add each component of the soft link value */
       Ptr1 = strchr(Ptr,'/');
       if ( Ptr1 ) *Ptr1 = 0;   /* null terminate name ptd. by Ptr */
       if ( ! AddNameInternal(Ptr) ) 
       {
           if ( Ptr1 ) *Ptr1 = '/'; /* restore, just in case */
           return NULL;    /* error! */
       }
       if ( Ptr1 ) 
       {
           *Ptr1 = '/';     /* restore, just in case */
           Ptr = Ptr1+1;    /* if there was more, go on... */
       }
       else break;
    }
    return OutBuf;
}


/*-F- GetTruePath -- get true pathname of any file or directory.
*   Returns a pointer to a static buffer overwritten on each call,
*   or returns NULL if error (e.g. if file does not exist).
*   Logs a message in case of an unusual error.
*   Path may contain softlink components, including the last one,
*   so long as it points to an existing file, directory etc.
*   All softlink components are followed, including the last one.
*/
STRING GetTruePath( STRING Path )
{
    char PathBuf[GETTRUEPATH_BUFSIZE+GETTRUEPATH_NAMESIZE];
    if ( ! Path ) return NULL;
    if ( strlen(Path) > GETTRUEPATH_BUFSIZE-GETTRUEPATH_NAMESIZE )
    {
        ErrPrint("Name too long: %.40s...", Path ); /* unusual error! */
        return NULL;
    }
    if ( ! FileExists(Path) ) return NULL;   /* common error! */
    strcpy( PathBuf, Path );    /* because we may mangle buffer */
    if ( PathBuf[0] == '/' )
    {   /* absolute pathname, no special treatment */
        strcpy(OutBuf,"/");
        if ( ! AddPathInternal( PathBuf+1 ) ) return NULL;
    }
    else
    {   /* relative pathname, no special treatment */
        if ( ! GetCwd() ) return NULL;
        if ( ! AddPathInternal( PathBuf ) ) return NULL;
    }
    return OutBuf;
}

/*-F- AllocGetTruePath -- As with GetTruePath but returns allocated memory. */
 STRING AllocGetTruePath(STRING Path)
{
    return AllocCopyString(GetTruePath(Path));
}



/*-F- GetTruePathSoft -- get true pathname of any file or directory.
*   Returns a pointer to a static buffer overwritten on each call,
*   or returns NULL if error (e.g. if it does not exist).
*   Logs a message in case of an unusual error.
*   Path may contain softlink components, including the last one,
*   so long as it points to an existing file, directory etc.
*   If the file identified directly by the Path is a soft link,
*   the true path of the soft link file
*   is returned instead of what it may point to.
*/
 STRING GetTruePathSoft( STRING Path )
{
    char PathBuf[GETTRUEPATH_BUFSIZE+GETTRUEPATH_NAMESIZE];
    STRING Ptr;

    if ( ! Path ) return NULL;
    /* We don't do this if it is not a soft link */
    if ( ! IsSoftLink(Path) ) return GetTruePath(Path);

    if ( strlen(Path) > GETTRUEPATH_BUFSIZE-GETTRUEPATH_NAMESIZE )
    {
        ErrPrint("Name too long: %.40s...", Path ); /* unusual error! */
        return NULL;
    }
    strcpy( PathBuf, Path );    /* because we may mangle buffer */

    Ptr = strrchr(PathBuf,'/');
    if ( Ptr && Ptr == PathBuf )
    {       /* form: /name   -- must be the real thing */
        strcpy(OutBuf, PathBuf);
    }
    else
    if ( Ptr )
    {   /* form: xxxx/yyy  -- resolve the xxx, then tack on /yyy */
        *Ptr = 0;       /* null terminate leading path */
        if ( ! GetTruePath( PathBuf ) )  return NULL;
        *Ptr = '/';     /* just in case */
        if ( OutBuf[1] ) strcat( OutBuf, Ptr );  /* name is good -- add here!*/
        else strcpy( OutBuf, Ptr );     /* avoid double slash at begin */
    }
    else
    {   /* form: yyy  (no slashes) -- resolve cwd, then tack on /yyy */
        if ( ! GetCwd() ) return NULL;
        if ( OutBuf[1] ) strcat(OutBuf,"/"); /* avoid double slash at begin */
        strcat(OutBuf,PathBuf); /* name is good symlink -- add here!*/
    }
    return OutBuf;
}

/*-F- AllocGetTruePathSoft -- As with GetTrue... but returns allocated memory.*/
 STRING AllocGetTruePathSoft(STRING Path)
{
    return AllocCopyString(GetTruePathSoft(Path));
}



/*-F- GetTruePathPossible -- get possible true pathname of file.
*   Returns a pointer to a static buffer overwritten on each call,
*   or returns NULL if error (e.g. an unusual error).
*   Logs a message in case of an unusual error.
*   Path may contain softlink components, including the last one;
*   missing components are assumed to exist in the given form
*   in the future, and are thus "invented" on to the end.
*   All softlink components are followed.
*
*   BUGS:
*   This is not a very efficient implementation if very many components
*   are missing.
*   Also, it assumes the added components will not include soft link
*   components.
*/
 STRING GetTruePathPossible( STRING Path )
{
    char PathBuf[GETTRUEPATH_BUFSIZE+GETTRUEPATH_NAMESIZE];
    STRING Ptr;

    if ( ! Path ) return NULL;
    if ( strlen(Path) > GETTRUEPATH_BUFSIZE-GETTRUEPATH_NAMESIZE )
    {
        ErrPrint("Name too long: %.40s...", Path ); /* unusual error! */
        return NULL;
    }
    if ( FileExists(Path) ) return GetTruePath( Path );
    if ( Path[0] != '/' )
    {   /* relative path */
        if ( ! GetCwd() ) return NULL;
        if ( strlen(Path) + strlen(OutBuf) > 
            GETTRUEPATH_BUFSIZE-GETTRUEPATH_NAMESIZE ) 
        {
            ErrPrint("CWD+Name too long: %.40s...", Path ); /* unusual error! */
            return NULL;
        }
        strcpy( PathBuf, OutBuf );
        if ( PathBuf[1] ) strcat( PathBuf, "/" ); /* (avoid double slash) */
        strcat( PathBuf, Path );
        return GetTruePathPossible( PathBuf );
    }
    /* Abs. path... */
    /* If it does not exist, chop off last etc.. */
    strcpy( PathBuf, Path );    /* because we may mangle buffer */
    Ptr = strrchr(PathBuf, '/');
    if ( Ptr && Ptr == PathBuf )
    {   /* at root dir; as possible as we can get... */
        /* note that /. and /.. would have been caught before */
        strcpy( OutBuf, Ptr );
    }
    else
    {   /* absolute path but not at root dir */
        *Ptr = 0;
        if ( ! GetTruePathPossible(PathBuf) ) return NULL;
        *Ptr = '/';         /* restore, just in case */
        if ( Ptr[1] == '.' && Ptr[2] == 0 ) ; /* ignore added . */
        else
        if ( Ptr[1] == '.' && Ptr[2] == '.' && Ptr[3] == 0 )
        {   /* parent? strip off one */
            Ptr = strrchr(OutBuf,'/');
            if ( Ptr == OutBuf ) OutBuf[1] = 0;
            else *Ptr = 0;
        }
        else
        if ( OutBuf[1] ) strcat( OutBuf, Ptr );  /* add future component */
        else strcpy(OutBuf,Ptr);    /*(avoid double slash) */
    }
    return OutBuf;
}


/*-F- AllocGetTruePathPossible -- As with GetTrue... but returns allocated mem*/
 STRING AllocGetTruePathPossible(STRING Path)
{
    return AllocCopyString(GetTruePathPossible(Path));
}


#ifdef TESTPROGRAM
/* This test program reads filepaths from stdin and
*   emits to stdout an evaluation of them.
*/
#include <stdio.h>

int main(int argc, char **argv)
{
    char buf[2048];
    (void) argc;
    (void) argv;
    setbuf(stdout,0);
    while ( gets(buf) )
    {
        printf("Input: %s\n", buf );
        printf("TruePath: %s\n", GetTruePath(buf));
        printf("TruePathSoft: %s\n", GetTruePathSoft(buf));
        printf("TruePathPossible: %s\n", GetTruePathPossible(buf));
        printf("\n");
    }
    return 0;
}
#endif


#ifdef PROGRAM_TRUEPATH
/* This program reads filepaths from argv
*   emits to stdout an evaluation of them.
*   With no args, emits current working directory.
*/
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv)
{
    char *Arg;
    int SoftMode = 0;
    int PossibleMode = 0;
    (void) argc;
    ErrExitProgramName = *argv++;
    if ( ! *argv ) printf("%s\n", GetCwd());
    while ( (Arg = *argv++) != NULL )
    {
        if ( ! strcmp( Arg, "-h" ) )
        {
            printf("Usage: %s [-s | -p] [<path>]...\n", ErrExitProgramName);
            printf("With no args, prints true path of curr. working dir.\n");
            printf("With -s, shows path of soft link instead of object.\n");
            printf("With -p, shows possible path for new file (ignores -s).\n");
            printf("The truepaths for <path>... are printed one per line.\n");
            printf("Aborts with msg to stderr if not found or other error.\n");
            exit(0);
        }
        else if ( ! strcmp( Arg, "-s" ) ) SoftMode = 1;
        else if ( ! strcmp( Arg, "-p" ) ) PossibleMode = 1;
        else
        {
            char * Path;
            if ( PossibleMode ) Path = GetTruePathPossible(Arg);
            else if ( SoftMode ) Path = GetTruePathSoft(Arg);
            else Path = GetTruePath(Arg);
            if ( ! Path ) ErrExit("Not found: %s", Arg );
            printf("%s\n", Path );
        }
    }
    return 0;
}
#endif
