/*
 *   cddbd - CD Database Protocol Server
 *
 *   Copyright (C) 1996-1997  Steve Scherf
 *   Email: steve@moonsoft.com
 *   Moondog Software Productions - makers of fine public domain software.
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

#ifndef LINT
static char *_db_c_ident_ = "@(#)$Id: db.c,v 1.8 1997/09/14 08:36:16 steve Exp steve $";
#endif

#include <sys/types.h>
#include <sys/stat.h>
#include <ctype.h>
#include <stdio.h>
#include <errno.h>
#include <dirent.h>
#include <time.h>
#include "list.h"
#include "cddbd.h"
#include "patchlevel.h"


/* Prototypes. */

unsigned int db_gen_discid(db_t *);

int db_classify(char *, int *, char **);
int db_cmp(db_t *, db_t *);
int db_merge(db_t *, db_t *);
int db_parse_discid(char **, unsigned int *);
int db_strlen(char *);
int db_strip_list(lhead_t *);
int db_strip_multi(lhead_t *);
int db_sum_discid(int);
int db_write_multi(FILE *, lhead_t *, db_parse_t *);
int db_write_num(FILE *, lhead_t *, db_parse_t *);
int db_write_str(FILE *, lhead_t *, db_parse_t *, int);
int is_blank(char *, int);
int is_iso(int);
int is_numeric(char *);
int is_us_ascii(unsigned char);
int is_xnumeric(char *);

void db_cleanup(FILE *, db_t *, int, int);
void db_free_multi(void *);
void db_free_string(void *);

char *db_field(db_t *, int, int);

db_content_t *db_content_check(db_t *, int, int);


/* Variables. */

int db_errno;
int hdrlen;
int prclen;
int sublen;
int trklen;

char *hdrstr = " xmcd";
char *lenstr = " Disc length: %hd";
char *offstr = "%d";
char *trkstr = " Track frame offsets:";
char *revstr = " Revision: %d";
char *substr = " Submitted via:";
char *prcstr = " Processed by:";

char *db_errmsg[] = {
	"no error",
	"out of memory",
	"file access error",
	"system call error",
	"internal server error",
	"invalid DB entry"
};


db_parse_t parstab[] = {
	{ "#",	       "comment",   "#",
	    PF_IGNORE },
	{ "DISCID",    "DISCID",    "DISCID=",
	    (PF_NUMERIC | PF_REQUIRED) },
	{ "DTITLE",    "DTITLE",    "DTITLE=",
	    (PF_REQUIRED | PF_CKTIT) },
	{ "TTITLE",    "TTITLE",    "TTITLE%d=",
	    (PF_MULTI | PF_ONEREQ | PF_CKTRK) },
	{ "EXTD",      "EXTD",      "EXTD=",
	    0 },
	{ "EXTT",      "EXTT",      "EXTT%d=",
	    (PF_MULTI | PF_OSTRIP) },
	{ "PLAYORDER", "PLAYORDER", "PLAYORDER=",
	    PF_STRIP }
};


lhead_t *
list_init(void *data, int (*comp_func)(void *, void *),
    void (*free_func)(void *), void (*hfree_func)(void *))
{
	lhead_t *lh;

	lh = (lhead_t *)malloc(sizeof(lhead_t));
	if(lh == 0)
		return 0;

	lh->lh_count = 0;
	lh->lh_data = data;
	lh->lh_comp = comp_func;
	lh->lh_free = free_func;
	lh->lh_hfree = hfree_func;

	lh->lh_cur = (link_t *)lh;
	lh->lh_list.l_forw = (link_t *)lh;
	lh->lh_list.l_back = (link_t *)lh;

	return lh;
}


void
list_free(lhead_t *lh)
{
	list_rewind(lh);
	list_forw(lh);

	while(!list_empty(lh))
		list_delete(lh, list_cur(lh));

	if(lh->lh_hfree != 0)
		(*lh->lh_hfree)(lh->lh_data);

	free(lh);
}


link_t *
list_find(lhead_t *lh, void *data)
{
	int cmp;
	link_t *lp;

	for(list_rewind(lh), list_forw(lh); !list_rewound(lh); list_forw(lh)) {
		lp = list_cur(lh);

		if(lh->lh_comp != 0) {
			cmp = (*lh->lh_comp)(lp->l_data, data);
			if(cmp == 0)
				return lp;
			else if(cmp > 0)
				return 0;
		}
		else if((unsigned int)lp->l_data == (unsigned int)data)
			return lp;
		else if((unsigned int)lp->l_data > (unsigned int)data)
			return 0;
	}

	return 0;
}


void
list_delete(lhead_t *lh, link_t *lp)
{
	if(lh->lh_free != 0)
		(*lh->lh_free)(lp->l_data);

	lp->l_forw->l_back = lp->l_back;
	lp->l_back->l_forw = lp->l_forw;

	lh->lh_count--;
	if(lh->lh_cur == lp)
		lh->lh_cur = lp->l_forw;

	free(lp);
}


link_t *
list_add_back(lhead_t *lh, void *data)
{
	link_t *lp;

	lp = (link_t *)malloc(sizeof(link_t));
	if(lp == 0)
		return 0;

	lp->l_data = data;

	lp->l_forw = &lh->lh_list;
	lp->l_back = lh->lh_list.l_back;
	lp->l_back->l_forw = lp;
	lp->l_forw->l_back = lp;

	lh->lh_count++;

	return lp;
}


link_t *
list_add_forw(lhead_t *lh, void *data)
{
	link_t *lp;

	lp = (link_t *)malloc(sizeof(link_t));
	if(lp == 0)
		return 0;

	lp->l_data = data;

	lp->l_forw = lh->lh_list.l_forw;
	lp->l_back = &lh->lh_list;
	lp->l_back->l_forw = lp;
	lp->l_forw->l_back = lp;

	lh->lh_count++;

	return lp;
}


link_t *
list_add_cur(lhead_t *lh, void *data)
{
	link_t *lp;

	lp = (link_t *)malloc(sizeof(link_t));
	if(lp == 0)
		return 0;

	lp->l_data = data;

	lp->l_forw = lh->lh_cur;
	lp->l_back = lh->lh_cur->l_back;
	lp->l_back->l_forw = lp;
	lp->l_forw->l_back = lp;

	lh->lh_count++;

	return lp;
}


int *
cddbd_count(void)
{
	int i;
	int cnt;
	DIR *dirp;
	struct dirent *dp;
	char cdir[CDDBBUFSIZ];
	static int counts[CDDBMAXDBDIR + 1];

	counts[0] = 0;

	for(i = 0; categlist[i] != 0; i++) {
		cddbd_snprintf(cdir, sizeof(cdir), "%s/%s", cddbdir,
		    categlist[i]);

		if((dirp = opendir(cdir)) == NULL)
			continue;

		cnt = 0;

		while((dp = readdir(dirp)) != NULL)
			if(strlen(dp->d_name) == CDDBDISCIDLEN &&
			    !is_parent_dir(dp->d_name))
				cnt++;

		counts[0] += cnt;
		counts[i + 1] = cnt;

		closedir(dirp);
	}

	return counts;
}

void
cddbd_update(void)
{
	int i;
	int x;
	int copy;
	int merge;
	int dup;
	int bad;
	int duped;
	int noread;
	int updated;
	int failed;
	int dowrite;
	int entries;
	char cdir[CDDBBUFSIZ];
	char pdir[CDDBBUFSIZ];
	char ddir[CDDBBUFSIZ];
	char file[CDDBBUFSIZ];
	char file2[CDDBBUFSIZ];
	char file3[CDDBBUFSIZ];
	char errstr[CDDBBUFSIZ];
	struct stat sbuf;
	struct dirent *dp;
	DIR *dirp;
	FILE *fp;
	db_t *db;
	db_t *db2;

	/* No update necessary. */
	if(!strcmp(cddbdir, postdir))
		return;

	if(!cddbd_lock(locks[LK_UPDATE], 0)) {
		cddbd_log(LOG_INFO, "Update already in progress.");
		quit(QUIT_RETRY);
	}

	cddbd_log(LOG_INFO, "Updating the database:");

	db = 0;
	db2 = 0;
	bad = 0;
	duped = 0;
	failed = 0;
	noread = 0;
	updated = 0;
	entries = 0;

	for(i = 0; categlist[i] != 0; i++) {
		cddbd_snprintf(cdir, sizeof(cdir), "%s/%s", cddbdir,
		    categlist[i]);
		cddbd_snprintf(pdir, sizeof(pdir), "%s/%s", postdir,
		    categlist[i]);

		if(dup_ok)
			cddbd_snprintf(ddir, sizeof(ddir), "%s/%s", dupdir,
			    categlist[i]);

		if((dirp = opendir(pdir)) == NULL)
			continue;

		cddbd_log(LOG_INFO, "Updating %s.", cdir);

		db = 0;
		db2 = 0;

		while((dp = readdir(dirp)) != NULL) {
			copy = 0;
			dup = 0;
			merge = 0;

			if(db != 0) {
				db_free(db);
				db = 0;
			}

			if(db2 != 0) {
				db_free(db2);
				db2 = 0;
			}

			cddbd_snprintf(file, sizeof(file), "%s/%s", pdir,
			    dp->d_name);

			/* Make sure this is a database file. */
			if(strlen(dp->d_name) != CDDBDISCIDLEN) {
				if(!is_parent_dir(dp->d_name)) {
					if(stat(file, &sbuf)) {
						cddbd_log(LOG_ERR, "Warning: "
						    "can't stat %s", file);
						continue;
					}

					if(S_ISDIR(sbuf.st_mode))
						rmdir(file);
					else
						unlink(file);

					bad++;

					cddbd_log(LOG_INFO | LOG_UPDATE,
					    "Non-CDDB file: %s", file);
				}

				continue;
			}

			entries++;

			if((fp = fopen(file, "r")) == NULL) {
				cddbd_log(LOG_INFO | LOG_UPDATE,
				    "Can't read CDDB file: %s", file);

				continue;
			}

			db = db_read(fp, errstr, 0);

			fclose(fp);

			if(db == 0) {
				switch(db_errno) {
				case DE_INVALID:
					cddbd_log(LOG_ERR | LOG_UPDATE,
					    "Invalid DB file: %s: %s",
					    file, errstr);

					bad++;
					unlink(file);
					break;

				case DE_FILE:
				default:
					cddbd_log(LOG_ERR | LOG_UPDATE,
					    "Can't read %s: %s (%d)",
					    file, errstr, errno);

					noread++;
					break;
				}

				continue;
			}

			cddbd_snprintf(file2, sizeof(file2), "%s/%s", cdir,
			    dp->d_name);

			if((fp = fopen(file2, "r")) != NULL) {
				db2 = db_read(fp, errstr, 0);
				fclose(fp);

				if(db2 == 0) {
					if(db_errno != DE_INVALID) {
						cddbd_log(LOG_ERR | LOG_UPDATE,
						    "Can't read %s: %s (%d)",
						    file2, errstr, errno);

						continue;
					}
				}
			}

			switch(dup_policy) {
			case DUP_NEVER:
				/* Only copy if there's no dup. */
				if(db2 == 0)
					copy++;
				else
					dup++;

				break;

			case DUP_ALWAYS:
				/* Always copy. */
				copy++;
				break;

			case DUP_COMPARE:
				/* Copy if the new entry is better. */
				if(db2 == 0)
					copy++;
				else {
					x = db_cmp(db, db2);

					if(x == 0)
						merge++;
					else if(x > 0)
						copy++;
					else
						dup++;
				}

				break;

			default:
				cddbd_log(LOG_ERR | LOG_UPDATE,
				    "Unknown dup policy: %d",
				    dup_policy);

				quit(QUIT_ERR);
			}

			failed++;
			dowrite = 0;

			if(copy || merge) {
				dowrite++;
			}
			else if(dup && dup_ok) {
				/* Check for dupdir, and create it. */
				if(stat(dupdir, &sbuf)) {
					if(mkdir(dupdir, (mode_t)db_dir_mode)) {
						cddbd_log(LOG_ERR | LOG_UPDATE,
						    "Failed to create dup "
						    "dir %s (%d).",
						    dupdir, errno);

						quit(QUIT_ERR);
					}

					(void)cddbd_fix_file(dupdir,
					    db_dir_mode, db_uid, db_gid);
				}
				else if(!S_ISDIR(sbuf.st_mode)) {
					cddbd_log(LOG_ERR | LOG_UPDATE,
					    "%s is not a directory.", dupdir);

					quit(QUIT_ERR);
				}

				/* Check for category dir, and create it. */
				if(stat(ddir, &sbuf)) {
					if(mkdir(ddir, (mode_t)db_dir_mode)) {
						cddbd_log(LOG_ERR | LOG_UPDATE,
						    "Failed to create dup "
						    "dir %s (%d).",
						    file3, errno);

						quit(QUIT_ERR);
					}

					(void)cddbd_fix_file(ddir, db_dir_mode,
					    db_uid, db_gid);
				}
				else if(!S_ISDIR(sbuf.st_mode)) {
					cddbd_log(LOG_ERR | LOG_UPDATE,
					    "%s is not a directory.", ddir);

					quit(QUIT_ERR);
				}

				/* The name of the file we're writing to. */
				cddbd_snprintf(file2, sizeof(file2), "%s/%s",
				    ddir, dp->d_name);

				dowrite++;
			}

			if(!dowrite)
				continue;

			if(db2 != 0) {
				/* If we're merging, write the old one out. */
				if(merge) {
					(void)db_merge(db2, db);
					db_free(db);
					db = db2;
					db2 = 0;
				}
				else
					(void)db_merge(db, db2);
			}

			/* Write the new file. */
			if((fp = fopen(file2, "w")) == NULL) {
				cddbd_log(LOG_ERR | LOG_UPDATE,
				    "Can't write CDDB file: %s", file2);

				continue;
			}

			if(db_write(fp, db) == 0) {
				cddbd_log(LOG_ERR | LOG_UPDATE,
				    "Can't write CDDB file: %s", file2);

				fclose(fp);
				unlink(file2);
			}

			/* Note if we can't set stats, but continue. */
			(void)cddbd_fix_file(file2, db_file_mode, db_uid,
			   db_gid);

			if(copy || merge) {
				db_link(db, cdir, dp->d_name, 1);

				if(!cddbd_write_ulist(categlist[i],
				    dp->d_name)) {
					cddbd_log(LOG_ERR | LOG_UPDATE,
					    "Warning: couldn't update the"
					    " history for %s/%08x",
					    categlist[i], dp->d_name);
				}

				if(verbose)
					cddbd_log(LOG_INFO,
					    "Updated %s.", file2);
				updated++;
			}
			else {
				if(verbose)
					cddbd_log(LOG_INFO,
					    "Duped %s.", file2);
				duped++;
			}

			failed--;

			fclose(fp);

			/* Remove the old file. */
			unlink(file);
		}

		closedir(dirp);
	}

	if(db != 0) {
		db_free(db);
		db = 0;
	}

	if(db2 != 0) {
		db_free(db2);
		db2 = 0;
	}

	if(!cddbd_merge_ulist("all", 1)) {
		cddbd_log(LOG_ERR | LOG_UPDATE,
		    "Warning: couldn't merge the xmit history update.");
	}

	cddbd_unlock(locks[LK_UPDATE]);

	cddbd_log(LOG_INFO, "Processed %d database entries.", entries);
	cddbd_log(LOG_INFO,
	    "Updated %d, %d duplicate, %d bad, %d unreadable.",
	    updated, duped, bad, noread);
	cddbd_log(LOG_INFO, "Done updating the database.");
}


void
cddbd_match(void)
{
	int i;
	int bad;
	int links;
	int count;
	int noread;
	int entries;
	int found;
	int first;
	int cross;
	int match;
	int matched;
	db_t *db;
	FILE *fp;
	DIR *dirp;
	struct stat sbuf;
	struct stat sbuf2;
	struct dirent *dp;
	unsigned int discid;
	char file[CDDBBUFSIZ];
	char file2[CDDBBUFSIZ];
	char cdir[CDDBBUFSIZ];
	char buf[CDDBBUFSIZ];
	char errstr[CDDBBUFSIZ];
	link_t *lp;
	lhead_t *fuz;
	lhead_t **fuz_ino;
	lhead_t **fuz_categ;
	fmatch_t *fm;

	cddbd_log(LOG_INFO, "Checking the database for matching entries.");

	bad = 0;
	links = 0;
	count = 0;
	cross = 0;
	match = 0;
	matched = 0;
	noread = 0;
	entries = 0;

	if((fuz_categ = (lhead_t **)malloc(categcnt * sizeof(lhead_t))) == 0) {
		cddbd_log(LOG_ERR, "Can't allocate list heads.");
		quit(1);
	}

	for(i = 0; categlist[i] != 0; i++) {
		fuz_categ[i] = list_init(0, 0, 0, 0);

		if(fuz_categ[i] == 0) {
			cddbd_log(LOG_ERR, "Can allocate list head.");
			quit(1);
		}
	}

	if((fuz_ino = (lhead_t **)malloc(categcnt * sizeof(lhead_t))) == 0) {
		cddbd_log(LOG_ERR, "Can't allocate list heads.");
		quit(1);
	}

	for(i = 0; categlist[i] != 0; i++) {
		fuz_ino[i] = list_init(0, 0, 0, 0);

		if(fuz_ino[i] == 0) {
			cddbd_log(LOG_ERR, "Can't malloc linked list.");
			quit(QUIT_ERR);
		}
	}

	for(i = 0; categlist[i] != 0; i++) {
		cddbd_snprintf(cdir, sizeof(cdir), "%s/%s", cddbdir,
		    categlist[i]);

		if((dirp = opendir(cdir)) == NULL) {
			cddbd_log(LOG_ERR, "Can't open %s for reading.", cdir);
			quit(QUIT_ERR);
		}

		while((dp = readdir(dirp)) != NULL) {
			cddbd_snprintf(file, sizeof(file), "%s/%s", cdir,
			    dp->d_name);

			if(stat(file, &sbuf)) {
				cddbd_log(LOG_ERR, 
				    "Warning: can't stat file: %s", file);
				noread++;
				continue;
			}

			if(!S_ISREG(sbuf.st_mode)) {
				if(!is_parent_dir(dp->d_name)) {
					cddbd_log(LOG_ERR, 
					    "Warning: ignoring non-file: %s",
					    file);
				}

				continue;
			}

			/* Make sure this is a database file. */
			if(strlen(dp->d_name) != CDDBDISCIDLEN) {
				cddbd_log(LOG_ERR, 
				    "Warning: ignoring non-DB entry: %s", file);
				continue;
			}

			entries++;

			sscanf(dp->d_name, "%08x", &discid);

			if(list_add_cur(fuz_categ[i], (void *)discid) == 0) {
				cddbd_log(LOG_ERR,
				    "Can't malloc linked list entry.");
				quit(QUIT_ERR);
			}

			if(sbuf.st_nlink > 1 && list_find(fuz_ino[i],
			    (void *)(int)sbuf.st_ino) != 0) {
				links++;
				continue;
			}

			if(list_add_cur(fuz_ino[i],
			    (void *)(int)sbuf.st_ino) == 0) {
				cddbd_log(LOG_ERR,
				    "Can't malloc linked list entry.");
				quit(QUIT_ERR);
			}

			if((fp = fopen(file, "r")) == NULL) {
				cddbd_log(LOG_ERR, 
				    "Warning: can't read CDDB file: %s", file);
				noread++;
				continue;
			}

			db = db_read(fp, errstr, 0);
			fclose(fp);

			if(db == 0) {
				switch(db_errno) {
				case DE_INVALID:
					cddbd_log(LOG_ERR, 
					    "Warning: invalid DB file: %s: %s",
					    file, errstr);

					bad++;
					break;

				case DE_FILE:
				default:
					cddbd_log(LOG_ERR,
					    "Warning: Can't read %s: %s (%d)",
					    file, errstr, errno);

					noread++;
					break;

				}

				continue;
			}

			/* Count the database entry. */
			count++;

			if((fuz = fuzzy_search(db->db_trks,
			    db->db_offset, db->db_disclen)) == 0) {
				db_free(db);
				continue;
			}

			found = 0;
			first = 1;

			for(list_rewind(fuz), list_forw(fuz);
			    !list_rewound(fuz); list_forw(fuz)) {
				lp = list_cur(fuz);
				fm = (fmatch_t *)lp->l_data;

				/* We've already checked this DB entry. */
				if(list_find(fuz_categ[fm->fm_catind],
				    (void *)fm->fm_discid))
					continue;

				db_strcpy(db, DP_DTITLE, 0, buf, sizeof(buf));

				if(fm->fm_catind != i) {
					if(first) {
						first = 0;

						if(match) {
							cddbd_log(LOG_INFO,
							    "----------------");
						}

						cddbd_log(LOG_INFO, "--> "
						    "%s/%08x: %s",
						    categlist[i], discid, buf);
					}

					cddbd_log(LOG_INFO,
					    "*   %s/%08x: %s",
					    categlist[fm->fm_catind],
					    fm->fm_discid, fm->fm_dtitle);

					match++;
					cross++;
					continue;
				}

				cddbd_snprintf(file2, sizeof(file2), "%s/%08x",
				    cdir, fm->fm_discid);

				if(stat(file2, &sbuf2)) {
					cddbd_log(LOG_ERR, "Warning: can't "
					    "stat file: %s", file2);
					continue;
				}

				/* This entry is a link to the current one. */
				if(list_find(db->db_phase[DP_DISCID],
				    (void *)discid) && (sbuf.st_ino ==
				    sbuf2.st_ino))
					continue;

				match++;
				found++;

				if(first) {
					first = 0;

					if(match) {
						cddbd_log(LOG_INFO,
						    "----------------");
					}

					cddbd_log(LOG_INFO,
					    "--> %s/%08x: %s",
					    categlist[i], discid, buf);
				}

				cddbd_log(LOG_INFO, "    %s/%08x: %s",
				    categlist[fm->fm_catind],
				    fm->fm_discid, fm->fm_dtitle);
			}

			if(found)
				matched++;

			list_free(fuz);
			db_free(db);
		}

		closedir(dirp);
	}

	for(i = 0; categlist[i] != 0; i++) {
		list_free(fuz_ino[i]);
		list_free(fuz_categ[i]);
	}

	if(count == 0) {
		cddbd_log(LOG_ERR, "No valid database entries.");
		quit(QUIT_ERR);
	}

	cddbd_log(LOG_INFO,
	    "Found %d matches and %d cross-categorizations for %d database "
	    "entries of %d.",
	    match, cross, matched, entries);

	cddbd_log(LOG_INFO,
	    "Ignored %d files: %d links, %d invalid, %d unreadable.",
	    (entries - count), links, bad, noread);

	cddbd_log(LOG_INFO, "Done checking the database.");
}


/* ARGSUSED */
void
cddbd_check_db(int check_level, int fix_level)
{
	int i;
	int bad;
	int post;
	int flags;
	int links;
	int count;
	int noread;
	int entries;
	int islink;
	db_t *db;
	FILE *fp;
	DIR *dirp;
	lhead_t *lh;
	struct stat sbuf;
	struct dirent *dp;
	unsigned int discid;
	char file[CDDBBUFSIZ];
	char file2[CDDBBUFSIZ];
	char errstr[CDDBBUFSIZ];

	cddbd_log(LOG_INFO, "Checking the database.");

	bad = 0;
	links = 0;
	islink = 0;
	count = 0;
	noread = 0;
	entries = 0;

	for(i = 0; categlist[i] != 0; i++) {
		cddbd_snprintf(file2, sizeof(file2), "%s/%s", cddbdir,
		    categlist[i]);

		cddbd_log(LOG_INFO, "Scanning %s.", file2);

		if((dirp = opendir(file2)) == NULL) {
			cddbd_log(LOG_ERR, "Can't open %s for reading.", file2);
			quit(QUIT_ERR);
		}

		lh = list_init(0, 0, 0, 0);
		if(lh == 0) {
			cddbd_log(LOG_ERR, "Can't malloc linked list.");
			quit(QUIT_ERR);
		}

		while((dp = readdir(dirp)) != NULL) {
			cddbd_snprintf(file, sizeof(file), "%s/%s", file2,
			    dp->d_name);

			if(stat(file, &sbuf)) {
				cddbd_log(LOG_ERR, 
				    "Warning: can't stat file: %s", file);
				noread++;
				continue;
			}

			if(!S_ISREG(sbuf.st_mode)) {
				if(!is_parent_dir(dp->d_name)) {
					cddbd_log(LOG_ERR, 
					    "Warning: ignoring non-file: %s",
					    file);
				}

				continue;
			}

			/* Make sure this is a database file. */
			if(strlen(dp->d_name) != CDDBDISCIDLEN) {
				if(!is_parent_dir(dp->d_name)) {
					if(fix_level >= FL_REMOVE) {
						unlink(file);
						cddbd_log(LOG_INFO,
						    "Removing non-CDDB file: "
						    "%s", file);
					}
					else {
						cddbd_log(LOG_ERR,
						    "Warning: ignoring non-CDDB"
						    "file: %s", file);
					}
				}

				continue;
			}

			islink = 0;
			entries++;

			if(sbuf.st_nlink > 1) {
				if(list_find(lh, (void *)(int)sbuf.st_ino)
				    != 0) {
					links++;
					islink++;
					continue;
				}
				else if(list_add_cur(lh,
				    (void *)(int)sbuf.st_ino) == 0) {
					cddbd_log(LOG_ERR,
					    "Can't malloc linked list entry.");
					quit(QUIT_ERR);
				}
			}

			/* Check the file permissions. */
			if((sbuf.st_mode & 0777) != db_file_mode) {
				if(fix_level >= FL_REPAIR) {
					if(chmod(file, (mode_t)db_file_mode)) {
						cddbd_log(LOG_ERR | LOG_UPDATE,
						    "Warning: Couldn't change "
						    "perms on %s.", file);
					}
					else
						cddbd_log(LOG_INFO,
						    "Set file mode on DB file:"
						    " %s", file);
				}
				else
					cddbd_log(LOG_ERR, "Warning: incorrect"
					    " mode %04o on DB file: %s", 
					    (sbuf.st_mode & 0777), file);
			}


			/* Check the file ownership. */
			if(sbuf.st_uid != db_uid || sbuf.st_gid != db_gid) {
				if(fix_level >= FL_REPAIR) {
					if(chown(file, db_uid, db_gid)) {
						cddbd_log(LOG_ERR | LOG_UPDATE,
						    "Warning: Couldn't change "
						    "owner on DB file: %s",
						    file);
					}
					else
						cddbd_log(LOG_INFO,
						    "Set owner/grp on DB file:"
						    " %s", file);
				}
				else
					cddbd_log(LOG_ERR, "Warning: incorrect "
					    "owner/group on DB file: %s", file);
			}

			if((fp = fopen(file, "r")) == NULL) {
				cddbd_log(LOG_ERR, 
				    "Warning: can't read CDDB file: %s", file);
				noread++;
				continue;
			}

			flags = DF_SUBMITTER | DF_CK_SUBMIT;

			db = db_read(fp, errstr, flags);
			fclose(fp);

			if(db == 0) {
				switch(db_errno) {
				case DE_INVALID:
					if(fix_level >= FL_REMOVE) {
						unlink(file);
						cddbd_log(LOG_INFO, 
						    "Removing invalid DB file:"
						    " %s: %s", file, errstr);
					}
					else {
						cddbd_log(LOG_ERR, 
						    "Warning: invalid DB file:"
						    " %s: %s", file, errstr);
					}

					bad++;
					break;

				case DE_FILE:
				default:
					cddbd_log(LOG_ERR,
					    "Warning: Can't read %s: %s (%d)",
					    file, errstr, errno);

					noread++;
					break;

				}

				continue;
			}

			sscanf(dp->d_name, "%08x", &discid);

			if(fix_level >= FL_REPAIR) {
				post = 0;

				if(!list_find(db->db_phase[DP_DISCID],
				    (void *)discid)) {
					/* Add the discid to the entry. */
					if(!list_add_cur(
					    db->db_phase[DP_DISCID],
					    (void *)discid)) {
						cddbd_log(LOG_ERR, 
						    "Can't malloc list entry");

						quit(QUIT_ERR);
					}

					cddbd_log(LOG_INFO,
					    "Added %s %08x to DB entry: %s",
					    parstab[DP_DISCID].dp_name, discid,
					    file);

					post++;
				}
				else {
					/* Make any missing links. */
					db_link(db, file2, dp->d_name, 1);
				}

				/* Write it out if we added a rev string. */
				if(!(db->db_flags & DB_REVISION)) {
					cddbd_log(LOG_INFO,
					    "Added revision to DB entry: %s",
					    file);

					post++;
				}

				/* Write it out if we added a proc string. */
				if(!(db->db_flags & DB_PROCESSOR))
					post++;

				if(strip_ext && (db->db_flags & DB_STRIP)) {
					cddbd_log(LOG_ERR, 
					    "Stripping optional fields"
					    " in DB entry: %s", file);

					if(!db_strip(db)) {
						cddbd_log(LOG_ERR, 
						    "Failed to allocate memory"
						    " to strip entry.");
						quit(QUIT_ERR);
					}

					post++;
				}

				if(post && !db_post(db, file2, discid,
				    errstr)) {
					cddbd_log(LOG_ERR,
					    "Warning: can't fix DB entry: %s",
					    file);
				}
			}
			else {
				/* Make sure discid is in the DB entry. */
				if(!list_find(db->db_phase[DP_DISCID],
				    (void *)discid)) {
					cddbd_log(LOG_ERR,
					    "Warning: DB entry %s missing %s",
					    parstab[DP_DISCID].dp_name,
					    dp->d_name);
				}

				if(verbose && !(db->db_flags & DB_REVISION)) {
					cddbd_log(LOG_ERR,
					    "Warning: DB entry missing rev: %s",
					    file);
				}

				if(verbose && strip_ext &&
				    (db->db_flags & DB_STRIP)) {
					cddbd_log(LOG_ERR, 
					    "Warning: strippable optional "
					    "fields in DB entry: %s", file);
				}

				/* Verify the links. */
				db_link(db, file2, dp->d_name, 0);
			}

			/* Count the database entry. */
			if(!islink)
				count++;

			db_free(db);
		}

		list_free(lh);
		closedir(dirp);
	}

	if(count == 0) {
		cddbd_log(LOG_ERR, "No valid database entries.");
		quit(QUIT_ERR);
	}

	cddbd_log(LOG_INFO, "Checked %d database entries.", entries);
	cddbd_log(LOG_INFO,
	    "Found %d normal files, %d links, %d invalid, %d unreadable.",
	    count, links, bad, noread);
	cddbd_log(LOG_INFO, "Done checking the database.");
}


db_t *
db_read(FILE *fp, char *errstr, int flags)
{
	int i;
	int j;
	int n;
	int fc;
	int off;
	int loff;
	int line;
	int pcnt;
	int trks;
	int phase;
	int class;
	unsigned int discid;
	db_t *db;
	lhead_t *lh;
	lhead_t *lh2;
	link_t *lp;
	void (*func)();
	db_parse_t *dp;
	db_content_t *dc;
	char *p;
	char buf[CDDBBUFSIZ];
	char buf2[CDDBBUFSIZ];

	db_errno = 0;

	db = (db_t *)malloc(sizeof(db_t));
	if(db == 0) {
		cddbd_snprintf(errstr, CDDBBUFSIZ,
		    "can't get memory for DB entry");

		db_cleanup(fp, db, DE_NOMEM, flags);
		return 0;
	}

	db->db_flags = 0;
	db->db_rev = 0;
	db->db_trks = 0;
	db->db_disclen = 0;

	for(i = 0; i < DP_NPHASE; i++)
		db->db_phase[i] = 0;

	pcnt = 0;
	line = 0;
	phase = 0;

	for(;;) {
		if(flags & DF_STDIN) {
			if(cddbd_gets(buf, sizeof(buf)) == NULL)
				break;
		}
		else {
			if(fgets(buf, sizeof(buf), fp) == NULL)
				break;
		}

		line++;
		dp = &parstab[phase];

		if(line > max_lines) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "too many lines in input, max %d", max_lines);

			db_cleanup(fp, db, DE_INVALID, flags);
			return 0;
		}

		if(db_strlen(buf) > DBLINESIZ) {
			printf("len = %d, buf = %s\n", db_strlen(buf), buf);
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "input too long on line %d", line);

			db_cleanup(fp, db, DE_INVALID, flags);
			return 0;
		}

		if(!is_valid(buf)) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "garbage character on line %d", line);

			db_cleanup(fp, db, DE_INVALID, flags);
			return 0;
		}

		if(is_dot(buf)) {
			if(!(flags & DF_STDIN)) {
				cddbd_snprintf(errstr, CDDBBUFSIZ,
				    "illegal input on line %d", line);

				db_cleanup(fp, db, DE_INVALID, flags);
				return 0;
			}

			if(phase < (DP_NPHASE - 1)) {
				cddbd_snprintf(errstr, CDDBBUFSIZ,
				    "unexpected end on line %d", line);

				db_cleanup(fp, db, DE_INVALID,
				    (flags & ~DF_STDIN));

				return 0;
			}

			break;
		}

		if(is_blank(buf, 0)) {
			if(phase == (DP_NPHASE - 1) && (!(flags & DF_STDIN)))
				break;

			if(phase == 0 && db->db_phase[phase] == 0)
				continue;

			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "blank line in input on line %d", line);

			db_cleanup(fp, db, DE_INVALID, flags);
			return 0;
		}

		class = db_classify(buf, &n, &p);
		if(class < 0) {
			if(phase == (DP_NPHASE - 1) && (!(flags & DF_STDIN)))
				break;

			/* Skip junk at beginning of email. */
			if(phase == 0 && db->db_phase[phase] == 0 &&
			    (flags & DF_MAIL))
				continue;

			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "unrecognized input on line %d", line);

			db_cleanup(fp, db, DE_INVALID, flags);
			return 0;
		}

		/* If email and we found the entry, start counting again. */
		if(phase == 0 && db->db_phase[phase] == 0 && (flags & DF_MAIL))
			line = 1;

		if(n >= CDDBMAXTRK) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "invalid numerical argument in %s on line %d",
			    dp->dp_name, line);

			db_cleanup(fp, db, DE_INVALID, flags);
			return 0;
		}

		if(class < phase) {
			if(parstab[class].dp_flags & PF_IGNORE)
				continue;

			/* Can't go backwards. */
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "missing expected %s on line %d",
			    dp->dp_name, line);

			db_cleanup(fp, db, DE_INVALID, flags);
			return 0;
		}
		else if(class > phase) {
			/* Are we skipping a phase? */
			if(db->db_phase[phase] == 0 || class > (phase + 1)) {
				if(parstab[class].dp_flags & PF_IGNORE)
					continue;

				cddbd_snprintf(errstr, CDDBBUFSIZ,
				    "missing expected %s on line %d",
				    parstab[phase + 1].dp_name, line);

				db_cleanup(fp, db, DE_INVALID, flags);
				return 0;
			}

			phase++;
			dp = &parstab[phase];
		}

		if(!(dp->dp_flags & PF_MULTI) && n >= 0) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "unexpected track # `%d'in %s on line %d",
			    n, dp->dp_name, line);

			db_cleanup(fp, db, DE_INVALID, flags);
			return 0;
		}

		/* Get a list head for this phase. */
		if(db->db_phase[phase] == 0) {
			if((dp->dp_flags & PF_MULTI))
				func = db_free_multi;
			else if((dp->dp_flags & PF_NUMERIC))
				func = 0;
			else
				func = db_free_string;

			db->db_phase[phase] = list_init(0, 0, func, 0);

			if(db->db_phase[phase] == 0) {
				cddbd_snprintf(errstr, CDDBBUFSIZ,
				    "can't malloc list head");
				db_cleanup(fp, db, DE_NOMEM, flags);
				return 0;
			}

			pcnt = 0;
		}

		lh = db->db_phase[phase];

		/* Deal with phases that have multiple subphases. */
		if(dp->dp_flags & PF_MULTI) {
			/* Can't go backwards. */
			if(n < pcnt) {
				cddbd_snprintf(errstr, CDDBBUFSIZ,
				    "unexpected track # `%d' in %s on line %d",
				    n, dp->dp_name, line);

				db_cleanup(fp, db, DE_INVALID, flags);
				return 0;
			}
			else if(n > pcnt) {
				pcnt++;

				/* Can't skip a subphase. */
				if(list_count(lh) < pcnt || n > pcnt) {
					cddbd_snprintf(errstr, CDDBBUFSIZ,
					    "unexpected track # `%d' in %s on "
					    "line %d", n, dp->dp_name, line);

					db_cleanup(fp, db, DE_INVALID, flags);
					return 0;
				}
			}

			/* Initialize list head for subphase. */
			if(list_count(lh) == pcnt) {
				lh2 = list_init(0, 0, db_free_string, 0);
				if(lh2 == 0) {
					cddbd_snprintf(errstr, CDDBBUFSIZ,
					    "can't malloc list head");

					db_cleanup(fp, db, DE_NOMEM, flags);
					return 0;
				}

				/* Add subphase to the list. */
				if(list_add_back(lh, (void *)lh2) == 0) {
					cddbd_snprintf(errstr, CDDBBUFSIZ,
					    "can't malloc list entry");

					db_cleanup(fp, db, DE_NOMEM, flags);
					return 0;
				}

				lh = lh2;
			}
			else {
				list_rewind(lh);
				lh = (lhead_t *)list_last(lh)->l_data;
			}
		}

		/* Note if strippable fields are not emtpy. */
		if(dp->dp_flags & PF_OSTRIP && !is_blank(p, 0))
			db->db_flags |= DB_STRIP;

		if(dp->dp_flags & PF_NUMERIC) {
			do {
				if(db_parse_discid(&p, &discid) != 0) {
					cddbd_snprintf(errstr, CDDBBUFSIZ,
					    "bad disc ID on line %d", line);

					db_cleanup(fp, db, DE_INVALID, flags);
					return 0;
				}

				if(list_find(lh, (void *)discid) != 0) {
					cddbd_snprintf(errstr, CDDBBUFSIZ,
					    "duplicate disc ID on line %d",
					    line);

					db_cleanup(fp, db, DE_INVALID, flags);
					return 0;
				}

				if(list_add_cur(lh, (void *)discid) == 0) {
					cddbd_snprintf(errstr, CDDBBUFSIZ,
					    "can't malloc list entry");

					db_cleanup(fp, db, DE_NOMEM, flags);
					return 0;
				}
			} while(*p != '\0');
		}
		else {
			/* Strip data from certain entries. */
			if(dp->dp_flags & PF_STRIP) {
				/* Empty out the list. */
				if(list_count(lh) > 0) {
					list_rewind(lh);
					list_forw(lh);

					while(!list_empty(lh))
						list_delete(lh, list_cur(lh));
				}

				p = "\n";
			}

			p = (char *)strdup(p);
			if(p == 0) {
				cddbd_snprintf(errstr, CDDBBUFSIZ,
				    "can't malloc list data");

				db_cleanup(fp, db, DE_NOMEM, flags);
				return 0;
			}

			/* Put string in the list. */
			if(list_add_back(lh, (void *)p) == 0) {
				cddbd_snprintf(errstr, CDDBBUFSIZ,
				    "can't malloc list entry");
				db_cleanup(fp, db, DE_NOMEM, flags);
				return 0;
			}
		}
	}

	/* Make sure we went through all the phases. */
	if(phase != (DP_NPHASE - 1)) {
		cddbd_snprintf(errstr, CDDBBUFSIZ, "unexpected end on line %d",
		    line);
		db_cleanup(fp, db, DE_INVALID, (flags | DF_DONE));
		return 0;
	}

	/* Check the comments. */

	/* Check for the header. */
	lh = db->db_phase[DP_COMMENT];
	list_rewind(lh);
	list_forw(lh);
	lp = list_cur(lh);

	if(strncmp((char *)lp->l_data, hdrstr, hdrlen)) {
		cddbd_snprintf(errstr, CDDBBUFSIZ, "missing header");
		db_cleanup(fp, db, DE_INVALID, (flags | DF_DONE));
		return 0;
	}

	/* Look for the revision. */
	list_rewind(lh);
	list_back(lh);

	for(; !list_rewound(lh); list_back(lh)) {
		lp = list_cur(lh);
		if(sscanf((char *)lp->l_data, revstr, &db->db_rev) == 1)
			break;
	}

	/* We have a revision. */
	if(!list_rewound(lh)) {
		/* Make sure the revision is positive. */
		if(db->db_rev < 0 && db->db_rev != DB_MAX_REV) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "revision in comments less 0 (%d)", db->db_rev);

			db_cleanup(fp, db, DE_INVALID, (flags | DF_DONE));
			return 0;
		}

		db->db_flags |= DB_REVISION;

		/* Delete extra revisions. */
		for(list_back(lh); !list_rewound(lh);) {
			lp = list_cur(lh);
			list_back(lh);

			if(sscanf((char *)lp->l_data, revstr, &n) == 1) {
				list_delete(lh, lp);
				db->db_flags &= ~DB_REVISION;
			}
		}
	}
	else {
		cddbd_snprintf(buf, sizeof(buf), revstr, CDDBMINREV);
		strcat(buf, "\n");

		p = (char *)strdup(buf);
		if(p == 0) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "can't malloc list data");

			db_cleanup(fp, db, DE_NOMEM, flags);
			return 0;
		}

		/* Put string in the list. */
		if(list_add_back(lh, (void *)p) == 0) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "can't malloc list entry");

			db_cleanup(fp, db, DE_NOMEM, flags);
			return 0;
		}
	}

	/* Look for the submitter. */
	list_rewind(lh);
	list_back(lh);

	for(; !list_rewound(lh); list_back(lh)) {
		lp = list_cur(lh);
		if(!strncmp((char *)lp->l_data, substr, sublen))
			break;
	}

	/* Do we have a submitter? */
	if(list_rewound(lh)) {
		/* No submitter, this may be an error. */
		if(flags & DF_SUBMITTER) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "missing submitter in comments");

			db_cleanup(fp, db, DE_INVALID, (flags | DF_DONE));
			return 0;
		}

		if((flags & DF_CK_SUBMIT) &&
		    ck_client_perms("", "", IF_SUBMIT) != CP_ALLOW) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "missing submitter in comments");

			db_cleanup(fp, db, DE_INVALID, (flags | DF_DONE));
			return 0;
		}
	}
	else {
		/* Make sure the submitter is valid. */
		if(sscanf(&((char *)lp->l_data)[sublen], "%s%s", buf, buf2)
		    != 2) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "invalid submitter definition");

			db_cleanup(fp, db, DE_INVALID, (flags | DF_DONE));
			return 0;
		}

		if(flags & DF_CK_SUBMIT &&
		    ck_client_perms(buf, buf2, IF_SUBMIT) != CP_ALLOW) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "unauthorized client/revision: %s %s", buf, buf2);

			db_cleanup(fp, db, DE_INVALID, (flags | DF_DONE));
			return 0;
		}

		db->db_flags |= DB_SUBMITTER;

		/* Delete extra submitters. */
		for(list_back(lh); !list_rewound(lh);) {
			lp = list_cur(lh);
			list_back(lh);

			if(!strncmp((char *)lp->l_data, substr, sublen)) {
				list_delete(lh, lp);
				db->db_flags &= ~DB_SUBMITTER;
			}
		}
	}

	/* Look for the last software to process this entry. */
	list_rewind(lh);
	list_forw(lh);

	for(; !list_rewound(lh); list_forw(lh)) {
		lp = list_cur(lh);
		if(!strncmp((char *)lp->l_data, prcstr, prclen))
			break;
	}

	/* Do we have a processor? */
	if(!list_rewound(lh)) {
		db->db_flags |= DB_PROCESSOR;

		list_rewind(lh);

		/* Delete all processors. */
		for(list_forw(lh); !list_rewound(lh);) {
			lp = list_cur(lh);
			list_forw(lh);

			if(!strncmp((char *)lp->l_data, prcstr, prclen)) {
				list_delete(lh, lp);
				db->db_flags &= ~DB_PROCESSOR;
			}
		}
	}

	cddbd_snprintf(buf, sizeof(buf), "%s ", prcstr);
	cddbd_snprintf(buf2, sizeof(buf2), verstr2, VERSION, PATCHLEVEL);
	strcat(buf, buf2);
	strcat(buf, "\n");

	p = (char *)strdup(buf);
	if(p == 0) {
		cddbd_snprintf(errstr, CDDBBUFSIZ, "can't malloc list data");
		db_cleanup(fp, db, DE_NOMEM, flags);
		return 0;
	}

	/* Put it after the revision. */
	list_rewind(lh);
	list_forw(lh);

	for(; !list_rewound(lh); list_forw(lh)) {
		lp = list_cur(lh);
		if(sscanf((char *)lp->l_data, revstr, &db->db_rev) == 1)
			break;
	}

	/* No rev, put it before the terminating blanks. */
	if(list_rewound(lh)) {
		for(list_back(lh); !list_rewound(lh); list_back(lh))
			if(!is_blank((char *)list_cur(lh)->l_data, 0))
				break;
	}

	list_forw(lh);

	/* Put string in the list. */
	if(list_add_cur(lh, (void *)p) == 0) {
		cddbd_snprintf(errstr, CDDBBUFSIZ, "can't malloc list entry");
		db_cleanup(fp, db, DE_NOMEM, flags);
		return 0;
	}

	/* Make sure the last line is blank. */
	list_rewind(lh);

	if(!is_blank((char *)list_back(lh)->l_data, 0)) {
		p = (char *)strdup("\n");
		if(p == 0) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "can't malloc list data");

			db_cleanup(fp, db, DE_NOMEM, flags);
			return 0;
		}

		/* Put string in the list. */
		if(list_add_back(lh, (void *)p) == 0) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "can't malloc list entry");

			db_cleanup(fp, db, DE_NOMEM, flags);
			return 0;
		}
	}

	/* Are there track offsets in the comments? */
	list_rewind(lh);
	list_forw(lh);

	for(list_forw(lh); !list_rewound(lh); list_forw(lh)) {
		lp = list_cur(lh);
		if(!strncmp((char *)lp->l_data, trkstr, trklen))
			break;
	}

	/* Fail if not. */
	if(list_rewound(lh)) {
		cddbd_snprintf(errstr, CDDBBUFSIZ,
		    "missing TOC information in comments");

		db_cleanup(fp, db, DE_INVALID, (flags | DF_DONE));
		return 0;
	}

	loff = -1;
	trks = 0;

	for(list_forw(lh); !list_rewound(lh); list_forw(lh), trks++) {
		lp = list_cur(lh);

		if(sscanf((char *)lp->l_data, offstr, &off) != 1)
		    break;

		/* Too many tracks. */
		if(trks >= CDDBMAXTRK) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "too many track offsets in comments (%d), max %d",
			    trks, CDDBMAXTRK);

			db_cleanup(fp, db, DE_INVALID,
			    (flags | DF_DONE));
			return 0;
		}

		/* No negative or zero offsets. */
		if(off <= 0) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "negative or zero offset (%d) in comments", off);

			db_cleanup(fp, db, DE_INVALID,
			    (flags | DF_DONE));
			return 0;
		}

		/* Offsets must be monotonically increasing. */
		if(off <= loff) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "offset (%d) less than or equal to previous "
			    "offset (%d) in comments", off, loff);

			db_cleanup(fp, db, DE_INVALID,
			    (flags | DF_DONE));
			return 0;
		}

		db->db_offset[trks] = off;
		loff = off;
	}

	db->db_flags |= DB_OFFSET;

	/* Count the number of tracks. */
	db->db_trks = (char)trks;
	
	/* Look for the disclen. */
	list_rewind(lh);
	list_forw(lh);
	list_forw(lh);

	for(; !list_rewound(lh); list_forw(lh)) {
		lp = list_cur(lh);
		if(sscanf((char *)lp->l_data, lenstr, &db->db_disclen) == 1)
			break;
	}

	/* We have no disclen. */
	if(list_rewound(lh)) {
		cddbd_snprintf(errstr, CDDBBUFSIZ,
		    "missing disc length in comments");

		db_cleanup(fp, db, DE_INVALID, (flags | DF_DONE));
		return 0;
	}

	/* Delete extra revisions. */
	for(list_forw(lh); !list_rewound(lh);) {
		lp = list_cur(lh);
		if(sscanf((char *)lp->l_data, lenstr, &n) == 1)
			list_delete(lh, lp);
		else
			list_forw(lh);
	}

	/* Make sure the disclen is positive. */
	if(db->db_disclen <= 0) {
		cddbd_snprintf(errstr, CDDBBUFSIZ,
		    "disc length in comments less than or equal to 0 (%d)",
		    db->db_disclen);

		db_cleanup(fp, db, DE_INVALID, (flags | DF_DONE));
		return 0;
	}

	/* Make sure the disclen isn't too short. */
	if((loff / CDDBFRAMEPERSEC) > db->db_disclen) {
		cddbd_snprintf(errstr, CDDBBUFSIZ,
		    "disc length in comments is too short (%d)",
		    db->db_disclen);

		db_cleanup(fp, db, DE_INVALID, (flags | DF_DONE));
		return 0;
	}

	db->db_flags |= DB_DISCLEN;

	/* Ensure the discid is in the DB entry. */
	discid = db_gen_discid(db);

	if(list_find(db->db_phase[DP_DISCID], (void *)discid) == 0) {
		cddbd_snprintf(errstr, CDDBBUFSIZ,
		    "disc ID generated from track offsets (%08x) not found "
		    "in %s", discid, parstab[DP_DISCID].dp_name);

		db_cleanup(fp, db, DE_INVALID, (flags | DF_DONE));
		return 0;

	}

	/* Ensure the discids are legal. */
	lh = db->db_phase[DP_DISCID];
	list_rewind(lh);
	list_forw(lh);

	for(; !list_rewound(lh); list_forw(lh)) {
		lp = list_cur(lh);
		discid = (unsigned int)lp->l_data;

		if((discid & 0xFF) != db->db_trks) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "invalid disc ID %08x in %s",
			    discid, parstab[DP_DISCID].dp_name);

			db_cleanup(fp, db, DE_INVALID, (flags | DF_DONE));
			return 0;
		}
	}

	/* Check the fields for various things. */
	for(i = 0; i < DP_NPHASE; i++) {
		lh = db->db_phase[i];
		dp = &parstab[i];

		/* Make sure multi fields match the track count. */
		if((dp->dp_flags & PF_MULTI) && db->db_trks != lh->lh_count) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "# of %s fields (%d) does not match # of trks (%d)",
			    dp->dp_name, lh->lh_count, db->db_trks);

			db_cleanup(fp, db, DE_INVALID, (flags | DF_DONE));
			return 0;
		}

		if(dp->dp_flags & PF_MULTI)
			fc = lh->lh_count;
		else
			fc = 1;

		/* Make sure the field contents aren't bogus. */
		for(j = 0, n = 0; j < fc; j++) {
			/* Check for invalid stuff. */
			dc = db_content_check(db, i, j);

			if(dc->dc_flags & CF_INVALID) {
				cddbd_snprintf(errstr, CDDBBUFSIZ,
				    "invalid %s", db_field(db, i, j));

				db_cleanup(fp, db, DE_INVALID, flags);
				return 0;
			}

			if((dp->dp_flags & PF_REQUIRED) &&
			    (dc->dc_flags & CF_BLANK)) {
				cddbd_snprintf(errstr, CDDBBUFSIZ,
				    "empty %s", db_field(db, i, j));

				db_cleanup(fp, db, DE_INVALID, flags);
				return 0;
			}

			if(!(dc->dc_flags & CF_BLANK))
				n++;
		}

		/* Check fields that must have at least one nonempty entry. */
		if((dp->dp_flags & PF_ONEREQ) && n == 0 && db->db_trks > 1) {
			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "no valid %s in entry", dp->dp_name);

			db_cleanup(fp, db, DE_INVALID, (flags | DF_DONE));
			return 0;
		}
		else if(n != i)
			db->db_flags |= DB_EMPTY;
	}

	return db;
}


int
db_write(FILE *fp, db_t *db)
{
	int i;
	int x;
	lhead_t *lh;
	db_parse_t *dp;

	for(i = 0; i < DP_NPHASE; i++) {
		dp = &parstab[i];
		lh = db->db_phase[i];

		if(dp->dp_flags & PF_MULTI)
			x = db_write_multi(fp, lh, dp);
		else if(dp->dp_flags & PF_NUMERIC)
			x = db_write_num(fp, lh, dp);
		else
			x = db_write_str(fp, lh, dp, 0);

		if(x == 0)
			return 0;
	}

	return 1;
}


int
db_write_multi(FILE *fp, lhead_t *lh, db_parse_t *dp)
{
	int i;
	link_t *lp;

	for(i = 0, list_rewind(lh), list_forw(lh); !list_rewound(lh);
	    i++, list_forw(lh)) {
		lp = list_cur(lh);
		if(db_write_str(fp, (lhead_t *)lp->l_data, dp, i) == 0)
			return 0;
	}

	return 1;
}


int
db_write_num(FILE *fp, lhead_t *lh, db_parse_t *dp)
{
	int i;
	int n;
	char *s;
	link_t *lp;

	/* Compute how many discids we can put on a line. */
	n = DBLINESIZ - strlen(dp->dp_pstr);
	n /= CDDBDISCIDLEN + 1;

	for(i = 0, list_rewind(lh), list_forw(lh); i < list_count(lh); i++,
	    list_forw(lh)) {
		lp = list_cur(lh);

		if(!(i % n) && fputs(dp->dp_pstr, fp) == EOF)
			return 0;

		if(fprintf(fp, "%08x", (unsigned int)lp->l_data) == EOF)
			return 0;

		if(i == (list_count(lh) - 1) || !((i + 1) % n)) {
			if(fp == stdout)
				s = "\r\n";
			else
				s = "\n";
		}
		else {
			s = ",";
		}

		if(fputs(s, fp) == EOF)
			return 0;
	}

	return 1;
}


int
db_write_str(FILE *fp, lhead_t *lh, db_parse_t *dp, int trk)
{
	link_t *lp;
	char buf[CDDBBUFSIZ];

	for(list_rewind(lh), list_forw(lh); !list_rewound(lh); list_forw(lh)) {
		lp = list_cur(lh);

		if(fprintf(fp, dp->dp_pstr, trk) == EOF)
			return 0;

		if(fp == stdout) {
			strncpy(buf, (char *)lp->l_data, sizeof(buf));
			buf[sizeof(buf) - 1] = '\0';
			strip_crlf(buf);

			if(fprintf(fp, "%s\r\n", buf) == EOF)
				return 0;
		}
		else {
			if(fputs((char *)lp->l_data, fp) == EOF)
				return 0;
		}
	}

	return 1;
}


int
db_post(db_t *db, char *dir, unsigned int discid, char *errstr)
{
	FILE *fp;
	int link;
	char file[CDDBBUFSIZ];
	char name[CDDBDISCIDLEN + 1];
	struct stat sbuf;

	cddbd_snprintf(name, sizeof(name), "%08x", discid);

	/* Make sure discid is in the DB entry. */
	if(!list_find(db->db_phase[DP_DISCID], (void *)discid)) {
		cddbd_snprintf(errstr, CDDBBUFSIZ,
		    "discid %s is not in the DB entry", name);

		db_errno = DE_INVALID;
		return 0;
	}

	/* Check to see if we're writing to the database directly. */
	if(!strcmp(dir, cddbdir))
		link = 1;
	else
		link = 0;

	if(link)
		db_unlink(dir, name);

	if(stat(dir, &sbuf)) {
		if(mkdir(dir, (mode_t)db_dir_mode)) {
			cddbd_log(LOG_ERR, "Failed to create post dir %s.",
			    dir);

			cddbd_snprintf(errstr, CDDBBUFSIZ,
			    "file access failed");

			db_errno = DE_FILE;
			return 0;
		}

		(void)cddbd_fix_file(dir, db_dir_mode, db_uid, db_gid);
	}
	else if(!S_ISDIR(sbuf.st_mode)) {
		cddbd_log(LOG_ERR, "%s is not a directory.", dir);
		cddbd_snprintf(errstr, CDDBBUFSIZ, "file access failed");
		db_errno = DE_FILE;

		return 0;
	}

	/* Open file to be written. */
	cddbd_snprintf(file, sizeof(file), "%s/%s", dir, name);

	if((fp = fopen(file, "w")) == NULL) {
		cddbd_log(LOG_ERR, "Couldn't open post file: %s (%d).",
		    file, errno);

		cddbd_snprintf(errstr, CDDBBUFSIZ, "file access failed");
		db_errno = DE_FILE;
		return 0;
	}

	if(!db_write(fp, db)) {
		cddbd_snprintf(errstr, CDDBBUFSIZ, "server filesystem full");
		db_errno = DE_FILE;

		fclose(fp);
		unlink(file);

		return 0;
	}

	fclose(fp);

	/* Create any needed links. */
	if(link)
		db_link(db, dir, name, 1);

	/* Note if we can't set stats, but continue. */
	(void)cddbd_fix_file(file, db_file_mode, db_uid, db_gid);

	db_errno = DE_NO_ERROR;
	return 1;
}


void
db_free(db_t *db)
{
	int i;

	for(i = 0; i < DP_NPHASE; i++)
		if(db->db_phase[i] != 0)
			list_free(db->db_phase[i]);

	free(db);
}


void
db_free_string(void *p)
{
	free(p);
}


void
db_free_multi(void *lh)
{
	list_free((lhead_t *)lh);
}


int
db_classify(char *buf, int *n, char **p)
{
	int i;
	int len;
	db_parse_t *dp;
	char kbuf[CDDBBUFSIZ];

	for(i = 0; i < DP_NPHASE; i++)
		if(!strncmp(parstab[i].dp_str, buf, strlen(parstab[i].dp_str)))
			break;

	if(i == DP_NPHASE)
		return -1;

	dp = &parstab[i];

	if(dp->dp_flags & PF_MULTI) {
		if(sscanf(buf, dp->dp_pstr, n) != 1)
			return -1;
		cddbd_snprintf(kbuf, sizeof(kbuf), dp->dp_pstr, *n);
	}
	else {
		*n = -1;
		strcpy(kbuf, dp->dp_pstr);
	}

	len = strlen(kbuf);
	if(strncmp(kbuf, buf, len))
		return -1;

	*p = &buf[len];

	return i;
}


int
db_parse_discid(char **p, unsigned int *discid)
{
	if(sscanf(*p, "%08x", discid) != 1)
		return 1;

	if(strlen(*p) < CDDBDISCIDLEN)
		return 1;

	*p += CDDBDISCIDLEN;

	if(**p != ',' && **p != '\n' && **p != '\0')
		return 1;

	if(**p != '\0')
		(*p)++;

	return 0;
}


int
db_sum_discid(int n)
{
	int ret;
	char *p;
	char buf[12];

	cddbd_snprintf(buf, sizeof(buf), "%lu", (unsigned int)n);

	for(ret = 0, p = buf; *p != '\0'; p++)
		ret += *p - '0';

	return ret;
}


unsigned int
db_gen_discid(db_t *db)
{
	int i;
	int n;
	int t;
	int offtab[CDDBMAXTRK + 1];

	/* Convert to seconds. */
	for(i = 0; i < db->db_trks; i++)
		offtab[i] = db->db_offset[i] / CDDBFRAMEPERSEC;

	/* Compute the discid. */
	for(i = 0, t = 0, n = 0; i < (db->db_trks - 1); i++) {
		n += db_sum_discid(offtab[i]);
		t += offtab[i + 1] - offtab[i];
	}

	n += db_sum_discid(offtab[i]);
	t += db->db_disclen - offtab[i];

	return(((n % 0xff) << 24) | (t << 8) | db->db_trks);
}


void
db_cleanup(FILE *fp, db_t *db, int err, int flags)
{
	int n;
	char *p;
	char buf[CDDBBUFSIZ];

	/* Eat input until we hit the end. */
	if(!(flags & DF_DONE)) {
		for(;;) {
			if((flags & DF_STDIN) && !cddbd_timer_sleep(timers))
				continue;

			if(fgets(buf, sizeof(buf), fp) == NULL)
				break;

			/* If the line can't be discerned, terminate. */
			if(db_classify(buf, &n, &p) < 0)
				break;
		}
	}

	db_free(db);

	db_errno = err;
}


void
db_strcpy(db_t *db, int phase, int n, char *to, int size)
{
	int len;
	int rsize;
	link_t *lp;
	lhead_t *lh;

	lh = db->db_phase[phase];

	if(parstab[phase].dp_flags & PF_MULTI) {
		list_rewind(lh);

		while(n >= 0) {
			list_forw(lh);
			n--;
		}

		lp = list_cur(lh);
		lh = (lhead_t *)lp->l_data;
	}

	list_rewind(lh);
	list_forw(lh);

	/* Make space for the null terminator. */
	size--;
	rsize = size;
	to[0] = '\0';

	for(; !list_rewound(lh) && size > 0; list_forw(lh)) {
		lp = list_cur(lh);

		len = strlen((char *)lp->l_data);
		if(len > size)
			len = size;

		strncat(to, (char *)lp->l_data, len);
		strip_crlf(to);
		to[rsize] = '\0';

		size -= len;
	}
}


int
db_strlen(char *p)
{
	int i;

	i = 0;
	while(*p != '\0') {
		if(!(*p == '\\' && (*(p + 1) == 'n' || *(p + 1) == 't' ||
		    (*(p + 1) == '\\'))))
			i++;

		p++;
	}

	return i;
}


int
db_cmp(db_t *db1, db_t *db2)
{
	/* Make sure they're the same CD. Give db2 precedence. */
	if(db1->db_trks != db2->db_trks || !is_fuzzy_match(db1->db_offset,
	    db2->db_offset, db1->db_disclen, db2->db_disclen, db1->db_trks))
		return -1;

	/* Check for the magic unalterable revision. */
	if(db1->db_rev == DB_MAX_REV && db2->db_rev != DB_MAX_REV)
		return 1;

	if(db2->db_rev == DB_MAX_REV && db1->db_rev != DB_MAX_REV)
		return -1;

	/* Otherwise, compare the revision. */
	return(db1->db_rev - db2->db_rev);
}


void
db_unlink(char *dir, char *name)
{
	FILE *fp;
	db_t *db;
	link_t *lp;
	lhead_t *lh;
	char buf[CDDBBUFSIZ];

	cddbd_snprintf(buf, sizeof(buf), "%s/%s", dir, name);

	if((fp = fopen(buf, "r")) == NULL) {
		unlink(buf);
		return;
	}

	db = db_read(fp, buf, 0);
	fclose(fp);

	if(db == 0) {
		unlink(buf);
	}
	else {
		lh = db->db_phase[DP_DISCID];

		for(list_rewind(lh), list_forw(lh); !list_rewound(lh);
		    list_forw(lh)) {
			lp = list_cur(lh);

			cddbd_snprintf(buf, sizeof(buf), "%s/%08x",
			    dir, (unsigned int)lp->l_data);

			unlink(buf);
		}

		db_free(db);
	}
}


void
db_link(db_t *db, char *dir, char *name, int fix)
{
	link_t *lp;
	lhead_t *lh;
	struct stat sbuf;
	struct stat sbuf2;
	char dbname[CDDBBUFSIZ];
	char dblink[CDDBBUFSIZ];

	cddbd_snprintf(dbname, sizeof(dbname), "%s/%s", dir, name);

	if(stat(dbname, &sbuf) != 0) {
		cddbd_log(LOG_ERR, "Can't stat DB entry: %s.", dbname);
		return;
	}

	lh = db->db_phase[DP_DISCID];

	for(list_rewind(lh), list_forw(lh); !list_rewound(lh); list_forw(lh)) {
		lp = list_cur(lh);

		cddbd_snprintf(dblink, sizeof(dblink), "%s/%08x", dir,
		    (unsigned int)lp->l_data);

		/* This should already exist. */
		if(!strcmp(dbname, dblink))
			continue;

		/* The link exists already. */
		if(stat(dblink, &sbuf2) == 0) {
			if(verbose && sbuf.st_ino != sbuf2.st_ino) {
				cddbd_log(LOG_ERR,
				    "Warning: %s (ino %u) not linked to %s "
				    "(ino %u)",
				    dblink, sbuf2.st_ino, dbname, sbuf.st_ino);
			}
			continue;
		}

		/* Can't stat it, so complain. */
		if(errno != ENOENT) {
			cddbd_log(LOG_ERR, "Can't stat DB entry: %s", dblink);
			continue;
		}

		if(fix) {
			/* Create the link. */
			if(cddbd_link(dbname, dblink) != 0) {
				cddbd_log(LOG_ERR, "Can't link %s to %s",
				    dblink, dbname);
			}

			if(verbose) {
				cddbd_log(LOG_INFO,
				    "Linked %s to %s.", dblink, dbname);
			}
		}
		else {
			if(verbose) {
				cddbd_log(LOG_INFO,
				    "Link from %s to %s missing.", dblink,
				    dbname);
			}
		}
	}
}


int
db_merge(db_t *tdb, db_t *fdb)
{
	int merged;
	link_t *lp;
	lhead_t *tlh;
	lhead_t *flh;

	merged = 0;

	tlh = tdb->db_phase[DP_DISCID];
	flh = fdb->db_phase[DP_DISCID];

	for(list_rewind(flh), list_forw(flh); !list_rewound(flh);
	    list_forw(flh)) {
		lp = list_cur(flh);

		if(list_find(tlh, lp->l_data) == 0) {
			if(list_add_cur(tlh, lp->l_data) == 0) {
				cddbd_log(LOG_ERR,
				    "Can't malloc linked list entry.");
				quit(QUIT_ERR);
			}

			merged++;
		}
	}

	return merged;
}


int
db_strip(db_t *db)
{
	int i;
	int x;
	lhead_t *lh;
	db_parse_t *dp;

	x = 1;

	for(i = 0; i < DP_NPHASE; i++) {
		dp = &parstab[i];

		if(!(dp->dp_flags & PF_OSTRIP))
			continue;

		lh = db->db_phase[i];

		if(dp->dp_flags & PF_MULTI)
			x = db_strip_multi(lh);
		else
			x = db_strip_list(lh);
	}

	return x;
}


int
db_strip_multi(lhead_t *lh)
{
	link_t *lp;

	for(list_rewind(lh), list_forw(lh); !list_rewound(lh); list_forw(lh)) {
		lp = list_cur(lh);
		if(!db_strip_list((lhead_t *)lp->l_data))
			return 0;
	}

	return 1;
}


int
db_strip_list(lhead_t *lh)
{
	char *p;

	if(list_count(lh) <= 0)
		return 1;

	/* Empty out the list. */
	list_rewind(lh);
	list_forw(lh);

	while(!list_empty(lh))
		list_delete(lh, list_cur(lh));

	p = (char *)strdup("\n");
	if(p == 0)
		return 0;

	/* Put string in the list. */
	if(list_add_back(lh, (void *)p) == 0) {
		return 0;
	}

	return 1;
}


db_content_t *
db_content_check(db_t *db, int phase, int subphase)
{
	int n;
	arg_t args;
	db_parse_t *dp;
	static db_content_t dc;

	dp = &parstab[phase];
	cddbd_bzero((char *)&dc, sizeof(dc));

	if(!(dp->dp_flags & PF_NUMERIC)) {
		db_strcpy(db, phase, subphase, args.buf, sizeof(args.buf));
		cddbd_parse_args(&args, 0);

		if(is_blank(args.buf, 1))
			dc.dc_flags |= CF_BLANK;
	}

	if(dp->dp_flags & PF_CKTRK) {
		if(args.nargs >= 1 && !cddbd_strcasecmp(args.arg[0], "track")) {
			if(args.nargs == 1)
				dc.dc_flags |= CF_INVALID;
			else if(args.nargs == 2 && is_numeric(args.arg[1])) {
				n = atoi(args.arg[1]);
				if(n == subphase || n == (subphase + 1))
					dc.dc_flags |= CF_INVALID;
			}
		}
		else if(args.nargs == 1 &&
		    !cddbd_strcasecmp(args.arg[0], "-")) {
			dc.dc_flags |= CF_INVALID;
		}
		else if(args.nargs == 2 &&
		    !cddbd_strcasecmp(args.arg[0], "empty") &&
		    !cddbd_strcasecmp(args.arg[1], "track")) {
			dc.dc_flags |= CF_INVALID;
		}
		else if(args.nargs == 2 &&
		    !cddbd_strcasecmp(args.arg[0], "new") &&
		    !cddbd_strcasecmp(args.arg[2], "track"))
			dc.dc_flags |= CF_INVALID;
	}

	if(dp->dp_flags & PF_CKTIT) {
		if(args.nargs == 0)
			dc.dc_flags |= CF_INVALID;
		else if(args.nargs == 1 &&
		    !cddbd_strcasecmp(args.arg[0], "empty"))
			dc.dc_flags |= CF_INVALID;
		else if(args.nargs >= 2 &&
		    (!cddbd_strcasecmp(args.arg[0], "new") ||
		    !cddbd_strcasecmp(args.arg[0], "nieuwe")) &&
		    (!cddbd_strcasecmp(args.arg[1], "artist") ||
		    !cddbd_strcasecmp(args.arg[1], "titel")))
			dc.dc_flags |= CF_INVALID;
	}

	return &dc;
}

/* ARGSUSED */
char *
db_field(db_t *db, int phase, int subphase)
{
	db_parse_t *dp;
	static char name[CDDBPHASESZ];

	dp = &parstab[phase];

	if(dp->dp_flags & PF_MULTI)
		cddbd_snprintf(name, sizeof(name), "%s %d", dp->dp_name,
		    subphase);
	else
		cddbd_snprintf(name, sizeof(name), "%s", dp->dp_name);

	return name;
}


/* Check for a blank line. */
int
is_blank(char *buf, int slash)
{
	while(*buf != '\0' && (isspace(*buf) || (slash && (*buf == '/')))) {
		/* Only allow one of these. */
		if(*buf == '/')
			slash = 0;
		buf++;
	}

	if(*buf == '\n')
		buf++;

	if(*buf == '\0')
		return 1;

	return 0;
}


/* Check for numeric input. */
int
is_numeric(char *buf)
{
	while(*buf != '\0') {
		if(!isdigit(*buf))
			return 0;
		buf++;
	}

	return 1;
}


/* Check for hex input. */
int
is_xnumeric(char *buf)
{
	while(*buf != '\0') {
		if(!isxdigit(*buf))
			return 0;
		buf++;
	}

	return 1;
}


/* Check for a valid line. */
int
is_valid(char *buf)
{
	unsigned char *p;

	p = (unsigned char *)buf;

	while(*p != '\0')
		if(isprint((int)*p) || is_space((int)*p) || is_iso((int)*p))
			p++;
		else
			return 0;

	if(*p == '\n')
		p++;

	if(*p == '\0')
		return 1;

	return 0;
}


int
is_space(char c)
{
	/* Do not include ^L as white space. */
	return(isspace((unsigned int)c) && c != 0x0C);
}


/* Check for a terminating period. */
int
is_dot(char *buf)
{
	return(!strcmp(buf, ".\n") || !strcmp(buf, ".\r\n"));
}


/* Check for a terminating period or two. */
int
is_dbl_dot(char *buf)
{
	return(is_dot(buf) || !strncmp(buf, "..", 2));
}


int
is_iso(int c)
{
	/* The ISO 8859 Latin-1 extended character set starts at 0xA0. */
	return(c >= 0xA0);
}


int
is_crlf(int c)
{
	return(c == '\r' || c == '\n');
}


int
is_wspace(int c)
{
	return(c == ' ' || c == '\t');
}


int
categ_index(char *categ)
{
	int i;

	for(i = 0; categlist[i] != 0; i++)
		if(!strcmp(categ, categlist[i]))
			return i;

	return -1;
}


int
is_instr(char c, char *str)
{

	if(*str == '\0')
		return 1;

	while(*str != '\0') {
		if(c == *str)
			return 1;
		++str;
	}

	return 0;
}


void
strip_crlf(char *buf)
{
	char *p;

	/* Strip out newlines. */
	p = &buf[strlen(buf) - 1];

	while(*p == '\n' || *p == '\r') {
		*p = '\0';

		if(p == buf)
			break;
		p--;
	}
}


int
is_parent_dir(char *name)
{
	return(!strcmp(name, ".") || !strcmp(name, ".."));
}


int
is_us_ascii(unsigned char c)
{
	return(c <= 127);
}


void
asy_decode(char *p)
{
	int c;
	char *p2;
	char *p3;

	while(*p != '\0') {
		/* If we have a mapping, unmap it and compress. */
		c = octet_to_char((unsigned char *)p, '%');

		if(c >= 0) {
			p3 = p;

			*p = (char)c;
			p++;
			p2 = p + 2;

			while(*p2 != '\0') {
				*p = *p2;
				p++;
				p2++;
			}

			*p = '\0';
			p = p3;
		}

		p++;
	}
}


int
asy_encode(char *buf, char **out)
{
	int len;
	char *p;
	char *o;
	static char *obuf;
	static int size = 0;

	len = (strlen(buf) * 3) + 1;

	if(len > size) {
		if(size != 0)
			free(obuf);

		obuf = (char *)malloc(len);

		if(obuf == NULL) {
			size = 0;
			return 0;
		}
	}

	for(p = obuf; *buf != '\0'; buf++) {
		if(asy_mappable((unsigned char)*buf)) {
			o = char_to_octet((unsigned char)*buf, '%');
			len = strlen(o);
			strncpy(p, o, len);
			p += len;
		}
		else {
			*p = *buf;
			p++;
		}
	}

	*p = '\0';
	*out = obuf;

	return 1;
}


int
asy_mappable(unsigned char c)
{
	return(c == '&' || c == '+' || c == '?' || c == '%' ||
	    !isprint((int)c));
}
