/*
 * file.c -- Frontend to /usr/bin/file that follows symlinks visibly.
 *           Very useful for figuring out why a symlink is dangling
 *           when it goes through lots of indirection and ../../.. stuff.
 *
 *  This is how the output should look:
 *
 *      % file /hosts/rum/usr/tmp
 *      {/hosts/rum -> /tmp_mnt/hosts/rum}/usr/tmp
 *      /tmp_mnt/hosts/rum/usr/{tmp -> ../var/tmp}
 *      /tmp_mnt/hosts/rum/var/tmp:   append-only directory
 *
 *      % file /proj/irix5.3/isms/irix
 *      {/proj -> /hosts/bonnie/proj}/irix5.3/isms/irix
 *      {/hosts/bonnie -> /tmp_mnt/hosts/bonnie}/proj/irix5.3/isms/irix
 *      /tmp_mnt/hosts/bonnie/proj/irix5.3/isms/{irix -> ../../../depot/irix/irix_sherwood}
 *      /tmp_mnt/hosts/bonnie/depot/irix/{irix_sherwood -> ../../jake/irix_sherwood}
 *      /tmp_mnt/hosts/bonnie/{jake -> xlv9/jake}/irix_sherwood
 *      /tmp_mnt/hosts/bonnie/xlv9/jake/irix_sherwood:      directory
 *
 * XXX not robust about symlink loops or overflows.
 *     For example, the symlink foo -> foo will cause an endless loop,
 *     and foo -> foo/foo will cause a stack overflow.
 * XXX doesn't pass args to the real file command.
 #
 * Author: Don Hatch (hatch@hadron.org)
 * Last modified: Wed Aug 16 04:13:15 PDT 2000
 *
 * This software may be used for any purpose
 * as long as it is good and not evil.
 */

#include <stdio.h>
#include <limits.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <errno.h>
#undef NDEBUG
#include <assert.h>

#define streq(a,b) (strcmp(a,b) == 0)
#define strneq(a,b,n) (strncmp(a,b,n) == 0)

#if !sgi
void setoserror(int i)
{
    errno = i;
}
#endif /* !sgi */

/*
 * Like strcpy, but overlaps are allowed.
 */
static char *strmove(char *to, const char *from)
{
    return memmove(to, from, strlen(from)+1);
}

/*
 * Tell whether a string is all a given char.
 */
static int is_all(const char *s, int c)
{
    for (; *s; s++)
	if (*s != c)
	    return 0;
    return 1;
}

/*
 * Given a file name, append a '/', prepend a "./" if it's not absolute, and
 * then make the following substitutions as many times as possible, in place:
 *       "//" -> "/"
 *	 "/./" -> "/"
 *       "^/../" -> "/"
 *       "/foo/../" -> "/" (where foo is any name except .. or .)
 * then remove the '/' off the end (unless the entire string is then "/")
 * and remove the "./" off the beginning (if it's still there).
 * NOTE this makes the assumption that foo/.. is really .,
 * i.e. that foo is not a symlink.
 */
static void canonicalize(char path[PATH_MAX])
{
    char *p;
    int len;

    assert(path[0] != '\0');

    if (path[0] != '/') {
	assert(strlen(path)+3 <= PATH_MAX);
	strmove(path+2, path);
	strncpy(path, "./", 2);
    }
    assert(strlen(path)+2 <= PATH_MAX);
    strcat(path, "/");

    p = path;
    while ((p = strstr(p, "//")) != NULL)
	strmove(p+1, p+2);

    p = path;
    while ((p = strstr(p, "/./")) != NULL)
	strmove(p+1, p+3);

    p = path;
    while ((p = strstr(p, "/../")) != NULL) {
	if (p == path) {
	    strmove(p+1, p+4);
	} else {
	    /* back up to beginning of previous component... */
	    char *q = p;
	    while (q != path && q[-1] != '/')
		q--;
	    if (q != path && !strneq(q, "../", 3)) {
		strmove(q, p+4);
		p = q-1;
	    } else
		p++;
	}
    }

    len = strlen(path);
    assert(len > 0);
    assert(path[len-1] == '/');
    assert(path[0] == '/' || (path[0] == '.' &&  path[1] == '/'));
    if (len > 1)
	path[len-1] = '\0';
    if (path[0] == '.' && path[1] == '/')
	strmove(path, path+2);
}

static int keep_relative = 0;

static int process(const char *_filename)
{
    int childstatus;
    struct stat statbuf;
    char *p, *dest;
    char filename[PATH_MAX], prefix[PATH_MAX], contents[PATH_MAX];
    strcpy(filename, _filename);

    if (filename[0] != '\0') {
	if (!keep_relative) {
	    if (filename[0] != '/') {
		/* change from relative to absolute */
		char cwd[PATH_MAX];
		int len;
		if (getcwd(cwd, sizeof(cwd)-1) == NULL) {
		    perror("getcwd");
		    return 1;
		}
		len = strlen(cwd);
		strmove(filename+len+1, filename);
		strncpy(filename, cwd, len);
		filename[len] = '/';
	    }
	}
	p = filename;
	while (1) {
	    if (p == filename && *p == '/') {
		p++;
		continue;
	    }
	    if (*p == '/' || *p == '\0') {
		strncpy(prefix, filename, p-filename);
		prefix[p-filename] = '\0';
		if (lstat(prefix, &statbuf) == -1) {
		    if (!streq(filename, prefix))
			printf("%s\n", filename);
		    perror(prefix);
		    return 1;
		}
		if (S_ISLNK(statbuf.st_mode)) {
		    int n;
		    if ((n = readlink(prefix, contents, sizeof(contents)-1)) < 0){
			if (!streq(filename, prefix))
			    printf("%s\n", filename);
			fprintf(stderr, "Can't readlink ");
			perror(filename);
			return 1;
		    }
		    contents[n] = '\0';
		    if (contents[0] == '/') {
			/* symlink is absolute-- replace entire prefix */
			dest = filename;
		    } else {
			/* symlink is relative-- replace just the prefix's tail */
			for (dest = p-1; dest!=filename && dest[-1] != '/'; dest--)
			    ;
		    }
		    if (dest == filename && *p == '\0')
			printf("%s -> %s\n", filename, contents);
		    else
			printf("%.*s{%.*s -> %s}%s\n",
			    dest-filename, filename,
			    p-dest, dest,
			    contents, p);
			    
		    strmove(dest+n, p);
		    strncpy(dest, contents, n);
		    p = dest;
		    if (*p == 0)
			break;
		} else if (S_ISDIR(statbuf.st_mode)) {
		    /* we know there's no symlinks in the path up to p,
		       so it's safe to canonicalize it */
		    canonicalize(prefix);
		    /* If it's "." and it's followed by "/" and then
		     * something other than null or "/" or "\0",
		     * can get rid of the .
		     * XXX not sure this is quite right,
		     * and it's too confusing...
		     * maybe put the canonicalization at the top of the loop? */
		    if (streq(prefix, ".")
		     && p[0] == '/' && p[1] != '/' && p[1] != '\0') {
			prefix[0] = '\0';
			p++;
		    }

		    /* replace the beginning of filename with it */
		    p = strmove(filename+strlen(prefix), p);
		    strncpy(filename, prefix, strlen(prefix));

		    if (*p == '\0') {
			break;
		    }
		    p++;
		    continue;
		} else {
		    /* allow ending with /////// even if not a dir... */
		    if (is_all(p, '/')) {
			*p = '\0';
			break;
		    } else {
			if (!streq(filename, prefix))
			    printf("%s\n", filename);
			setoserror(ENOTDIR);
			perror(prefix);
			return 1;
		    }
		}
	    } else
		p++;
	}
    }

    /*
     * Run the real /usr/bin/file on the target
     */
    fflush(stdout);
    fflush(stderr);
    switch(fork()) {
	case -1:
	    perror("can't fork");
	    return 1;
	case 0:	/* child */
	    execl("/usr/bin/file", "file", filename, NULL);
	    perror("can't exec /usr/bin/file");
	    exit(1);
	default: /* parent */
	    (void)wait(&childstatus);
	    break;
    }
    return WEXITSTATUS(childstatus);
}

int main(int argc, char **argv)
{
    int nerrors = 0;
    if (argc < 2) {
	fprintf(stderr, "Usage: %s [-r] filename [filename ... ]\n", argv[0]);
	return 1;
    }
    if (streq(argv[1], "-r")) {
	keep_relative = 1;
	argv++;
    }
    if (argv[1][0] == '-')
    {
	/* just call the real file... no idea what this platform supports. */
	execv("/usr/bin/file", argv);
	perror("execv /usr/bin/file");
	exit(1);
    }
    while (*++argv)
	nerrors += process(*argv);
    return nerrors;
}
