/*
 * derefsymlink - a program to dereference symbolic links
 * 
 * Copyright (c) 1996 by Peter Chang
 * Peter.Chang@nottingham.ac.uk
 * 
 * It takes in pathnames and spits them out fully dereference.
 * If any of the components of the pathnames do not exist or
 * contain circular symlinks, the program stays silent or
 * prints a message to stderr.
 * [Some of the error checking is rather paranoid but it's better
 * to be safe than sorry.]
 * 
 * Revision 0.1 created on 1996/8/21
 *  made skeleton program which processes options and filenames
 *  correctly, derefname() just echos back at the moment
 * Revision 0.2 written on 1996/8/23
 *  filled in rest of program, including derefname() and striprel()
 *  added verbose flag
 * Revision 0.3 debugged on 1997/12/22
 *  the last test in derefname() did not cope with a single link to
 *  a top directory
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>

#define IN_USE_ARGV  0
#define IN_USE_FILE  1
#define IN_USE_STDIN 2
#define OUT_USE_STDOUT 0
#define OUT_USE_FILE   1
#define DEPTH_LIMIT 25 /* max directory depth */

int in_method = IN_USE_ARGV;
int out_method = OUT_USE_STDOUT;
int verbose = 0;

int main(int argc, char *argv[])
{
   static char iname[FILENAME_MAX], oname[FILENAME_MAX],
     cname[FILENAME_MAX];
   int al, i;
   FILE *fi, *fo;

   int pcl(int , char **, char *, char *);
   int derefname(char *, int );

   /* look for options */
   al = pcl(argc, argv, iname, oname);

   /* set up output */
   if (out_method == OUT_USE_FILE) {
      if ((fo = fopen(oname, "w")) == NULL) {
	 if (verbose) fprintf(stderr, "can't open output file %s", oname);
	 exit(EXIT_FAILURE);
      }
   } else fo = stdout;

   /* if there is a list from a file or stdin
    * then go through this first */
   if (in_method != IN_USE_ARGV) {
      if (in_method == IN_USE_FILE) {
	 if ((fi = fopen(iname, "r")) == NULL) {
	    if (verbose) fprintf(stderr, "can't open input file %s", iname);
	    exit(EXIT_FAILURE);
	 }
      } else fi = stdin;
      while ((i = fscanf(fi, "%s", cname)) != EOF) {
	 if (derefname(cname, 0) >= 0) fprintf(fo, "%s\n", cname);
      }
   }

   /* now finish off the remaining arguments */
   while (al < argc) {
      strncpy(cname, argv[al++], FILENAME_MAX-1);
      if (derefname(cname, 0) >= 0) fprintf(fo, "%s\n", cname);
   }

   if (out_method == OUT_USE_FILE) fclose(fo);
   
   return EXIT_SUCCESS;
}


static char curdir[FILENAME_MAX];

/* do the real work here */
int derefname(char *name, int depth)
{
   char tempa[FILENAME_MAX], cname[FILENAME_MAX];
   struct stat sbuf;
   int len, sc;
   char *ptra;

   int striprel(char *);

   /* check if we are bombing out */
   if (depth < 0) return depth;
   if (depth > DEPTH_LIMIT) {
      /* recursion limit to prevent circular reference */
      if (verbose) fprintf(stderr, "circular symbolic link: %s\n", name);
      return -1;
   }
   strncpy(cname, name, FILENAME_MAX-1);
   if (name[0] != '/') { /* is name absolute */
      if (curdir[0] == '\0') { /* initialise current directory */
	 getcwd(curdir, FILENAME_MAX-1);
	 strncat(curdir, "/", FILENAME_MAX-1);
      }
      /* append cwd to local name */
      strncpy(cname, curdir, FILENAME_MAX-1);
      strncat(cname, name, FILENAME_MAX-1);
   }
   if (striprel(cname) != 0) return -1;
   if (lstat(cname, &sbuf) != 0) {
      if (verbose) fprintf(stderr, "%s does not exist\n", cname);
      return -1;
   }
   sc = 0;
   while (S_ISLNK(sbuf.st_mode)) { /* check for symlink and deref it */
      if (sc > DEPTH_LIMIT) {
	 /* recursion limit to prevent circular reference */
	 if (verbose) fprintf(stderr, "circular symbolic link: %s\n", cname);
	 return -1;
      }
      len = readlink(cname, tempa, FILENAME_MAX-1);
      tempa[len] = '\0';
      if (tempa[0] != '/') {
	 if ((ptra = strrchr(cname, '/')) == NULL) {
	    if (verbose) fprintf(stderr, "Error in stripping link\n");
	    return -1;
	 }
	 *(ptra+1) = '\0';
	 strncat(cname, tempa, FILENAME_MAX-1);
      } else {
	 strncpy(cname, tempa, FILENAME_MAX-1);
      }
      if (striprel(cname) != 0) return -1;
      if (lstat(cname, &sbuf) != 0) {
	 if (verbose) fprintf(stderr, "%s does not exist\n", cname);
	 return -1;
      }
      sc++;
   }

   /* strip last name */
   if ((ptra = strrchr(cname, '/')) == NULL) {
      if (verbose) fprintf(stderr, "Error in stripping name\n");
      return -1;
   }
   if (ptra != cname) {
      strncpy(tempa, ptra, FILENAME_MAX-1);
      *ptra = '\0';
      /* dereference rest of path */
      depth = derefname(cname, depth+1);
      strncpy(name, cname, FILENAME_MAX-1);
      strncat(name, tempa, FILENAME_MAX-1);
   } else strncpy(name, cname, FILENAME_MAX-1);

   return depth;
}


/* remove all "/../" and "/./" in pathnames */
int striprel(char *name)
{
   static char tempa[FILENAME_MAX], tempb[FILENAME_MAX];
   char *ptra, *ptrb;

   strncpy(tempa, name, FILENAME_MAX-1);
   while ((ptra = strstr(tempa, "/../")) != NULL) {
      *ptra = '\0';
      if ((ptrb = strrchr(tempa, '/')) == NULL) {
	 if (verbose) fprintf(stderr, "Error in stripping '..'\n");
	 return -1;
      }
      *(ptrb+1) = '\0';
      strncpy(tempb, ptra+4, FILENAME_MAX-1);
      strncat(tempa, tempb, FILENAME_MAX-1);
   }

   while ((ptra = strstr(tempa, "/./")) != NULL) {
      *ptra = '\0';
      strncpy(tempb, ptra+2, FILENAME_MAX-1);
      strncat(tempa, tempb, FILENAME_MAX-1);
   }
   strncpy(name, tempa, FILENAME_MAX-1);   
   return 0;
}


/*
 * Parse the command line set the filename and the appropriate flags
 */
int pcl(int argc, char **argv, char *iname, char *oname)
{
   static const char *options[] = {
      "help                    print this message",
      "file <list>        use a list of filenames",
      "output <file>   use file instead of stdout",
      "stdin                   use stdin as input",
      "verbose               print error messages",
      NULL
   };
   static const char usage0[] = "usage: derefsymlink [options] [filename] ...",
     usage1[] = " try derefsymlink -help for a list of options";
   int i, optno, olen;
   const char **sptr;

   for (i = 1; i < argc; i++) {
      olen = strlen(argv[i])-1;
      if (argv[i][0] == '-') {
	 for (sptr = options, optno = 0; *sptr != NULL; sptr++, optno++)
	   if (strncmp(argv[i]+1, *sptr, olen) == 0) break;

	 switch(optno) {
	  case 0:
	    printf("%s\n", usage0);
	    printf("Options:\n");
	    for (sptr = options; *sptr != NULL; sptr++)
	      printf(" -%s\n", *sptr);
	    printf("\n");
	    exit(EXIT_SUCCESS);
	    break;
	  case 1:
	    in_method = IN_USE_FILE;
	    strcpy(iname, argv[++i]);
	    break;
	  case 2:
	    out_method = OUT_USE_FILE;
	    strcpy(oname, argv[++i]);
	    break;
	  case 3:
	    in_method = IN_USE_STDIN;
	    break;
	  case 4:
	    verbose = 1;
	    break;
	  default:
	    fprintf(stderr, "%s\n%s\n", usage0, usage1);
	    exit(EXIT_FAILURE);
	    break;
	 }
      } else return i;
   }
   return i;
}
