/*-
 * Copyright (c) 2003-2006, Maxime Henrion <mux@FreeBSD.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. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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.
 *
 * $FreeBSD: releng/9.2/usr.bin/csup/config.c 204556 2010-03-02 07:26:07Z lulf $
 */

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

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

#include "config.h"
#include "globtree.h"
#include "keyword.h"
#include "misc.h"
#include "parse.h"
#include "stream.h"
#include "token.h"

static int		 config_parse_refusefiles(struct coll *);
static int		 config_parse_refusefile(struct coll *, char *);

extern FILE *yyin;

/* These are globals because I can't think of a better way with yacc. */
static STAILQ_HEAD(, coll) colls;
static struct coll *cur_coll;
static struct coll *defaults;
static struct coll *ovcoll;
static int ovmask;
static const char *cfgfile;

/*
 * Extract all the configuration information from the config
 * file and some command line parameters.
 */
struct config *
config_init(const char *file, struct coll *override, int overridemask)
{
	struct config *config;
	struct coll *coll;
	size_t slen;
	char *prefix;
	int error;
	mode_t mask;

	config = xmalloc(sizeof(struct config));
	memset(config, 0, sizeof(struct config));
	STAILQ_INIT(&colls);

	defaults = coll_new(NULL);
	/* Set the default umask. */
	mask = umask(0);
	umask(mask);
	defaults->co_umask = mask;
	ovcoll = override;
	ovmask = overridemask;

	/* Extract a list of collections from the configuration file. */
	cur_coll = coll_new(defaults);
	yyin = fopen(file, "r");
	if (yyin == NULL) {
		lprintf(-1, "Cannot open \"%s\": %s\n", file, strerror(errno));
		goto bad;
	}
	cfgfile = file;
	error = yyparse();
	fclose(yyin);
	if (error)
		goto bad;

	memcpy(&config->colls, &colls, sizeof(colls));
	if (STAILQ_EMPTY(&config->colls)) {
		lprintf(-1, "Empty supfile\n");
		goto bad;
	}

	/* Fixup the list of collections. */
	STAILQ_FOREACH(coll, &config->colls, co_next) {
 		if (coll->co_base == NULL)
			coll->co_base = xstrdup("/usr/local/etc/cvsup");
		if (coll->co_colldir == NULL)
			coll->co_colldir = "sup";
		if (coll->co_prefix == NULL) {
			coll->co_prefix = xstrdup(coll->co_base);
		/*
		 * If prefix is not an absolute pathname, it is
		 * interpreted relative to base.
		 */
		} else if (coll->co_prefix[0] != '/') {
			slen = strlen(coll->co_base);
			if (slen > 0 && coll->co_base[slen - 1] != '/')
				xasprintf(&prefix, "%s/%s", coll->co_base,
				    coll->co_prefix);
			else
				xasprintf(&prefix, "%s%s", coll->co_base,
				    coll->co_prefix);
			free(coll->co_prefix);
			coll->co_prefix = prefix;
		}
		coll->co_prefixlen = strlen(coll->co_prefix);
		/* Determine whether to checksum RCS files or not. */
		if (coll->co_options & CO_EXACTRCS)
			coll->co_options |= CO_CHECKRCS;
		else
			coll->co_options &= ~CO_CHECKRCS;
		/* In recent versions, we always try to set the file modes. */
		coll->co_options |= CO_SETMODE;
		coll->co_options |= CO_NORSYNC;
		error = config_parse_refusefiles(coll);
		if (error)
			goto bad;
	}

	coll_free(cur_coll);
	coll_free(defaults);
	config->host = STAILQ_FIRST(&config->colls)->co_host;
	return (config);
bad:
	coll_free(cur_coll);
	coll_free(defaults);
	config_free(config);
	return (NULL);
}

int
config_checkcolls(struct config *config)
{
	char linkname[4];
	struct stat sb;
	struct coll *coll;
	int error, numvalid, ret;

	numvalid = 0;
	STAILQ_FOREACH(coll, &config->colls, co_next) {
		error = stat(coll->co_prefix, &sb);
		if (error || !S_ISDIR(sb.st_mode)) {
			/* Skip this collection, and warn about it unless its
			   prefix is a symbolic link pointing to "SKIP". */
			coll->co_options |= CO_SKIP;
			ret = readlink(coll->co_prefix, linkname,
			    sizeof(linkname));
			if (ret != 4 || memcmp(linkname, "SKIP", 4) != 0) {
				lprintf(-1,"Nonexistent prefix \"%s\" for "
				    "%s/%s\n", coll->co_prefix, coll->co_name,
				    coll->co_release);
			}
			continue;
		}
		numvalid++;
	}
	return (numvalid);
}

static int
config_parse_refusefiles(struct coll *coll)
{
	char *collstem, *suffix, *supdir, *path;
	int error;

	if (coll->co_colldir[0] == '/')
		supdir = xstrdup(coll->co_colldir);
	else
		xasprintf(&supdir, "%s/%s", coll->co_base, coll->co_colldir);

	/* First, the global refuse file that applies to all collections. */
	xasprintf(&path, "%s/refuse", supdir);
	error = config_parse_refusefile(coll, path);
	free(path);
	if (error) {
		free(supdir);
		return (error);
	}

	/* Next the per-collection refuse files that applies to all release/tag
	   combinations. */
	xasprintf(&collstem, "%s/%s/refuse", supdir, coll->co_name);
	free(supdir);
	error = config_parse_refusefile(coll, collstem);
	if (error) {
		free(collstem);
		return (error);
	}

	/* Finally, the per-release and per-tag refuse file. */
	suffix = coll_statussuffix(coll);
	if (suffix != NULL) {
		xasprintf(&path, "%s%s", collstem, suffix);
		free(suffix);
		error = config_parse_refusefile(coll, path);
		free(path);
	}
	free(collstem);
	return (error);
}

/*
 * Parses a "refuse" file, and records the relevant information in
 * coll->co_refusals.  If the file does not exist, it is silently
 * ignored.
 */
static int
config_parse_refusefile(struct coll *coll, char *path)
{
	struct stream *rd;
	char *cp, *line, *pat;

	rd = stream_open_file(path, O_RDONLY);
	if (rd == NULL)
		return (0);
	while ((line = stream_getln(rd, NULL)) != NULL) {
		pat = line;
		for (;;) {
			/* Trim leading whitespace. */
			pat += strspn(pat, " \t");
			if (pat[0] == '\0')
				break;
			cp = strpbrk(pat, " \t");
			if (cp != NULL)
				*cp = '\0';
			pattlist_add(coll->co_refusals, pat);
			if (cp == NULL)
				break;
			pat = cp + 1;
		}
	}
	if (!stream_eof(rd)) {
		stream_close(rd);
		lprintf(-1, "Read failure from \"%s\": %s\n", path,
		    strerror(errno));
		return (-1);
	}
	stream_close(rd);
	return (0);
}

void
config_free(struct config *config)
{
	struct coll *coll;

	while (!STAILQ_EMPTY(&config->colls)) {
		coll = STAILQ_FIRST(&config->colls);
		STAILQ_REMOVE_HEAD(&config->colls, co_next);
		coll_free(coll);
	}
	if (config->server != NULL)
		stream_close(config->server);
	if (config->laddr != NULL)
		free(config->laddr);
	free(config);
}

/* Create a new collection, inheriting options from the default collection. */
struct coll *
coll_new(struct coll *def)
{
	struct coll *new;

	new = xmalloc(sizeof(struct coll));
	memset(new, 0, sizeof(struct coll));
	if (def != NULL) {
		new->co_options = def->co_options;
		new->co_umask = def->co_umask;
		if (def->co_host != NULL)
			new->co_host = xstrdup(def->co_host);
		if (def->co_base != NULL)
			new->co_base = xstrdup(def->co_base);
		if (def->co_date != NULL)
			new->co_date = xstrdup(def->co_date);
		if (def->co_prefix != NULL)
			new->co_prefix = xstrdup(def->co_prefix);
		if (def->co_release != NULL)
			new->co_release = xstrdup(def->co_release);
		if (def->co_tag != NULL)
			new->co_tag = xstrdup(def->co_tag);
		if (def->co_listsuffix != NULL)
			new->co_listsuffix = xstrdup(def->co_listsuffix);
	} else {
		new->co_tag = xstrdup(".");
		new->co_date = xstrdup(".");
	}
	new->co_keyword = keyword_new();
	new->co_accepts = pattlist_new();
	new->co_refusals = pattlist_new();
	new->co_attrignore = FA_DEV | FA_INODE;
	return (new);
}

void
coll_override(struct coll *coll, struct coll *from, int mask)
{
	size_t i;
	int newoptions, oldoptions;

	newoptions = from->co_options & mask;
	oldoptions = coll->co_options & (CO_MASK & ~mask);

	if (from->co_release != NULL) {
		if (coll->co_release != NULL)
			free(coll->co_release);
		coll->co_release = xstrdup(from->co_release);
	}
	if (from->co_host != NULL) {
		if (coll->co_host != NULL)
			free(coll->co_host);
		coll->co_host = xstrdup(from->co_host);
	}
	if (from->co_base != NULL) {
		if (coll->co_base != NULL)
			free(coll->co_base);
		coll->co_base = xstrdup(from->co_base);
	}
	if (from->co_colldir != NULL)
		coll->co_colldir = from->co_colldir;
	if (from->co_prefix != NULL) {
		if (coll->co_prefix != NULL)
			free(coll->co_prefix);
		coll->co_prefix = xstrdup(from->co_prefix);
	}
	if (newoptions & CO_CHECKOUTMODE) {
		if (from->co_tag != NULL) {
			if (coll->co_tag != NULL)
				free(coll->co_tag);
			coll->co_tag = xstrdup(from->co_tag);
		}
		if (from->co_date != NULL) {
			if (coll->co_date != NULL)
				free(coll->co_date);
			coll->co_date = xstrdup(from->co_date);
		}
	}
	if (from->co_listsuffix != NULL) {
		if (coll->co_listsuffix != NULL)
			free(coll->co_listsuffix);
		coll->co_listsuffix = xstrdup(from->co_listsuffix);
	}
	for (i = 0; i < pattlist_size(from->co_accepts); i++) {
		pattlist_add(coll->co_accepts,
		    pattlist_get(from->co_accepts, i));
	}
	for (i = 0; i < pattlist_size(from->co_refusals); i++) {
		pattlist_add(coll->co_refusals,
		    pattlist_get(from->co_refusals, i));
	}
	coll->co_options = oldoptions | newoptions;
}

char *
coll_statussuffix(struct coll *coll)
{
	const char *tag;
	char *suffix;

	if (coll->co_listsuffix != NULL) {
		xasprintf(&suffix, ".%s", coll->co_listsuffix);
	} else if (coll->co_options & CO_USERELSUFFIX) {
		if (coll->co_tag == NULL)
			tag = ".";
		else
			tag = coll->co_tag;
		if (coll->co_release != NULL) {
			if (coll->co_options & CO_CHECKOUTMODE) {
				xasprintf(&suffix, ".%s:%s",
				    coll->co_release, tag);
			} else {
				xasprintf(&suffix, ".%s", coll->co_release);
			}
		} else if (coll->co_options & CO_CHECKOUTMODE) {
			xasprintf(&suffix, ":%s", tag);
		}
	} else
		suffix = NULL;
	return (suffix);
}

char *
coll_statuspath(struct coll *coll)
{
	char *path, *suffix;

	suffix = coll_statussuffix(coll);
	if (suffix != NULL) {
		if (coll->co_colldir[0] == '/')
			xasprintf(&path, "%s/%s/checkouts%s", coll->co_colldir,
			    coll->co_name, suffix);
		else
			xasprintf(&path, "%s/%s/%s/checkouts%s", coll->co_base,
			    coll->co_colldir, coll->co_name, suffix);
	} else {
		if (coll->co_colldir[0] == '/')
			xasprintf(&path, "%s/%s/checkouts", coll->co_colldir,
			    coll->co_name);
		else
			xasprintf(&path, "%s/%s/%s/checkouts", coll->co_base,
			    coll->co_colldir, coll->co_name);
	}
	free(suffix);
	return (path);
}

void
coll_add(char *name)
{
	struct coll *coll;

	cur_coll->co_name = name;
	coll_override(cur_coll, ovcoll, ovmask);
	if (cur_coll->co_release == NULL) {
		lprintf(-1, "Release not specified for collection "
		    "\"%s\"\n", cur_coll->co_name);
		exit(1);
	}
	if (cur_coll->co_host == NULL) {
		lprintf(-1, "Host not specified for collection "
		    "\"%s\"\n", cur_coll->co_name);
		exit(1);
	}
	if (!STAILQ_EMPTY(&colls)) {
		coll = STAILQ_LAST(&colls, coll, co_next);
		if (strcmp(coll->co_host, cur_coll->co_host) != 0) {
			lprintf(-1, "All \"host\" fields in the supfile "
			    "must be the same\n");
			exit(1);
		}
	}
	STAILQ_INSERT_TAIL(&colls, cur_coll, co_next);
	cur_coll = coll_new(defaults);
}

void
coll_free(struct coll *coll)
{

	if (coll == NULL)
		return;
	if (coll->co_host != NULL)
		free(coll->co_host);
	if (coll->co_base != NULL)
		free(coll->co_base);
	if (coll->co_date != NULL)
		free(coll->co_date);
	if (coll->co_prefix != NULL)
		free(coll->co_prefix);
	if (coll->co_release != NULL)
		free(coll->co_release);
	if (coll->co_tag != NULL)
		free(coll->co_tag);
	if (coll->co_cvsroot != NULL)
		free(coll->co_cvsroot);
	if (coll->co_name != NULL)
		free(coll->co_name);
	if (coll->co_listsuffix != NULL)
		free(coll->co_listsuffix);
	keyword_free(coll->co_keyword);
	if (coll->co_dirfilter != NULL)
		globtree_free(coll->co_dirfilter);
	if (coll->co_dirfilter != NULL)
		globtree_free(coll->co_filefilter);
	if (coll->co_norsync != NULL)
		globtree_free(coll->co_norsync);
	if (coll->co_accepts != NULL)
		pattlist_free(coll->co_accepts);
	if (coll->co_refusals != NULL)
		pattlist_free(coll->co_refusals);
	free(coll);
}

void
coll_setopt(int opt, char *value)
{
	struct coll *coll;
	int error, mask;

	coll = cur_coll;
	switch (opt) {
	case PT_HOST:
		if (coll->co_host != NULL)
			free(coll->co_host);
		coll->co_host = value;
		break;
	case PT_BASE:
		if (coll->co_base != NULL)
			free(coll->co_base);
		coll->co_base = value;
		break;
	case PT_DATE:
		if (coll->co_date != NULL)
			free(coll->co_date);
		coll->co_date = value;
		coll->co_options |= CO_CHECKOUTMODE;
		break;
	case PT_PREFIX:
		if (coll->co_prefix != NULL)
			free(coll->co_prefix);
		coll->co_prefix = value;
		break;
	case PT_RELEASE:
		if (coll->co_release != NULL)
			free(coll->co_release);
		coll->co_release = value;
		break;
	case PT_TAG:
		if (coll->co_tag != NULL)
			free(coll->co_tag);
		coll->co_tag = value;
		coll->co_options |= CO_CHECKOUTMODE;
		break;
	case PT_LIST:
		if (strchr(value, '/') != NULL) {
			lprintf(-1, "Parse error in \"%s\": \"list\" suffix "
			    "must not contain slashes\n", cfgfile);
			exit(1);
		}
		if (coll->co_listsuffix != NULL)
			free(coll->co_listsuffix);
		coll->co_listsuffix = value;
		break;
	case PT_UMASK:
		error = asciitoint(value, &mask, 8);
		free(value);
		if (error) {
			lprintf(-1, "Parse error in \"%s\": Invalid "
			    "umask value\n", cfgfile);
			exit(1);
		}
		coll->co_umask = mask;
		break;
	case PT_USE_REL_SUFFIX:
		coll->co_options |= CO_USERELSUFFIX;
		break;
	case PT_DELETE:
		coll->co_options |= CO_DELETE | CO_EXACTRCS;
		break;
	case PT_COMPRESS:
		coll->co_options |= CO_COMPRESS;
		break;
	case PT_NORSYNC:
		coll->co_options |= CO_NORSYNC;
		break;
	}
}

/* Set "coll" as being the default collection. */
void
coll_setdef(void)
{

	coll_free(defaults);
	defaults = cur_coll;
	cur_coll = coll_new(defaults);
}
