/*
 *   cddb - CD Database Management Library
 *
 *   Copyright (C) 1993-1996  Ti Kan
 *   E-mail: ti@amb.org
 *
 *   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 *_cddb_c_ident_ = "@(#)cddb.c	6.77 96/04/07";
#endif

#ifdef __VMS
typedef char *	caddr_t;
#endif

#include "common_d/appenv.h"
#include "common_d/patchlevel.h"
#include "common_d/util.h"
#include "cddb_d/cddbp.h"
#include "cddb_d/cddb.h"


#define CDDB_MIN(a,b)		(((a) > (b)) ? (b) : (a))
#define CDDB_MAX(a,b)		(((a) > (b)) ? (a) : (b))

#define STATCODE_1ST(p)		((p)[0])
#define STATCODE_2ND(p)		((p)[1])
#define STATCODE_3RD(p)		((p)[2])
#define STATCODE_CHECK(p)	\
	(isdigit(STATCODE_1ST(p)) && \
	 isdigit(STATCODE_2ND(p)) && \
	 isdigit(STATCODE_3RD(p)))


/* Time interval to run workproc while waiting for I/O */
#define CDDB_WAIT_SEC		0
#define CDDB_WAIT_USEC		200000


extern appdata_t	app_data;
extern FILE		*errfp;


STATIC cddb_client_t	*cddb_clinfo;		/* Client info */
STATIC cddb_path_t	*cddb_pathhead,		/* CDDB path list head */
			*cddb_pathptr;		/* CDDB path list ptr */
STATIC cddb_linkopts_t	*cddb_linkhead;		/* Link options list head */
STATIC pid_t		child_pid;		/* Child pid */
STATIC bool_t		ischild;		/* Child process */
STATIC char		curfile[FILE_PATH_SZ];	/* Current disc info file */


/***********************
 *  internal routines  *
 ***********************/


/*
 * onalrm
 *	Signal handler for SIGALRM
 *
 * Args:
 *	signo - The signal number
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
void
onalrm(int signo)
{
	signal(SIGALRM, onalrm);

	/* Do nothing: all we want is to interrupt ourselves */
	DBGPRN(errfp, "SIGALRM received\n");
}


/*
 * onterm
 *	Signal handler for SIGTERM
 *
 * Args:
 *	signo - The signal number
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
void
onterm(int signo)
{
	DBGPRN(errfp, "SIGTERM received\n");
	exit(0);
}


/*
 * cddb_skip_whitespace
 *	Given a character string, return a pointer to the position
 *	where the first whitespace (space or tab) occurs.  No end-of-string
 *	checking is performed.
 *
 * Args:
 *	str - The input string
 *
 * Return:
 *	Pointer to the next whitespace in str.  NULL is returned if the
 *	input string is NULL.
 */
STATIC char *
cddb_skip_whitespace(char *str)
{
	if (str == NULL)
		return NULL;

	while (*str == ' ' || *str == '\t')
		str++;

	return (str);
}


/*
 * cddb_strcat
 *	Concatenate two text strings with special handling for newline
 *	and tab character translations.
 *
 * Args:
 *	s1 - The first text string and destination string.
 *	s2 - The second text string.
 *
 * Return:
 *	Pointer to the resultant string, or NULL if failed.
 */
STATIC char *
cddb_strcat(char *s1, char *s2)
{
	char	*cp = s1;

	if (s1 == NULL || s2 == NULL)
		return NULL;

	/* Concatenate two strings, with special handling for newline
	 * and tab characters.
	 */
	for (s1 += strlen(s1); *s2 != '\0'; s1++, s2++) {
		if (*s2 == '\\') {
			switch (*(s2 + 1)) {
			case 'n':
				*s1 = '\n';
				s2++;
				break;
			case 't':
				*s1 = '\t';
				s2++;
				break;
			default:
				*s1 = *s2;
				break;
			}
		}
		else
			*s1 = *s2;
	}
	*s1 = '\0';

	return (cp);
}


/*
 * cddb_strcasecmp
 *	Compare two strings a la strcmp(), except it is case-insensitive.
 *
 * Args:
 *	s1 - The first text string.
 *	s2 - The second text string.
 *
 * Return:
 *	Compare value.  See strcmp(3).
 */
STATIC int
cddb_strcasecmp(char *s1, char *s2)
{
	char	*buf1,
		*buf2,
		*p;
	int	ret;

	if (s1 == NULL || s2 == NULL)
		return 0;

	/* Allocate tmp buffers */
	if ((buf1 = (char *) MEM_ALLOC(strlen(s1)+1)) == NULL ||
	    (buf2 = (char *) MEM_ALLOC(strlen(s2)+1)) == NULL) {
		CDDB_FATAL(app_data.str_nomemory);
		return 0;
	}

	/* Convert both strings to lower case and store in tmp buffer */
	for (p = buf1; *s1 != '\0'; s1++, p++)
		*p = (isupper(*s1)) ? tolower(*s1) : *s1;
	*p = '\0';
	for (p = buf2; *s2 != '\0'; s2++, p++)
		*p = (isupper(*s2)) ? tolower(*s2) : *s2;
	*p = '\0';

	ret = strcmp(buf1, buf2);

	MEM_FREE(buf1);
	MEM_FREE(buf2);

	return (ret);
}


/*
 * cddb_sum
 *	Convert an integer to its text string representation, and
 *	compute its checksum.  Used by cddb_discid to derive the
 *	disc ID.
 *
 * Args:
 *	n - The integer value.
 *
 * Return:
 *	The integer checksum.
 */
STATIC int
cddb_sum(int n)
{
	char	buf[12],
		*p;
	int	ret = 0;

	/* For backward compatibility this algorithm must not change */
	sprintf(buf, "%lu", n);
	for (p = buf; *p != '\0'; p++)
		ret += (*p - '0');

	return (ret);
}


/*
 * cddb_waitio
 *	Check if read data is pending.  If no data and a workproc is
 *	specified, then call workproc.
 *
 * Args:
 *	fd - File descriptor on which we are expecting data
 *	do_workproc - Whether we should run workproc while waiting
 *
 * Return:
 *	Nothing.
 */
void
cddb_waitio(int fd, bool_t do_workproc)
{
#ifndef SYNCHRONOUS
	int		ret;
	fd_set		rfds;
	fd_set		efds;
	struct timeval	to;

	do {
		FD_ZERO(&efds);
		FD_ZERO(&rfds);
		FD_SET(fd, &rfds);

		to.tv_sec = CDDB_WAIT_SEC;
		to.tv_usec = CDDB_WAIT_USEC;
#ifdef __hpux
		ret = select(fd+1, (int *) &rfds, NULL, (int *) &efds, &to);
#else
		ret = select(fd+1, &rfds, NULL, &efds, &to);
#endif

		if (do_workproc && cddb_clinfo->workproc != NULL)
			cddb_clinfo->workproc(cddb_clinfo->arg);

	} while (ret == 0);
#endif	/* SYNCHRONOUS */
}


/*
 * cddb_getc
 *	Return a character from the file stream.  Perform buffered
 *	read from file if necessary.
 *
 * Args:
 *	fp - Pointer to the cddb_file_t structure.
 *	do_workproc - set to TRUE if the workproc should be invoked
 *		while waiting for I/O.
 *
 * Return:
 *	The character, or -1 on EOF or failure.
 */
STATIC int
cddb_getc(cddb_file_t *fp, bool_t do_workproc)
{
	static int	i = 0;

	/* Do some work every 10 characters read */
	if ((i % 10) == 0 && do_workproc && cddb_clinfo->workproc != NULL)
		cddb_clinfo->workproc(cddb_clinfo->arg);

	if (++i > 100)
		i = 0;	/* Reset count */

	if (fp->pos == fp->cnt) {
		/* Wait for data */
		cddb_waitio(fp->fd, do_workproc);

		/* Load cache */
		for (;;) {
			fp->cnt = read(fp->fd, fp->cache, CDDB_CACHESZ);
			if (fp->cnt == 0) {
				fp->cnt = fp->pos;
				return -1;
			}
			else if (fp->cnt < 0) {
				if (errno != EINTR) {
					fp->cnt = fp->pos;
					return -1;
				}
			}
			else
				break;
		}
		fp->pos = 1;
	}
	else
		fp->pos++;

	return ((int) fp->cache[fp->pos-1]);
}


/*
 * cddb_gets
 *	Read a line of text from the file.  The read terminates
 *	when it encounters a newline character, EOF, or if len
 *	characters have been read.
 *
 * Args:
 *	fp - The CDDB file pointer obtained via cddb_open()
 *	buf - Pointer to return string buffer
 *	len - Maximum number of characters to read
 *	do_workproc - set to TRUE if the workproc should be invoked
 *		while waiting for I/O.
 *
 * Return:
 *	TRUE - Successfully read a line of text
 *	FALSE - Reached EOF or read failure
 */
STATIC bool_t
cddb_gets(cddb_file_t *fp, char *buf, int len, bool_t do_workproc)
{
	int	tot = 0,
		c = 0;

	/* Sanity check */
	if (fp->magic != CDDB_FMAGIC)
		return FALSE;

	while ((c = cddb_getc(fp, do_workproc)) > 0) {
		*buf = (char) c;

		tot++;
		buf++;
		len--;

		if (c == '\n') {
			/* Translate CR-LF into just LF */
			if (*(buf-2) == '\r') {
				*(buf-2) = '\n';
				*(buf-1) = '\0';
			}
			break;
		}

		if (len <= 0)
			break;
	}
	*buf = '\0';

	return ((bool_t) (tot > 0));
}


/*
 * cddb_puts
 *	Write a line of text to the file.
 *
 * Args:
 *	fp - The CDDB file pointer obtained via cddb_open()
 *	buf - Pointer to string buffer
 *
 * Return:
 *	TRUE - Success
 *	FALSE - Failure
 */
STATIC bool_t
cddb_puts(cddb_file_t *fp, char *buf)
{
	int	i,
		n,
		len;

	/* Sanity check */
	if (fp->magic != CDDB_FMAGIC)
		return FALSE;

	if (fp->type != CDDB_LOCAL)
		/* Remote writes not supported */
		return FALSE;

	len = strlen(buf);

	for (i = 0; i < len; i++) {
		fp->cache[fp->pos] = (unsigned char) *buf;

		fp->pos++;
		buf++;

		if (fp->pos == CDDB_CACHESZ) {
			/* Flush cache */
			n = write(fp->fd, fp->cache, fp->pos);

			if (n < 0 || n != fp->pos) {
				fp->pos = 0;
				return FALSE;
			}

			fp->pos = 0;
		}
	}

	return TRUE;
}


/*
 * cddb_remote_cmd
 *	Send a command to the remote CDDB server
 *
 * Args:
 *	fp - The CDDB file pointer obtained via cddb_open()
 *	buf - Pointer to command string buffer
 *	buflen - Size of the command string buffer
 *
 * Return:
 *	Pointer to the three-digit status code string
 */
STATIC char *
cddb_remote_cmd(cddb_file_t *fp, char *buf, int buflen)
{
	int		len,
			tot,
			ret;
	char		*p;
	static char	code[4];

	p = buf;
	len = strlen(p);
	tot = 0;

	/* Send command to server */
	DBGPRN(errfp, "-> %s", p);

	while (len > 0) {
		if ((ret = write(fp->fd, p, len)) < 0) {
			DBGPRN(errfp, "Command write failed: errno = %d\n",
				errno);
			return NULL;
		}

		p += ret;
		tot += ret;
		len -= ret;
	}

	if (tot == 0) {
		DBGPRN(errfp, "Command write failed\n");
		return NULL;
	}

	/* Read server response to command */
	if (!cddb_gets(fp, buf, buflen, FALSE)) {
		DBGPRN(errfp, "CDDB server not responding to command\n");
		return NULL;
	}

	p = cddb_skip_whitespace(buf);

	DBGPRN(errfp, "<- %s", p);

	if (!STATCODE_CHECK(p)) {
		/* Server error */
		DBGPRN(errfp, "Invalid CDDB server response.\n");
		return NULL;
	}

	strncpy(code, p, 3);
	code[3] = '\0';

	return (code);
}


/*
 * cddb_eat_server_output
 *	Read remaining server output and discard
 *
 * Args:
 *	fp - The CDDB file pointer obtained via cddb_open()
 *	buf - Pointer to command string buffer
 *	buflen - Size of the command string buffer
 *
 * Return:
 *	TRUE - Success
 *	FALSE - Failure
 */
STATIC bool_t
cddb_eat_server_output(cddb_file_t *fp, char *buf, int buflen)
{
	char	*p;

	p = cddb_skip_whitespace(buf);

	if (!STATCODE_CHECK(p)) {
		/* Server error */
		DBGPRN(errfp, "Invalid CDDB server response.\n");
		return FALSE;
	}

	while (STATCODE_2ND(p) == STAT_SUB_OUTPUT) {
		if (!cddb_gets(fp, buf, buflen, FALSE)) {
			DBGPRN(errfp, "CDDB server read error!\n");
			return FALSE;
		}

		p = cddb_skip_whitespace(buf);

		DBGPRN(errfp, "<- %s", p);

		if (!STATCODE_CHECK(p)) {
			/* Server error */
			DBGPRN(errfp, "Invalid CDDB server response.\n");
			return FALSE;
		}

		switch (STATCODE_1ST(p)) {
		case STAT_GEN_INFO:
		case STAT_GEN_OK:
		case STAT_GEN_OKCONT:
			/* OK */
			break;
		case STAT_GEN_OKFAIL:
		case STAT_GEN_ERROR:
			/* Error */
			DBGPRN(errfp,
				"CDDB server code '%c%c%c'\n",
				STATCODE_1ST(p),
				STATCODE_2ND(p),
				STATCODE_3RD(p));
			return FALSE;
		}
	}

	return TRUE;
}


/*
 * cddb_remote_banner_ck
 *	Check the remote CDDB server sign-on banner
 *
 * Args:
 *	fp - The CDDB file pointer obtained via cddb_open()
 *	buf - Pointer to command string buffer
 *	buflen - Size of the command string buffer
 *
 * Return:
 *	TRUE - Success
 *	FALSE - Failure
 */
STATIC bool_t
cddb_remote_banner_ck(cddb_file_t *fp, char *buf, int buflen)
{
	char	*p;

	/* Read server start-up banner */
	if (!cddb_gets(fp, buf, buflen, FALSE)) {
		DBGPRN(errfp, "No startup banner on CDDB server!\n");
		return FALSE;
	}

	/* Check sign on banner */
	p = cddb_skip_whitespace(buf);

	DBGPRN(errfp, "<- %s", p);

	if (!STATCODE_CHECK(p)) {
		/* Invalid server sign-on banner */
		DBGPRN(errfp, "Invalid CDDB server response.\n");
		return FALSE;
	}

	switch (STATCODE_1ST(p)) {
	case STAT_GEN_OK:
	case STAT_GEN_OKCONT:
		/* Banner OK: eat remaining lines, if any. */
		return (cddb_eat_server_output(fp, buf, buflen));

	case STAT_GEN_INFO:
	case STAT_GEN_OKFAIL:
	case STAT_GEN_ERROR:
	default:
		/* Error */
		DBGPRN(errfp, "CDDB server code '%c%c%c'\n",
			STATCODE_1ST(p), STATCODE_2ND(p), STATCODE_3RD(p));
		return FALSE;
	}
}


/*
 * cddb_cmd_hello
 *	Perform "hello" handshake to remote CDDB server
 *
 * Args:
 *	fp - The CDDB file pointer obtained via cddb_open()
 *	buf - Pointer to command string buffer
 *	buflen - Size of the command string buffer
 *
 * Return:
 *	TRUE - Success
 *	FALSE - Failure
 */
STATIC bool_t
cddb_cmd_hello(cddb_file_t *fp, char *buf, int buflen)
{
	char	*code;

	/* Send hello */
	sprintf(buf, "%s %s %s %s v%s%sPL%d\n",
		CDDB_HELLO, cddb_clinfo->user, cddb_clinfo->host,
		cddb_clinfo->prog, VERSION, VERSION_EXT, PATCHLEVEL);

	if ((code = cddb_remote_cmd(fp, buf, buflen)) == NULL)
		return FALSE;

	switch (STATCODE_1ST(code)) {
	case STAT_GEN_OK:
	case STAT_GEN_OKCONT:
		/* OK: eat remaining lines, if any. */
		return (cddb_eat_server_output(fp, buf, buflen));

	case STAT_GEN_INFO:
	case STAT_GEN_OKFAIL:
	case STAT_GEN_ERROR:
	default:
		/* Error */
		DBGPRN(errfp, "CDDB server code '%c%c%c'\n",
			STATCODE_1ST(code),
			STATCODE_2ND(code),
			STATCODE_3RD(code));
		return FALSE;
	}
}


/*
 * cddb_cmd_query
 *	Perform CDDB "query" on remote CDDB server
 *
 * Args:
 *	fp - The CDDB file pointer obtained via cddb_open()
 *	buf - Pointer to command string buffer
 *	buflen - Size of the command string buffer
 *	dbp - Pointer to the cddb_incore_t structure
 *	s - Pointer to the curstat_t structure
 *
 * Return:
 *	TRUE - Success
 *	FALSE - Failure
 */
STATIC bool_t
cddb_cmd_query(cddb_file_t *fp, char *buf, int buflen,
	       cddb_incore_t *dbp, curstat_t *s)
{
	int		i;
	char		*code,
			*p;
	cddb_match_t	*q,
			*r;

	dbp->category[0] = '\0';
	dbp->flags &= ~CDDB_INEXACT;

	/* Send query */
	sprintf(buf, "%s %08x %d",
		CDDB_QUERY, dbp->discid, s->tot_trks);
	for (i = 0; i < (int) s->tot_trks; i++)
		sprintf(buf, "%s %d", buf, s->trkinfo[i].addr + MSF_OFFSET(s));
	sprintf(buf, "%s %d\n", buf,
		s->trkinfo[(int) s->tot_trks].min * 60 +
		s->trkinfo[(int) s->tot_trks].sec);

	if ((code = cddb_remote_cmd(fp, buf, buflen)) == NULL)
		return FALSE;

	switch (STATCODE_1ST(code)) {
	case STAT_GEN_OK:
	case STAT_GEN_OKCONT:
		switch (STATCODE_3RD(code)) {
		case STAT_QURY_EXACT:
			p = cddb_skip_whitespace(buf) + 4;

			/* Get category and discid */
			i = sscanf(p, "%s", dbp->category);
			if (i < 1) {
				/* Invalid server response */
				dbp->category[0] = '\0';
				return FALSE;
			}

			return TRUE;

		case STAT_QURY_INEXACT:
			if (STATCODE_2ND(code) != STAT_SUB_OUTPUT)
				/* Invalid server response */
				return FALSE;

			while (cddb_gets(fp, buf, buflen, FALSE)) {
				p = cddb_skip_whitespace(buf);

				DBGPRN(errfp, "<- %s", p);

				if (*p == '.')
					break;

				q = (cddb_match_t *)
					MEM_ALLOC(sizeof(cddb_match_t));
				if (q == NULL)
					/* No memory */
					return FALSE;

				q->dtitle = (char *) MEM_ALLOC(strlen(p));
				if (q->dtitle == NULL) {
					/* No memory */
					MEM_FREE(q);
					return FALSE;
				}

				/* Get category, discid, and disc title */
				i = sscanf(p, "%s %x %[^\n]\n",
					q->category, &q->discid, q->dtitle);

				if (i < 3) {
					/* Invalid server response */
					MEM_FREE(q->dtitle);
					MEM_FREE(q);
					return FALSE;
				}

				for (r = dbp->matchlist; r != NULL;
				     r = r->next) {
					/* Duplicate found */
					if (r->discid == q->discid) {
						MEM_FREE(q->dtitle);
						MEM_FREE(q);
						q = NULL;
						break;
					}
				}

				if (q != NULL) {
					/* Add to list */
					q->next = dbp->matchlist;
					dbp->matchlist = q;

					/* Set flag */
					dbp->flags |= CDDB_INEXACT;
				}
			}

			return TRUE;

		case STAT_QURY_NONE:
			return FALSE;
		}
		break;

	case STAT_GEN_INFO:
	case STAT_GEN_OKFAIL:
	case STAT_GEN_ERROR:
	default:
		/* Error */
		DBGPRN(errfp, "CDDB server code '%c%c%c'\n",
			STATCODE_1ST(code),
			STATCODE_2ND(code),
			STATCODE_3RD(code));
		return FALSE;
	}
}


/*
 * cddb_cmd_read
 *	Perform CDDB "read" on remote CDDB server
 *
 * Args:
 *	fp - The CDDB file pointer obtained via cddb_open()
 *	buf - Pointer to command string buffer
 *	buflen - Size of the command string buffer
 *	dbp - Pointer to the cddb_incore_t structure.
 *
 * Return:
 *	TRUE - Success
 *	FALSE - Failure
 */
STATIC bool_t
cddb_cmd_read(cddb_file_t *fp, char *buf, int buflen, cddb_incore_t *dbp)
{
	char	*code;

	/* Send read command */
	sprintf(buf, "%s %s %08x\n", CDDB_READ, dbp->category, dbp->discid);

	if ((code = cddb_remote_cmd(fp, buf, buflen)) == NULL)
		return FALSE;

	switch (STATCODE_1ST(code)) {
	case STAT_GEN_OK:
	case STAT_GEN_OKCONT:
		if (STATCODE_2ND(code) == '1')
			return TRUE;

		return FALSE;

	case STAT_GEN_INFO:
	case STAT_GEN_OKFAIL:
	case STAT_GEN_ERROR:
	default:
		/* Error */
		DBGPRN(errfp, "CDDB server code '%c%c%c'\n",
			STATCODE_1ST(code),
			STATCODE_2ND(code),
			STATCODE_3RD(code));
		return FALSE;
	}
}


/*
 * cddb_putentry
 *	Write one information item into a database file.  Used by
 *	cddb_write to update the CD database file.
 *
 * Args:
 *	fp - CDDB file pointer
 *	idstr - The information identifier keyword text string
 *	entry - The information text string
 *
 * Return:
 *	Nothing.
 */
STATIC bool_t
cddb_putentry(cddb_file_t *fp, char *idstr, char *entry)
{
	int	i,
		n;
	char	*cp,
		tmpbuf[STR_BUF_SZ * 2];

	if (entry == NULL) {
		/* Null entry */
		sprintf(tmpbuf, "%s=\n", idstr);
		if (!cddb_puts(fp, tmpbuf))
			return FALSE;
	}
	else {
		/* Write entry to file, splitting into multiple lines
		 * if necessary.  Special handling for newline and tab
		 * characters.
		 */
		cp = entry;

		do {
			sprintf(tmpbuf, "%s=", idstr);
			if (!cddb_puts(fp, tmpbuf))
				return FALSE;

			n = CDDB_MIN((int) strlen(cp), STR_BUF_SZ);

			for (i = 0; i < n; i++, cp++) {
				switch (*cp) {
				case '\n':
					if (!cddb_puts(fp, "\\n"))
						return FALSE;
					break;
				case '\t':
					if (!cddb_puts(fp, "\\t"))
						return FALSE;
					break;
				default:
					sprintf(tmpbuf, "%c", *cp);
					if (!cddb_puts(fp, tmpbuf))
						return FALSE;
					break;
				}
			}

			if (!cddb_puts(fp, "\n"))
				return FALSE;

		} while (n == STR_BUF_SZ);
	}

	return TRUE;
}


/*
 * cddb_add_pathent
 *	Add a cddb path list component.
 *
 * Args:
 *	path - The path component string
 *
 * Return:
 *	TRUE - success
 *	FALSE - failure
 */
STATIC bool_t
cddb_add_pathent(char *path)
{
	char		*cp;
	cddb_path_t	*pp;

	if (*path == '\0' || *path == '\n')
		return TRUE;

	pp = (cddb_path_t *)(void *) MEM_ALLOC(sizeof(cddb_path_t));
	if (pp == NULL) {
		CDDB_FATAL(app_data.str_nomemory);
		return FALSE;
	}

	memset(pp, 0, sizeof(cddb_path_t));

	if (cddb_pathhead == NULL) {
		cddb_pathhead = cddb_pathptr = pp;
	}
	else {
		cddb_pathptr->next = pp;
		cddb_pathptr = pp;
	}

	if (path[0] == '@') {
#ifdef NOREMOTE
		pp->type = CDDB_INVALID;
#else
		/* Remote host */
		pp->type = CDDB_REMOTE;

		if ((cp = strchr(path, ':')) != NULL)
			*cp = '\0';

		/* Get port number, if specified */
		if (cp != NULL) {
			if (!isdigit(*(cp+1)))
				pp->type = CDDB_INVALID;
			else
				pp->port = atoi(cp+1);
		}
		else {
			pp->port = app_data.cddb_port;
		}

		/* Remote host name */
		pp->host = (char *) MEM_ALLOC(strlen(path));
		if (pp->host == NULL) {
			CDDB_FATAL(app_data.str_nomemory);
			return FALSE;
		}

		strcpy(pp->host, path+1);

		if (cp != NULL)
			*cp = ':';
#endif	/* NOREMOTE */
	}
	else {
		pp->type = CDDB_LOCAL;

		if (path[0] == '/') {
			/* Absolute local path name */
			pp->path = (char *) MEM_ALLOC(strlen(path) + 1);
			if (pp->path == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				return FALSE;
			}

			strcpy(pp->path, path);
		}
		else if (path[0] == '~') {
			/* Local path name with tilde expansion */
			pp->path = (char *) MEM_ALLOC(strlen(path) + 128);

			if (pp->path == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				return FALSE;
			}

			/* Perform tilde expansion a la [ck]sh */
			if (path[1] == '/') {
				sprintf(pp->path, "%s%s",
					homedir(get_ouid()),
					&path[1]);
			}
			else if (path[1] == '\0') {
				strcpy(pp->path, homedir(get_ouid()));
			}
			else {
				cp = strchr(path, '/');
				if (cp == NULL) {
					strcpy(pp->path, uhomedir(&path[1]));
				}
				else {
					*cp = '\0';
					sprintf(pp->path, CONCAT_PATH,
						uhomedir(&path[1]), cp+1);
				}
			}
		}
		else {
			/* Relative local path name */
			pp->path = (char *) MEM_ALLOC(
				strlen(path) +
				strlen(app_data.libdir) + 7
			);

			if (pp->path == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				return FALSE;
			}

			sprintf(pp->path, REL_DBDIR_PATH,
				app_data.libdir, path);
		}
	}

	return TRUE;
}


/*
 * cddb_add_linkent
 *	Add an entry to the link-search list in sorted order.  Used
 *	by cddb_init_linkopts.
 *
 * Args:
 *	dtitle - Disc artist/title text string
 *	idstr - A text string representation of the magic number
 *	offset - A measure how good the track addresses match
 *
 * Return:
 *	TRUE - success
 *	FALSE - failure
 */
STATIC bool_t
cddb_add_linkent(char *dtitle, char *idstr, word32_t offset)
{
	int		n;
	cddb_linkopts_t	*p,
			*q,
			*r;

	if (dtitle == NULL || idstr == NULL)
		/* Paranoia */
		return FALSE;

	if ((p = (cddb_linkopts_t *)(void *)
		 MEM_ALLOC(sizeof(cddb_linkopts_t))) == NULL) {
		CDDB_FATAL(app_data.str_nomemory);
		return FALSE;
	}
	if ((p->dtitle = (char *) MEM_ALLOC(strlen(dtitle) + 1)) == NULL) {
		CDDB_FATAL(app_data.str_nomemory);
		return FALSE;
	}

	strcpy(p->idstr, idstr);
	strcpy(p->dtitle, dtitle);
	p->offset = offset;

	if (cddb_linkhead == NULL) {
		/* This is the first element */
		cddb_linkhead = p;
		p->next = NULL;
	}
	else {
		/* Add to list in sorted order */
		for (q = cddb_linkhead, r = NULL; q != NULL; q = q->next) {
			if ((n = cddb_strcasecmp(dtitle, q->dtitle)) == 0) {
				/* Already in list: no need to add */
				MEM_FREE(p->dtitle);
				MEM_FREE((char *) p);
				return TRUE;
			}
			else if (n < 0) {
				/* Track timings outside of criteria:
				 * just sort alphabetically.
				 */
				break;
			}
			r = q;
		}
		if (r == NULL) {
			p->next = cddb_linkhead;
			cddb_linkhead = p;
		}
		else {
			p->next = r->next;
			r->next = p;
		}
	}

	return TRUE;
}


/*
 * cddb_remote_open
 *	Open a connection to the remote CDDB server, and query the server
 *	if an entry for the CD exists.
 *
 * Args:
 *	fp - Pointer to the cddb_file_t structure
 *	pp - Pointer to the cddb_path_t structure
 *	dbp - Pointer to the cddb_incore_t structure
 *	s - Pointer to the curstat_t structure
 *
 * Return:
 *	TRUE - success and remote server has an entry for the current CD
 *	FALSE - Cannot connect, or the server has no entry for the CD
 */
STATIC bool_t
cddb_remote_open(
	cddb_file_t	*fp,
	cddb_path_t	*pp,
	cddb_incore_t	*dbp,
	curstat_t	*s
)
{
#ifdef NOREMOTE
	return FALSE;
#else
	struct hostent		*hp;
	struct in_addr		ad;
	struct sockaddr_in	sin;
	char			*buf;

	sin.sin_port = bswap16((unsigned short) pp->port);
	sin.sin_family = AF_INET;

	/* Set timeout in case remote server is unavailable */
	signal(SIGALRM, onalrm);
	alarm(app_data.cddb_timeout);

	/* Find server host address */
	if ((hp = gethostbyname(pp->host)) != NULL) {
		memcpy((char *) &sin.sin_addr, hp->h_addr, hp->h_length);
	}
	else {
		if ((ad.s_addr = inet_addr(pp->host)) != -1) {
			memcpy((char *) &sin.sin_addr,
			       (char *) &ad.s_addr, sizeof(ad.s_addr));
		}
		else {
			DBGPRN(errfp, "Unknown host!\n");
			return FALSE;
		}
	}

	/* Open socket */
	if ((fp->fd = SOCKET(AF_INET, SOCK_STREAM, 0)) < 0) {
		if (app_data.debug)
			perror("Socket failed");
		return FALSE;
	}

	/* Connect to server */
	if (CONNECT(fp->fd, (struct sockaddr *) &sin, sizeof(sin)) < 0) {
		if (app_data.debug)
			perror("Connect failed");
		close(fp->fd);
		return FALSE;
	}

	/* Cancel alarm */
	alarm(0);

	/* Allocate command buffer */
	if ((buf = (char *) MEM_ALLOC(CDDBP_CMDLEN)) == NULL) {
		close(fp->fd);
		CDDB_FATAL(app_data.str_nomemory);
		return FALSE;
	}

	/* Verify server sign-on banner */
	if (!cddb_remote_banner_ck(fp, buf, CDDBP_CMDLEN)) {
		close(fp->fd);
		MEM_FREE(buf);
		return FALSE;
	}

	/* Perform hello handshake */
	if (!cddb_cmd_hello(fp, buf, CDDBP_CMDLEN)) {
		close(fp->fd);
		MEM_FREE(buf);
		return FALSE;
	}

	/* Query server about current CD */
	if (!cddb_cmd_query(fp, buf, CDDBP_CMDLEN, dbp, s)) {
		close(fp->fd);
		MEM_FREE(buf);
		return FALSE;
	}

	/* Succeeded */
	MEM_FREE(buf);
	return TRUE;
#endif	/* NOREMOTE */
}


/*
 * cddb_open
 *	Open CD database entry and return a file descriptor.  If
 *	the open is for reading, this routine will search for the
 *	appropriate entry.  Currently, opens for reading supports
 *	local database directories as well as internet remote
 *	databases, but writing is supported on local directories only.
 *
 *	The dbp->discid field should be filled prior to calling
 *	this function.
 *
 * Args:
 *	dbp - Pointer to the cddb_incore_t structure
 *	s - Pointer to the curstat structure
 *	rw - Open for read or write (O_RDONLY, O_WRONLY, etc.)
 *
 * Return:
 *	The CDDB file pointer, or NULL on failure.
 */
STATIC cddb_file_t *
cddb_open(cddb_incore_t *dbp, curstat_t *s, int rw)
{
	int		i;
	cddb_file_t	*fp;
	cddb_path_t	*pp;
	char		dbfile[FILE_PATH_SZ];

	/* Allocate cddb_file_t */
	fp = (cddb_file_t *)(void *) MEM_ALLOC(sizeof(cddb_file_t));
	if (fp == NULL) {
		CDDB_FATAL(app_data.str_nomemory);
		return NULL;
	}

	/* Allocate storage for I/O cache */
	fp->cache = (unsigned char *) MEM_ALLOC(CDDB_CACHESZ);
	if (fp->cache == NULL) {
		CDDB_FATAL(app_data.str_nomemory);
		return NULL;
	}

	fp->magic = CDDB_FMAGIC;
	fp->rw = rw;
	fp->pos = fp->cnt = 0;

	if (rw & O_WRONLY) {
		/* Open file */
		if ((fp->fd = open(dbp->dbfile, rw)) < 0) {
			MEM_FREE(fp->cache);	
			MEM_FREE(fp);	
			return NULL;
		}

		fp->type = CDDB_LOCAL;
	}
	else {
		DBGPRN(errfp, "\nCDDB QUERY id=%08x:\n", dbp->discid);

		/* Loop through all database paths */
		for (pp = cddb_pathhead; pp != NULL; pp = pp->next) {
			switch (pp->type) {
			case CDDB_LOCAL:
				DBGPRN(errfp, "Local CDDB %s\n", pp->path);

				sprintf(dbfile, CDDBFILE_PATH,
					pp->path, dbp->discid);

				if ((fp->fd = open(dbfile, rw)) < 0)
					break;

				fp->type = pp->type;

				/* Record the category */
				strcpy(dbp->category, basename(pp->path));

				/* Record the path to the database file. */
				dbp->dbfile = (char *)
					MEM_ALLOC(strlen(dbfile) + 1);
				if (dbp->dbfile == NULL) {
					CDDB_FATAL(app_data.str_nomemory);
					return NULL;
				}
				strcpy(dbp->dbfile, dbfile);
				break;

			case CDDB_REMOTE:
				DBGPRN(errfp, "Remote CDDB %s:%d\n",
					pp->host, pp->port);

				fp->type = pp->type;

				if (!cddb_remote_open(fp, pp, dbp, s))
					fp->fd = -1;

				break;

			case CDDB_INVALID:
			default:
				fp->fd = -1;
				break;
			}

			if (fp->fd >= 0)
				/* Found a CDDB entry */
				break;
		}

		if (fp->fd < 0) {
			MEM_FREE(fp->cache);	
			MEM_FREE(fp);	
			return NULL;
		}
	}

	return (fp);
}


/*
 * cddb_close
 *	Close the file that was previously opened via cddb_open().
 *
 * Args:
 *	fp - CDDB file pointer
 *
 * Return:
 *	TRUE - Success
 *	FALSE - Failure
 */
STATIC bool_t
cddb_close(cddb_file_t *fp)
{
	int	n;

	/* Sanity check */
	if (fp->magic != CDDB_FMAGIC)
		return FALSE;

	if ((fp->rw & O_WRONLY) && fp->pos > 0) {
		/* Flush write cache */
		n = write(fp->fd, fp->cache, fp->pos);

		if (n < 0 || n != fp->pos) {
			fp->pos = 0;
			return FALSE;
		}
	}

	/* Close file */
	if (close(fp->fd) < 0)
		return FALSE;

	/* Free cache storage */
	MEM_FREE(fp->cache);

	/* Free cddb_file_t structure */
	MEM_FREE(fp);

	return TRUE;
}


#ifdef SYNCHRONOUS

/*
 * cddb_read0
 *	Read CDDB data and update incore structures.
 *
 * Args:
 *	fp - CDDB file pointer
 *	dbp - Pointer to the cddb_incore_t structure
 *
 * Return:
 *	TRUE - Success
 *	FALSE - Failure
 */
STATIC bool_t
cddb_read0(cddb_file_t *fp, cddb_incore_t *dbp)
{
	int	lineno,
		pos;
	char	buf[STR_BUF_SZ * 2],
		tmpbuf[STR_BUF_SZ * 2];

	/* Initialize */
	dbp->type = CDDB_WAIT;

	if (dbp->flags & CDDB_INEXACT) {
		dbp->type = fp->type;
		return TRUE;
	}

	/* Set up for remote CDDB server read */
	if (fp->type == CDDB_REMOTE &&
	    !cddb_cmd_read(fp, buf, sizeof(buf), dbp)) {
		dbp->type = CDDB_INVALID;
		return FALSE;
	}

	/* Read first line of database entry */
	if (!cddb_gets(fp, buf, sizeof(buf), TRUE)) {
		dbp->type = CDDB_INVALID;
		return FALSE;
	}

	/* Database file signature check */
	if (strncmp(buf, "# xmcd ", 7) != 0) {
		/* Not a supported database file */
		dbp->type = CDDB_INVALID;
		return FALSE;
	}

	/* Read the rest of the database entry */
	for (lineno = 0; cddb_gets(fp, buf, sizeof(buf), TRUE); lineno++) {
		/* Comment line */
		if (buf[0] == '#') {
			/* Concatenated cddb file */
			if (strncmp(buf, "# xmcd ", 7) == 0)
				break;

			lineno--;
			continue;
		}

		/* "EOF" reached */
		if (buf[0] == '.')
			break;

		buf[strlen(buf)-1] = '\n';

		/* Disk ID sanity check */
		if (lineno == 0) {
			if (strncmp(buf, "DISCID=", 7) == 0)
				/* Okay */
				continue;

			dbp->type = CDDB_INVALID;
			return FALSE;
		}

		/* Disk title */
		if (sscanf(buf, "DTITLE=%[^\n]\n", tmpbuf) > 0) {
			if (dbp->dtitle == NULL) {
				dbp->dtitle = (char *)
					MEM_ALLOC(strlen(tmpbuf) + 1);

				if (dbp->dtitle != NULL)
					dbp->dtitle[0] = '\0';
			}
			else {
				dbp->dtitle = (char *)
					MEM_REALLOC(dbp->dtitle,
						strlen(dbp->dtitle) +
						strlen(tmpbuf) + 1);
			}

			if (dbp->dtitle == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				break;
			}

			cddb_strcat(dbp->dtitle, tmpbuf);
			continue;
		}

		/* Track title */
		if (sscanf(buf, "TTITLE%u=%[^\n]\n", &pos, tmpbuf) >= 2) {
			if (pos >= (int) (dbp->discid & 0xff))
				continue;

			if (dbp->trklist[pos] == NULL) {
				dbp->trklist[pos] = (char *)
					MEM_ALLOC(strlen(tmpbuf) + 1);

				if (dbp->trklist[pos] != NULL)
					dbp->trklist[pos][0] = '\0';
				
			}
			else {
				dbp->trklist[pos] = (char *)
					MEM_REALLOC(dbp->trklist[pos],
						strlen(dbp->trklist[pos]) +
						strlen(tmpbuf) + 1);
			}

			if (dbp->trklist[pos] == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				break;
			}

			cddb_strcat(dbp->trklist[pos], tmpbuf);
			continue;
		}

		/* Disk extended info */
		if (sscanf(buf, "EXTD=%[^\n]\n", tmpbuf) > 0) {
			if (dbp->extd == NULL) {
				dbp->extd = (char *)
					MEM_ALLOC(strlen(tmpbuf) + 1);

				if (dbp->extd != NULL)
					dbp->extd[0] = '\0';
			}
			else {
				dbp->extd = (char *)
					MEM_REALLOC(dbp->extd,
						strlen(dbp->extd) +
						strlen(tmpbuf) + 1);
			}

			if (dbp->extd == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				break;
			}

			cddb_strcat(dbp->extd, tmpbuf);
			continue;
		}

		/* Track extended info */
		if (sscanf(buf, "EXTT%u=%[^\n]\n", &pos, tmpbuf) >= 2) {
			if (pos >= (int) (dbp->discid & 0xff))
				continue;

			if (dbp->extt[pos] == NULL) {
				dbp->extt[pos] = (char *)
					MEM_ALLOC(strlen(tmpbuf) + 1);

				if (dbp->extt[pos] != NULL)
					dbp->extt[pos][0] = '\0';
			}
			else {
				dbp->extt[pos] = (char *)
					MEM_REALLOC(dbp->extt[pos],
						strlen(dbp->extt[pos]) +
						strlen(tmpbuf) + 1);
			}

			if (dbp->extt[pos] == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				break;
			}

			cddb_strcat(dbp->extt[pos], tmpbuf);
			continue;
		}

		/* Play order */
		if (sscanf(buf, "PLAYORDER=%[^\n]\n", tmpbuf) > 0) {
			if (dbp->playorder == NULL) {
				dbp->playorder = (char *)
					MEM_ALLOC(strlen(tmpbuf) + 1);

				if (dbp->playorder != NULL)
					dbp->playorder[0] = '\0';
					
			}
			else {
				dbp->playorder = (char *)
					MEM_REALLOC(dbp->playorder,
						strlen(dbp->playorder) +
						strlen(tmpbuf) + 1);
			}

			if (dbp->playorder == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				break;
			}

			cddb_strcat(dbp->playorder, tmpbuf);
			continue;
		}
	}

	/* Update database type */
	dbp->type = fp->type;

	return TRUE;
}

#else	/* SYNCHRONOUS */

/*
 * cddb_read1
 *	Read CDDB data and write into pipe.  Used by child process.
 *
 * Args:
 *	fp - CDDB file pointer
 *	pfd - Pipe descriptor to write CDDB data into
 *	dbp - Pointer to the cddb_incore_t structure
 *
 * Return:
 *	TRUE - Success
 *	FALSE - Failure
 */
STATIC bool_t
cddb_read1(cddb_file_t *fp, cddb_file_t *pfp, cddb_incore_t *dbp)
{
	int		lineno;
	char		buf[STR_BUF_SZ * 2];
	cddb_match_t	*p;

	/* CDDB type */
	sprintf(buf, "CDDBTYPE=%d\n", fp->type);
	if (!cddb_puts(pfp, buf))
		return FALSE;

	/* CDDB category */
	if (dbp->category[0] != '\0') {
		sprintf(buf, "CATEGORY=%s\n", dbp->category);
		if (!cddb_puts(pfp, buf))
			return FALSE;
	}

	/* Flags */
	sprintf(buf, "FLAGS=%x\n", dbp->flags);
	if (!cddb_puts(pfp, buf))
		return FALSE;

	switch (fp->type) {
	case CDDB_LOCAL:
		/* CDDB file path */
		if (dbp->dbfile != NULL) {
			sprintf(buf, "FILEPATH=%s\n", dbp->dbfile);
			if (!cddb_puts(pfp, buf))
				return FALSE;
		}
		break;

	case CDDB_REMOTE:
		/* Match entry list */
		for (p = dbp->matchlist; p != NULL; p = p->next) {
			sprintf(buf, "RMATCH=%s %08x %s\n",
				p->category, p->discid, p->dtitle);
			if (!cddb_puts(pfp, buf))
				return FALSE;
		}
		break;

	default:
		break;
	}

	if (dbp->flags & CDDB_INEXACT)
		return TRUE;

	/* Set up for remote CDDB server read */
	if (fp->type == CDDB_REMOTE &&
	    !cddb_cmd_read(fp, buf, sizeof(buf), dbp))
		return FALSE;

	/* Read database entry */
	if (!cddb_gets(fp, buf, sizeof(buf), FALSE))
		return FALSE;

	/* Database file signature check */
	if (strncmp(buf, "# xmcd ", 7) != 0)
		/* Not a supported database file */
		return FALSE;

	/* Read the database file */
	for (lineno = 0; cddb_gets(fp, buf, sizeof(buf), FALSE); lineno++) {
		/* "EOF" reached */
		if (buf[0] == '.')
			break;

		/* Comment line */
		if (buf[0] == '#') {
			/* Concatenated cddb file */
			if (strncmp(buf, "# xmcd ", 7) == 0)
				break;

			lineno--;
			continue;
		}

		buf[strlen(buf)-1] = '\n';

		/* Disk ID sanity check */
		if (lineno == 0) {
			if (strncmp(buf, "DISCID=", 7) == 0)
				/* Okay */
				continue;
				return FALSE;
			}

		/* Write CDDB data into pipe */
		if (!cddb_puts(pfp, buf))
			return FALSE;
	}

	return TRUE;
}


/*
 * cddb_read2
 *	Read CDDB data from pipe and update in-core structures.
 *	Used by parent process.
 *
 * Args:
 *	pfd - Pipe descriptor to write CDDB data into.
 *	dbp - Pointer to cddb_incore_t structure.
 *	s - Pointer to curstat_t structure.
 *
 * Return:
 *	TRUE - Success
 *	FALSE - Failure
 */
STATIC bool_t
cddb_read2(cddb_file_t *pfp, cddb_incore_t *dbp, curstat_t *s)
{
	int		pos;
	char		buf[STR_BUF_SZ * 2],
			tmpbuf[STR_BUF_SZ * 2];
	cddb_match_t	*p,
			*q;

	/* Initialize */
	dbp->type = CDDB_WAIT;

	/* Read the rest of the database entry */
	while (cddb_gets(pfp, buf, sizeof(buf), TRUE)) {
		/* Disk title */
		if (sscanf(buf, "DTITLE=%[^\n]\n", tmpbuf) > 0) {
			if (dbp->dtitle == NULL) {
				dbp->dtitle = (char *)
					MEM_ALLOC(strlen(tmpbuf) + 1);

				if (dbp->dtitle != NULL)
					dbp->dtitle[0] = '\0';
			}
			else {
				dbp->dtitle = (char *)
					MEM_REALLOC(dbp->dtitle,
						strlen(dbp->dtitle) +
						strlen(tmpbuf) + 1);
			}

			if (dbp->dtitle == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				break;
			}

			cddb_strcat(dbp->dtitle, tmpbuf);
			continue;
		}

		/* Track title */
		if (sscanf(buf, "TTITLE%u=%[^\n]\n", &pos, tmpbuf) >= 2) {
			if (pos >= (int) (dbp->discid & 0xff))
				continue;

			if (dbp->trklist[pos] == NULL) {
				dbp->trklist[pos] = (char *)
					MEM_ALLOC(strlen(tmpbuf) + 1);

				if (dbp->trklist[pos] != NULL)
					dbp->trklist[pos][0] = '\0';
				
			}
			else {
				dbp->trklist[pos] = (char *)
					MEM_REALLOC(dbp->trklist[pos],
						strlen(dbp->trklist[pos]) +
						strlen(tmpbuf) + 1);
			}

			if (dbp->trklist[pos] == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				break;
			}

			cddb_strcat(dbp->trklist[pos], tmpbuf);
			continue;
		}

		/* Disk extended info */
		if (sscanf(buf, "EXTD=%[^\n]\n", tmpbuf) > 0) {
			if (dbp->extd == NULL) {
				dbp->extd = (char *)
					MEM_ALLOC(strlen(tmpbuf) + 1);

				if (dbp->extd != NULL)
					dbp->extd[0] = '\0';
			}
			else {
				dbp->extd = (char *)
					MEM_REALLOC(dbp->extd,
						strlen(dbp->extd) +
						strlen(tmpbuf) + 1);
			}

			if (dbp->extd == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				break;
			}

			cddb_strcat(dbp->extd, tmpbuf);
			continue;
		}

		/* Track extended info */
		if (sscanf(buf, "EXTT%u=%[^\n]\n", &pos, tmpbuf) >= 2) {
			if (pos >= (int) (dbp->discid & 0xff))
				continue;

			if (dbp->extt[pos] == NULL) {
				dbp->extt[pos] = (char *)
					MEM_ALLOC(strlen(tmpbuf) + 1);

				if (dbp->extt[pos] != NULL)
					dbp->extt[pos][0] = '\0';
			}
			else {
				dbp->extt[pos] = (char *)
					MEM_REALLOC(dbp->extt[pos],
						strlen(dbp->extt[pos]) +
						strlen(tmpbuf) + 1);
			}

			if (dbp->extt[pos] == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				break;
			}

			cddb_strcat(dbp->extt[pos], tmpbuf);
			continue;
		}

		/* Play order */
		if (sscanf(buf, "PLAYORDER=%[^\n]\n", tmpbuf) > 0) {
			if (dbp->playorder != NULL) {
				/* An existing playorder is in effect,
				 * do not override it.
				 */
				continue;
			}
			else {
				/* Load new play order */
				dbp->playorder = (char *)
					MEM_ALLOC(strlen(tmpbuf) + 1);

				if (dbp->playorder != NULL)
					dbp->playorder[0] = '\0';
			}

			if (dbp->playorder == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				break;
			}

			cddb_strcat(dbp->playorder, tmpbuf);
			continue;
		}

		/* Database file type */
		if (sscanf(buf, "CDDBTYPE=%d\n", &dbp->type) > 0)
			continue;

		/* Database category */
		if (sscanf(buf, "CATEGORY=%s\n", dbp->category) > 0)
			continue;

		/* Flags */
		if (sscanf(buf, "FLAGS=%x\n", &dbp->flags) > 0)
			continue;

		/* Database file path */
		if (sscanf(buf, "FILEPATH=%s\n", tmpbuf) > 0) {
			if (dbp->dbfile == NULL) {
				dbp->dbfile = (char *)
					MEM_ALLOC(strlen(tmpbuf) + 1);

				if (dbp->dbfile != NULL)
					dbp->dbfile[0] = '\0';
			}
			else {
				dbp->dbfile = (char *)
					MEM_REALLOC(dbp->dbfile,
						strlen(dbp->dbfile) +
						strlen(tmpbuf) + 1);
			}

			if (dbp->dbfile == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				break;
			}

			cddb_strcat(dbp->dbfile, tmpbuf);
			continue;
		}

		/* Match list entry */
		if (sscanf(buf, "RMATCH=%[^\n]\n", tmpbuf) > 0) {
			p = (cddb_match_t *) MEM_ALLOC(sizeof(cddb_match_t));
			if (p == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				break;
			}

			p->dtitle = (char *) MEM_ALLOC(strlen(tmpbuf));
			if (p->dtitle == NULL) {
				CDDB_FATAL(app_data.str_nomemory);
				break;
			}

			if (sscanf(tmpbuf, "%s %x %[^\n]\n",
				   p->category, &p->discid, p->dtitle) < 3) {
				MEM_FREE(p->dtitle);
				MEM_FREE(p);
				continue;
			}

			for (q = dbp->matchlist; q != NULL; q = q->next) {
				/* Duplicate found */
				if (q->discid == p->discid) {
					MEM_FREE(p->dtitle);
					MEM_FREE(p);
					p = NULL;
					break;
				}
			}

			if (p != NULL) {
				/* Add to list */
				p->next = dbp->matchlist;
				dbp->matchlist = p;
			}

			continue;
		}
	}

	if (dbp->type == CDDB_WAIT)
		dbp->type = CDDB_INVALID;

	if (dbp->type == CDDB_INVALID)
		return FALSE;

	return TRUE;
}

#endif	/* SYNCHRONOUS */


/*
 * cddb_write
 *	Write the cddb_incore_t structure into the appropriate CD
 *	database file.
 *
 * Args:
 *	fp - CDDB file pointer
 *	dbp - Pointer to the cddb_incore_t structure
 *	s - Pointer to the curstat_t structure
 *
 * Return:
 *	TRUE - Success
 *	FALSE - Failure
 */
STATIC bool_t
cddb_write(cddb_file_t *fp, cddb_incore_t *dbp, curstat_t *s)
{
	int	i;
	char	idstr[12],
		tmpbuf[STR_BUF_SZ * 2];

	/* File header */
	sprintf(tmpbuf, "# xmcd %s CD database file\n", VERSION);
	if (!cddb_puts(fp, tmpbuf))
		return FALSE;
	if (!cddb_puts(fp, "# Copyright (C) 1993-1996 Ti Kan\n"))
		return FALSE;

	if (!cddb_puts(fp, "#\n# Track frame offsets:\n"))
		return FALSE;
	for (i = 0; i < (int) dbp->ntrk; i++) {
		sprintf(tmpbuf, "#\t%u\n", s->trkinfo[i].addr + MSF_OFFSET(s));
		if (!cddb_puts(fp, tmpbuf))
			return FALSE;
	}

	sprintf(tmpbuf, "#\n# Disc length: %u seconds\n#\n",
		s->trkinfo[(int) dbp->ntrk].min * 60 +
		s->trkinfo[(int) dbp->ntrk].sec);
	if (!cddb_puts(fp, tmpbuf))
		return FALSE;

	/* Disc ID magic number */
	sprintf(tmpbuf, "DISCID=%08x\n", dbp->discid);
	if (!cddb_puts(fp, tmpbuf))
		return FALSE;

	/* Disc artist/title */
	if (!cddb_putentry(fp, "DTITLE", dbp->dtitle))
		return FALSE;

	/* Track titles */
	for (i = 0; i < (int) dbp->ntrk; i++) {
		sprintf(idstr, "TTITLE%u", i);
		if (!cddb_putentry(fp, idstr, dbp->trklist[i]))
			return FALSE;
	}

	/* Extended disc information */
	if (!cddb_putentry(fp, "EXTD", dbp->extd))
		return FALSE;

	/* Extended track information */
	for (i = 0; i < (int) dbp->ntrk; i++) {
		sprintf(idstr, "EXTT%u", i);
		if (!cddb_putentry(fp, idstr, dbp->extt[i]))
			return FALSE;
	}

	/* Track program sequence */
	if (!cddb_putentry(fp, "PLAYORDER",
			   s->onetrk_prog ? NULL : dbp->playorder))
		return FALSE;

	return TRUE;
}


/*
 * cddb_setperm
 *	Set the file permissions of the CD database file.  The
 *	dbp->dbfile field must be filled prior to calling this
 *	function.
 *
 * Args:
 *	dbp - Pointer to the cddb_incore_t structure
 *
 * Return:
 *	Nothing
 */
STATIC void
cddb_setperm(cddb_incore_t *dbp)
{
	int	mode;

	/* Set the database file permissions */
	sscanf(app_data.cddb_filemode, "%o", &mode);

	/* Make sure the file is at least readable to the user just
	 * in case mode is bogus.
	 */
	mode |= S_IRUSR;

	/* Turn off extraneous bits */
	mode &= ~(S_ISUID | S_ISGID | S_IXUSR | S_IXGRP | S_IXOTH);

	/* Set file permission */
	chmod(dbp->dbfile, (mode_t) mode);
}


/***********************
 *   public routines   *
 ***********************/


/*
 * cddb_init
 *	Initialize CD database management services
 *
 * Args:
 *	progname - The client program name string
 *	username - The client user login name string
 *
 * Return:
 *	Nothing.
 */
void
cddb_init(cddb_client_t *clp)
{
	char	*cp,
		*path;

	DBGPRN(errfp, "\nlibcddb: %s %s %s\n",
#ifdef SYNCHRONOUS
		"SYNCHRONOUS",
#else
		"ASYNCHRONOUS",
#endif
#ifdef NOREMOTE
		"NOREMOTE",
		""
#else
		"REMOTE",
#ifdef SOCKS
		"SOCKS"
#else
		""
#endif
#endif
	);

	cddb_clinfo = clp;

	/* Sanity check */
	if (clp->prog[0] == '\0')
		strcpy(clp->prog, "unknown");

#ifndef NOREMOTE
	SOCKSINIT(clp->prog);
#endif

	if (clp->host[0] == '\0') {
#ifndef NOREMOTE
		/* Try to determine host name */
		if (gethostname(clp->host, MAXHOSTNAMELEN) < 0)
#endif
			strcpy(clp->host, "unknown");
	}

	if (clp->user[0] == '\0')
		strcpy(clp->user, "unknown");

	/* Load XMCD_CDDBPATH environment variable, if specified */
	if ((cp = getenv("XMCD_CDDBPATH")) != NULL) {
		app_data.cddb_path = (char *) MEM_ALLOC(strlen(cp) + 1);
		if (app_data.cddb_path == NULL) {
			CDDB_FATAL(app_data.str_nomemory);
			return;
		}

		strcpy(app_data.cddb_path, cp);
	}

	if (app_data.cddb_path == NULL || app_data.cddb_path[0] == '\0')
		return;

	/* Create the CDDB path list */
	path = app_data.cddb_path;
	while ((cp = strchr(path, CDDBPATH_SEPCHAR)) != NULL) {
		*cp = '\0';

		if (!cddb_add_pathent(path))
			return;

		*cp = CDDBPATH_SEPCHAR;
		path = cp + 1;
	}
	cddb_add_pathent(path);
}


/*
 * cddb_halt
 *	Shut down cddb subsystem.
 *
 * Args:
 *	s - Pointer to the curstat_t structure
 *
 * Return:
 *	Nothing
 */
void
cddb_halt(curstat_t *s)
{
	if (curfile[0] != '\0')
		unlink(curfile);
}


/*
 * cddb_load
 *	Load CD database entry for the currently inserted CD.
 *	A search will be performed on all database paths in the
 *	cddb path list.
 *
 * Args:
 *	dbp - Pointer to the cddb_incore_t structure
 *	s - Pointer to the curstat_t structure
 *
 * Return:
 *	return code as defined by cddb_ret_t
 */
cddb_ret_t
cddb_load(cddb_incore_t *dbp, curstat_t *s)
{
#ifdef SYNCHRONOUS
	cddb_file_t	*fp;

	/* Get a CDDB file handle */
	if ((fp = cddb_open(dbp, s, O_RDONLY)) == NULL) {
		/* Database entry does not exist */
		return CDDB_SET_CODE(OPEN_ERR, 0);
	}

	/* Read contents of CD database entry and update incore structure */
	if (!cddb_read0(fp, dbp)) {
		/* Invalid CD database contents */
		cddb_close(fp);
		return CDDB_SET_CODE(READ_ERR, 0);
	}

	/* Close file */
	cddb_close(fp);

	/* An inexact CDDB match was found */
	if (dbp->flags & CDDB_INEXACT)
		return CDDB_SET_CODE(MATCH_ERR, 0);

	return 0;

#else	/* SYNCHRONOUS */
	int		pfd[2],
			ret;
	pid_t		cpid;
	waitret_t	stat_val;
	cddb_file_t	*fp,
			*wfp,
			*rfp;

	/* Set up a pipe */
	if (PIPE(pfd) < 0)
		return CDDB_SET_CODE(OPEN_ERR, errno);

	/* Fork child to performs actual I/O */
	switch (cpid = FORK()) {
	case 0:
		/* Child process */
		ischild = TRUE;
		break;

	case -1:
		close(pfd[0]);
		close(pfd[1]);
		return CDDB_SET_CODE(FORK_ERR, errno);

	default:
		/* Parent process */
		child_pid = cpid;

		/* Close un-needed pipe descriptor */
		close(pfd[1]);

		/* Allocate cddb_file_t structures for read pipe */
		rfp = (cddb_file_t *)(void *) MEM_ALLOC(sizeof(cddb_file_t));
		if (rfp == NULL) {
			CDDB_FATAL(app_data.str_nomemory);
			return CDDB_SET_CODE(OPEN_ERR, 0);
		}

		/* Allocate read cache */
		rfp->cache = (unsigned char *) MEM_ALLOC(CDDB_CACHESZ);
		if (rfp->cache == NULL) {
			CDDB_FATAL(app_data.str_nomemory);
			return CDDB_SET_CODE(OPEN_ERR, 0);
		}

		rfp->magic = CDDB_FMAGIC;
		rfp->fd = pfd[0];
		rfp->rw = O_RDONLY;
		rfp->pos = 0;
		rfp->cnt = 0;
		rfp->type = CDDB_LOCAL;	/* It's really a pipe, but that's ok */

		/* Read contents of CD database entry from pipe into
		 * in-core structure
		 */
		(void) cddb_read2(rfp, dbp, s);

		/* Close read pipe */
		cddb_close(rfp);

		/* Parent process: wait for child to exit */
		while ((ret = WAITPID(cpid, &stat_val, 0)) != cpid) {
			if (ret < 0) {
				child_pid = 0;
				return 0;
			}

			/* Do some work */
			if (cddb_clinfo->workproc != NULL)
				cddb_clinfo->workproc(cddb_clinfo->arg);
		}

		child_pid = 0;

		if (WIFEXITED(stat_val)) {
			if (WEXITSTATUS(stat_val) == 0) {
				if (dbp->flags & CDDB_INEXACT)
					return CDDB_SET_CODE(MATCH_ERR, 0);
				else
					return 0;
			}
			else
				return CDDB_SET_CODE(WEXITSTATUS(stat_val), 0);
		}
		else if (WIFSIGNALED(stat_val))
			return CDDB_SET_CODE(KILLED_ERR, WTERMSIG(stat_val));
		else
			return 0;
	}

	/* Exit on receipt of SIGTERM */
	signal(SIGTERM, onterm);

	DBGPRN(errfp, "\nSetting uid to %d, gid to %d\n",
		get_ouid(), get_ogid());

	/* Force uid and gid to original setting */
	if (setuid(get_ouid()) < 0 || setgid(get_ogid()) < 0)
		exit(SETUID_ERR);

	/* Close un-needed pipe descriptor */
	close(pfd[0]);

	/* Allocate cddb_file_t structures for write pipe */
	wfp = (cddb_file_t *)(void *) MEM_ALLOC(sizeof(cddb_file_t));
	if (wfp == NULL)
		exit(OPEN_ERR);

	/* Allocate write cache */
	wfp->cache = (unsigned char *) MEM_ALLOC(CDDB_CACHESZ);
	if (wfp->cache == NULL)
		exit(OPEN_ERR);

	wfp->magic = CDDB_FMAGIC;
	wfp->fd = pfd[1];
	wfp->rw = O_WRONLY;
	wfp->pos = 0;
	wfp->cnt = 0;
	wfp->type = CDDB_LOCAL;	/* It's really a pipe, but that's ok */

	/* Get a CDDB file handle */
	if ((fp = cddb_open(dbp, s, O_RDONLY)) == NULL) {
		/* Database entry does not exist */
		cddb_close(wfp);
		exit(OPEN_ERR);
	}

	/* Read contents of CD database entry and write into pipe */
	if (!cddb_read1(fp, wfp, dbp)) {
		/* Invalid CD database contents */
		cddb_close(fp);
		cddb_close(wfp);
		exit(READ_ERR);
	}

	/* Close file */
	cddb_close(fp);
	cddb_close(wfp);

	/* Child exits here */
	exit(0);
#endif	/* SYNCHRONOUS */
}


/*
 * cddb_save
 *	Save current in-core CD database data into file
 *
 * Args:
 *	dbp - Pointer to the cddb_incore_t structure
 *	s - Pointer to the curstat_t structure
 *
 * Return:
 *	return code as defined by cddb_ret_t
 */
cddb_ret_t
cddb_save(cddb_incore_t *dbp, curstat_t *s)
{
	int		ret;
	pid_t		cpid;
	waitret_t	stat_val;
	cddb_file_t	*fp;

	/* Fork child to perform actual I/O */
	switch (cpid = FORK()) {
	case 0:
		/* Child process */
		ischild = TRUE;
		break;

	case -1:
		return CDDB_SET_CODE(FORK_ERR, errno);

	default:
		/* Parent process: wait for child to exit */
		while ((ret = WAITPID(cpid, &stat_val, 0)) != cpid) {
			if (ret < 0)
				return 0;

			/* Do some work */
			if (cddb_clinfo->workproc != NULL)
				cddb_clinfo->workproc(cddb_clinfo->arg);
		}

		if (WIFEXITED(stat_val))
			return CDDB_SET_CODE(WEXITSTATUS(stat_val), 0);
		else if (WIFSIGNALED(stat_val))
			return CDDB_SET_CODE(KILLED_ERR, WTERMSIG(stat_val));
		else
			return 0;
	}

	DBGPRN(errfp, "\nSetting uid to %d, gid to %d\n",
		get_ouid(), get_ogid());

	/* Force uid and gid to original setting */
	if (setuid(get_ouid()) < 0 || setgid(get_ogid()) < 0)
		exit(SETUID_ERR);

	DBGPRN(errfp, "\nWriting CD database file %s\n", dbp->dbfile);

	/* Get a CDDB file handle */
	if ((fp = cddb_open(dbp, s, O_WRONLY | O_TRUNC | O_CREAT)) == NULL)
		exit(OPEN_ERR);

	/* Write to CD database */
	if (!cddb_write(fp, dbp, s))
		exit(WRITE_ERR);

	/* Close file */
	if (!cddb_close(fp))
		exit(CLOSE_ERR);

	/* Set CD database file permissions */
	cddb_setperm(dbp);

	/* Child exits here */
	exit(0);
}


/*
 * cddb_link
 *	Create a link of an existing local CD database entry to another.
 *
 * Args:
 *	srcpath - Source directory path string
 *	tgtpath - target directory path string
 *
 * Return:
 *	return code as defined by cddb_ret_t
 */
cddb_ret_t
cddb_link(char *srcpath, char *tgtpath)
{
	int		ret;
	pid_t		cpid;
	waitret_t	stat_val;

	/* Fork child to perform actual I/O */
	switch (cpid = FORK()) {
	case 0:
		/* Child process */
		ischild = TRUE;
		break;

	case -1:
		return CDDB_SET_CODE(FORK_ERR, errno);

	default:
		/* Parent process: wait for child to exit */
		while ((ret = WAITPID(cpid, &stat_val, 0)) != cpid) {
			if (ret < 0)
				return 0;

			/* Do some work */
			if (cddb_clinfo->workproc != NULL)
				cddb_clinfo->workproc(cddb_clinfo->arg);
		}

		if (WIFEXITED(stat_val))
			return CDDB_SET_CODE(WEXITSTATUS(stat_val), 0);
		else if (WIFSIGNALED(stat_val))
			return CDDB_SET_CODE(KILLED_ERR, WTERMSIG(stat_val));
		else
			return 0;
	}

	DBGPRN(errfp, "\nSetting uid to %d, gid to %d\n",
		get_ouid(), get_ogid());

	/* Force uid and gid to original setting */
	if (setuid(get_ouid()) < 0 || setgid(get_ogid()) < 0)
		exit(SETUID_ERR);

#ifdef USE_SYMLINK
	DBGPRN(errfp, "\nSymbolic Linking CD database file %s -> %s\n",
		srcpath, tgtpath);

	if (symlink(srcpath, tgtpath) < 0)
#else
	DBGPRN(errfp, "\nHard Linking CD database file %s -> %s\n",
		srcpath, tgtpath);

	if (link(srcpath, tgtpath) < 0)
#endif
		exit(LINK_ERR);

	/* Child exits here. */
	exit(0);
}


/*
 * cddb_send
 *	Send current CD database entry to archive server via e-mail.
 *
 * Args:
 *	dbp - Pointer to the cddb_incore_t structure
 *	s - Pointer to the curstat_t structure
 *
 * Return:
 *	return code as defined by cddb_ret_t
 */
cddb_ret_t
cddb_send(cddb_incore_t *dbp, curstat_t *s)
{
	int		ret;
	pid_t		cpid;
	waitret_t	stat_val;
	char		*p,
			*q,
			subject[STR_BUF_SZ],
			cmd[STR_BUF_SZ * 4];

	/* Fork child to perform actual I/O */
	switch (cpid = FORK()) {
	case 0:
		/* Child process */
		ischild = TRUE;
		break;

	case -1:
		return CDDB_SET_CODE(FORK_ERR, errno);

	default:
		/* Parent process: wait for child to exit */
		while ((ret = WAITPID(cpid, &stat_val, 0)) != cpid) {
			if (ret < 0)
				return 0;

			/* Do some work */
			if (cddb_clinfo->workproc != NULL)
				cddb_clinfo->workproc(cddb_clinfo->arg);
		}

		if (WIFEXITED(stat_val))
			return CDDB_SET_CODE(WEXITSTATUS(stat_val), 0);
		else if (WIFSIGNALED(stat_val))
			return CDDB_SET_CODE(KILLED_ERR, WTERMSIG(stat_val));
		else
			return 0;
	}

	DBGPRN(errfp, "\nSetting uid to %d, gid to %d\n",
		get_ouid(), get_ogid());

	/* Force uid and gid to original setting */
	if (setuid(get_ouid()) < 0 || setgid(get_ogid()) < 0)
		exit(SETUID_ERR);

	/* Mail command */
	for (p = cmd, q = app_data.cddb_mailcmd; *q != '\0'; p++, q++) {
		if (*q ==  '%') {
			/* Support the special meanings of %S, %A and %F */
			switch (*(q+1)) {
			case 'S':
				/* Mail subject */
				sprintf(subject, "cddb %s %08x",
					dbp->category[0] == '\0' ?
					"unknown" : dbp->category,
					dbp->discid);
				strcpy(p, subject);
				p += strlen(subject) - 1;
				q++;
				break;

			case 'A':
				/* Mail address */
				strcpy(p, app_data.cddb_mailsite);
				p += strlen(app_data.cddb_mailsite) - 1;
				q++;
				break;

			case 'F':
				/* CD database file path */
				strcpy(p, dbp->dbfile);
				p += strlen(dbp->dbfile) - 1;
				q++;
				break;

			default:
				p++;
				q++;
				sprintf(p, "%%c", *q);
				break;
			}
		}
		else {
			*p = *q;
		}
	}
	*p = '\0';

	DBGPRN(errfp, "\nSend CDDB: [%s]\n", cmd);

	/* Send the mail */
	if (cddb_clinfo->isdemo != NULL && cddb_clinfo->isdemo()) {
		/* Don't send mail if in demo mode */
		fprintf(errfp, "DEMO mode: mail not sent.\n");
	}
	else if (system(cmd) != 0)
		exit(1);

	/* Child exits here */
	exit(0);
}


/*
 * cddb_clear
 *	Clear the in-core cddb_incore_t structure
 *
 * Args:
 *	dbp - Pointer to the cddb_incore_t structure
 *	s - Pointer to the curstat_t structure
 *	reload - Whether this operation is due to a reload of the CDDB
 *		(We don't want to clear a play sequence in this case).
 *
 * Return:
 *	Nothing
 */
void
cddb_clear(cddb_incore_t *dbp, curstat_t *s, bool_t reload)
{
	int		i;
	cddb_match_t	*p,
			*q;

	dbp->category[0] = '\0';

	if (dbp->dbfile != NULL) {
		MEM_FREE(dbp->dbfile);
		dbp->dbfile = NULL;
	}

	if (dbp->dtitle != NULL) {
		MEM_FREE(dbp->dtitle);
		dbp->dtitle = NULL;
	}

	if (dbp->extd != NULL) {
		MEM_FREE(dbp->extd);
		dbp->extd = NULL;
	}

	for (i = MAXTRACK-1; i >= 0; i--) {
		if (dbp->trklist[i] != NULL) {
			MEM_FREE(dbp->trklist[i]);
			dbp->trklist[i] = NULL;
		}

		if (dbp->extt[i] != NULL) {
			MEM_FREE(dbp->extt[i]);
			dbp->extt[i] = NULL;
		}

		if (dbp->sav_extt[i] != NULL) {
			MEM_FREE(dbp->sav_extt[i]);
			dbp->sav_extt[i] = NULL;
		}
	}

	if (!reload && dbp->playorder != NULL) {
		MEM_FREE(dbp->playorder);
		dbp->playorder = NULL;
	}

	dbp->discid = 0;
	dbp->ntrk = 0;
	dbp->type = CDDB_INVALID;
	dbp->flags = 0;

	for (p = q = dbp->matchlist; p != NULL; p = q) {
		q = p->next;
		MEM_FREE(p->dtitle);
		MEM_FREE(p);
	}
	dbp->matchlist = dbp->match_cur = NULL;
}


/*
 * cddb_load_cancel
 *	Cancel asynchronous CDDB load operation, if active.
 *
 * Args:
 *	None.
 *
 * Return:
 *	nothing.
 */
void
cddb_load_cancel(void)
{
	if (child_pid > 0) {
		/* Kill child process */
		kill(child_pid, SIGTERM);
		child_pid = 0;
	}
}


/*
 * cddb_discid
 *	Compute a magic disc ID based on the number of tracks,
 *	the length of each track, and a checksum of the string
 *	that represents the offset of each track.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	The integer disc ID.
 */
word32_t
cddb_discid(curstat_t *s)
{
	int	i,
		t = 0,
		n = 0;

	/* For backward compatibility this algorithm must not change */
	for (i = 0; i < (int) s->tot_trks; i++) {
		n += cddb_sum((s->trkinfo[i].min * 60) + s->trkinfo[i].sec);

		t += ((s->trkinfo[i+1].min * 60) + s->trkinfo[i+1].sec) -
		     ((s->trkinfo[i].min * 60) + s->trkinfo[i].sec);
	}

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


/*
 * cddb_pathlist
 *	Return a pointer to the CDDB path list.
 *
 * Args:
 *	None.
 *
 * Return:
 *	A pointer to the path list head, or NULL if the list is empty.
 */
cddb_path_t *
cddb_pathlist(void)
{
	return (cddb_pathhead);
}


/*
 * cddb_init_linkopts
 *	Build a sorted linked list of track titles which are to be
 *	used to present to the user for database search-link.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	TRUE - success
 *	FALSE - failure
 */
bool_t
cddb_init_linkopts(cddb_incore_t *dbp, curstat_t *s)
{
	int		i,
			ntrk,
			trk_off[MAXTRACK];
	word32_t	offset,
			trkaddr;
	char		*dbdir,
			*bname,
			tmppath[FILE_PATH_SZ],
			buf[STR_BUF_SZ * 2];
	FILE		*fp;
	DIR		*dp;
	struct dirent	*de;
	bool_t		found;

	/* Warning: This code is SYSV-ish.  Porting to other
	 * environment may require some modification here.
	 */

	if (dbp->dbfile == NULL)
		/* Error */
		return FALSE;

	dbdir = dirname(dbp->dbfile);
	bname = basename(dbp->dbfile);

	if ((dp = opendir(dbdir)) == NULL)
		return FALSE;

	while ((de = readdir(dp)) != NULL) {
		/* Find all entries in this directory with the same
		 * number of tracks as this disc.
		 */
		if (strncmp(de->d_name + 6, bname + 6, 2) != 0)
			continue;

		sprintf(tmppath, CONCAT_PATH, dbdir, de->d_name);
		if ((fp = fopen(tmppath, "r")) == NULL)
			continue;
		
		/* Read first line of database file */
		if (fgets(buf, sizeof(buf), fp) == NULL) {
			fclose(fp);
			continue;
		}

		/* Database file signature check */
		if (strncmp(buf, "# xmcd ", 7) != 0) {
			/* Not a supported database file */
			fclose(fp);
			continue;
		}

		memset(trk_off, 0, sizeof(trk_off));
		ntrk = 0;
		found = FALSE;

		while (fgets(buf, sizeof(buf), fp) != NULL) {
			/* Look for track addresses of possible links */
			if (strncmp(buf, "# Track frame offsets", 21) == 0) {
				found = TRUE;
				continue;
			}
			else if (strncmp(buf, "# Disc length", 13) == 0) {
				i = sscanf(buf, "# Disc length: %u seconds\n",
					   &trkaddr);
				if (i > 0) {
					trk_off[ntrk] =
						(trkaddr * FRAME_PER_SEC) -
						s->trkinfo[ntrk].addr -
						MSF_OFFSET(s);
				}
				else {
					/* File format error */
					ntrk = 0;
				}
				found = FALSE;
				continue;
			}

			if (found &&
			    (i = sscanf(buf, "# %u\n", &trkaddr)) > 0) {
				trk_off[ntrk] =
					trkaddr - s->trkinfo[ntrk].addr -
					MSF_OFFSET(s);
				ntrk++;
			}

			/* Look for disk title */
			if (strncmp(buf, "DTITLE=", 7) == 0) {
				/* Eat newline */
				i = strlen(buf) - 1;
				if (buf[i] == '\n')
					buf[i] = '\0';
				
				/* Check whether a valid offset can be
				 * calculated (compare # of tracks).
				 */
				if (ntrk == (int) dbp->ntrk) {
					/* Compute the average block
					 * number difference per track.
					 */
					offset = 0;
					for (i = 0; i <= ntrk; i++) {
						if (trk_off[i] < 0)
						    trk_off[i] = -trk_off[i];
						offset += trk_off[i];
					}
					offset /= ntrk;
				}
				else {
					/* Track offsets not specified or
					 * not valid in database file.
					 */
					offset = OFFSET_UNKN;
				}

				/* Add to list in sorted order */
				cddb_add_linkent(buf + 7, de->d_name, offset);
				break;
			}
		}

		fclose(fp);
	}
	closedir(dp);

	return TRUE;
}


/*
 * cddb_free_linkopts
 *	Dismantle the sorted linked list of track titles created by
 *	cddb_init_linkopts.
 *
 * Args:
 *	Nothing.
 *
 * Return:
 *	Nothing.
 */
void
cddb_free_linkopts(void)
{
	cddb_linkopts_t	*p,
			*q;

	for (p = q = cddb_linkhead; p != NULL; p = q) {
		q = p->next;
		if (p->dtitle != NULL)
			MEM_FREE(p->dtitle);
		MEM_FREE((char *) p);
	}
	cddb_linkhead = NULL;
}


/*
 * cddb_linkopts
 *	Return a pointer to the CDDB linkopts list.
 *
 * Args:
 *	None.
 *
 * Return:
 *	A pointer to the linkopts list head, or NULL if the list is empty.
 */
cddb_linkopts_t *
cddb_linkopts(void)
{
	return (cddb_linkhead);
}


/*
 * cddb_rmt_support
 *	Returns information whether this cddb library supports remote
 *	CDDB servers.
 *
 * Args:
 *	None.
 *
 * Return:
 *	TRUE  - Remote CDDB server supported
 *	FALSE - Remote CDDB server not supported
 */
bool_t
cddb_rmt_support(void)
{
	cddb_path_t	*pp;

	for (pp = cddb_pathhead; pp != NULL; pp = pp->next) {
		if (pp->type == CDDB_REMOTE)
			return TRUE;
	}
	return FALSE;
}


/*
 * cddb_curfileupd
 *	Update the current disc info file.
 *
 * Args:
 *	dbp - Pointer to the cddb_incore_t structure.
 *
 * Return:
 *	Nothing
 */
void
cddb_curfileupd(cddb_incore_t *dbp)
{
#ifndef __VMS
	FILE		*fp;
	struct stat	stbuf;
	char		str[STR_BUF_SZ + FILE_PATH_SZ];
	static char	prev[STR_BUF_SZ + FILE_PATH_SZ];

	if (dbp->discid != 0)
		sprintf(str, "Device:\t\t%s\nCategory:\t%s\nDiscID:\t\t%08x\n",
			app_data.device, dbp->category, dbp->discid);
	else
		sprintf(str,
			"Device:\t\t%s\nCategory:\t-\nDiscID:\t\tNo_Disc\n",
			app_data.device);

	if (strcmp(str, prev) == 0)
		/* No change */
		return;

	/* Write to file */
	if (curfile[0] == '\0') {
		if (stat(app_data.device, &stbuf) < 0) {
			DBGPRN(errfp, "\nCannot stat %s\n", app_data.device);
			return;
		}

		sprintf(curfile, "%s/curr.%x", TEMP_DIR, stbuf.st_rdev);
		DBGPRN(errfp, "\nCurrent disc info file: %s\n", curfile);
	}

	/* Remove original file */
	if (unlink(curfile) < 0 && errno != ENOENT) {
		DBGPRN(errfp, "\nCannot unlink old %s\n", curfile);
		return;
	}

	/* Write new file */
	if ((fp = fopen(curfile, "w")) == NULL) {
		DBGPRN(errfp, "\nCannot open %s for writing\n", curfile);
		return;
	}
	fprintf(fp, "#\n# Xmcd current CD information\n#\n%s", str);
	fclose(fp);
	chmod(curfile, 0644);
	
	strcpy(prev, str);
#endif	/* __VMS */
}


/*
 * cddb_dump_incore
 *	Displays the contents of the cddb_incore_t structure.
 *
 * Args:
 *	dbp - Pointer to the cddb_incore_t structure.
 *
 * Returns:
 *	Nothing.
 */
void
cddb_dump_incore(cddb_incore_t *dbp)
{
	int		i;
	cddb_match_t	*p;

	fprintf(errfp,
		"\nDumping the cddb_incore_t structure at 0x%x:\n", dbp);
	fprintf(errfp, "discid=%08x type=%d flags=0x%x ntrk=%d\n",
		dbp->discid, dbp->type, dbp->flags, dbp->ntrk);
	fprintf(errfp, "dbfile=%s category=%s\n",
		dbp->dbfile == NULL ? "NULL" : dbp->dbfile, dbp->category);
	fprintf(errfp, "dtitle=%s\n",
		dbp->dtitle == NULL ? "NULL" : dbp->dtitle);

	for (i = 0; i < (int) dbp->ntrk; i++) {
		fprintf(errfp, "trklist[%d]=%s\n", i,
			dbp->trklist[i] == NULL ? "NULL" : dbp->trklist[i]);
	}

	if (dbp->extd == NULL)
		fprintf(errfp, "extd=NULL\n");
	else
		fprintf(errfp, "extd=\n%s\n", dbp->extd);

	for (i = 0; i < (int) dbp->ntrk; i++) {
		if (dbp->extt[i] == NULL)
			fprintf(errfp, "extt[%d]=NULL\n", i);
		else
			fprintf(errfp, "extt[%d]=\n%s\n", i, dbp->extt[i]);
	}

	for (i = 0; i < (int) dbp->ntrk; i++) {
		if (dbp->sav_extt[i] == NULL)
			fprintf(errfp, "sav_extt[%d]=NULL\n", i);
		else
			fprintf(errfp, "sav_extt[%d]=\n%s\n", i,
				dbp->sav_extt[i]);
	}

	fprintf(errfp, "playorder=%s\n",
		dbp->playorder == NULL ? "NULL" : dbp->playorder);

	if (dbp->matchlist == NULL)
		fprintf(errfp, "matchlist=NULL\n");
	else {
		for (p = dbp->matchlist, i = 0; p != NULL; p = p->next, i++) {
			fprintf(errfp, "matchlist[%d] ptr=0x%x categ=%s ",
				i, p, p->category);
			fprintf(errfp, "discid=%08x dtitle=%s\n",
				p->discid, p->dtitle);
		}
	}
	if (dbp->match_cur == NULL)
		fprintf(errfp, "match_cur=NULL\n");
	else
		fprintf(errfp, "match_cur=0x%x\n", dbp->match_cur);
}


