/*
 *   libcddb - CDDB Interface Library for xmcd/cda
 *
 *	This library implements an interface to access the "classic"
 *	CDDB1 services.
 *
 *   Copyright (C) 1993-2001  Ti Kan
 *   E-mail: xmcd@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     *_fcddb_c_ident_ = "@(#)fcddb.c	1.52 01/07/09";
#endif

#include "fcddb.h"
#include "cddbp.h"
#include "genretbl.h"
#include "regiontbl.h"
#include "roletbl.h"

#ifndef __VMS
/* UNIX */

#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#if defined(_AIX) || defined(__QNX__)
#include <sys/select.h>
#endif

/* Directory path name convention */
#define DIR_BEG			'/'		/* Directory start */
#define DIR_SEP			'/'		/* Directory separator */
#define DIR_END			'/'		/* Directory end */

/* Defines used by fcddb_runcmd */
#define STR_SHPATH		"/bin/sh"	/* Path to shell */
#define STR_SHNAME		"sh"		/* Name of shell */
#define STR_SHARG		"-c"		/* Shell arg */
#define EXE_ERR			255		/* Exec error */

#else
/* OpenVMS */

#ifndef NOREMOTE
#include <socket.h>
#include <time.h>
#include <in.h>
#include <netdb.h>
#endif	/* NOREMOTE */

/* Directory path name convention */
#define DIR_BEG			'['		/* Directory start */
#define DIR_SEP			'.'		/* Directory separator */
#define DIR_END			']'		/* Directory end */

#endif	/* __VMS */

/* Max host name length */
#ifndef MAXHOSTNAMELEN
#define MAXHOSTNAMELEN		64
#endif

/* Minimum/maximum macros */
#define FCDDB2_MIN(a,b)		(((a) > (b)) ? (b) : (a))
#define FCDDB2_MAX(a,b)		(((a) > (b)) ? (a) : (b))

/* Skip white space macro */
#define SKIP_SPC(p)		while (*(p) == ' ' || *(p) == '\t') (p)++;

/* CDDBD status code macros */
#define STATCODE_1ST(p)		((p)[0])
#define STATCODE_2ND(p)		((p)[1])
#define STATCODE_3RD(p)		((p)[2])
#define STATCODE_CHECK(p)	\
	(STATCODE_1ST(p) != '\0' && isdigit((int) STATCODE_1ST(p)) && \
	 STATCODE_2ND(p) != '\0' && isdigit((int) STATCODE_2ND(p)) && \
	 STATCODE_3RD(p) != '\0' && isdigit((int) STATCODE_3RD(p)))

/* HTTP status codes */
#define HTTP_PROXYAUTH_FAIL	407

/* Number of seconds per day */
#define SECS_PER_DAY		(24 * 60 * 60)


bool_t		fcddb_debug;		/* Debug flag */
FILE		*fcddb_errfp;		/* Debug message file stream */

STATIC char	fcddb_hellostr[MAXHOSTNAMELEN + (STR_BUF_SZ * 2)],
		fcddb_extinfo[MAXHOSTNAMELEN + (STR_BUF_SZ * 2)],
		*fcddb_auth_buf = NULL;


/*
 * fcddb_sum
 *	Compute a checksum number for use by fcddb_discid
 *
 * Args:
 *	n - A numeric value
 *
 * Return:
 *	The checksum of n
 */
STATIC int
fcddb_sum(int n)
{
 	int	ret;

	/* For backward compatibility this algorithm must not change */
	for (ret = 0; n > 0; n /= 10)
		ret += n % 10;

	return (ret);
}


/*
 * fcddb_line_filter
 *	Line filter to prevent multi-line strings from being stored in
 *	single-line fields.
 *
 * Args:
 *	str - The field string
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_line_filter(char *str)
{
	if (str == NULL)
		return;

	for (; *str != '\0'; str++) {
		if (*str == '\\' && *(str+1) == 'n') {
			*str = '\0';
			break;
		}
	}
}


/*
 * fcddb_strcat
 *	Similar to strcat() except this handles special meta characters
 *	in CDDB data.
 *
 * Args:
 *	s1 - target string
 *	s2 - source string
 *
 * Return:
 *	Pointer to target string if successful, or NULL on failure
 */
STATIC char *
fcddb_strcat(char *s1, char *s2)
{
	int	n;
	char	*cp = s1;
	bool_t	proc_slash;

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

	/* Concatenate two strings, with special handling for newline
	 * and tab characters.
	 */
	proc_slash = FALSE;
	n = strlen(s1);
	s1 += n;

	if (n > 0 && *(s1 - 1) == '\\') {
		proc_slash = TRUE;	/* Handle broken escape sequences */
		s1--;
	}

	for (; *s2 != '\0'; s1++, s2++) {
		if (*s2 == '\\') {
			if (proc_slash) {
				proc_slash = FALSE;
				continue;
			}
			proc_slash = TRUE;
			s2++;
		}

		if (proc_slash) {
			proc_slash = FALSE;

			switch (*s2) {
			case 'n':
				*s1 = '\n';
				break;
			case 't':
				*s1 = '\t';
				break;
			case '\\':
				*s1 = '\\';
				break;
			case '\0':
				*s1 = '\\';
				s2--;
				break;
			default:
				*s1++ = '\\';
				*s1 = *s2;
				break;
			}
		}
		else
			*s1 = *s2;
	}
	*s1 = '\0';

	return (cp);
}


/*
 * fcddb_http_xlat
 *	String translator that handles HTTP character escape sequences
 *
 * Args:
 *	s1 - source string
 *	s2 - target string
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_http_xlat(char *s1, char *s2)
{
	char	*p,
		*q;

	for (p = s1, q = s2; *p != '\0'; p++) {
		switch (*p) {
		case '?':
		case '=':
		case '+':
		case '&':
		case ' ':
		case '%':
			(void) sprintf(q, "%%%02X", *p);
			q += 3;
			break;
		default:
			*q = *p;
			q++;
			break;
		}
	}
	*q = '\0';
}


/*
 * Data used by fcddb_b64encode
 */
STATIC char	b64map[] = {
	'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
	'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
	'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
	'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
	'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
	'8', '9', '+', '/'
};

#define B64_PAD		'='


/*
 * fcddb_b64encode
 *	Base64 encoding function
 *
 * Args:
 *	ibuf - Input string buffer
 *	len - Number of characters
 *	obuf - Output string buffer
 *	brklines - Whether to break output into multiple lines
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_b64encode(byte_t *ibuf, int len, byte_t *obuf, bool_t brklines)
{
	int	i, j, k, n,
		c[4];
	byte_t	sbuf[4];

	for (i = k = 0; (i + 3) <= len; i += 3, ibuf += 3) {
		c[0] = ((int) ibuf[0] >> 2);
		c[1] = ((((int) ibuf[0] & 0x03) << 4) |
			(((int) ibuf[1] & 0xf0) >> 4));
		c[2] = ((((int) ibuf[1] & 0x0f) << 2) |
			(((int) ibuf[2] & 0xc0) >> 6));
		c[3] = ((int) ibuf[2] & 0x3f);

		for (j = 0; j < 4; j++)
			*obuf++ = b64map[c[j]];

		if (brklines && ++k == 16) {
			k = 0;
			*obuf++ = '\n';
		}
	}

	if (i < len) {
		n = len - i;
		(void) strncpy((char *) sbuf, (char *) ibuf, n);
		for (j = n; j < 3; j++)
			sbuf[j] = (unsigned char) 0;

		n++;
		ibuf = sbuf;
		c[0] = ((int) ibuf[0] >> 2);
		c[1] = ((((int) ibuf[0] & 0x03) << 4) |
			(((int) ibuf[1] & 0xf0) >> 4));
		c[2] = ((((int) ibuf[1] & 0x0f) << 2) |
			(((int) ibuf[2] & 0xc0) >> 6));
		c[3] = ((int) ibuf[2] & 0x3f);

		for (j = 0; j < 4; j++)
			*obuf++ = (j < n) ? b64map[c[j]] : B64_PAD;

		if (brklines && ++k == 16)
			*obuf++ = '\n';
	}

	if (brklines)
		*obuf++ = '\n';

	*obuf = '\0';
}


/*
 * fcddb2_clear_url
 *	Clear a URL structure
 *
 * Args:
 *	up - Pointer to the URL structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb2_clear_url(cddb_url_t *up)
{
	if (up->type != NULL) {
		MEM_FREE(up->type);
		up->type = NULL;
	}
	if (up->href != NULL) {
		MEM_FREE(up->href);
		up->href = NULL;
	}
	if (up->displaylink != NULL) {
		MEM_FREE(up->displaylink);
		up->displaylink = NULL;
	}
	if (up->displaytext != NULL) {
		MEM_FREE(up->displaytext);
		up->displaytext = NULL;
	}
	if (up->category != NULL) {
		MEM_FREE(up->category);
		up->category = NULL;
	}
	if (up->description != NULL) {
		MEM_FREE(up->description);
		up->description = NULL;
	}
}


/*
 * fcddb2_clear_urllist
 *	Clear a URL list structure
 *
 * Args:
 *	up - Pointer to the URL list structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_urllist(cddb_urllist_t *ulp)
{
	cddb_url_t	*up,
			*nextp;


	for (up = ulp->urls; up != NULL; up = nextp) {
		nextp = up->next;
		fcddb2_clear_url(up);
		MEM_FREE(up);
	}
	ulp->urls = NULL;
	ulp->count = 0;
}


/*
 * fcddb2_clear_urlmanager
 *	Clear a URL manager structure
 *
 * Args:
 *	up - Pointer to the URL manager structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_urlmanager(cddb_urlmanager_t *mp)
{
	cddb_urllist_t	*ulp = &mp->urllist;

	fcddb_clear_urllist(ulp);
	mp->control = NULL;
}


/*
 * fcddb2_clear_fullname
 *	Clear a full name structure
 *
 * Args:
 *	up - Pointer to the full name structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_fullname(cddb_fullname_t *fnp)
{
	if (fnp->name != NULL) {
		MEM_FREE(fnp->name);
		fnp->name = NULL;
	}
	if (fnp->lastname != NULL) {
		MEM_FREE(fnp->lastname);
		fnp->lastname = NULL;
	}
	if (fnp->firstname != NULL) {
		MEM_FREE(fnp->firstname);
		fnp->firstname = NULL;
	}
	if (fnp->the != NULL) {
		MEM_FREE(fnp->the);
		fnp->the = NULL;
	}
}


/*
 * fcddb2_clear_credit
 *	Clear a credit structure
 *
 * Args:
 *	up - Pointer to the credit structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_credit(cddb_credit_t *cp)
{
	fcddb_clear_fullname(&cp->fullname);
	if (cp->notes != NULL) {
		MEM_FREE(cp->notes);
		cp->notes = NULL;
	}
	cp->role = NULL;
	cp->next = NULL;
}


/*
 * fcddb2_clear_credits
 *	Clear a credits structure
 *
 * Args:
 *	up - Pointer to the credits structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_credits(cddb_credits_t *csp)
{
	cddb_credit_t	*cp,
			*nextp;

	for (cp = csp->credits; cp != NULL; cp = nextp) {
		nextp = cp->next;
		fcddb_clear_credit(cp);
		MEM_FREE(cp);
	}
	csp->credits = NULL;
	csp->count = 0;
}


/*
 * fcddb2_clear_segment
 *	Clear a segment structure
 *
 * Args:
 *	up - Pointer to the segment structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_segment(cddb_segment_t *sp)
{
	fcddb_clear_credits(&sp->credits);

	if (sp->name != NULL) {
		MEM_FREE(sp->name);
		sp->name = NULL;
	}
	if (sp->notes != NULL) {
		MEM_FREE(sp->notes);
		sp->notes = NULL;
	}
	if (sp->starttrack != NULL) {
		MEM_FREE(sp->starttrack);
		sp->starttrack = NULL;
	}
	if (sp->startframe != NULL) {
		MEM_FREE(sp->startframe);
		sp->startframe = NULL;
	}
	if (sp->endtrack != NULL) {
		MEM_FREE(sp->endtrack);
		sp->endtrack = NULL;
	}
	if (sp->endframe != NULL) {
		MEM_FREE(sp->endframe);
		sp->endframe = NULL;
	}
	sp->control = NULL;
	sp->next = NULL;
}


/*
 * fcddb2_clear_segments
 *	Clear a segments structure
 *
 * Args:
 *	up - Pointer to the segments structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_segments(cddb_segments_t *ssp)
{
	cddb_segment_t	*sp,
			*nextp;

	for (sp = ssp->segments; sp != NULL; sp = nextp) {
		nextp = sp->next;
		fcddb_clear_segment(sp);
		MEM_FREE(sp);
	}
	ssp->segments = NULL;
	ssp->count = 0;
}


/*
 * fcddb2_clear_genretree
 *	Clear a genre tree structure
 *
 * Args:
 *	up - Pointer to the genre tree structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_genretree(cddb_genretree_t *gp)
{
	cddb_genre_t	*p,
			*q,
			*nextp,
			*nextq;

	/* Free genre tree */
	nextp = NULL;
	for (p = gp->genres; p != NULL; p = nextp) {
		nextp = p->nextmeta;

		nextq = NULL;
		for (q = p->next; q != NULL; q = nextq) {
			nextq = q->next;

			if (q->id != NULL)
				MEM_FREE(q->id);
			if (q->name != NULL)
				MEM_FREE(q->name);

			MEM_FREE(q);
		}

		if (p->id != NULL)
			MEM_FREE(p->id);
		if (p->name != NULL)
			MEM_FREE(p->name);

		MEM_FREE(p);
	}
	gp->genres = NULL;
	gp->count = 0;
}


/*
 * fcddb2_clear_roletree
 *	Clear a role tree structure
 *
 * Args:
 *	up - Pointer to the role tree structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_roletree(cddb_roletree_t *rtp)
{
	cddb_rolelist_t	*rlp,
			*nextp;

	nextp = NULL;
	for (rlp = rtp->rolelists; rlp != NULL; rlp = nextp) {
		nextp = rlp->next;
		MEM_FREE(rlp);
	}
	rtp->rolelists = NULL;
	rtp->count = 0;
}


/*
 * fcddb2_clear_userinfo
 *	Clear a user info structure
 *
 * Args:
 *	up - Pointer to the user info structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_userinfo(cddb_userinfo_t *up)
{
	if (up->userhandle != NULL) {
		MEM_FREE(up->userhandle);
		up->userhandle = NULL;
	}
	if (up->password != NULL) {
		MEM_FREE(up->password);
		up->password = NULL;
	}
	if (up->passwordhint != NULL) {
		MEM_FREE(up->passwordhint);
		up->passwordhint = NULL;
	}
	if (up->emailaddress != NULL) {
		MEM_FREE(up->emailaddress);
		up->emailaddress = NULL;
	}
	if (up->regionid != NULL) {
		MEM_FREE(up->regionid);
		up->regionid = NULL;
	}
	if (up->postalcode != NULL) {
		MEM_FREE(up->postalcode);
		up->postalcode = NULL;
	}
	if (up->age != NULL) {
		MEM_FREE(up->age);
		up->age = NULL;
	}
	if (up->sex != NULL) {
		MEM_FREE(up->sex);
		up->sex = NULL;
	}
	up->allowemail = 0;
	up->allowstats = 0;
}


/*
 * fcddb2_clear_options
 *	Clear a options structure
 *
 * Args:
 *	up - Pointer to the options structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_options(cddb_options_t *op)
{
	if (op->proxyserver != NULL) {
		MEM_FREE(op->proxyserver);
		op->proxyserver = NULL;
	}
	if (op->proxyusername != NULL) {
		MEM_FREE(op->proxyusername);
		op->proxyusername = NULL;
	}
	if (op->proxypassword != NULL) {
		MEM_FREE(op->proxypassword);
		op->proxypassword = NULL;
	}
	if (op->localcachepath != NULL) {
		MEM_FREE(op->localcachepath);
		op->localcachepath = NULL;
	}
	op->proxyport = 0;
	op->servertimeout = 0;
	op->testsubmitmode = 0;
	op->localcachetimeout = 0;
}


/*
 * fcddb2_clear_track
 *	Clear a track structure
 *
 * Args:
 *	up - Pointer to the track structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_track(cddb_track_t *tp)
{
	fcddb_clear_credits(&tp->credits);

	if (tp->title != NULL) {
		MEM_FREE(tp->title);
		tp->title = NULL;
	}
	if (tp->notes != NULL) {
		MEM_FREE(tp->notes);
		tp->notes = NULL;
	}
	tp->control = NULL;
}


/*
 * fcddb2_clear_tracks
 *	Clear a tracks structure
 *
 * Args:
 *	up - Pointer to the tracks structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_tracks(cddb_tracks_t *tsp)
{
	int	i;

	for (i = 0; i < MAXTRACK; i++)
		fcddb_clear_track(&tsp->track[i]);

	tsp->count = 0;
}


/*
 * fcddb2_clear_disc
 *	Clear a disc structure
 *
 * Args:
 *	up - Pointer to the disc structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_disc(cddb_disc_t *dp)
{
	fcddb_idlist_t	*ip,
			*nextp;

	fcddb_clear_credits(&dp->credits);
	fcddb_clear_segments(&dp->segments);
	fcddb_clear_fullname(&dp->fullname);
	fcddb_clear_tracks(&dp->tracks);

	if (dp->toc != NULL) {
		MEM_FREE(dp->toc);
		dp->toc = NULL;
	}
	if (dp->title != NULL) {
		MEM_FREE(dp->title);
		dp->title = NULL;
	}
	if (dp->notes != NULL) {
		MEM_FREE(dp->notes);
		dp->notes = NULL;
	}
	if (dp->revision != NULL) {
		MEM_FREE(dp->revision);
		dp->revision = NULL;
	}
	if (dp->submitter != NULL) {
		MEM_FREE(dp->submitter);
		dp->submitter = NULL;
	}

	dp->next = NULL;
	dp->discid = 0;
	dp->genre = NULL;
	dp->control = NULL;

	nextp = NULL;
	for (ip = dp->idlist; ip != NULL; ip = nextp) {
		nextp = ip->next;
		MEM_FREE(ip);
	}
	dp->idlist = NULL;
}


/*
 * fcddb2_clear_discs
 *	Clear a discs structure
 *
 * Args:
 *	up - Pointer to the discs structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_discs(cddb_discs_t *dsp)
{
	cddb_disc_t	*dp,
			*nextp;

	nextp = NULL;
	for (dp = dsp->discs; dp != NULL; dp = nextp) {
		nextp = dp->next;
		fcddb_clear_disc(dp);
		MEM_FREE(dp);
	}

	dsp->discs = NULL;
	dsp->count = 0;
}


/*
 * fcddb2_clear_control
 *	Clear a control structure
 *
 * Args:
 *	up - Pointer to the control structure
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_clear_control(cddb_control_t *cp)
{
	if (cp->hostname != NULL) {
		MEM_FREE(cp->hostname);
		cp->hostname = NULL;
	}
	if (cp->clientname != NULL) {
		MEM_FREE(cp->clientname);
		cp->clientname = NULL;
	}
	if (cp->clientid != NULL) {
		MEM_FREE(cp->clientid);
		cp->clientid = NULL;
	}
	if (cp->clientver != NULL) {
		MEM_FREE(cp->clientver);
		cp->clientver = NULL;
	}
	fcddb_clear_userinfo(&cp->userinfo);
	fcddb_clear_options(&cp->options);
	fcddb_clear_disc(&cp->disc);
	fcddb_clear_discs(&cp->discs);
	fcddb_clear_genretree(&cp->genretree);
	fcddb_clear_roletree(&cp->roletree);
	fcddb_clear_urllist(&cp->urllist);
}


/*
 * fcddb2_putentry
 *	Write an entry into a CDDB file, used by fcddb_write_cddb()
 *
 * Args:
 *	fp - FILE stream pointer
 *	idstr - The entry identifier
 *	entry - The entry string 
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_putentry(FILE *fp, char *idstr, char *entry)
{
	int	i,
		n;
	char	*cp,
		tmpbuf[STR_BUF_SZ];

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

		do {
			(void) sprintf(tmpbuf, "%s=", idstr);
			(void) fputs(tmpbuf, fp);

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

			for (i = 0; i < n; i++, cp++) {
				switch (*cp) {
				case '\n':
					(void) fputs("\\n", fp);
					break;
				case '\t':
					(void) fputs("\\t", fp);
					break;
				case '\\':
					(void) fputs("\\\\", fp);
					break;
				default:
					(void) fputc(*cp, fp);
					break;
				}
			}

			(void) fputs("\r\n", fp);
			if (*cp == '\0')
				break;

		} while (n == STR_BUF_SZ);
	}
}


/*
 * fcddb_write_cddb
 *	Write a CDDB file
 *
 * Args:
 *	path - file path
 *	dp - Pointer to disc structure
 *	issubmit - Whether this function is invoked during a submit
 *
 * Return:
 *	CddbResult status code
 */
STATIC CddbResult
fcddb_write_cddb(char *path, cddb_disc_t *dp, bool_t issubmit)
{
	cddb_control_t	*cp = (cddb_control_t *) dp->control;
	FILE		*fp;
	CddbResult	ret;
	int		i,
			revision,
			ntrks;
	fcddb_idlist_t	*ip;
	char		*dir,
			idstr[12],
			tmpbuf[STR_BUF_SZ * 2];
	unsigned int	addr[MAXTRACK];

	if (cp == NULL)
		return Cddb_E_INVALIDARG;

	dir = fcddb_dirname(path);
	FCDDBDBG(fcddb_errfp, "fcddb_write_cddb: making %s\n", dir);
	if ((ret = fcddb_mkdir(dir, 0755)) != Cddb_OK)
		return (ret);

	(void) unlink(path);

	FCDDBDBG(fcddb_errfp, "fcddb_write_cddb: writing %s\n", path);
	if ((fp = fopen(path, "w")) == NULL)
		return CDDBTRNCannotCreateFile;

	/* File header */
	(void) fputs(
		"# xmcd CD database file\r\n"
		"# Copyright (C) 2001 CDDB, Inc.\r\n#\r\n",
		fp
	);

	ntrks = fcddb_parse_toc((char *) dp->toc, addr);
	if (ntrks == 0 || ntrks != (int) (dp->discid & 0xff)) {
		(void) fclose(fp);
		(void) unlink(path);
		return Cddb_E_INVALIDARG;
	}

	/* Track frame offsets */
	(void) fputs("# Track frame offsets:\r\n", fp);
	for (i = 0; i < ntrks; i++) {
		(void) sprintf(tmpbuf, "#\t%u\r\n", addr[i]);
		(void) fputs(tmpbuf, fp);
	}

	/* Disc length */
	(void) sprintf(tmpbuf, "#\r\n# Disc length: %u seconds\r\n#\r\n",
		       addr[ntrks] / FRAME_PER_SEC);
	(void) fputs(tmpbuf, fp);

	/* Revision */
	if (dp->revision == NULL)
		revision = 1;
	else {
		revision = atoi((char *) dp->revision);
		if (revision >= 0 && issubmit)
			revision++;
	}
	(void) sprintf(tmpbuf, "# Revision: %d\r\n", revision);
	(void) fputs(tmpbuf, fp);

	/* Submitter */
	if (issubmit || dp->submitter == NULL)
		(void) sprintf(tmpbuf, "# Submitted via: %s %s\r\n#\r\n",
			       cp->clientname, cp->clientver);
	else
		(void) sprintf(tmpbuf, "# Submitted via: %s\r\n#\r\n",
			       dp->submitter);
	(void) fputs(tmpbuf, fp);

	/* Disc IDs */
	(void) sprintf(tmpbuf, "DISCID=%08x", dp->discid);
	i = 1;
	for (ip = dp->idlist; ip != NULL; ip = ip->next) {
		if (ip->discid == dp->discid)
			/* This is our own ID, which we already processed */
			continue;

		if (i == 0)
			(void) sprintf(tmpbuf, "DISCID=%08x", ip->discid);
		else
			(void) sprintf(tmpbuf, "%s,%08x", tmpbuf, ip->discid);

		i++;
		if (i == 8) {
			(void) strcat(tmpbuf, "\r\n");
			(void) fputs(tmpbuf, fp);
			i = 0;
		}
	}
	if (i != 0) {
		(void) strcat(tmpbuf, "\r\n");
		(void) fputs(tmpbuf, fp);
	}

	/* Disc artist/title */
	(void) sprintf(tmpbuf, "%s%s%s",
		       (dp->fullname.name == NULL) ?
				"" : dp->fullname.name,
		       (dp->fullname.name != NULL && dp->title != NULL) ?
				" / " : "",
		       (dp->title == NULL) ?
				"" : dp->title);
	fcddb_putentry(fp, "DTITLE", tmpbuf);

	/* Track titles */
	for (i = 0; i < ntrks; i++) {
		(void) sprintf(idstr, "TTITLE%u", i);
		fcddb_putentry(fp, idstr, dp->tracks.track[i].title);
	}

	/* Disc notes */
	fcddb_putentry(fp, "EXTD", dp->notes);

	/* Track notes */
	for (i = 0; i < ntrks; i++) {
		(void) sprintf(idstr, "EXTT%u", i);
		fcddb_putentry(fp, idstr, dp->tracks.track[i].notes);
	}

	/* Track program sequence */
	fcddb_putentry(fp, "PLAYORDER", NULL);

	(void) fclose(fp);

	return Cddb_OK;
}


/*
 * fcddb_parse_cddb_data
 *	Parse CDDB data and fill the disc structure
 *
 * Args:
 *	cp - Pointer to the control structure
 *	buf - CDDB data
 *	discid - The disc ID
 *	category - The category
 *	toc - The TOC
 *	wrcache - Whether to write to cache
 *	dp - Pointer to disc structure
 *
 * Return:
 *	CddbResult status code
 */
STATIC CddbResult
fcddb_parse_cddb_data(
	cddb_control_t	*cp,
	char		*buf,
	word32_t	discid,
	char		*category,
	char		*toc,
	bool_t		wrcache,
	cddb_disc_t	*dp
)
{
	char		*p,
			*q,
			*r,
			filepath[FILE_PATH_SZ];
	fcddb_idlist_t	*ip;
	int		n;
	CddbResult	ret;

	FCDDBDBG(fcddb_errfp, "fcddb_parse_cddb_data: %s/%08x\n",
		 category, discid);

	ret = Cddb_OK;
	p = buf;
	for (;;) {
		SKIP_SPC(p);

		if (*p == '.' || *p == '\0')
			break;			/* Done */

		if ((q = strchr(p, '\n')) != NULL)
			*q = '\0';

		n = strlen(p) - 1;
		if (*(p + n) == '\r')
			*(p + n) = '\0';	/* Zap carriage return */

		if (*p == '#') {		/* Comment */
			if (strncmp(p, "# Revision: ", 12) == 0)
				dp->revision = fcddb_strdup(p + 12);
			else if (strncmp(p, "# Submitted via: ", 17) == 0)
				dp->submitter = fcddb_strdup(p + 17);
		}
		else if (strncmp(p, "DISCID=", 7) == 0) {
			p += 7;
			while ((r = strchr(p, ',')) != NULL) {
				*r = '\0';
				ip = (fcddb_idlist_t *) MEM_ALLOC(
					"idlist",
					sizeof(fcddb_idlist_t)
				);
				if (ip == NULL) {
					ret = CDDBTRNOutOfMemory;
					break;
				}
				(void) sscanf(p, "%x",
					      (unsigned int *) &ip->discid);
				ip->next = dp->idlist;
				dp->idlist = ip;
				p = r+1;
			}
			if (ret != Cddb_OK)
				break;

			ip = (fcddb_idlist_t *) MEM_ALLOC(
				"idlist",
				sizeof(fcddb_idlist_t)
			);
			if (ip == NULL) {
				ret = CDDBTRNOutOfMemory;
				break;
			}
			(void) sscanf(p, "%x", (unsigned int *) &ip->discid);
			ip->next = dp->idlist;
			dp->idlist = ip;
		}
		else if (strncmp(p, "DTITLE=", 7) == 0) {
			/* Disc artist / title */
			p += 7;

			fcddb_line_filter(p);

			/* Put it all in artist field first,
			 * we'll separate artist and title later.
			 */
			if (dp->fullname.name == NULL) {
				dp->fullname.name = (char *) MEM_ALLOC(
					"dp->fullname.name",
					strlen(p) + 1
				);
				if (dp->fullname.name != NULL)
					dp->fullname.name[0] = '\0';
			}
			else {
				dp->fullname.name = (char *) MEM_REALLOC(
					"dp->fullname.name",
					dp->fullname.name,
					strlen(dp->fullname.name) +
						strlen(p) + 1
				);
			}

			if (dp->fullname.name == NULL) {
				ret = CDDBTRNOutOfMemory;
				break;
			}

			(void) fcddb_strcat(dp->fullname.name, p);
		}
		else if (sscanf(p, "TTITLE%u=", &n) > 0) {
			/* Track title */
			p = strchr(p, '=');
			p++;

			if (n >= (int) (discid & 0xff))
				continue;

			fcddb_line_filter(p);

			if (dp->tracks.track[n].title == NULL) {
				dp->tracks.track[n].title = (char *) MEM_ALLOC(
					"track[n].title",
					strlen(p) + 1
				);
				if (dp->tracks.track[n].title != NULL)
					dp->tracks.track[n].title[0] = '\0';
			}
			else {
				dp->tracks.track[n].title = (char *)
				    MEM_REALLOC(
					"track[n].title",
					dp->tracks.track[n].title,
					strlen(dp->tracks.track[n].title) +
						strlen(p) + 1
				    );
			}

			if (dp->tracks.track[n].title == NULL) {
				ret = CDDBTRNOutOfMemory;
				break;
			}

			(void) fcddb_strcat(dp->tracks.track[n].title, p);
		}
		else if (strncmp(p, "EXTD=", 5) == 0) {
			/* Disc notes */
			p += 5;

			if (dp->notes == NULL) {
				dp->notes = (char *) MEM_ALLOC(
					"dp->notes",
					strlen(p) + 1
				);
				if (dp->notes != NULL)
					dp->notes[0] = '\0';
			}
			else {
				dp->notes = (char *) MEM_REALLOC(
					"dp->notes",
					dp->notes,
					strlen(dp->notes) +
						strlen(p) + 1
				);
			}

			if (dp->notes == NULL) {
				ret = CDDBTRNOutOfMemory;
				break;
			}

			(void) fcddb_strcat(dp->notes, p);
		}
		else if (sscanf(p, "EXTT%u=", &n) > 0) {
			/* Track notes */
			p = strchr(p, '=');
			p++;

			if (n >= (int) (discid & 0xff))
				continue;

			if (dp->tracks.track[n].notes == NULL) {
				dp->tracks.track[n].notes = (char *)
				    MEM_ALLOC(
					"track[n].notes",
					strlen(p) + 1
				    );

				if (dp->tracks.track[n].notes != NULL)
					dp->tracks.track[n].notes[0] = '\0';
			}
			else {
				dp->tracks.track[n].notes = (char *)
				    MEM_REALLOC(
					"track[n].notes",
					dp->tracks.track[n].notes,
					strlen(dp->tracks.track[n].notes) +
						strlen(p) + 1
				    );
			}

			if (dp->tracks.track[n].notes == NULL) {
				ret = CDDBTRNOutOfMemory;
				break;
			}

			(void) fcddb_strcat(dp->tracks.track[n].notes, p);
		}

		p = q + 1;
	}

	if (ret != Cddb_OK)
		return (ret);

	/* Separate into disc artist & title fields */
	if ((p = strchr(dp->fullname.name, '/')) != NULL &&
	     p > (dp->fullname.name + 1) &&
	    *(p-1) == ' ' && *(p+1) == ' ' && *(p+2) != '\0') {
		q = dp->fullname.name;

		/* Artist */
		*(p-1) = '\0';
		dp->fullname.name = (CddbStr) fcddb_strdup(q);

		/* Title */
		dp->title = (CddbStr) fcddb_strdup(p+2);

		MEM_FREE(q);
	}
	else {
		/* Move the whole string to title */
		dp->title = dp->fullname.name;
		dp->fullname.name = NULL;
	}

	/* Set up other fields */
	dp->control = (void *) cp;
	dp->discid = discid;
	dp->toc = (CddbStr) fcddb_strdup(toc);
	dp->tracks.count = (long) (discid & 0xff);
	dp->genre = fcddb_genre_categ2gp(cp, category);

	if (wrcache) {
#ifdef __VMS
		(void) sprintf(filepath, "%s.%s]%08x",
			       cp->options.localcachepath, category, discid);
#else
		(void) sprintf(filepath, "%s/%s/%08x",
			       cp->options.localcachepath, category, discid);
#endif

		(void) fcddb_write_cddb(filepath, dp, FALSE);
	}

	return Cddb_OK;
}


/*
 * fcddb_connect
 *	Make a network connection to the server
 *
 * Args:
 *	host - Server host name
 *	port - Server port
 *	fd - Return file descriptor
 *
 * Return:
 *	CddbResult status code
 */
#ifdef NOREMOTE
/*ARGSUSED*/
#endif
STATIC CddbResult
fcddb_connect(char *host, unsigned short port, int *fd)
{
#ifndef NOREMOTE
	struct hostent		*hp;
	struct in_addr		ad;
	struct sockaddr_in	sin;

	sin.sin_port = htons(port);
	sin.sin_family = AF_INET;

	FCDDBDBG(fcddb_errfp, "fcddb_connect: %s:%d\n", host, (int) port);
	/* Find server host address */
	if ((hp = gethostbyname(host)) != NULL) {
		(void) memcpy((char *) &sin.sin_addr, hp->h_addr,
			      hp->h_length);
	}
	else {
		if ((ad.s_addr = inet_addr(host)) != -1)
			(void) memcpy((char *) &sin.sin_addr,
				      (char *) &ad.s_addr, sizeof(ad.s_addr));
		else
			return CDDBTRNHostNotFound;
	}

	/* Open socket */
	if ((*fd = SOCKET(AF_INET, SOCK_STREAM, 0)) < 0) {
		*fd = -1;
		return (errno == EINTR ?
			CDDBTRNServerTimeout : CDDBTRNSockCreateErr);
	}

	/* Connect to server */
	if (CONNECT(*fd, (struct sockaddr *) &sin, sizeof(sin)) < 0) {
		(void) close(*fd);
		*fd = -1;
		return (errno == EINTR ?
			CDDBTRNServerTimeout : CDDBTRNSockOpenErr);
	}

	return Cddb_OK;
#else
	return CDDBCTLDisabled;
#endif	/* NOREMOTE */
}


/*
 * fcddb_sendcmd
 *	Send a command to the server, and get server response
 *
 * Args:
 *	fd - file descriptor returned by fcddb_connect()
 *	buf - command and response buffer (this function may reallocate
 *	      the buffer depending on response data size)
 *	buflen - The buffer size
 *	isproxy - Whether we're using a proxy server
 *
 * Return:
 *	CddbResult status code
 */
#ifdef NOREMOTE
/*ARGSUSED*/
#endif
STATIC CddbResult
fcddb_sendcmd(int fd, char **buf, size_t *buflen, bool_t isproxy)
{
#ifndef NOREMOTE
	int	len,
		tot,
		ret,
		rbuflen;
	char	*p;

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

	/* Send command to server */
	FCDDBDBG(fcddb_errfp,
		 "fcddb_sendcmd: Sending command:\n------\n%s\n------\n",
		 *buf);

	while (len > 0) {
		if ((ret = send(fd, p, len, 0)) < 0) {
			return (errno == EINTR ?
				CDDBTRNServerTimeout : CDDBTRNSendFailed);
		}

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

	if (tot == 0)
		return CDDBTRNSendFailed;

	/* Read server response to command */
	p = *buf;
	rbuflen = *buflen;

	for (;;) {
		/* Check remaining buffer size, enlarge if needed */
		if (rbuflen <= 64) {
			int	offset = p - *buf;

			*buf = (char *) MEM_REALLOC(
				"http_buf",
				*buf,
				*buflen + CDDBP_CMDLEN
			);
			if (*buf == NULL)
				return CDDBTRNOutOfMemory;

			rbuflen += CDDBP_CMDLEN;
			*buflen += CDDBP_CMDLEN;
			p = *buf + offset;
		}

		/* Receive response from server */
		if ((len = recv(fd, p, rbuflen-1, 0)) < 0) {
			return (errno == EINTR ?
				CDDBTRNServerTimeout : CDDBTRNRecvFailed);
		}

		*(p + len) = '\0';	/* Mark end of data */
		if (len == 0)
			break;

		rbuflen -= len;
		p += len;
	}

	FCDDBDBG(fcddb_errfp,
		 "fcddb_sendcmd: Server response:\n------\n%s\n------\n",
		 *buf);

	/* Check for proxy authorization failure */
	if (isproxy && strncmp(*buf, "HTTP/", 5) == 0) {
		p = strchr((*buf)+5, ' ');
		if (p != NULL && isdigit((int) *(p+1)) &&
		    (atoi(p+1) == HTTP_PROXYAUTH_FAIL)) {
			/* Need proxy authorization */
			return CDDBTRNHTTPProxyError;
		}
	}

	return Cddb_OK;
#else
	return CDDBCTLDisabled;
#endif	/* NOREMOTE */
}


/*
 * fcddb_disconnect
 *	Disconnect from a server
 *
 * Args:
 *	fd - File descriptor returned from fcddb_connect()
 *
 * Return:
 *	Nothing
 */
STATIC void
fcddb_disconnect(int fd)
{
	(void) close(fd);
}


/*
 * fcddb_query_cddb
 *	Send a query command to the server
 *
 * Args:
 *	cp - Pointer to the control structure
 *	discid - The disc ID
 *	toc - The TOC
 *	addr - Array of track offset frames
 *	conhost - Server host
 *	conport - Server port
 *	isproxy - Whether we're using a proxy server
 *	category - Return category string
 *	matchcode - Return match code
 *
 * Return:
 *	CddbResult status code
 */
#ifdef NOREMOTE
/*ARGSUSED*/
#endif
STATIC CddbResult
fcddb_query_cddb(
	cddb_control_t		*cp,
	word32_t		discid,
	char			*toc,
	unsigned int		*addr,
	char			*conhost,
	unsigned short		conport,
	bool_t			isproxy,
	char			*category,
	int			*matchcode
)
{
	int		i;
	size_t		buflen;
	FILE		*fp;
	cddb_disc_t	*dp;
	char		*buf,
			*p,
			*q,
			*r,
			*s,
			filepath[FILE_PATH_SZ];
	time_t		t;
	struct stat	stbuf;
#ifndef NOREMOTE
	CddbResult	ret;
	int		fd,
			ntrks;
	char		urlstr[MAXHOSTNAMELEN + FILE_PATH_SZ + 12];
	bool_t		valid_resp;
#endif

	fcddb_clear_discs(&cp->discs);

	buflen = CDDBP_CMDLEN;
	if ((buf = (char *) MEM_ALLOC("http_buf", buflen)) == NULL) {
		*matchcode = MATCH_NONE;
		return CDDBTRNOutOfMemory;
	}

	(void) time(&t);

	/* Try to locate the entry in the local cache */
	for (i = 0; fcddb_genre_map[i].cddb1name != NULL; i++) {
#ifdef __VMS
		(void) sprintf(filepath, "%s.%s]%08x",
			       cp->options.localcachepath,
			       fcddb_genre_map[i].cddb1name, discid);
#else
		(void) sprintf(filepath, "%s/%s/%08x",
			       cp->options.localcachepath,
			       fcddb_genre_map[i].cddb1name, discid);
#endif

		if (stat(filepath, &stbuf) < 0 || !S_ISREG(stbuf.st_mode))
			continue;

#ifndef NOREMOTE
		if ((cp->options.localcacheflags & CACHE_DONT_CONNECT) == 0 &&
		    (t - stbuf.st_mtime) >
			(int) (cp->options.localcachetimeout * SECS_PER_DAY)) {

			/* If any potential cached match is expired,
			 * force a server query.
			 */
			fcddb_clear_discs(&cp->discs);
			break;
		}
#endif

		fp = NULL;
		if ((fp = fopen(filepath, "r")) != NULL &&
		    fgets(buf, buflen, fp) != NULL &&
		    strncmp(buf, "# xmcd", 6) == 0) {

			/* Don't use fcddb_obj_alloc here because
			 * we don't want CddbReleaseObject to
			 * free it up.
			 */
			dp = (cddb_disc_t *) MEM_ALLOC(
				"CddbDisc", sizeof(cddb_disc_t)
			);
			if (dp == NULL) {
				MEM_FREE(buf);
				*matchcode = MATCH_NONE;
				return CDDBTRNOutOfMemory;
			}
			(void) memset(dp, 0, sizeof(cddb_disc_t));
			(void) strcpy(dp->objtype, "CddbDisc");

			/* Category: convert to genre */
			dp->genre = fcddb_genre_categ2gp(
				cp, fcddb_genre_map[i].cddb1name
			);

			/* Disc ID */
			dp->discid = discid;

			/* Artist/title */
			while (fgets(buf, buflen, fp) != NULL) {
				if (buf[0] == '#' ||
				    strncmp(buf, "DTITLE=", 7) != 0)
					continue;

				p = buf + 7;
				fcddb_line_filter(p);

				if ((q = strchr(p, '\r')) != NULL ||
				    (q = strchr(p, '\n')) != NULL)
					*q = '\0';

				/* Put it all in artist first */
				if (dp->fullname.name == NULL) {
					dp->fullname.name = (char *)
					MEM_ALLOC(
						"dp->fullname.name",
						strlen(p) + 1
					);
					if (dp->fullname.name != NULL)
						dp->fullname.name[0] = '\0';
				}
				else {
					dp->fullname.name = (char *)
					MEM_REALLOC(
						"dp->fullname.name",
						dp->fullname.name,
						strlen(dp->fullname.name) +
							strlen(p) + 1
					);
				}

				if (dp->fullname.name == NULL) {
					MEM_FREE(buf);
					*matchcode = MATCH_NONE;
					return CDDBTRNOutOfMemory;
				}

				(void) fcddb_strcat(dp->fullname.name, p);
			}

			/* Separate into disc artist & title fields */
			r = strchr(dp->fullname.name, '/');
			if (r != NULL && r > (dp->fullname.name + 1) &&
			    *(r-1) == ' ' && *(r+1) == ' '
			    && *(r+2) != '\0') {
				s = dp->fullname.name;

				/* Artist */
				*(r-1) = '\0';
				dp->fullname.name = (CddbStr) fcddb_strdup(s);

				/* Title */
				dp->title = (CddbStr) fcddb_strdup(r+2);

				MEM_FREE(s);
			}
			else {
				/* Move the whole string to title */
				dp->title = dp->fullname.name;
				dp->fullname.name = NULL;
			}

			/* TOC */
			dp->toc = (CddbStr) fcddb_strdup(toc);

			/* Add to discs list */
			dp->next = cp->discs.discs;
			cp->discs.discs = dp;
			cp->discs.count++;
		}

		if (fp != NULL)
			(void) fclose(fp);
	}

	if (cp->discs.count == 1) {
		/* Matched one local cache entry */
		*matchcode = MATCH_EXACT;
		(void) strcpy(category,
			fcddb_genre_id2categ(cp->discs.discs->genre->id));
		fcddb_clear_discs(&cp->discs);
		MEM_FREE(buf);

		FCDDBDBG(fcddb_errfp,
			 "\nfcddb_query_cddb: Exact match in cache %s/%08x\n",
			 category, discid);
		return Cddb_OK;
	}
	else if (cp->discs.count > 1) {
		/* Matched multiple local cache entries */
		*matchcode = MATCH_MULTIPLE;
		MEM_FREE(buf);

		FCDDBDBG(fcddb_errfp,
			 "\nfcddb_query_cddb: Multiple matches in cache\n");
		return Cddb_OK;
	}

#ifdef NOREMOTE
	*matchcode = MATCH_NONE;
	MEM_FREE(buf);

	FCDDBDBG(fcddb_errfp, "\nfcddb_query_cddb: No match in cache\n");
	return Cddb_OK;
#else
	if ((cp->options.localcacheflags & CACHE_DONT_CONNECT) != 0) {
		*matchcode = MATCH_NONE;
		MEM_FREE(buf);

		FCDDBDBG(fcddb_errfp,
			"\nfcddb_query_cddb: No match in cache\n");
		return Cddb_OK;
	}

	/*
	 * Set up CDDB query command string
	 */

	FCDDBDBG(fcddb_errfp, "\nfcddb_query_cddb: server query\n");

	ntrks = (int) (discid & 0xff);
	urlstr[0] = '\0';

	if (isproxy) {
		(void) sprintf(urlstr, "http://%s:%d",
			       CDDB_SERVER_HOST, HTTP_PORT);
	}

	(void) strcat(urlstr, CDDB_CGI_PATH);

	(void) sprintf(buf, "GET %s?cmd=cddb+query+%08x+%d",
		       urlstr, discid, ntrks);

	for (i = 0; i < ntrks; i++)
		(void) sprintf(buf, "%s+%u", buf, addr[i]);

	(void) sprintf(buf, "%s+%u&%s&proto=4 HTTP/1.0\r\n", buf,
		       addr[ntrks] / FRAME_PER_SEC,
		       fcddb_hellostr);

	if (isproxy && fcddb_auth_buf != NULL) {
		(void) sprintf(buf, "%sProxy-Authorization: Basic %s\r\n", buf,
			       fcddb_auth_buf);
	}

	(void) sprintf(buf, "%s%s%s\r\n%s\r\n", buf,
		       "Host: ", CDDB_SERVER_HOST,
		       fcddb_extinfo);

	/* Open network connection to server */
	if ((ret = fcddb_connect(conhost, conport, &fd)) != Cddb_OK) {
		if (buf != NULL)
			MEM_FREE(buf);
		*matchcode = MATCH_NONE;
		return (ret);
	}

	/*
	 * Send the command to the CDDB server and get response code
	 */
	if ((fcddb_sendcmd(fd, &buf, &buflen, isproxy)) != Cddb_OK) {
		if (buf != NULL)
			MEM_FREE(buf);
		*matchcode = MATCH_NONE;
		return (ret);
	}

	/* Close server connection */
	fcddb_disconnect(fd);

	/* Check for CDDB command response */
	valid_resp = FALSE;
	p = buf;
	while ((q = strchr(p, '\n')) != NULL) {
		*q = '\0';
	
		if (STATCODE_CHECK(p)) {
			valid_resp = TRUE;
			*q = '\n';
			break;
		}

		*q = '\n';
		p = q + 1;
		SKIP_SPC(p);
	}

	if (!valid_resp) {
		/* Server error */
		return CDDBTRNBadResponseSyntax;
	}

	/*
	 * Check command status
	 */

	switch (STATCODE_1ST(p)) {
	case STAT_GEN_OK:
	case STAT_GEN_OKCONT:
		switch (STATCODE_3RD(p)) {
		case STAT_QURY_EXACT:
			if (STATCODE_2ND(p) == STAT_SUB_READY) {
				/* Single exact match */
				p += 4;
				SKIP_SPC(p);

				if (*p == '\0' ||
				    (q = strchr(p, ' ')) == NULL) {
					/* Invalid server response */
					ret = CDDBTRNBadResponseSyntax;
					break;
				}
				*q = '\0';

				/* Get category */
				(void) strncpy(category, p,
						FILE_BASE_SZ - 1);
				category[FILE_BASE_SZ - 1] = '\0';

				*q = ' ';

				*matchcode = MATCH_EXACT;
				ret = Cddb_OK;
				break;
			}
			/*FALLTHROUGH*/

		case STAT_QURY_INEXACT:
			if (STATCODE_2ND(p) != STAT_SUB_OUTPUT) {
				/* Invalid server response */
				ret = CDDBTRNBadResponseSyntax;
				break;
			}

			ret = Cddb_OK;

			p += 4;
			SKIP_SPC(p);
			if (*p == '\0' || (q = strchr(p, '\n')) == NULL) {
				/* Invalid server response */
				ret = CDDBTRNBadResponseSyntax;
				break;
			}

			for (;;) {
				p = q + 1;
				SKIP_SPC(p);
				if ((q = strchr(p, '\n')) != NULL)
					*q = '\0';

				if (*p == '.' || *p == '\0')
					/* Done */
					break;

				/* Zap carriage return if needed */
				i = strlen(p) - 1;
				if (*(p+i) == '\r')
					*(p+i) = '\0';

				/* Don't use fcddb_obj_alloc here because
				 * we don't want CddbReleaseObject to
				 * free it up.
				 */
				dp = (cddb_disc_t *) MEM_ALLOC(
					"CddbDisc", sizeof(cddb_disc_t)
				);
				if (dp == NULL) {
					*matchcode = MATCH_NONE;
					return CDDBTRNOutOfMemory;
				}
				(void) memset(dp, 0, sizeof(cddb_disc_t));
				(void) strcpy(dp->objtype, "CddbDisc");

				r = p;
				if ((s = strchr(r, ' ')) == NULL) {
					/* Invalid server response */
					MEM_FREE(dp);
					*matchcode = MATCH_NONE;
					return CDDBTRNBadResponseSyntax;
				}
				*s = '\0';

				/* Category: convert to genre */
				dp->genre = fcddb_genre_categ2gp(cp, r);

				*s = ' ';
				r = s + 1;
				SKIP_SPC(r);
				if ((s = strchr(r, ' ')) == NULL) {
					/* Invalid server response */
					MEM_FREE(dp);
					*matchcode = MATCH_NONE;
					return CDDBTRNBadResponseSyntax;
				}
				*s = '\0';

				/* Disc ID */
				(void) sscanf(r, "%x",
					      (unsigned int *) &dp->discid);

				*s = ' ';
				r = s + 1;
				SKIP_SPC(r);
				if (*r == '\0') {
					/* Invalid server response */
					MEM_FREE(dp);
					*matchcode = MATCH_NONE;
					return CDDBTRNBadResponseSyntax;
				}

				/* Artist / Title */

				/* Put it all in artist first */
				dp->fullname.name = (CddbStr) fcddb_strdup(r);

				/* Separate into disc artist & title fields */
				r = strchr(dp->fullname.name, '/');
				if (r != NULL && r > (dp->fullname.name + 1) &&
				    *(r-1) == ' ' && *(r+1) == ' '
				    && *(r+2) != '\0') {
					s = dp->fullname.name;

					/* Artist */
					*(r-1) = '\0';
					dp->fullname.name =
						(CddbStr) fcddb_strdup(s);

					/* Title */
					dp->title =
						(CddbStr) fcddb_strdup(r+2);

					MEM_FREE(s);
				}
				else {
					/* Move the whole string to title */
					dp->title = dp->fullname.name;
					dp->fullname.name = NULL;
				}

				/* TOC */
				dp->toc = (CddbStr) fcddb_strdup(toc);

				if (dp != NULL) {
					/* Add to discs list */
					dp->next = cp->discs.discs;
					cp->discs.discs = dp;
					cp->discs.count++;
				}
			}

			*matchcode = MATCH_MULTIPLE;
			break;

		case STAT_QURY_NONE:
			ret = Cddb_OK;
			break;
		}
		break;

	case STAT_GEN_CLIENT:
		switch (STATCODE_3RD(p)) {
		case STAT_QURY_AUTHFAIL:
			if (STATCODE_2ND(p) != STAT_SUB_CLOSE) {
				/* Invalid server response */
				ret = CDDBTRNBadResponseSyntax;
				break;
			}

			if (isproxy) {
				ret = CDDBTRNHTTPProxyError;
				break;
			}
			break;

		default:
			break;
		}
		/*FALLTHROUGH*/

	case STAT_GEN_OKFAIL:
	case STAT_GEN_INFO:
	case STAT_GEN_ERROR:
	default:
		/* Error */
		ret = CDDBTRNRecordNotFound;
		break;
	}

	if (buf != NULL)
		MEM_FREE(buf);

	if (ret != Cddb_OK)
		*matchcode = MATCH_NONE;

	return (ret);
#endif	/* NOREMOTE */
}


/*
 * fcddb_region_init
 *	Initialize the region list
 *
 * Args:
 *	cp - Pointer to the control structure
 *
 * Return:
 *	CddbResult status code
 */
STATIC CddbResult
fcddb_region_init(cddb_control_t *cp)
{
	cddb_region_t	*rp,
			*prevp;
	int		i;

	prevp = NULL;
	for (i = 0; fcddb_region_tbl[i].id != NULL; i++) {
		rp = &fcddb_region_tbl[i];
		(void) strcpy(rp->objtype, "CddbRegion");

		if (i == 0)
			cp->regionlist.regions = rp;
		else
			prevp->next = rp;

		cp->regionlist.count++;
		prevp = rp;
	}

	return Cddb_OK;
}


/*
 * fcddb_genre_init
 *	Initialize the genre tree
 *
 * Args:
 *	cp - Pointer to the control structure
 *
 * Return:
 *	CddbResult status code
 */
STATIC CddbResult
fcddb_genre_init(cddb_control_t *cp)
{
	cddb_genre_t	*gp,
			*gp2,
			*gp3;
	int		i;

	/* Set up genre tree to mimick CDDB1 categories */
	gp3 = NULL;
	cp->genretree.count = 0;
	for (i = 0; fcddb_genre_map[i].cddb1name != NULL; i++) {
		/* Don't use fcddb_obj_alloc() here because we don't want
		 * CddbReleaseObject() to free these.
		 */
		gp = (cddb_genre_t *) MEM_ALLOC(
			"CddbGenre",
			sizeof(cddb_genre_t)
		);
		if (gp == NULL)
			return CDDBTRNOutOfMemory;

		(void) memset(gp, 0, sizeof(cddb_genre_t));
		(void) strcpy(gp->objtype, "CddbGenre");

		gp->id   = (CddbStr) fcddb_strdup(fcddb_genre_map[i].meta.id);
		gp->name =
			(CddbStr) fcddb_strdup(fcddb_genre_map[i].meta.name);

		gp2 = (cddb_genre_t *) MEM_ALLOC(
			"CddbGenre",
			sizeof(cddb_genre_t)
		);
		if (gp2 == NULL)
			return CDDBTRNOutOfMemory;

		(void) memset(gp2, 0, sizeof(cddb_genre_t));
		(void) strcpy(gp2->objtype, "CddbGenre");

		gp2->id   = (CddbStr) fcddb_strdup(fcddb_genre_map[i].sub.id);
		gp2->name =
			(CddbStr) fcddb_strdup(fcddb_genre_map[i].sub.name);
		gp->next  = gp2;

		if (cp->genretree.genres == NULL)
			cp->genretree.genres = gp;
		else
			gp3->nextmeta = gp;

		gp3 = gp;

		cp->genretree.count++;
	}

	return Cddb_OK;
}


/*
 * fcddb_url_init
 *	Initialize the URL list
 *
 * Args:
 *	cp - Pointer to the control structure
 *
 * Return:
 *	CddbResult status code
 */
STATIC CddbResult
fcddb_url_init(cddb_control_t *cp)
{
	cddb_url_t	*up;

	/* Hard-wire the CDDB web site */
	up = (cddb_url_t *) MEM_ALLOC("CddbURL", sizeof(cddb_url_t));
	if (up == NULL)
		return CDDBTRNOutOfMemory;

	(void) memset(up, 0, sizeof(cddb_url_t));
	(void) strcpy(up->objtype, "CddbURL");
	up->type = (CddbStr) fcddb_strdup("menu");
	up->href = (CddbStr) fcddb_strdup("http://www.cddb.com/");
	up->displaytext =
		(CddbStr) fcddb_strdup("CDDB, the #1 Music Info Source");
	up->category = (CddbStr) fcddb_strdup("Music Sites");

	cp->urllist.urls = up;
	cp->urllist.count++;

	return Cddb_OK;
}


/*
 * fcddb_role_init
 *	Initialize the role tree
 *
 * Args:
 *	cp - Pointer to the control structure
 *
 * Return:
 *	CddbResult status code
 */
STATIC CddbResult
fcddb_role_init(cddb_control_t *cp)
{
	cddb_role_t	*rp,
			*prevrp;
	cddb_rolelist_t	*rlp,
			*prevlp;
	int		i;

	prevrp = NULL;
	prevlp = NULL;
	rlp = NULL;
	for (i = 0; fcddb_role_tbl[i].id != NULL; i++) {
		rp = &fcddb_role_tbl[i];

		if (rp->objtype[0] == 'm') {
			rlp = (cddb_rolelist_t *) MEM_ALLOC(
				"CddbRoleList", sizeof(cddb_rolelist_t)
			);
			if (rlp == NULL)
				break;

			(void) memset(rlp, 0, sizeof(cddb_rolelist_t));
			(void) strcpy(rlp->objtype, "CddbRoleList");

			rlp->metarole = rp;

			if (cp->roletree.rolelists == NULL)
				cp->roletree.rolelists = rlp;
			else
				prevlp->next = rlp;

			cp->roletree.count++;
			prevlp = rlp;
		}
		else if (rp->objtype[0] == 's') {
			if (rlp == NULL)
				break;

			if (rlp->subroles == NULL)
				rlp->subroles = rp;
			else
				prevrp->next = rp;

			rlp->count++;
			prevrp = rp;
		}
		(void) strcpy(rp->objtype, "CddbRole");
	}

	return Cddb_OK;
}


/*
 * fcddb_init
 *	Initialize the fcddb module
 *
 * Args:
 *	cp - Pointer to the control structure
 *
 * Return:
 *	CddbResult status code
 */
CddbResult
fcddb_init(cddb_control_t *cp)
{
	CddbResult	ret;
	char		tmpuser[STR_BUF_SZ],
			tmphost[MAXHOSTNAMELEN + STR_BUF_SZ];

	/* Initialize debug message stream */
	fcddb_errfp = stderr;

	/* Initialize regions */
	if ((ret = fcddb_region_init(cp)) != Cddb_OK)
		return (ret);

	/* Initialize genres */
	if ((ret = fcddb_genre_init(cp)) != Cddb_OK)
		return (ret);

	/* Initialize roles */
	if ((ret = fcddb_role_init(cp)) != Cddb_OK)
		return (ret);

	/* Initialize URLs */
	if ((ret = fcddb_url_init(cp)) != Cddb_OK)
		return (ret);

	/* Misc initializations */
	fcddb_http_xlat(cp->userinfo.userhandle, tmpuser),
	fcddb_http_xlat(cp->hostname, tmphost),

	(void) sprintf(fcddb_hellostr, "hello=%.64s+%.64s+%s+%s",
		       tmpuser, tmphost, cp->clientname, cp->clientver);
	(void) sprintf(fcddb_extinfo,
		       /* Believe it or not, this is how MS Internet Explorer
		        * does it, so don't blame me...
		        */
		       "User-Agent: %s (compatible; %s %s)\r\nAccept: %s\r\n",
		       "Mozilla/4.7",
		       cp->clientname,
		       cp->clientver,
		       "text/plain");

	/* For SOCKS support */
	SOCKSINIT(cp->clientname);

	return Cddb_OK;
}


/*
 * fcddb_halt
 *	Shut down the fcddb module
 *
 * Args:
 *	cp - Pointer to the control structure
 *
 * Return:
 *	CddbResult status code
 */
/*ARGSUSED*/
CddbResult
fcddb_halt(cddb_control_t *cp)
{
	if (fcddb_auth_buf != NULL) {
		MEM_FREE(fcddb_auth_buf);
		fcddb_auth_buf = NULL;
	}

	return Cddb_OK;
}


/*
 * fcddb_obj_alloc
 *	Allocate a CDDB object
 *
 * Args:
 *	objtype - The object type to allocate
 *	len - The size of the object
 *
 * Return:
 *	Pointer to allocated memory
 */
void *
fcddb_obj_alloc(char *objtype, size_t len)
{
	void	*retp;
	char	*cp;

	if (objtype == NULL || strlen(objtype) >= len)
		return NULL;

	retp = MEM_ALLOC(objtype, len);
	if (retp != NULL) {
		cp = (char *) retp;
		(void) memset(cp, 0, len);
		(void) sprintf(cp, "+%s", objtype);
	}

	return (retp);
}


/*
 * fcddb_obj_free
 *	Free CDDB object
 *
 * Args:
 *	obj - Pointer to object previously allocated via fcddb_obj_alloc()
 *
 * Return:
 *	Nothing
 */
void
fcddb_obj_free(void *obj)
{
	char	*cp = (char *) obj;

	if (cp == NULL)
		return;

	if (cp[0] == '+') {
		/* Need to add to this list if fcddb_obj_alloc is used
		 * to allocate other types of objects
		 */
		if (strcmp(cp+1, "CddbControl") == 0)
			fcddb_clear_control((cddb_control_t *) obj);
		else if (strcmp(cp+1, "CddbDisc") == 0)
			fcddb_clear_disc((cddb_disc_t *) obj);
		else if (strcmp(cp+1, "CddbDiscs") == 0)
			fcddb_clear_discs((cddb_discs_t *) obj);
		else if (strcmp(cp+1, "CddbOptions") == 0)
			fcddb_clear_options((cddb_options_t *) obj);
		else if (strcmp(cp+1, "CddbUserInfo") == 0)
			fcddb_clear_userinfo((cddb_userinfo_t *) obj);
		else if (strcmp(cp+1, "CddbGenreTree") == 0)
			fcddb_clear_genretree((cddb_genretree_t *) obj);
		else if (strcmp(cp+1, "CddbURLManager") == 0)
			fcddb_clear_urlmanager((cddb_urlmanager_t *) obj);
		else if (strcmp(cp+1, "CddbCredit") == 0)
			fcddb_clear_credit((cddb_credit_t *) obj);
		else if (strcmp(cp+1, "CddbSegment") == 0)
			fcddb_clear_segment((cddb_segment_t *) obj);

		MEM_FREE(obj);
	}
}


/*
 * fcddb_runcmd
 *	Spawn an external command
 *
 * Args:
 *	cmd - The command string
 *
 * Return:
 *	The command exit status
 */
int
fcddb_runcmd(char *cmd)
{
	int		ret;
#ifndef __VMS
	int		fd;
	pid_t		cpid;
	waitret_t	stat_val;

	FCDDBDBG(fcddb_errfp, "fcddb_runcmd: [%s]\n", cmd);

	/* Fork child to invoke external command */
	switch (cpid = FORK()) {
	case 0:
		break;

	case -1:
		/* Fork failed */
		return EXE_ERR;

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

		if (WIFEXITED(stat_val))
			ret = WEXITSTATUS(stat_val);
		else
			ret = EXE_ERR;

		FCDDBDBG(fcddb_errfp, "Command exit status %d\n", ret);
		return (ret);
	}

	/* Child process */
	for (fd = 3; fd < 255; fd++) {
		/* Close unneeded file descriptors */
		(void) close(fd);
	}

	/* Restore SIGTERM */
	(void) signal(SIGTERM, SIG_DFL);

	/* Ignore SIGALRM */
	(void) signal(SIGALRM, SIG_IGN);

	/* Exec a shell to run the command */
	(void) execl(STR_SHPATH, STR_SHNAME, STR_SHARG, cmd, NULL);
	exit(EXE_ERR);
	/*NOTREACHED*/
#else
	/* Do the command */
	ret = system(cmd);

	FCDDBDBG(fcddb_errfp, "Command exit status %d\n", ret);
	return (ret);
#endif	/* __VMS */
}


/*
 * fcddb_set_auth
 *	Set the HTTP/1.1 proxy-authorization string
 *
 * Args:
 *	name - The proxy user name
 *	passwd - The proxy password
 *
 * Return:
 *	CddbResult status code
 */
CddbResult
fcddb_set_auth(char *name, char *passwd)
{
	int	i,
		j,
		n1,
		n2;
	char	*buf;

	if (name == NULL || passwd == NULL)
		return Cddb_E_INVALIDARG;

	i = strlen(name);
	j = strlen(passwd);
	n1 = i + j + 2;
	n2 = (n1 * 4 / 3) + 8;

	if ((buf = (char *) MEM_ALLOC("strbuf", n1)) == NULL)
		return CDDBTRNOutOfMemory;

	(void) sprintf(buf, "%s:%s", name, passwd);

	(void) memset(name, 0, i);
	(void) memset(passwd, 0, j);

	if (fcddb_auth_buf != NULL)
		MEM_FREE(fcddb_auth_buf);

	fcddb_auth_buf = (char *) MEM_ALLOC("fcddb_auth_buf", n2);
	if (fcddb_auth_buf == NULL)
		return CDDBTRNOutOfMemory;

	/* Base64 encode the name/password pair */
	fcddb_b64encode(
		(byte_t *) buf,
		strlen(buf),
		(byte_t *) fcddb_auth_buf,
		FALSE
	);

	(void) memset(buf, 0, n1);
	MEM_FREE(buf);
	return Cddb_OK;
}


/*
 * fcddb_strdup
 *	fcddb2 internal version of strdup().
 *
 * Args:
 *	str - source string
 *
 * Return:
 *	Return string
 */
char *
fcddb_strdup(char *str)
{
	char	*buf;

	if (str == NULL)
		return NULL;

	buf = (char *) MEM_ALLOC("strdup_buf", strlen(str) + 1);
	if (buf != NULL)
		(void) strcpy(buf, str); 

	return (buf);
}


/*
 * fcddb_basename
 *	fcddb2 internal version of basename()
 *
 * Args:
 *	path - File path
 *
 * Return:
 *	Base name of file path
 */
char *
fcddb_basename(char *path)
{
	char		*p;
#ifdef __VMS
	char		*q;
#endif
	static char	buf[FILE_PATH_SZ];

	if (path == NULL)
		return NULL;

	if ((int) strlen(path) >= FILE_PATH_SZ)
		/* Error: path name too long */
		return NULL;

	(void) strcpy(buf, path);

	if ((p = strrchr(buf, DIR_END)) == NULL)
		return (buf);

	p++;

#ifdef __VMS
	if (*p == '\0') {
		/* The supplied path is a directory - special handling */
		*--p = '\0';
		if ((q = strrchr(buf, DIR_SEP)) != NULL)
			p = q + 1;
		else if ((q = strrchr(buf, DIR_BEG)) != NULL)
			p = q + 1;
		else if ((q = strrchr(buf, ':')) != NULL)
			p = q + 1;
		else {
			p = buf;
			*p = '\0';
		}
	}
#endif

	return (p);
}


/*
 * fcddb_dirname
 *	fcddb2 internal version of dirname()
 *
 * Args:
 *	path - File path
 *
 * Return:
 *	Directory name of file path
 */
char *
fcddb_dirname(char *path)
{
	char		*p;
#ifdef __VMS
	char		*q;
#endif
	static char	buf[FILE_PATH_SZ];

	if (path == NULL)
		return NULL;

	if ((int) strlen(path) >= FILE_PATH_SZ)
		/* Error: path name too long */
		return NULL;

	(void) strcpy(buf, path);

	if ((p = strrchr(buf, DIR_END)) == NULL)
		return (buf);

#ifdef __VMS
	if (*++p == '\0') {
		/* The supplied path is a directory - special handling */
		if ((q = strrchr(buf, DIR_SEP)) != NULL) {
			*q = DIR_END;
			*++q = '\0';
		}
		else if ((q = strrchr(buf, ':')) != NULL)
			*++q = '\0';
		else
			p = buf;
	}
	*p = '\0';
#else
	if (p == buf)
		*++p = '\0';
	else
		*p = '\0';
#endif

	return (buf);
}


/*
 * fcddb_mkdir
 *	fcddb2 internal version of mkdir().  Will recursively make all
 *	needed parent directories similar to "mkdir -p".
 *
 * Args:
 *	path - Directory path
 *	mode - Directory permissions
 *
 * Return:
 *	CddbResult status code
 */
CddbResult
fcddb_mkdir(char *path, mode_t mode)
{
	char		*cp,
			*mypath;
	struct stat	stbuf;

	if ((mypath = fcddb_strdup(path)) == NULL)
		return CDDBTRNOutOfMemory;

	cp = mypath;
#ifndef __VMS
	/* Make parent directories, if needed */
	while (*(cp+1) == DIR_BEG)
		cp++;	/* Skip extra initial '/'s if absolute path */
#endif

	while ((cp = strchr(cp+1, DIR_SEP)) != NULL) {
#ifdef __VMS
		char	c;

		c = *(cp+1);
		*(cp+1) = '\0';
		*cp = DIR_END;
#else
		*cp = '\0';
#endif

		/*
		 * Make sure directory exists.  If not, create it.
		 */
		if (stat(mypath, &stbuf) < 0) {
			if (errno == ENOENT) {
				if (mkdir(mypath, 0777) < 0) {
					MEM_FREE(mypath);
					return CDDBTRNCannotCreateFile;
				}
				(void) chmod(mypath, mode);
			}
			else {
				MEM_FREE(mypath);
				return CDDBTRNCannotCreateFile;
			}
		}
		else if (!S_ISDIR(stbuf.st_mode)) {
			MEM_FREE(mypath);
			return CDDBTRNCannotCreateFile;
		}

		*cp = DIR_SEP;
#ifdef __VMS
		*(cp+1) = c;
#endif
	}

	/* Make the directory itself, if needed */
	if (stat(mypath, &stbuf) < 0) {
		if (errno == ENOENT) {
			if (mkdir(mypath, 0777) < 0) {
				MEM_FREE(mypath);
				return CDDBTRNCannotCreateFile;
			}
		}
		else {
			MEM_FREE(mypath);
			return CDDBTRNCannotCreateFile;
		}
	}
	else if (!S_ISDIR(stbuf.st_mode)) {
		MEM_FREE(mypath);
		return CDDBTRNCannotCreateFile;
	}

	(void) chmod(mypath, mode);
	MEM_FREE(mypath);
	return Cddb_OK;
}


/*
 * fcddb_username
 *	Return user login name
 *
 * Args:
 *	None
 *
 * Return:
 *	The login name
 */
char *
fcddb_username(void)
{
	char		*cp;
#ifdef __VMS
	cp = getlogin();
	if (cp != NULL)
		return (cp);
#else
	struct passwd	*pw;

	/* Get login name from the password file if possible */
	setpwent();
	if ((pw = getpwuid(getuid())) != NULL) {
		endpwent();
		return (pw->pw_name);
	}
	endpwent();

	/* Try the LOGNAME environment variable */
	if ((cp = (char *) getenv("LOGNAME")) != NULL)
		return (cp);

	/* Try the USER environment variable */
	if ((cp = (char *) getenv("USER")) != NULL)
		return (cp);
#endif
	/* If we still can't get the login name, just set it
	 * to "nobody" (shrug).
	 */
	return ("nobody");
}


/*
 * fcddb_homedir
 *	Return the user's home directory
 *
 * Args:
 *	None
 *
 * Return:
 *	The user's home directory
 */
char *
fcddb_homedir(void)
{
#ifndef __VMS
	struct passwd	*pw;
	char		*cp;

	/* Get home directory from the password file if possible */
	setpwent();
	if ((pw = getpwuid(getuid())) != NULL) {
		endpwent();
		return (pw->pw_dir);
	}
	endpwent();

	/* Try the HOME environment variable */
	if ((cp = (char *) getenv("HOME")) != NULL)
		return (cp);

	/* If we still can't get the home directory, just set it to the
	 * current directory (shrug).
	 */
	return (".");
#else
	char		*cp;
	static char	buf[FILE_PATH_SZ];

	if ((cp = (char *) getenv("HOME")) != NULL &&
	    (int) strlen(cp) < sizeof(buf)) {
		(void) strcpy(buf, cp);
		buf[strlen(buf)-1] = '\0';	/* Drop the "]" */
	}
	else
		(void) strcpy(buf, "SYS$DISK:[");

	return (buf);
#endif	/* __VMS */
}


/*
 * fcddb_hostname
 *	Get the host name (with fully qualified domain name if possible)
 *
 * Args:
 *	None
 *
 * Return:
 *	Nothing
 */
char *
fcddb_hostname(void)
{
#ifndef NOREMOTE
	struct hostent	*he;
	char		**ap,
			hname[MAXHOSTNAMELEN+1];
	static char	buf[MAXHOSTNAMELEN+1];

	buf[0] = '\0';

	/* Try to determine host name */
	if (gethostname(hname, MAXHOSTNAMELEN) < 0 ||
	    (he = gethostbyname(hname)) == NULL) {
		struct utsname	un;

		if (uname(&un) < 0)
			(void) strcpy(buf, "localhost"); /* shrug */
		else
			(void) strncpy(buf, un.nodename,
					       MAXHOSTNAMELEN);
	}
	else {
		/* Look for a a fully-qualified hostname
		 * (with domain)
		 */
		if (strchr(he->h_name, '.') != NULL)
			(void) strcpy(buf, he->h_name);
		else {
			for (ap = he->h_aliases; *ap != NULL; ap++) {
				if (strchr(*ap, '.') != NULL) {
				    (void) strncpy(buf, *ap, MAXHOSTNAMELEN);
				    break;
				}
			}
		}

		if (buf[0] == '\0')
			(void) strcpy(buf, hname);
	}

	buf[MAXHOSTNAMELEN] = '\0';
	return (buf);
#else
	struct utsname	un;
	static char	buf[MAXHOSTNAMELEN+1];

	if (uname(&un) < 0)
		(void) strcpy(buf, "localhost"); /* shrug */
	else
		(void) strncpy(buf, un.nodename, MAXHOSTNAMELEN);

	buf[MAXHOSTNAMELEN] = '\0';
	return (buf);
#endif
}


/*
 * fcddb_genre_id2categ
 *	Convert CDDB2 genre ID to CDDB1 category string
 *
 * Args:
 *	id - The genre ID
 *
 * Return:
 *	The category string
 */
char *
fcddb_genre_id2categ(char *id)
{
	int	i;

	if (id == NULL)
		return NULL;

	for (i = 0; fcddb_genre_map[i].cddb1name != NULL; i++) {
		if (strcmp(fcddb_genre_map[i].sub.id, id) == 0)
			return (fcddb_genre_map[i].cddb1name);
	}
	return NULL;
}


/*
 * fcddb_genre_id2gp
 *	Convert CDDB2 genre ID to a genre structure pointer
 *
 * Args:
 *	cp - Pointer to the control structure
 *	id - The genre ID
 *
 * Return:
 *	Pointer to the genre structure
 */
cddb_genre_t *
fcddb_genre_id2gp(cddb_control_t *cp, char *id)
{
	cddb_genre_t	*p,
			*q;

	if (cp == NULL || id == NULL)
		return NULL;

	for (p = cp->genretree.genres; p != NULL; p = p->nextmeta) {
		if (p->id != NULL && strcmp(p->id, id) == 0)
			return (p);

		for (q = p->next; q != NULL; q = q->next) {
			if (q->id != NULL && strcmp(q->id, id) == 0)
				return (q);
		}
	}

	return NULL;
}


/*
 * fcddb_genre_categ2id
 *	Convert CDDB1 category string to a CDDB2 genre ID
 *
 * Args:
 *	id - The genre ID
 *
 * Return:
 *	The genre ID string
 */
char *
fcddb_genre_categ2id(char *categ)
{
	int	i;

	if (categ == NULL)
		return NULL;

	for (i = 0; fcddb_genre_map[i].cddb1name != NULL; i++) {
		if (strcmp(fcddb_genre_map[i].cddb1name, categ) == 0)
			return fcddb_genre_map[i].sub.id;
	}

	return NULL;
}


/*
 * fcddb_genre_categ2gp
 *	Convert CDDB1 category string to a genre structure pointer
 *
 * Args:
 *	cp - Pointer to the control structure
 *	categ - The category string
 *
 * Return:
 *	Pointer to the genre structure
 */
cddb_genre_t *
fcddb_genre_categ2gp(cddb_control_t *cp, char *categ)
{
	int	i;
	char	*id;

	if (cp == NULL || categ == NULL)
		return NULL;

	id = NULL;
	for (i = 0; fcddb_genre_map[i].cddb1name != NULL; i++) {
		if (strcmp(fcddb_genre_map[i].cddb1name, categ) == 0) {
			id = fcddb_genre_map[i].sub.id;
			break;
		}
	}

	if (id == NULL)
		return NULL;

	return (fcddb_genre_id2gp(cp, id));
}


/*
 * fcddb_parse_toc
 *	Parse a TOC string and populate an array of frame offsets
 *
 * Args:
 *	tocstr - The TOC
 *	addr - Return array of frame offsets
 *
 * Return:
 *	The number of tracks
 */
int
fcddb_parse_toc(char *tocstr, unsigned int *addr)
{
	char	*p,
		*q,
		sav;
	int	i;

	if (tocstr == NULL || addr == NULL)
		return 0;

	i = 0;
	p = tocstr;
	SKIP_SPC(p);

	/* Starting frames for each track */
	while ((q = strchr(p, ' ')) != NULL || (q = strchr(p, '\t')) != NULL) {
		sav = *q;
		*q = '\0';
		addr[i++] = (unsigned int) atoi(p);
		*q = sav;
		p = q+1;
		SKIP_SPC(p);
	}
	addr[i] = (unsigned int) atoi(p);	/* Lead out */

	return (i);
}


/*
 * fcddb_discid
 *	Compute CDDB1 disc ID
 *
 * Args:
 *	ntrks - Number of tracks
 *	addr - Array of frame offsets
 *
 * Return:
 *	The CDDB1 disc ID
 */
word32_t
fcddb_discid(int ntrks, unsigned int *addr)
{
	int	i,
		t = 0,
		n = 0;

	/* For backward compatibility this algorithm must not change */

	for (i = 0; i < ntrks; i++)
		n += fcddb_sum(addr[i] / FRAME_PER_SEC);

	t = (addr[ntrks] / FRAME_PER_SEC) - (addr[0] / FRAME_PER_SEC);

	return ((n % 0xff) << 24 | t << 8 | ntrks);
}


/*
 * fcddb_read_cddb
 *	Send a read command to the CDDB server
 *
 * Args:
 *	cp - Pointer to the control structure
 *	discid - The disc ID
 *	toc - The TOC
 *	category - The CDDB1 category
 *	conhost - Server host
 *	conport - The server port
 *	isproxy - Whether we're using a proxy server
 *
 * Return:
 *	CddbResult status code
 */
#ifdef NOREMOTE
/*ARGSUSED*/
#endif
CddbResult
fcddb_read_cddb(
	cddb_control_t		*cp,
	word32_t		discid,
	char			*toc,
	char			*category,
	char			*conhost,
	unsigned short		conport,
	bool_t			isproxy
)
{
	CddbResult	ret;
	int		fd,
			n,
			i;
	size_t		buflen,
			contentlen;
	char		*buf,
			*p,
			filepath[FILE_PATH_SZ];
	time_t		t;
	struct stat	stbuf;
#ifndef NOREMOTE
	char		*q,
			urlstr[MAXHOSTNAMELEN + FILE_PATH_SZ + 12];
	bool_t		valid_resp;
#endif

#ifdef __VMS
	(void) sprintf(filepath, "%s.%s]%08x",
		       cp->options.localcachepath, category, discid);
#else
	(void) sprintf(filepath, "%s/%s/%08x",
		       cp->options.localcachepath, category, discid);
#endif

	(void) time(&t);

	/*
	 * Attempt to read from local cache
	 */
	FCDDBDBG(fcddb_errfp, "\nfcddb_read_cddb: Checking cache\n");

	ret = CDDBTRNRecordNotFound;

	if (stat(filepath, &stbuf) == 0 && S_ISREG(stbuf.st_mode)
#ifndef NOREMOTE
	    &&
	    (((cp->options.localcacheflags & CACHE_DONT_CONNECT) != 0) ||
	     ((t - stbuf.st_mtime) <=
		    (int) (cp->options.localcachetimeout * SECS_PER_DAY)))
#endif
	   ) {
		/*
		 * Use local cache
		 */

		contentlen = stbuf.st_size;
		buflen = contentlen + 1;
		buf = (char *) MEM_ALLOC("cddb_buf", buflen);
		if (buf == NULL)
			return CDDBTRNOutOfMemory;

		ret = Cddb_OK;

		FCDDBDBG(fcddb_errfp, "fcddb_read_cddb: cache read %s/%08x\n",
			 category, discid);

		/* Open local cache file */
		if ((fd = open(filepath, O_RDONLY)) < 0) {
			ret = CDDBTRNRecordNotFound;
		}
		else {
			/* Read cache data into buf */

			for (i = contentlen, p = buf; i > 0; i -= n, p += n) {
				if ((n = read(fd, p, i)) < 0) {
					MEM_FREE(buf);
					ret = CDDBTRNRecordNotFound;
					break;
				}
			}
			*p = '\0';

			(void) close(fd);

			if (ret == Cddb_OK) {
				ret = fcddb_parse_cddb_data(
					cp, buf, discid, category, toc, FALSE,
					&cp->disc
				);
			}
		}

		MEM_FREE(buf);
		buf = NULL;

		if (ret == Cddb_OK)
			return (ret);

		/* Fall through */
	}

#ifdef NOREMOTE
	return (ret);
#else
	if ((cp->options.localcacheflags & CACHE_DONT_CONNECT) != 0)
		return (ret);

	/*
	 * Do CDDB server read
	 */

	FCDDBDBG(fcddb_errfp, "fcddb_read_cddb: server read %s/%08x\n",
		 category, discid);

	buflen = CDDBP_CMDLEN;
	if ((buf = (char *) MEM_ALLOC("http_buf", buflen)) == NULL)
		return CDDBTRNOutOfMemory;

	/*
	 * Set up CDDB read command string
	 */

	urlstr[0] = '\0';

	if (isproxy) {
		(void) sprintf(urlstr, "http://%s:%d",
			       CDDB_SERVER_HOST, HTTP_PORT);
	}

	(void) strcat(urlstr, CDDB_CGI_PATH);

	(void) sprintf(buf,
		       "GET %s?cmd=cddb+read+%s+%08x&%s&proto=4 HTTP/1.0\r\n",
		       urlstr, category, discid, fcddb_hellostr);

	if (isproxy && fcddb_auth_buf != NULL) {
		(void) sprintf(buf, "%sProxy-Authorization: Basic %s\r\n", buf,
			       fcddb_auth_buf);
	}

	(void) sprintf(buf, "%s%s%s\r\n%s\r\n", buf,
		       "Host: ", CDDB_SERVER_HOST,
		       fcddb_extinfo);

	/* Open network connection to server */
	if ((ret = fcddb_connect(conhost, conport, &fd)) != Cddb_OK) {
		if (buf != NULL)
			MEM_FREE(buf);
		return (ret);
	}

	/*
	 * Send the command to the CDDB server and get response code
	 */
	if ((ret = fcddb_sendcmd(fd, &buf, &buflen, isproxy)) != Cddb_OK) {
		if (buf != NULL)
			MEM_FREE(buf);
		return (ret);
	}

	/* Close server connection */
	fcddb_disconnect(fd);

	/* Check for CDDB command response */
	valid_resp = FALSE;
	p = buf;
	while ((q = strchr(p, '\n')) != NULL) {
		*q = '\0';

		if (STATCODE_CHECK(p)) {
			valid_resp = TRUE;
			*q = '\n';
			break;
		}

		*q = '\n';
		p = q + 1;
		SKIP_SPC(p);
	}

	if (!valid_resp) {
		/* Server error */
		return CDDBTRNBadResponseSyntax;
	}
	
	/*
	 * Check command status
	 */
	ret = Cddb_OK;

	switch (STATCODE_1ST(p)) {
	case STAT_GEN_OK:
	case STAT_GEN_OKCONT:
		if (STATCODE_2ND(p) == '1') {
			if ((q = strchr(p, '\n')) == NULL) {
				ret = CDDBTRNBadResponseSyntax;
				break;
			}

			p = q + 1;
			SKIP_SPC(p);
			if (*p == '\0') {
				ret = CDDBTRNBadResponseSyntax;
				break;
			}

			ret = fcddb_parse_cddb_data(
				cp, p, discid, category, toc, TRUE, &cp->disc
			);
			break;
		}

		/*FALLTHROUGH*/

	case STAT_GEN_INFO:
	case STAT_GEN_OKFAIL:
	case STAT_GEN_ERROR:
	case STAT_GEN_CLIENT:
	default:
		/* Error */
		ret = CDDBTRNRecordNotFound;
		break;
	}

	if (buf != NULL)
		MEM_FREE(buf);

	return (ret);
#endif
}


/*
 * fcddb_lookup_cddb
 *	High-level function to perform a CDDB server lookup
 *
 * Args:
 *	cp - Pointer to the control structure
 *	discid - The disc ID
 *	addr - Array of frame offsets
 *	toc - The TOC
 *	conhost - The server host
 *	conport - The server port
 *	isproxy - Whether we're using a proxy server
 *	matchcode - Return matchcode
 *
 * Return:
 *	CddbResult status code
 */
CddbResult
fcddb_lookup_cddb(
	cddb_control_t		*cp,
	word32_t		discid,
	unsigned int		*addr,
	char			*toc,
	char			*conhost,
	unsigned short		conport,
	bool_t			isproxy,
	int			*matchcode
)
{
	CddbResult	ret;
	char		category[FILE_BASE_SZ];

	*matchcode = MATCH_NONE;
	fcddb_clear_disc(&cp->disc);

	if ((ret = fcddb_query_cddb(cp, discid, toc, addr, conhost, conport,
			       isproxy, category, matchcode)) != Cddb_OK)
		return (ret);

	if (*matchcode == MATCH_EXACT)
		ret = fcddb_read_cddb(cp, discid, toc, category,
				      conhost, conport, isproxy);
	else
		ret = Cddb_OK;

	return (ret);
}


/*
 * fcddb_submit_cddb
 *	Submit a disc entry to CDDB
 *
 * Args:
 *	cp - Pointer to the control structure
 *	dp - Pointer to the disc structure
 *	conhost - The server host
 *	conport - The server port
 *	isproxy - Whether we're using a proxy server
 *
 * Return:
 *	CddbResult status code
 */
#ifdef NOREMOTE
/*ARGSUSED*/
#endif
CddbResult
fcddb_submit_cddb(
	cddb_control_t	*cp,
	cddb_disc_t	*dp,
	char		*conhost,
	unsigned short	conport,
	bool_t		isproxy
)
{
	CddbResult	ret;
	char		*category,
			filepath[FILE_PATH_SZ];
	struct stat	stbuf;
#ifndef NOREMOTE
	int		fd,
			n,
			i;
	size_t		contentlen = 0,
			buflen;
	char		*buf,
			*p,
			*q,
			urlstr[MAXHOSTNAMELEN + FILE_PATH_SZ + 12];
	bool_t		valid_resp;
#endif

	if ((category = fcddb_genre_id2categ(dp->genre->id)) == NULL)
		return Cddb_E_INVALIDARG;

#ifdef __VMS
	(void) sprintf(filepath, "%s.%s]%08x",
		       cp->options.localcachepath, category, dp->discid);
#else
	(void) sprintf(filepath, "%s/%s/%08x",
		       cp->options.localcachepath, category, dp->discid);
#endif

	if ((ret = fcddb_write_cddb(filepath, dp, TRUE)) != Cddb_OK)
		return (ret);

	if (stat(filepath, &stbuf) < 0)
		return CDDBTRNFileWriteError;

#ifdef NOREMOTE
	return Cddb_OK;
#else
	if ((cp->options.localcacheflags & CACHE_SUBMIT_OFFLINE) != 0)
		return Cddb_OK;

	contentlen = (size_t) stbuf.st_size;

	buflen = CDDBP_CMDLEN + contentlen;
	if ((buf = (char *) MEM_ALLOC("http_buf", buflen)) == NULL)
		return CDDBTRNOutOfMemory;

	FCDDBDBG(fcddb_errfp, "\nfcddb_submit_cddb: %s/%08x\n",
		 category, dp->discid);

	/*
	 * Set up CDDB submit command string
	 */

	urlstr[0] = '\0';

	if (isproxy) {
		(void) sprintf(urlstr, "http://%s:%d",
			       CDDB_SERVER_HOST, HTTP_PORT);
	}

	(void) strcat(urlstr, CDDB_SUBMIT_CGI_PATH);

	(void) sprintf(buf, "POST %s HTTP/1.0\r\n", urlstr);

	if (isproxy && fcddb_auth_buf != NULL) {
		(void) sprintf(buf, "%sProxy-Authorization: Basic %s\r\n", buf,
			       fcddb_auth_buf);
	}

	(void) sprintf(buf,
		       "%s%s%s\r\n%s%s%s\r\n%s%08x\r\n"
		       "%s%s@%s\r\n%s%s\r\n%s%u\r\n\r\n", buf,
		       "Host: ", CDDB_SERVER_HOST,
		       fcddb_extinfo,
		       "Category: ", category,
		       "Discid: ", dp->discid,
		       "User-Email: ", cp->userinfo.userhandle, cp->hostname,
		       "Submit-Mode: ",
				cp->options.testsubmitmode ? "test" : "submit",
		       "Content-Length: ", contentlen
	);

	/* Append CDDB file data to buf here */
	if ((fd = open(filepath, O_RDONLY)) < 0) {
		MEM_FREE(buf);
		return CDDBTRNRecordNotFound;
	}

	for (i = contentlen, p = buf + strlen(buf); i > 0; i -= n, p += n) {
		if ((n = read(fd, p, i)) < 0) {
			MEM_FREE(buf);
			return CDDBTRNRecordNotFound;
		}
	}
	*p = '\0';

	(void) close(fd);

	/* Open network connection to server */
	if ((ret = fcddb_connect(conhost, conport, &fd)) != Cddb_OK) {
		if (buf != NULL)
			MEM_FREE(buf);
		return (ret);
	}

	/*
	 * Send the command to the CDDB server and get response code
	 */
	if ((fcddb_sendcmd(fd, &buf, &buflen, isproxy)) != Cddb_OK) {
		if (buf != NULL)
			MEM_FREE(buf);
		return (ret);
	}

	/* Close server connection */
	fcddb_disconnect(fd);

	/* Check for CDDB command response */
	valid_resp = FALSE;
	p = buf;
	while ((q = strchr(p, '\n')) != NULL) {
		*q = '\0';
	
		if (STATCODE_CHECK(p)) {
			valid_resp = TRUE;
			*q = '\n';
			break;
		}

		*q = '\n';
		p = q + 1;
		SKIP_SPC(p);
	}

	if (!valid_resp) {
		/* Server error */
		return CDDBTRNBadResponseSyntax;
	}

	/*
	 * Check command status
	 */
	switch (STATCODE_1ST(p)) {
	case STAT_GEN_OK:
		ret = Cddb_OK;
		break;

	case STAT_GEN_ERROR:
		ret = CDDBSVCMissingField;
		break;

	case STAT_GEN_OKFAIL:
		ret = CDDBSVCServiceError;
		break;

	case STAT_GEN_OKCONT:
	case STAT_GEN_INFO:
	default:
		/* Error */
		ret = CDDBTRNBadResponseSyntax;
		break;
	}

	if (buf != NULL)
		MEM_FREE(buf);

	return (ret);
#endif	/* NOREMOTE */
}


/*
 * fcddb_flush_cddb
 *	Flush local cachestore
 *
 * Args:
 *	cp - Pointer to the control structure
 *
 * Return:
 *	CddbResult status code
 */
#ifdef NOREMOTE
/*ARGSUSED*/
#endif
CddbResult
fcddb_flush_cddb(cddb_control_t *cp, CDDBFlushFlags flags)
{
#ifndef NOREMOTE
	int	i;
	char	cmd[FILE_PATH_SZ + STR_BUF_SZ];

	if (flags == FLUSH_DEFAULT || (flags & FLUSH_CACHE_MEDIA)) {
		for (i = 0; fcddb_genre_map[i].cddb1name != NULL; i++) {
			(void) sprintf(cmd,
				    "rm -rf %s/%s >/dev/null 2>&1",
				    cp->options.localcachepath,
				    fcddb_genre_map[i].cddb1name);

			if (fcddb_runcmd(cmd) != 0)
				return CDDBTRNFileDeleteError;
		}
	}
#endif
	return Cddb_OK;
}


