/* Copyright 1999 by the Massachusetts Institute of Technology.
 *
 * Permission to use, copy, modify, and distribute this
 * software and its documentation for any purpose and without
 * fee is hereby granted, provided that the above copyright
 * notice appear in all copies and that both that copyright
 * notice and this permission notice appear in supporting
 * documentation, and that the name of M.I.T. not be used in
 * advertising or publicity pertaining to distribution of the
 * software without specific, written prior permission.
 * M.I.T. makes no representations about the suitability of
 * this software for any purpose.  It is provided "as is"
 * without express or implied warranty.
 */

static const char rcsid[] = "$Id: os-statfiles.c,v 1.4 2004/04/24 14:10:29 rbasch Exp $";

/* This program is used to generate the stats files used as input
 * to the os-checkfiles program.
 */

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

/* Bit mask for chmod() */
#define MODEMASK (S_ISUID|S_ISGID|S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO)

char *progname;

void usage();

char *get_link(const char *path, const struct stat *statp);
void check_links();


void usage()
{
  fprintf(stderr, "Usage: %s [-f <filelistfile>] [-r <root>]\n", progname);
  exit(1);
}

int main(int argc, char **argv)
{
  const char *infile = NULL;
  const char *target = "/";
  FILE *infp;
  char filenamebuf[PATH_MAX+2];
  char *filename;
  char linkbuf[PATH_MAX+1];
  struct stat sb;
  int c, len;
  char *master;

  progname = argv[0];

  while ((c = getopt(argc, argv, "f:r:")) != EOF)
    {
      switch(c)
	{
	case 'f':
	  infile = optarg;
	  break;
	case 'r':
	  target = optarg;
	  break;
	case '?':
	  usage();
	  break;
	}
    }

  if (infile != NULL)
    {
      infp = fopen(infile, "r");
      if (infp == NULL)
	{
	  fprintf(stderr, "%s: Cannot open %s: %s\n", progname, infile,
		  strerror(errno));
	  exit(1);
	}
    }
  else
    infp = stdin;

  /* Chdir to the target root. */
  if (chdir(target) != 0)
    {
      fprintf(stderr, "%s: Cannot chdir to %s: %s\n",
	      progname, target, strerror(errno));
      exit(1);
    }

  while (fgets(filenamebuf, sizeof(filenamebuf), infp) != NULL)
    {
      len = strlen(filenamebuf);
      if (filenamebuf[len-1] != '\n')
	{
	  fprintf(stderr, "%s: Invalid filename entry '%s', aborting\n",
		  progname, filenamebuf);
	  exit(1);
	}

      filenamebuf[len-1] = '\0';

      /* Strip leading "./" or "/". */
      if (filenamebuf[0] == '/')
	filename = &filenamebuf[1];
      else if (strncmp(filenamebuf, "./", 2) == 0)
	filename = &filenamebuf[2];
      else
	filename = &filenamebuf[0];

      if (lstat(filename, &sb) != 0)
	{
	  fprintf(stderr, "%s: Warning: cannot stat %s\n", progname, filename);
	  continue;
	}

      /* Handle hard links.  If it's the first instance seen of the
       * link, get_link() will record it as the "master", and we will
       * fall through to normal handling.  Subsequent instances are
       * listed here as links to the master.
       */
      if (sb.st_nlink > 1 && !S_ISDIR(sb.st_mode))
	{
	  master = get_link(filename, &sb);
	  if (master != NULL)
	    {
	      /* This is not the master.  List it as a hard link. */
	      printf("h %s %s\n", master, filename);
	      continue;
	    }
	}

      switch(sb.st_mode & S_IFMT)
	{
	case S_IFLNK:
	  len = readlink(filename, linkbuf, sizeof(linkbuf));
	  if (len < 0)
	    {
	      fprintf(stderr, "%s: Warning: Symbolic link %s has bad value\n",
		      progname, filename);
	      continue;
	    }
	  linkbuf[len] = '\0';
	  printf("l %s %s\n", linkbuf, filename);
	  break;

	case S_IFREG:
	  printf("f %#lo %llu %lu %lu %lu %s\n", sb.st_mode & MODEMASK,
		 (unsigned long long) sb.st_size, sb.st_uid, sb.st_gid,
		 sb.st_mtime, filename);
	  break;

	case S_IFDIR:
	  printf("d %#lo %lu %lu %s\n", sb.st_mode & MODEMASK,
		 sb.st_uid, sb.st_gid, filename);
	  break;

	default:
	  fprintf(stderr, "%s: Warning: ignoring %s\n", progname, filename);
	  break;
	}
    }

  /* Warn if we have not seen all of the links to any hard-linked files. */
  check_links();
  exit(0);
}

struct hardlink {
  ino_t inode;
  dev_t device;
  char *master;
  int nlinks;
  int count;
};

struct hardlink *links = NULL;
int nlinks = 0;
int linksize = 0;
  
/* Get the "master" path for the given hard link; if not yet seen,
 * remember it as the master.
 */
char *get_link(const char *path, const struct stat *statp)
{
  struct hardlink *newlinks;
  int i;

  for (i = 0; i < nlinks; ++i)
    {
      if (links[i].inode == statp->st_ino
	  && links[i].device == statp->st_dev)
	{
	  if (++links[i].count > statp->st_nlink)
	    {
	      fprintf(stderr,
		      "%s: Internal error: too many links for %s\n",
		      progname, path);
	      exit(1);
	    }
	  return links[i].master;
	}
    }

  /* Haven't seen this file yet.  Make this path the master. */
  if (nlinks >= linksize)
    {
      /* Need to grow the array. */
      linksize += 100;
      newlinks = malloc(linksize * sizeof(struct hardlink));
      if (newlinks == NULL)
	{
	  fprintf(stderr, "%s: Out of memory for link table.\n", progname);
	  exit(1);
	}
      if (links != NULL)
	{
	  memcpy(newlinks, links, nlinks * sizeof(struct hardlink));
	  free(links);
	}
      links = newlinks;
    }

  links[nlinks].inode = statp->st_ino;
  links[nlinks].device = statp->st_dev;
  links[nlinks].nlinks = statp->st_nlink;
  links[nlinks].master = strdup(path);
  if (links[nlinks].master == NULL)
    {
      fprintf(stderr, "%s: Out of memory.\n", progname);
      exit(1);
    }
  links[nlinks].count = 1;
  ++nlinks;

  /* Indicate to the caller that this is the master. */
  return NULL;
}

/* Check that we have seen all the hard links for a file. */
void check_links()
{
  int i;

  for (i = 0; i < nlinks; ++i)
    {
      if (links[i].count < links[i].nlinks)
	fprintf(stderr,
		"%s: Warning: Only saw %d out of %d links for %s\n",
		progname, links[i].count, links[i].nlinks,
		links[i].master);
    }
}
