/*
 * Program #1: time the effects of doing an access check at
 * each read upon the performance of a program
 *
 * Author: Matt Bishop, UC Davis
 * Version: 1.0
 * Date: April 10, 2004
 */
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/stat.h>

/*
 * Some systems, like MacOSX, don't have this defined;
 * others, like Linux, do. So we handle both cases
 */
#ifndef RUSAGE_SELF
#	define	RUSAGE_SELF	0
#endif

/*
 * Use these to process arguments to the program
 */
extern char *optarg;
extern int optind;

/*
 * Useful constants
 */
#define	DEF_ITER	1000000		/* default number of iterations */
#define DEF_BUFSIZ	10240		/* max buffer size */

/*
 * Forward declarations
 */
void loopcum(int, int);			/* iterate with check */
void loopsine(int, int);		/* iterate without check */
double mdifftime(struct timeval, struct timeval);	/* subtract times */
void err(int, char *);			/* program error message printer */
void perr(int, char *);			/* system error message printer */

/*
 * Globally accessible variables
 */
char *progname = "*notset*";		/* name of program (for errors, etc) */
int blocksz;				/* default size to read */
char *buf;				/* buffer for input */

/*
 * The main program
 */
int main(int argc, char *argv[])
{
	int i;			/* used to handle options to program */
	int n = DEF_ITER;	/* number of iterations */
	int fd;			/* descriptor of file to be used */
	double cum, sine;	/* time with, without checks */
	struct stat stbuf;	/* buffer for file attribute info */
	struct rusage r1, r2, r3, r4;		/* buffers for resources */
	struct timeval tp1, tp2, tp3, tp4;	/* wall clock times */

	/*
	 * get program name for error messages and such
	 */
	progname = argv[0];

	/*
	 * process arg list
	 */
	while ((i = getopt(argc, argv, "n:")) != EOF)
		switch(i){
		case 'n':		/* number of iterations */
			if ((n = atoi(optarg)) < 1)
				err(EXIT_FAILURE, "-n needs argument");
			break;
		default:		/* oops ... */
			err(EXIT_FAILURE, "unknown option");
			break;
		}

	/*
	 * see what we are to read, and yell if there's nothing there
	 * or there are too many files named
	 */
	if (optind == argc)
		err(EXIT_FAILURE, "no file to use!");
	if (optind != argc - 1)
		err(EXIT_FAILURE, "at most 1 file can be used");

	/*
	 * open the file and snarf the block size from the stat structure
	 */
	if ((fd = open(argv[optind], O_RDONLY)) < 0)
		perr(EXIT_FAILURE, argv[optind]);
	/* get the block size */
	if (fstat(fd, &stbuf) < 0)
		perr(EXIT_FAILURE, "fstat(initial)");
	blocksz = stbuf.st_blksize;

	/*
	 * allocate buffer
	 */
	if ((buf = malloc(blocksz * sizeof(char))) == NULL)
		perr(EXIT_FAILURE, "malloc");

	/*
	 ************************
	 * without the access control check
	 ************************
	 */
	/* Get the starting wall clock time and the user and system times */
	if (gettimeofday(&tp1, NULL) < 0)
		perr(EXIT_FAILURE, "gettimeofday(first call)");
	if (getrusage(RUSAGE_SELF, &r1) < 0)
		perr(EXIT_FAILURE, "getrusage(first call)");

	/* run the test */
	loopsine(fd, n);
	
	/* Get the ending wall clock time and the user and system times */
	if (getrusage(RUSAGE_SELF, &r2) < 0)
		perr(EXIT_FAILURE, "getrusage(second call)");
	if (gettimeofday(&tp2, NULL) < 0)
		perr(EXIT_FAILURE, "gettimeofday(second call)");
	
	/*
	 ************************
	 * with the access control check
	 ************************
	 */
	/* Get the starting wall clock time and the user and system times */
	if (gettimeofday(&tp3, NULL) < 0)
		perr(EXIT_FAILURE, "gettimeofday(third call)");
	if (getrusage(RUSAGE_SELF, &r3) < 0)
		perr(EXIT_FAILURE, "getrusage(third call)");

	/* run the test */
	loopcum(fd, n);
	
	/* Get the ending wall clock time and the user and system times */
	if (getrusage(RUSAGE_SELF, &r4) < 0)
		perr(EXIT_FAILURE, "getrusage(fourth call)");
	if (gettimeofday(&tp4, NULL) < 0)
		perr(EXIT_FAILURE, "gettimeofday(fourth call)");
	

	/*
	 * Print the results as a table
	 * In what follows, "sine" is the value without (sine) the check
	 * and "cum" is the value with (cum) the check
	 */
	/* print header */
	printf("\t\tWITHOUT\t\t\tWITH\t\t\tDIFF\n");
	/* print user times */
	sine = mdifftime(r1.ru_utime, r2.ru_utime);
	cum = mdifftime(r3.ru_utime, r4.ru_utime);
	printf("USER\t%#18.6g\t%#18.6g\t%#18.6g\n",
		sine / n, cum / n, (cum - sine) / n);
	/* print system times */
	sine = mdifftime(r1.ru_stime, r2.ru_stime);
	cum = mdifftime(r3.ru_stime, r4.ru_stime);
	printf("SYSTEM\t%#18.6g\t%#18.6g\t%#18.6g\n",
		sine / n, cum / n, (cum - sine) / n);
	/* print wall clock times */
	sine = mdifftime(tp1, tp2);
	cum = mdifftime(tp3, tp4);
	printf("CLOCK\t%#18.6g\t%#18.6g\t%#18.6g\n",
		sine / n, cum / n, (cum - sine) / n);

	/*
	 * Clean up: close file, say goodnight, Dick!
	 */
	(void) close(fd);
	return(EXIT_SUCCESS);
}

/*
 * This returns the difference of two times as a floating point number
 *
 * CALL:
 * double mdifftime(struct timeval t1, struct timeval t2)
 * 		t1	subtrahend
 * 		t2	minuend
 *
 * RETURNS:
 * Double containing number of seconds in t2 - t1
 *
 * PRINTS:
 * Nothing
 *
 * ERRORS:
 * None
 *
 * SIDE EFFECTS:
 * None
 */
double mdifftime(struct timeval t1, struct timeval t2)
{
	long sec, usec;		/* holds second, microsecond differences */

	/* compute the differences of seconds and microseconds (usec) */
	usec = t2.tv_usec - t1.tv_usec;
	sec = t2.tv_sec - t1.tv_sec;
	
	/* normalize */
	if (usec < 0){
		sec--;
		usec += 1000000;
	}

	/* combine the values into a double */
	return(((double) sec) + ((double) usec)/1000000.0);
}

/*
 * This iterates n times a read of the first block of the file that
 * fd names.
 *
 * CALL:
 * void loopsine(int fd, int n)
 * 		fd	descriptor of file to be read (and checked)
 * 		n	number of times to iterate
 *
 * RETURNS:
 * Nothing.
 *
 * PRINTS:
 * On success, nothing
 *
 * ERRORS:
 * read		--	read system call fails
 *
 * SIDE EFFECTS:
 * Reads file fd
 * Alters contents of buffer buf
 */
void loopsine(int fd, int n)
{
	register int i;			/* counter in a for loop */

	/*
	 * iterate as desired
	 */
        for(i = 0; i < n; i++){
		/* go to first block */
                (void) lseek(fd, 0, SEEK_SET);
		/* read it in */
                if (read(fd, buf, blocksz) < 0)
			perr(EXIT_FAILURE, "read(sine)");
        }
}

/*
 * This iterates n times an access check followed by a read of the first
 * block of the file that fd names. The access check determines if the
 * process can read the file.
 *
 * CALL:
 * void loopcum(int fd, int n)
 * 		fd	descriptor of file to be read (and checked)
 * 		n	number of times to iterate
 *
 * RETURNS:
 * Nothing.
 *
 * PRINTS:
 * On success, nothing
 *
 * ERRORS:
 * getgroups	--	getgroups system call fails
 * fstat	--	fstat system call fails
 * read		--	read system call fails
 * file permission turned off during run
 * 		==	file can no longer be read!
 *
 * SIDE EFFECTS:
 * Reads file fd
 * Alters contents of buffer buf
 */
void loopcum(int fd, int n)
{
	register int i, k;		/* counters in for loops */
	uid_t uid;			/* effective UID (EUID) of process */
	gid_t gid;			/* effective GID (EGID) of process */
	gid_t grplist[NGROUPS_MAX];	/* GIDs of secondary groups */
	int gct;			/* number of secondary groups */
	struct stat stbuf;		/* buffer for file attribute info */
	unsigned int mask = 0;		/* protection bitmask for read bit */
	
	/*
	 * Get effective UID, primary effective GID, secondary groups
	 */
	uid = geteuid();
	gid = getegid();
	if ((gct = getgroups(NGROUPS_MAX, grplist)) < 0)
		perr(EXIT_FAILURE, "getgroups");
	
	/*
	 * iterate as desired
	 */
        for(i = 0; i < n; i++){
		/*
		 * Access control check
		 */
		/* get the CURRENT file permission */
		if (fstat(fd, &stbuf) < 0)
			perr(EXIT_FAILURE, "fstat");
		/*
		 * Check in this order:
		 * 1. If EUID is UID of file, check owner read bit
		 *    (the 0400 bit)
		 * 2. Otherwise, if EGID is GID of file, check group
		 *    read bit (the 0040 bit)
		 * 3. Otherwise, if ANY secondary group is GID of file,
		 *    check group read bit (the 0040 bit, as above)
		 * 4. Otherwise, check the world read bit (the 0004 bit) 
		 */
		/* Check EUID and file UID (step 1) */
		if (uid == stbuf.st_uid)
			mask = 0400;
		/* Check EGID and file GID (step 2) */
		else if (gid == stbuf.st_gid)
			mask = 0040;
		else{
			/* Check secondary groups and file GID (step 3) */
			for(k = 0; k < gct; k++)
				if (grplist[k] == stbuf.st_gid)
					break;
			if (k != gct)
				mask = 0040;
			else
				/* Check world bit (step 4) */
				mask = 0004;
		}
		/* INCONSISTENCY: before the file could be read, */
		/* now it can't be read -- what happened? BAIL!! */
		if ((stbuf.st_mode&mask) != mask)
			err(EXIT_FAILURE, "file permission turned off during run");
		/*
		 * Okay to read the file, so do it
		 */
		/* go to first block */
                (void) lseek(fd, 0, SEEK_SET);
		/* read it in */
                if (read(fd, buf, blocksz) < 0)
			perr(EXIT_FAILURE, "read(cum)");
        }
}

/*
 * This prints the error message ermsg preceded by the program name.
 * It then exits with exit code rv.
 *
 * CALL:
 * void err(int rv, char *ermsg)
 * 		rv	exit code
 * 		ermsg	error message to be printed
 *
 * RETURNS:
 * No return
 *
 * PRINTS:
 * Given error message, preceded by progname and ": ", on stderr
 *
 * ERRORS:
 * None.
 *
 * SIDE EFFECTS:
 * Exits program
 */
void err(int rv, char *ermsg)
{
	/* print program error message */
	fprintf(stderr, "%s: %s\n", progname, ermsg);
	/* bye-bye! */
	exit(rv);
}

/*
 * This prints an error message using ermsg as the string
 * BEFORE the system error message. It then exits with
 * exit code rv.
 *
 * CALL:
 * void perr(int rv, char *ermsg)
 * 		rv	exit code
 * 		ermsg	error message to be printed
 *
 * RETURNS:
 * No return
 *
 * PRINTS:
 * System error message on stderr (using perror(3))
 *
 * ERRORS:
 * None.
 *
 * SIDE EFFECTS:
 * Exits program
 */
void perr(int rv, char *ermsg)
{
	/* print system error message */
	perror(ermsg);
	/* bye-bye! */
	exit(rv);
}