/* hbb.c   Copyright 1999   Robert Ransbottom */
 
/* BUG NOTE
 * XXX I haven't found the documentation that determines how many
 * XXX characters may be in a command string.  So the 1024 is a guess.
 *
 * This could be more efficient.  
 *	The kernel might talk to a daemon.  (Blame the other guy.)
 *	The configuration file could be cached.
 * Hopefully paging will ameliorate both these issues.
 */

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

#if !defined conffile
#	define	conffile	"./hbb.conf"
#endif
const char *const conf = conffile;	/* Configuration file */

/* Authorization signifiers in conf */
const char *const rootsign = "root";	
const char *const usersign = "user";

const char *const magic = "#!!";

const char NIL = '\0';
const char PATHSEP = ':';

enum { DEBUG = 0 };	/* boolean for printf's */

enum {
	LINESZ = 128,		/* size of 1st line */
	MAGICLEN = 3,		/* strlen of magic */
	ARSZ = (LINESZ-MAGICLEN)/2,	/* max # of args on line 1 */
	COMSZ = 1024,            /* max size of a command line */
	PATHSZ = 256,		/* max size of pseudo-PATH */
	ROOTLEN = 4,		/* strlen of rootsign */
	USERLEN = 4,		/* strlen of usersign */
	USER = 1,		/* index into path array */
	ROOT = 0		/* index into path array */
};

char line[LINESZ];		/* for 1st line of script */
char path[2][PATHSZ];		/* for the pseudo-paths */
char *arry[ARSZ+(COMSZ/2)];	/* to pass to execv */

static void chomp(char *s);
static void get_conf(char const *const conf);
static void get_line(char *line, char const *const file);
static char *find_interp(char *const path, char const *const interp);


int main(int argc, char *argv[])
{
	char const *f = argv[1]; 
	char *head;
	char *tail;
	char *binary;
	char tmp;
	int i, j;
	int whoami;
	struct stat buf;
	int u, g;

	if (DEBUG) { 
		printf("hbb: start of debugging output\n"); 
	}



	/* check args */

	if (DEBUG){
		printf( "hbb: argc=%d\n", argc);
		for ( i=0; i< argc; ++i) {
			printf( "hbb: arg[%d] -> %s\n", i, argv[i]);
		}
	}

	if ( argc < 2) {
		if (DEBUG) { printf("hbb: error: no script file\n"); }
		return EINVAL;
	}




	/* initialize */
	u = getuid();
	g = getgid();

	errno = 0;
	get_conf(conf);	/* XXX source config file on each invocation */

	if (DEBUG) {
		printf("hbb: config >%s<\nhbb: script >%s<\n", conf, f);
	}

	if (errno) { return errno; }

	/* Does argv[1] represent a file -- for deviant situations
	 * The script is deleted before we get here.  Or we are
	 * letting users use this on the command line.
	 *
	 * XXX Consider:
	 * Seems that the BINFMT_MISC facility should allow the
	 * registered binary to determine that it is being 
	 * invoked by the kernel; this would allow a quick
	 * exit if not.  
	 */

	if (!DEBUG) {
		/* if ( !called_by_kernel) {
		 * 	return( ENOSYS);
		 * }
		 */
	}

	if ( stat( f, &buf) != 0 ) {
		if (DEBUG) {
			printf("hbb: error: scriptname %s not a file\n", f);
		}
		return EINVAL;
	}

	/* get 1st line */
	get_line(line, f);
	if (errno) { return errno; }



	/* parse line */


	if ( strncmp( magic, line, MAGICLEN) != 0) {
		if (DEBUG) {
			printf("hbb: invalid file: bad magic\n");
		}
		return EINVAL;
	}

	if (DEBUG) { printf("hbb: first line: >%s<\n", line); }

	head = line + MAGICLEN;
	i = 0;
	while (1) {
		while (isspace(*head)) { ++head; }
		if (*head == NIL) {	/* end of line */
			break;
		}
		tail = head;
		while (!isspace(*tail) && *tail != '\0') {
			++tail;
		}
		if (*tail != NIL) {
			tmp = *tail;
			*tail = NIL;
			arry[i] = (char *) alloca(strlen(head) + 1);
			if (!arry[i]) { return ENOMEM; }
			strcpy(arry[i], head);
			*tail = tmp;
		} else {
			/* last arg */
			arry[i] = (char *) alloca(strlen(head) + 1);
			if (!arry[i]) { return ENOMEM; }
			strcpy(arry[i], head);
		}
		++i;
		head = tail;
	}

	/* add the scriptname */
	arry[i] = ( char *) alloca(strlen( f)+1);
	if (!arry[i]) { return ENOMEM; }
	strcpy( arry[i], f);



	/* add arguments given to the script via the command line */

	for ( j=2; j<argc; ++j) {
		++i;
		arry[i] = ( char *) alloca(strlen( argv[j]) +1);
		if (!arry[i]) { return ENOMEM; }
		strcpy( arry[i], argv[j]);
	}

	if (DEBUG) {
		printf("hbb: last arry member filled "
				"with data is arry[%d]\n", i);
	}

	/* add a nil terminator */
	++i;
	arry[i] = (char *) alloca(1);
	if (!arry[i]) { return ENOMEM; }
	arry[i][0] = NIL;


	if (DEBUG) {
		for (i = 0; arry[i][0] != NIL; ++i) {
			printf("hbb: arry[%d] >%s<  ", i, arry[i]);
		}
		printf("\n");
	}



	/* select path */
	whoami = !!getuid();
	if ( DEBUG) { 
		printf("hbb: using %s path\n", whoami ? "user" :"root"); 
	}


	if (errno ) { return errno; }


	/* seek interp */

	binary = find_interp(path[whoami], arry[0]);


	if ( ! binary) { 
		if ( DEBUG) { 
			printf( "hbb: interpreter >%s< not found.\n", arry[0]);
		}
		return ENOENT; 
	}

	if ( DEBUG) { printf( "hbb: binary is >%s<\n", binary); }

	if (errno ) { return errno; }


	/* check permissions */

	/* XXX not necessary the kernel is doing this for us */


	/*******/
	if ( DEBUG) { printf( "hbb: end of debugging output!\n"); }
	

	/* execute script */

	execv( binary, arry);

	if ( errno) { perror( "Error after execv"); return errno; } 


	/* NOT_REACHED */

	/* cleanup and go home */

	free( binary);
	return 0;
}





/* return the dirname where interpreter is or null */
static char *find_interp(char *const path, char const *const interp)
{

	char *buf;
	char *x;
	char *tail;
	struct stat st;


	buf = (char *) malloc( PATHSZ);
	if ( !buf) { 
		errno = ENOMEM; 
		return NULL; 
	}

	x = path;
	while (*x != NIL) {
		tail = x;
		while (*tail != PATHSEP && *tail != NIL) {
			++tail;
		}
		if (*tail == PATHSEP) {
			*tail = NIL;
			strcpy(buf, x);
			strcat(buf, "/");
			strcat(buf, interp);
			*tail = PATHSEP;
			x = tail  +1;
			if ( 0 == stat( buf, &st)) {
				return buf;
			}
			errno = 0;
		}else{
			strcpy(buf, x);
			strcat(buf, "/");
			strcat(buf, interp);
			x = tail;
			if ( 0 == stat( buf, &st)) {
				return buf;
			}
			errno = 0;
		}
	}

	free(buf);
	return NULL;
}





/* get the first line of the script file */
static void get_line(char *line, char const *const file)
{
	FILE *f;
	char *c;

	f = fopen(file, "r");

	c = fgets(line, LINESZ, f);
	if (!c) {
		return;
	}

	chomp(line);


	fclose(f);
}





/* remove last newline from a string */
static void chomp(char *s)
{
	char *nl;
	nl = strrchr(s, '\n');
	if (nl) { *nl = NIL; }
}





/* load configuration from conf file */
static void get_conf(char const *const conf)
{
	FILE *cf;
	char buf[PATHSZ];
	char *x;
	int who;

	cf = fopen(conf, "r");

	while (fgets(buf, PATHSZ, cf)) {
		x = buf;
		while (isspace(*x)) {
			++x;
		}
		if (memcmp(x, rootsign, ROOTLEN) == 0) {
			x += ROOTLEN;
			who = ROOT;
		} else {
			if (memcmp(x, usersign, USERLEN) == 0) {
				x += USERLEN;
				who = USER;
			} else {
				continue;
			}
		}
		while (isspace(*x)) {
			++x;
		}
		chomp(x);
		strcpy(path[who], x);
	}

	if (DEBUG) {
		printf("hbb: root path >%s<\n", path[ROOT]);
		printf("hbb: user path >%s<\n", path[USER]);
	}

	fclose(cf);
}
