/*	$OpenBSD: entries.c,v 1.15 2004/08/13 13:24:13 jfb Exp $	*/
/*
 * Copyright (c) 2004 Jean-Francois Brousseau <jfb@openbsd.org>
 * All rights reserved. 
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions 
 * are met: 
 *
 * 1. Redistributions of source code must retain the above copyright 
 *    notice, this list of conditions and the following disclaimer. 
 * 2. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#include <sys/param.h>
#include <sys/stat.h>

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#include "log.h"
#include "cvs.h"


#define CVS_ENTRIES_NFIELDS  6
#define CVS_ENTRIES_DELIM   '/'



/*
 * cvs_ent_open()
 *
 * Open the CVS Entries file for the directory <dir>.
 * Returns a pointer to the CVSENTRIES file structure on success, or NULL
 * on failure.
 */

CVSENTRIES*
cvs_ent_open(const char *dir, int flags)
{
	size_t len;
	int exists;
	char entpath[MAXPATHLEN], ebuf[128], mode[4];
	FILE *fp;
	struct stat st;
	struct cvs_ent *ent;
	CVSENTRIES *ep;

	exists = 0;
	memset(mode, 0, sizeof(mode));

	snprintf(entpath, sizeof(entpath), "%s/" CVS_PATH_ENTRIES, dir);

	switch (flags & O_ACCMODE) {
	case O_WRONLY:
	case O_RDWR:
		/* we have to use append otherwise the file gets truncated */
		mode[0] = 'w';
		mode[1] = '+';
		break;
	case O_RDONLY:
		mode[0] = 'r';
		break;
	}

	/* we can use 'r' if the file already exists */
	if (stat(entpath, &st) == 0) {
		exists = 1;
		mode[0] = 'r';
	}

	fp = fopen(entpath, mode);
	if (fp == NULL) {
		cvs_log(LP_ERRNO, "cannot open %s for %s", entpath,
		    mode[1] == '+' ? "writing" : "reading");
		return (NULL);
	}

	ep = (CVSENTRIES *)malloc(sizeof(CVSENTRIES));
	if (ep == NULL) {
		cvs_log(LP_ERRNO, "failed to allocate Entries data");
		(void)fclose(fp);
		return (NULL);
	}
	memset(ep, 0, sizeof(*ep));

	ep->cef_path = strdup(dir);
	if (ep->cef_path == NULL) {
		cvs_log(LP_ERRNO, "failed to copy Entries path");
		free(ep);
		(void)fclose(fp);
		return (NULL);
	}

	ep->cef_cur = NULL;
	TAILQ_INIT(&(ep->cef_ent));

	while (fgets(ebuf, sizeof(ebuf), fp) != NULL) {
		len = strlen(ebuf);
		if ((len > 0) && (ebuf[len - 1] == '\n'))
			ebuf[--len] = '\0';
		if (strcmp(ebuf, "D") == 0)
			break;
		ent = cvs_ent_parse(ebuf);
		if (ent == NULL)
			continue;

		TAILQ_INSERT_TAIL(&(ep->cef_ent), ent, ce_list);
	}
	if (ferror(fp)) {
		cvs_ent_close(ep);
		return (NULL);
	}

	/* only keep a pointer to the open file if we're in writing mode */
	if ((flags & O_WRONLY) || (flags & O_RDWR)) {
		ep->cef_flags |= CVS_ENTF_WR;
		ep->cef_file = fp;
	}
	else
		(void)fclose(fp);

	if (exists)
		ep->cef_flags |= CVS_ENTF_SYNC;

	return (ep);
}


/*
 * cvs_ent_close()
 *
 * Close the Entries file <ep> and free all data.  Any reference to entries
 * structure within that file become invalid.
 */

void
cvs_ent_close(CVSENTRIES *ep)
{
	struct cvs_ent *ent;

	if ((ep->cef_flags & CVS_ENTF_WR) &&
	    !(ep->cef_flags & CVS_ENTF_SYNC)) {
		/* implicit sync with disk */
		(void)cvs_ent_write(ep);
	}

	if (ep->cef_file != NULL)
		(void)fclose(ep->cef_file);
	if (ep->cef_path != NULL)
		free(ep->cef_path);

	while (!TAILQ_EMPTY(&(ep->cef_ent))) {
		ent = TAILQ_FIRST(&(ep->cef_ent));
		TAILQ_REMOVE(&(ep->cef_ent), ent, ce_list);
		cvs_ent_free(ent);
	}

	free(ep);
}


/*
 * cvs_ent_add()
 *
 * Add the entry <ent> to the Entries file <ef>.  The disk contents are not
 * modified until a call to cvs_ent_write() is performed.  This is done
 * implicitly on a call to cvs_ent_close() on an Entries file that has been
 * opened for writing.
 * Returns 0 on success, or -1 on failure.
 */

int
cvs_ent_add(CVSENTRIES *ef, struct cvs_ent *ent)
{
	if (ef->cef_file == NULL) {
		cvs_log(LP_ERR, "Entries file is opened in read-only mode");
		return (-1);
	}

	if (cvs_ent_get(ef, ent->ce_name) != NULL)
		return (-1);

	TAILQ_INSERT_TAIL(&(ef->cef_ent), ent, ce_list);

	ef->cef_flags &= ~CVS_ENTF_SYNC;

	return (0);
}


/*
 * cvs_ent_addln()
 *
 * Add a line to the Entries file.
 */

int
cvs_ent_addln(CVSENTRIES *ef, const char *line)
{
	struct cvs_ent *ent;

	if (ef->cef_file == NULL) {
		cvs_log(LP_ERR, "Entries file is opened in read-only mode");
		return (-1);
	}

	ent = cvs_ent_parse(line);
	if (ent == NULL)
		return (-1);

	if (cvs_ent_get(ef, ent->ce_name) != NULL)
		return (-1);

	TAILQ_INSERT_TAIL(&(ef->cef_ent), ent, ce_list);
	ef->cef_flags &= ~CVS_ENTF_SYNC;

	return (0);
}


/*
 * cvs_ent_remove()
 *
 * Remove an entry from the Entries file <ef>.  The entry's name is given
 * by <name>.
 */

int
cvs_ent_remove(CVSENTRIES *ef, const char *name)
{
	struct cvs_ent *ent;

	ent = cvs_ent_get(ef, name);
	if (ent == NULL)
		return (-1);

	TAILQ_REMOVE(&(ef->cef_ent), ent, ce_list);
	cvs_ent_free(ent);

	ef->cef_flags &= ~CVS_ENTF_SYNC;

	return (0);
}


/*
 * cvs_ent_get()
 *
 * Get the CVS entry from the Entries file <ef> whose 'name' portion matches
 * <file>.
 * Returns a pointer to the cvs entry structure on success, or NULL on failure.
 */

struct cvs_ent*
cvs_ent_get(CVSENTRIES *ef, const char *file)
{
	struct cvs_ent *ep;

	TAILQ_FOREACH(ep, &(ef->cef_ent), ce_list)
		if (strcmp(ep->ce_name, file) == 0)
			return (ep);

	return (NULL);
}


/*
 * cvs_ent_next()
 *
 * This function is used to iterate over the entries in an Entries file.  The
 * first call will return the first entry of the file and each subsequent call
 * will return the entry following the last one returned.
 * Returns a pointer to the cvs entry structure on success, or NULL on failure.
 */

struct cvs_ent*
cvs_ent_next(CVSENTRIES *ef)
{
	if (ef->cef_cur == NULL)
		ef->cef_cur = TAILQ_FIRST(&(ef->cef_ent));
	else
		ef->cef_cur = TAILQ_NEXT(ef->cef_cur, ce_list);
	return (ef->cef_cur);
}


/*
 * cvs_ent_parse()
 *
 * Parse a single line from a CVS/Entries file and return a cvs_entry structure
 * containing all the parsed information.
 */

struct cvs_ent*
cvs_ent_parse(const char *entry)
{
	int i;
	char *fields[CVS_ENTRIES_NFIELDS], *buf, *sp, *dp;
	struct cvs_ent *entp;

	buf = strdup(entry);
	if (buf == NULL) {
		cvs_log(LP_ERRNO, "failed to allocate entry copy");
		return (NULL);
	}

	sp = buf;
	i = 0;
	do {
		dp = strchr(sp, CVS_ENTRIES_DELIM);
		if (dp != NULL)
			*(dp++) = '\0';
		fields[i++] = sp;
		sp = dp;
	} while ((dp != NULL) && (i < CVS_ENTRIES_NFIELDS));

	if (i < CVS_ENTRIES_NFIELDS) {
		cvs_log(LP_ERR, "missing fields in entry line `%s'", entry);
		return (NULL);
	}

	entp = (struct cvs_ent *)malloc(sizeof(*entp));
	if (entp == NULL) {
		cvs_log(LP_ERRNO, "failed to allocate CVS entry");
		return (NULL);
	}
	memset(entp, 0, sizeof(*entp));
	entp->ce_buf = buf;

	entp->ce_rev = rcsnum_alloc();
	if (entp->ce_rev == NULL) {
		cvs_ent_free(entp);
		return (NULL);
	}

	entp->ce_line = strdup(entry);
	if (entp->ce_line == NULL) {
		cvs_ent_free(entp);
		return (NULL);
	}

	if (*fields[0] == '\0')
		entp->ce_type = CVS_ENT_FILE;
	else if (*fields[0] == 'D')
		entp->ce_type = CVS_ENT_DIR;
	else
		entp->ce_type = CVS_ENT_NONE;

	entp->ce_name = fields[1];

	if (entp->ce_type == CVS_ENT_FILE) {
		rcsnum_aton(fields[2], NULL, entp->ce_rev);
		entp->ce_mtime = cvs_datesec(fields[3], CVS_DATE_CTIME, 0);
		entp->ce_opts = fields[4];
		entp->ce_tag = fields[5];
	}

	return (entp);
}


/*
 * cvs_ent_free()
 *
 * Free a single CVS entries structure.
 */

void
cvs_ent_free(struct cvs_ent *ent)
{
	if (ent->ce_rev != NULL)
		rcsnum_free(ent->ce_rev);
	if (ent->ce_line != NULL)
		free(ent->ce_line);
	if (ent->ce_buf != NULL)
		free(ent->ce_buf);
	free(ent);
}


/*
 * cvs_ent_getent()
 *
 * Get a single entry from the CVS/Entries file of the basename portion of
 * path <path> and return that entry.  That entry must later be freed using
 * cvs_ent_free().
 */

struct cvs_ent*
cvs_ent_getent(const char *path)
{
	char base[MAXPATHLEN], *file;
	CVSENTRIES *entf;
	struct cvs_ent *ep;

	cvs_splitpath(path, base, sizeof(base), &file);

	entf = cvs_ent_open(base, O_RDONLY);
	if (entf == NULL)
		return (NULL);

	ep = cvs_ent_get(entf, file);
	if (ep != NULL) {
		/* take it out of the queue so it doesn't get freed */
		TAILQ_REMOVE(&(entf->cef_ent), ep, ce_list);
	}

	cvs_ent_close(entf);
	return (ep);
}


/*
 * cvs_ent_write()
 *
 * Explicitly write the contents of the Entries file <ef> to disk.
 * Returns 0 on success, or -1 on failure.
 */

int
cvs_ent_write(CVSENTRIES *ef)
{
	size_t len;
	char revbuf[64], timebuf[32];
	struct cvs_ent *ent;

	if (ef->cef_file == NULL)
		return (-1);

	if (ef->cef_flags & CVS_ENTF_SYNC)
		return (0);

	/* reposition ourself at beginning of file */
	rewind(ef->cef_file);
	TAILQ_FOREACH(ent, &(ef->cef_ent), ce_list) {
		if (ent->ce_type == CVS_ENT_DIR) {
			putc('D', ef->cef_file);
			timebuf[0] = '\0';
			revbuf[0] = '\0';
		}
		else {
			rcsnum_tostr(ent->ce_rev, revbuf, sizeof(revbuf));
			if (ent->ce_mtime == CVS_DATE_DMSEC)
				strlcpy(timebuf, CVS_DATE_DUMMY,
				    sizeof(timebuf));
			else {
				ctime_r(&(ent->ce_mtime), timebuf);
				len = strlen(timebuf);
				if ((len > 0) && (timebuf[len - 1] == '\n'))
					timebuf[--len] = '\0';
			}
		}

		fprintf(ef->cef_file, "/%s/%s/%s/%s/%s\n", ent->ce_name,
		    revbuf, timebuf, "", "");
	}

	/* terminating line */
	fprintf(ef->cef_file, "D\n");

	ef->cef_flags |= CVS_ENTF_SYNC;

	return (0);
}