/*
 * dbz - use and test dbz in various ways
 *
 * -Log-
 */

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <dbz.h>

#ifdef FUNNYSEEKS
#include <unistd.h>
#else
#define	SEEK_SET	0
#endif

#define	STREQ(a, b)	(*(a) == *(b) && strcmp((a), (b)) == 0)

#ifndef lint
static char RCSid[] = "$Header$";
#endif

char *progname;

char *inname = "(no file)";		/* filename for messages etc. */
long lineno;				/* line number for messages etc. */

char *basename;
char *pagname;
char *dirname;
char *str2dup();
FILE *base;

int op = 'b';			/* what to do, default build a new table */
int baseinput = 1;		/* is the base file also the input? */

char *from = NULL;		/* old table to use for dbzagain() */
int omitzero = 0;		/* omit lines tagged with 0 */
long every = 0;			/* report every n lines */
int syncs = 0;			/* dbzsync() on each report */
int quick = 0;			/* quick checking, not too thorough */
int sweep = 0;			/* sweep file checking all offsets */
int useincore = 1;		/* should we use incore facility? */
long xxx = 0;			/* debugging variable */
int printx = 0;			/* print xxx after all is done */
int unique = 1;			/* before store(), check with fetch() */
int usefresh = 0;		/* use dbzfresh? */
long siz = 0;			/* -p size */
char map = 'C';			/* -p map */
long tag = 0;			/* -p tag mask */
int exact = 0;			/* do not run dbzsize(siz) */
int dbzint = 1;			/* use new interface? */
char fs = '\t';			/* field separator, default tab */
int unopen = 0;			/* make base unopenable during dbminit? */
char *change = NULL;		/* chdir here before dbmclose */

#define	DEFBUF	1024		/* default line-buffer size */
int buflen = DEFBUF;		/* line length limit */
char lbuf[DEFBUF];
char *line = lbuf;
char cbuf[DEFBUF];
char *cmp = cbuf;

void fail();
void dofile();
void runs();
void dosweep();
void mkfiles();
void crfile();
void doline();
void process();

#ifdef HAVERFCIZE
extern char *rfc822ize();
#else
#define	rfc822ize(n)	(n)
#endif

extern char *malloc();

/*
 - main - parse arguments and handle options
 */
main(argc, argv)
int argc;
char *argv[];
{
	int c;
	int errflg = 0;
	extern int optind;
	extern char *optarg;
	int doruns = 0;
	extern long atol();

	progname = argv[0];

	while ((c = getopt(argc, argv, "axcmt:l:R0E:SqOiX:Yuf:p:eMUC:d")) != EOF)
		switch (c) {
		case 'a':	/* append to existing table */
			if (op != 'b')
				fail("only one of -a -x -c -m can be given", "");
			op = 'a';
			baseinput = 0;
			break;
		case 'x':	/* extract from existing table */
			if (op != 'b')
				fail("only one of -a -x -c -m can be given", "");
			op = 'x';
			baseinput = 0;
			break;
		case 'c':	/* check existing table */
			if (op != 'b')
				fail("only one of -a -x -c -m can be given", "");
			op = 'c';
			break;
		case 'm':	/* extract missing (complement of -x) */
			if (op != 'b')
				fail("only one of -a -x -c -m can be given", "");
			op = 'm';
			baseinput = 0;
			break;
		case 't':	/* set field separator */
			if (strlen(optarg) > 1)
				fail("only one field separator allowed", "");
			fs = *optarg;
			break;
		case 'l':	/* override line-length limit */
			buflen = atoi(optarg) + 1;
			if (buflen <= 2)
				fail("bad -l value `%s'", optarg);
			line = malloc(buflen);
			cmp = malloc(buflen);
			if (line == NULL || cmp == NULL)
				fail("cannot allocate %s-byte buffers", optarg);
			break;
		case 'R':	/* print run statistics */
			doruns = 1;
			break;
		case '0':	/* omit lines tagged (by fake -t) with 0 */
			omitzero = 1;
			break;
		case 'E':	/* report every n items */
			every = atol(optarg);
			break;
		case 'S':	/* dbzsync() on each -E report */
			syncs = 1;
			break;
		case 'q':	/* quick check or extract */
			quick = 1;
			break;
		case 'O':	/* sweep file checking all offsets */
			sweep = 1;
			break;
		case 'i':	/* don't use incore */
			useincore = 0;
			break;
		case 'X':	/* set xxx */
			xxx = atoi(optarg);
			break;
		case 'Y':	/* print xxx afterward */
			printx = 1;
			break;
		case 'u':	/* don't check uniqueness */
			unique = 0;
			break;
		case 'f':	/* init from existing table's parameters */
			from = optarg;
			break;
		case 'p':	/* parameters for dbzfresh */
			if (sscanf(optarg, "%ld %1s %lx", &siz, &map, &tag) != 3) {
				map = '?';
				tag = 0;
				if (sscanf(optarg, "%ld", &siz) != 1)
					fail("bad -n value `%s'", optarg);
			}
			usefresh = 1;
			break;
		case 'e':	/* -p size is exact, don't dbzsize() it */
			exact = 1;
			break;
		case 'M':	/* use old dbm interface + rfc822ize */
			dbzint = 0;
			break;
		case 'U':	/* make base unopenable during init */
			unopen = 1;
			break;
		case 'C':	/* change directories before dbmclose */
			change = optarg;
			break;
		case 'd':	/* Debugging. */
			if (dbzdebug(1) < 0)
				fail("dbz debugging not available", "");
			break;
		case '?':
		default:
			errflg++;
			break;
		}
	if (errflg || optind >= argc || (optind+1 < argc && baseinput)) {
		fprintf(stderr, "usage: %s ", progname);
		fprintf(stderr, "[-a] [-x] [-c] database [file] ...\n");
		exit(2);
	}

	(void) dbzincore(useincore);
	basename = argv[optind];
	pagname = str2dup(basename, ".pag");
	dirname = str2dup(basename, ".dir");
	mkfiles();
	optind++;

	if (baseinput)		/* implies no further arguments */
		process(base, basename);
	else if (optind >= argc)
		process(stdin, "stdin");
	else
		for (; optind < argc; optind++)
			dofile(argv[optind]);

	if (change != NULL)
		(void) chdir(change);
	if (dbmclose() < 0)
		fail("dbmclose failed", "");
	if (doruns)
		runs(pagname);
	if (sweep)
		dosweep(basename, pagname);
	if (printx)
		printf("%ld\n", xxx);
	exit(0);
}

/*
 - dofile - open a file and invoke process()
 */
void
dofile(name)
char *name;
{
	register FILE *in;

	if (STREQ(name, "-"))
		process(stdin, "-");
	else {
		in = fopen(name, "r");
		if (in == NULL)
			fail("cannot open `%s'", name);
		process(in, name);
		(void) fclose(in);
	}
}

/*
 - mkfiles - create empty files and open them up
 */
void
mkfiles()
{
	if (op == 'b' && !dbzint) {
		crfile(dirname);
		crfile(pagname);
	}

	base = fopen(basename, (op == 'a') ? "a" : "r");
	if (base == NULL)
		fail("cannot open `%s'", basename);
	if (unopen)
		(void) chmod(basename, 0);
	if (from != NULL) {
		if (dbzagain(basename, from) < 0)
			fail("dbzagain(`%s'...) failed", basename);
	} else if (op == 'b' && dbzint) {
		if (!exact)
			siz = dbzsize(siz);
		if (dbzfresh(basename, siz, (int)fs, map, tag) < 0)
			fail("dbzfresh(`%s'...) failed", basename);
	} else if (dbminit(basename) < 0)
		fail("dbminit(`%s') failed", basename);
	if (unopen)
		(void) chmod(basename, 0600);	/* hard to restore original */
}

/*
 - crfile - create a file
 */
void
crfile(name)
char *name;
{
	register int f;

	f = creat(name, 0666);
	if (f < 0)
		fail("cannot create `%s'", name);
	(void) close(f);
}

/*
 - process - process input file
 */
void
process(in, name)
FILE *in;
char *name;
{
	register off_t place;

	inname = name;
	lineno = 0;

	for (;;) {
		place = ftell(in);
		if (fgets(line, buflen, in) == NULL)
			return;
		lineno++;
		if (every > 0 && lineno%every == 0) {
			fprintf(stderr, "%ld\n", lineno);
			if (dbzsync() < 0)
				fail("dbzsync failed", "");
		}
		doline(line, place);
	}
	/* NOTREACHED */
}

/*
 - doline - process input line
 */
void
doline(lp, inoffset)
char *lp;
off_t inoffset;
{
	register char *p;
	register char pc;
	datum key, value;
	off_t place = inoffset;
	register int shouldfind;
	register int llen;
	char keytext[DBZMAXKEY+1];

	p = NULL;
	if (fs != '\0')
		p = strchr(lp, fs);
	if (p == NULL)
		p = lp + strlen(lp);
	if (p > lp && *(p-1) == '\n')
		p--;
	if (p - lp > DBZMAXKEY)
		fail("key of `%.40s...' too long", lp);
	pc = *p;
	*p = '\0';
	(void) strcpy(keytext, lp);
	*p = pc;
	key.dptr = (dbzint) ? keytext : rfc822ize(keytext);
	key.dsize = strlen(keytext)+1;

	switch (op) {
	case 'a':
		place = ftell(base);
		llen = strlen(lp);
		if (fwrite(lp, 1, llen, base) != llen)
			fail("write error in `%s'", basename);
		/* FALLTHROUGH */
	case 'b':
		if (omitzero && p != NULL && *(p+1) == '0')
			return;
		if (unique) {
			value = (dbzint) ? dbzfetch(key) : fetch(key);
			if (value.dptr != NULL)
				fail("`%.40s...' already present", lp);
		}
		value.dptr = (char *)&place;
		value.dsize = (int)sizeof(off_t);
		if (((dbzint) ? dbzstore(key, value) : store(key, value)) < 0)
			fail("store failed on `%.40s...'", lp);
		break;
	case 'c':
		value = (dbzint) ? dbzfetch(key) : fetch(key);
		shouldfind = (omitzero && p != NULL && *(p+1) == '0') ? 0 : 1;
		if (!shouldfind && (value.dptr != NULL || value.dsize != 0))
			fail("`%.40s...' found, shouldn't be", lp);
		if (shouldfind && (value.dptr == NULL ||
					value.dsize != sizeof(off_t)))
			fail("can't find `%.40s...'", lp);
		if (shouldfind && !quick) {
			(void) memcpy((char *)&place, value.dptr, sizeof(off_t));
			if (place != inoffset)
				fail("offset mismatch on `%.40s...'", lp);
			if (fseek(base, place, SEEK_SET) == -1)
				fail("fseek failed on `%.40s...'", lp);
			if (fgets(cmp, buflen, base) == NULL)
				fail("can't read line for `%.40s...'", lp);
			if (!STREQ(lp, cmp))
				fail("compare failed on `%.40s...'", lp);
		}
		break;
	case 'x':
		value = (dbzint) ? dbzfetch(key) : fetch(key);
		if (value.dptr != NULL && !quick) {
			(void) memcpy((char *)&place, value.dptr, sizeof(off_t));
			if (fseek(base, place, SEEK_SET) == -1)
				fail("fseek failed on `%.40s...'", lp);
			if (fgets(cmp, buflen, base) == NULL)
				fail("can't read line for `%.40s...'", lp);
			fputs(cmp, stdout);
		} else if (value.dptr != NULL)
			fputs(lp, stdout);
		break;
	case 'm':
		value = (dbzint) ? dbzfetch(key) : fetch(key);
		if (value.dptr == NULL) {
			fputs(keytext, stdout);
			putchar('\n');
		}
		break;
	default:
		fail("unknown operator -- can't happen", "");
		break;
	}
}

/*
 - runs - print run statistics
 */
void
runs(file)
char *file;
{
	register FILE *fd;
	off_t it;
	register long run;

	fd = fopen(file, "r");
	if (fd == NULL)
		fail("cannot reopen `%s'", file);
	run = 0;
	while (fread((char *)&it, sizeof(off_t), 1, fd) == 1) {
		if (it != 0)
			run++;
		else if (run > 0) {
			printf("%ld\n", run);
			run = 0;
		}
	}
	(void) fclose(fd);
}

/*
 - dosweep - sweep pag file checking for valid offsets
 */
void
dosweep(fn, pn)
char *fn;
char *pn;
{
	register FILE *pf;
	off_t it;
	char nl;
	register FILE *hf;

	hf = fopen(fn, "r");
	if (hf == NULL)
		fail("cannot reopen `%s'", fn);
	pf = fopen(pn, "r");
	if (pf == NULL)
		fail("cannot reopen `%s'", pn);
	while (fread((char *)&it, sizeof(off_t), 1, pf) == 1) {
		it = (it & ((off_t)0x80000000)) ? (it&~((off_t)0xff000000)) : it;
		if (it != 0 && it != 1) {	/* 0 empty, 1 known okay */
			it--;		/* get rid of bias */
			(void) fseek(hf, it-1, SEEK_SET);
			nl = getc(hf);
			if (nl != '\n')
				fprintf(stderr, "offset 0%lo does not point to line\n",
								(long)it);
		}
	}
	(void) fclose(hf);
	(void) fclose(pf);
}

/*
 - fail - complain and die
 */
void
fail(s1, s2)
char *s1;
char *s2;
{
	fprintf(stderr, "%s: (file `%s', line %ld) ", progname, inname, lineno);
	fprintf(stderr, s1, s2);
	fprintf(stderr, "\n");
	exit(1);
}

/*
 - str2dup - concatenate strings and malloc result
 */
char *
str2dup(s1, s2)
char *s1;
char *s2;
{
	register char *p;

	p = malloc((size_t)strlen(s1) + strlen(s2) + 1);
	if (p == NULL)
		fail("can't allocate space for strings", "");
	(void) strcpy(p, s1);
	(void) strcat(p, s2);
	return(p);
}
