/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright (c) 2001, 2010, Oracle and/or its affiliates. All rights reserved.
 * Copyright 2013, Joyent, Inc. All rights reserved.
 * Copyright 2018 Sebastian Wiedenroth
 */

/*
 * logadm/conf.c -- configuration file module
 */

#include <stdio.h>
#include <libintl.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <ctype.h>
#include <strings.h>
#include <unistd.h>
#include <stdlib.h>
#include <limits.h>
#include "err.h"
#include "lut.h"
#include "fn.h"
#include "opts.h"
#include "conf.h"

/* forward declarations of functions private to this module */
static void fillconflist(int lineno, const char *entry,
    struct opts *opts, const char *com, int flags);
static void fillargs(char *arg);
static char *nexttok(char **ptrptr);
static void conf_print(FILE *cstream, FILE *tstream);

static const char *Confname;	/* name of the confile file */
static int Conffd = -1;		/* file descriptor for config file */
static char *Confbuf;		/* copy of the config file (a la mmap()) */
static int Conflen;		/* length of mmap'd config file area */
static const char *Timesname;	/* name of the timestamps file */
static int Timesfd = -1;	/* file descriptor for timestamps file */
static char *Timesbuf;		/* copy of the timestamps file (a la mmap()) */
static int Timeslen;		/* length of mmap'd timestamps area */
static int Singlefile;		/* Conf and Times in the same file */
static int Changed;		/* what changes need to be written back */
static int Canchange;		/* what changes can be written back */
static int Changing;		/* what changes have been requested */
#define	CHG_NONE	0
#define	CHG_TIMES	1
#define	CHG_BOTH	3

/*
 * our structured representation of the configuration file
 * is made up of a list of these
 */
struct confinfo {
	struct confinfo *cf_next;
	int cf_lineno;		/* line number in file */
	const char *cf_entry;	/* name of entry, if line has an entry */
	struct opts *cf_opts;	/* parsed rhs of entry */
	const char *cf_com;	/* any comment text found */
	int cf_flags;
};

#define	CONFF_DELETED	1	/* entry should be deleted on write back */
#define	CONFF_TSONLY	2	/* entry should only be in timestamps file */

static struct confinfo *Confinfo;	/* the entries in the config file */
static struct confinfo *Confinfolast;	/* end of list */
static struct lut *Conflut;		/* lookup table keyed by entry name */
static struct fn_list *Confentries;	/* list of valid entry names */

/* allocate & fill in another entry in our list */
static void
fillconflist(int lineno, const char *entry,
    struct opts *opts, const char *com, int flags)
{
	struct confinfo *cp = MALLOC(sizeof (*cp));

	cp->cf_next = NULL;
	cp->cf_lineno = lineno;
	cp->cf_entry = entry;
	cp->cf_opts = opts;
	cp->cf_com = com;
	cp->cf_flags = flags;
	if (entry != NULL) {
		Conflut = lut_add(Conflut, entry, cp);
		fn_list_adds(Confentries, entry);
	}
	if (Confinfo == NULL)
		Confinfo = Confinfolast = cp;
	else {
		Confinfolast->cf_next = cp;
		Confinfolast = cp;
	}
}

static char **Args;	/* static buffer for args */
static int ArgsN;	/* size of our static buffer */
static int ArgsI;	/* index into Cmdargs as we walk table */
#define	CONF_ARGS_INC	1024

/* callback for lut_walk to build a cmdargs vector */
static void
fillargs(char *arg)
{
	if (ArgsI >= ArgsN) {
		/* need bigger table */
		Args = REALLOC(Args, sizeof (char *) * (ArgsN + CONF_ARGS_INC));
		ArgsN += CONF_ARGS_INC;
	}
	Args[ArgsI++] = arg;
}

/* isolate and return the next token */
static char *
nexttok(char **ptrptr)
{
	char *ptr = *ptrptr;
	char *eptr;
	char *quote = NULL;

	while (*ptr && isspace(*ptr))
		ptr++;

	if (*ptr == '"' || *ptr == '\'')
		quote = ptr++;

	for (eptr = ptr; *eptr; eptr++)
		if (quote && *eptr == *quote) {
			/* found end quote */
			*eptr++ = '\0';
			*ptrptr = eptr;
			return (ptr);
		} else if (!quote && isspace(*eptr)) {
			/* found end of unquoted area */
			*eptr++ = '\0';
			*ptrptr = eptr;
			return (ptr);
		}

	if (quote != NULL)
		err(EF_FILE|EF_JMP, "Unbalanced %c quote", *quote);
		/*NOTREACHED*/

	*ptrptr = eptr;

	if (ptr == eptr)
		return (NULL);
	else
		return (ptr);
}

/*
 * scan the memory image of a file
 *	returns: 0: error, 1: ok, 3: -P option found
 */
static int
conf_scan(const char *fname, char *buf, int buflen, int timescan)
{
	int ret = 1;
	int lineno = 0;
	int flags = 0;
	char *line;
	char *eline;
	char *ebuf;
	char *entry, *comment;

	ebuf = &buf[buflen];

	if (buf[buflen - 1] != '\n')
		err(EF_WARN|EF_FILE, "file %s doesn't end with newline, "
		    "last line ignored.", fname);

	for (line = buf; line < ebuf; line = eline) {
		char *ap;
		struct opts *opts = NULL;
		struct confinfo *cp;

		lineno++;
		err_fileline(fname, lineno);
		eline = line;
		comment = NULL;
		for (; eline < ebuf; eline++) {
			/* check for continued lines */
			if (comment == NULL && *eline == '\\' &&
			    eline + 1 < ebuf && *(eline + 1) == '\n') {
				*eline = ' ';
				*(eline + 1) = ' ';
				lineno++;
				err_fileline(fname, lineno);
				continue;
			}

			/* check for comments */
			if (comment == NULL && *eline == '#') {
				*eline = '\0';
				comment = (eline + 1);
				continue;
			}

			/* check for end of line */
			if (*eline == '\n')
				break;
		}
		if (comment >= ebuf)
			comment = NULL;
		if (eline >= ebuf) {
			/* discard trailing unterminated line */
			continue;
		}
		*eline++ = '\0';

		/*
		 * now we have the entry, if any, at "line"
		 * and the comment, if any, at "comment"
		 */

		/* entry is first token */
		entry = nexttok(&line);
		if (entry == NULL) {
			/* it's just a comment line */
			if (!timescan)
				fillconflist(lineno, entry, NULL, comment, 0);
			continue;
		}
		if (strcmp(entry, "logadm-version") == 0) {
			/*
			 * we somehow opened some future format
			 * conffile that we likely don't understand.
			 * if the given version is "1" then go on,
			 * otherwise someone is mixing versions
			 * and we can't help them other than to
			 * print an error and exit.
			 */
			if ((entry = nexttok(&line)) != NULL &&
			    strcmp(entry, "1") != 0)
				err(0, "%s version not supported "
				    "by this version of logadm.",
				    fname);
			continue;
		}

		/* form an argv array */
		ArgsI = 0;
		while (ap = nexttok(&line))
			fillargs(ap);

		/*
		 * If there is no next token on the line, make sure that
		 * we get a non-NULL Args array.
		 */
		if (Args == NULL)
			fillargs(NULL);

		Args[ArgsI] = NULL;

		LOCAL_ERR_BEGIN {
			if (SETJMP) {
				err(EF_FILE, "cannot process invalid entry %s",
				    entry);
				ret = 0;
				LOCAL_ERR_BREAK;
			}

			if (timescan) {
				/* append to config options */
				cp = lut_lookup(Conflut, entry);
				if (cp != NULL) {
					opts = cp->cf_opts;
				}
			}
			opts = opts_parse(opts, Args, OPTF_CONF);
			if (!timescan || cp == NULL) {
				/*
				 * If we're not doing timescan, we track this
				 * entry.  If we are doing timescan and have
				 * what looks like an orphaned entry (cp ==
				 * NULL) then we also have to track. See the
				 * comment in rotatelog. We need to allow for
				 * the case where the logname is not the same as
				 * the log file name.
				 */
				flags = 0;
				if (cp == NULL)
					flags = CONFF_TSONLY;
				fillconflist(lineno, entry, opts, comment,
				    flags);
			}
		LOCAL_ERR_END }

		if (ret == 1 && opts && opts_optarg(opts, "P") != NULL)
			ret = 3;
	}

	err_fileline(NULL, 0);
	return (ret);
}

/*
 * conf_open -- open the configuration file, lock it if we have write perms
 */
int
conf_open(const char *cfname, const char *tfname, struct opts *cliopts)
{
	struct stat stbuf1, stbuf2, stbuf3;
	struct flock	flock;
	int ret;

	Confname = cfname;
	Timesname = tfname;
	Confentries = fn_list_new(NULL);
	Changed = CHG_NONE;

	Changing = CHG_TIMES;
	if (opts_count(cliopts, "Vn") != 0)
		Changing = CHG_NONE;
	else if (opts_count(cliopts, "rw") != 0)
		Changing = CHG_BOTH;

	Singlefile = strcmp(Confname, Timesname) == 0;
	if (Singlefile && Changing == CHG_TIMES)
		Changing = CHG_BOTH;

	/* special case this so we don't even try locking the file */
	if (strcmp(Confname, "/dev/null") == 0)
		return (0);

	while (Conffd == -1) {
		Canchange = CHG_BOTH;
		if ((Conffd = open(Confname, O_RDWR)) < 0) {
			if (Changing == CHG_BOTH)
				err(EF_SYS, "open %s", Confname);
			Canchange = CHG_TIMES;
			if ((Conffd = open(Confname, O_RDONLY)) < 0)
				err(EF_SYS, "open %s", Confname);
		}

		flock.l_type = (Canchange == CHG_BOTH) ? F_WRLCK : F_RDLCK;
		flock.l_whence = SEEK_SET;
		flock.l_start = 0;
		flock.l_len = 1;
		if (fcntl(Conffd, F_SETLKW, &flock) < 0)
			err(EF_SYS, "flock on %s", Confname);

		/* wait until after file is locked to get filesize */
		if (fstat(Conffd, &stbuf1) < 0)
			err(EF_SYS, "fstat on %s", Confname);

		/* verify that we've got a lock on the active file */
		if (stat(Confname, &stbuf2) < 0 ||
		    !(stbuf2.st_dev == stbuf1.st_dev &&
		    stbuf2.st_ino == stbuf1.st_ino)) {
			/* wrong config file, try again */
			(void) close(Conffd);
			Conffd = -1;
		}
	}

	while (!Singlefile && Timesfd == -1) {
		if ((Timesfd = open(Timesname, O_CREAT|O_RDWR, 0644)) < 0) {
			if (Changing != CHG_NONE)
				err(EF_SYS, "open %s", Timesname);
			Canchange = CHG_NONE;
			if ((Timesfd = open(Timesname, O_RDONLY)) < 0)
				err(EF_SYS, "open %s", Timesname);
		}

		flock.l_type = (Canchange != CHG_NONE) ? F_WRLCK : F_RDLCK;
		flock.l_whence = SEEK_SET;
		flock.l_start = 0;
		flock.l_len = 1;
		if (fcntl(Timesfd, F_SETLKW, &flock) < 0)
			err(EF_SYS, "flock on %s", Timesname);

		/* wait until after file is locked to get filesize */
		if (fstat(Timesfd, &stbuf2) < 0)
			err(EF_SYS, "fstat on %s", Timesname);

		/* verify that we've got a lock on the active file */
		if (stat(Timesname, &stbuf3) < 0 ||
		    !(stbuf2.st_dev == stbuf3.st_dev &&
		    stbuf2.st_ino == stbuf3.st_ino)) {
			/* wrong timestamp file, try again */
			(void) close(Timesfd);
			Timesfd = -1;
			continue;
		}

		/* check that Timesname isn't an alias for Confname */
		if (stbuf2.st_dev == stbuf1.st_dev &&
		    stbuf2.st_ino == stbuf1.st_ino)
			err(0, "Timestamp file %s can't refer to "
			    "Configuration file %s", Timesname, Confname);
	}

	Conflen = stbuf1.st_size;
	Timeslen = stbuf2.st_size;

	if (Conflen == 0)
		return (1);	/* empty file, don't bother parsing it */

	if ((Confbuf = (char *)mmap(0, Conflen,
	    PROT_READ | PROT_WRITE, MAP_PRIVATE, Conffd, 0)) == (char *)-1)
		err(EF_SYS, "mmap on %s", Confname);

	ret = conf_scan(Confname, Confbuf, Conflen, 0);
	if (ret == 3 && !Singlefile && Canchange == CHG_BOTH) {
		/*
		 * arrange to transfer any timestamps
		 * from conf_file to timestamps_file
		 */
		Changing = Changed = CHG_BOTH;
	}

	if (Timesfd != -1 && Timeslen != 0) {
		if ((Timesbuf = (char *)mmap(0, Timeslen,
		    PROT_READ | PROT_WRITE, MAP_PRIVATE,
		    Timesfd, 0)) == (char *)-1)
			err(EF_SYS, "mmap on %s", Timesname);
		ret &= conf_scan(Timesname, Timesbuf, Timeslen, 1);
	}

	/*
	 * possible future enhancement:  go through and mark any entries:
	 * 		logfile -P <date>
	 * as DELETED if the logfile doesn't exist
	 */

	return (ret);
}

/*
 * conf_close -- close the configuration file
 */
void
conf_close(struct opts *opts)
{
	char cuname[PATH_MAX], tuname[PATH_MAX];
	int cfd, tfd;
	FILE *cfp = NULL, *tfp = NULL;
	boolean_t safe_update = B_TRUE;

	if (Changed == CHG_NONE || opts_count(opts, "n") != 0) {
		if (opts_count(opts, "v"))
			(void) out("# %s and %s unchanged\n",
			    Confname, Timesname);
		goto cleanup;
	}

	if (Debug > 1) {
		(void) fprintf(stderr, "conf_close, saving logadm context:\n");
		conf_print(stderr, NULL);
	}

	cuname[0] = tuname[0] = '\0';
	LOCAL_ERR_BEGIN {
		if (SETJMP) {
			safe_update = B_FALSE;
			LOCAL_ERR_BREAK;
		}
		if (Changed == CHG_BOTH) {
			if (Canchange != CHG_BOTH)
				err(EF_JMP, "internal error: attempting "
				    "to update %s without locking", Confname);
			(void) snprintf(cuname, sizeof (cuname), "%sXXXXXX",
			    Confname);
			if ((cfd = mkstemp(cuname)) == -1)
				err(EF_SYS|EF_JMP, "open %s replacement",
				    Confname);
			if (opts_count(opts, "v"))
				(void) out("# writing changes to %s\n", cuname);
			if (fchmod(cfd, 0644) == -1)
				err(EF_SYS|EF_JMP, "chmod %s", cuname);
			if ((cfp = fdopen(cfd, "w")) == NULL)
				err(EF_SYS|EF_JMP, "fdopen on %s", cuname);
		} else {
			/* just toss away the configuration data */
			cfp = fopen("/dev/null", "w");
		}
		if (!Singlefile) {
			if (Canchange == CHG_NONE)
				err(EF_JMP, "internal error: attempting "
				    "to update %s without locking", Timesname);
			(void) snprintf(tuname, sizeof (tuname), "%sXXXXXX",
			    Timesname);
			if ((tfd = mkstemp(tuname)) == -1)
				err(EF_SYS|EF_JMP, "open %s replacement",
				    Timesname);
			if (opts_count(opts, "v"))
				(void) out("# writing changes to %s\n", tuname);
			if (fchmod(tfd, 0644) == -1)
				err(EF_SYS|EF_JMP, "chmod %s", tuname);
			if ((tfp = fdopen(tfd, "w")) == NULL)
				err(EF_SYS|EF_JMP, "fdopen on %s", tuname);
		}

		conf_print(cfp, tfp);
		if (fclose(cfp) < 0)
			err(EF_SYS|EF_JMP, "fclose on %s", Confname);
		if (tfp != NULL && fclose(tfp) < 0)
			err(EF_SYS|EF_JMP, "fclose on %s", Timesname);
	LOCAL_ERR_END }

	if (!safe_update) {
		if (cuname[0] != 0)
			(void) unlink(cuname);
		if (tuname[0] != 0)
			(void) unlink(tuname);
		err(EF_JMP, "unsafe to update configuration file "
		    "or timestamps");
		return;
	}

	/* rename updated files into place */
	if (cuname[0] != '\0')
		if (rename(cuname, Confname) < 0)
			err(EF_SYS, "rename %s to %s", cuname, Confname);
	if (tuname[0] != '\0')
		if (rename(tuname, Timesname) < 0)
			err(EF_SYS, "rename %s to %s", tuname, Timesname);
	Changed = CHG_NONE;

cleanup:
	if (Conffd != -1) {
		(void) close(Conffd);
		Conffd = -1;
	}
	if (Timesfd != -1) {
		(void) close(Timesfd);
		Timesfd = -1;
	}
	if (Conflut) {
		lut_free(Conflut, free);
		Conflut = NULL;
	}
	if (Confentries) {
		fn_list_free(Confentries);
		Confentries = NULL;
	}
}

/*
 * conf_lookup -- lookup an entry in the config file
 */
void *
conf_lookup(const char *lhs)
{
	struct confinfo *cp = lut_lookup(Conflut, lhs);

	if (cp != NULL)
		err_fileline(Confname, cp->cf_lineno);
	return (cp);
}

/*
 * conf_opts -- return the parsed opts for an entry
 */
struct opts *
conf_opts(const char *lhs)
{
	struct confinfo *cp = lut_lookup(Conflut, lhs);

	if (cp != NULL)
		return (cp->cf_opts);
	return (opts_parse(NULL, NULL, OPTF_CONF));
}

/*
 * conf_replace -- replace an entry in the config file
 */
void
conf_replace(const char *lhs, struct opts *newopts)
{
	struct confinfo *cp = lut_lookup(Conflut, lhs);

	if (Conffd == -1)
		return;

	if (cp != NULL) {
		cp->cf_opts = newopts;
		/* cp->cf_args = NULL; */
		if (newopts == NULL)
			cp->cf_flags |= CONFF_DELETED;
	} else
		fillconflist(0, lhs, newopts, NULL, 0);

	Changed = CHG_BOTH;
}

/*
 * conf_set -- set options for an entry in the config file
 */
void
conf_set(const char *entry, char *o, const char *optarg)
{
	struct confinfo *cp = lut_lookup(Conflut, entry);

	if (Conffd == -1)
		return;

	if (cp != NULL) {
		cp->cf_flags &= ~CONFF_DELETED;
	} else {
		fillconflist(0, STRDUP(entry),
		    opts_parse(NULL, NULL, OPTF_CONF), NULL, 0);
		if ((cp = lut_lookup(Conflut, entry)) == NULL)
			err(0, "conf_set internal error");
	}
	(void) opts_set(cp->cf_opts, o, optarg);
	if (strcmp(o, "P") == 0)
		Changed |= CHG_TIMES;
	else
		Changed = CHG_BOTH;
}

/*
 * conf_entries -- list all the entry names
 */
struct fn_list *
conf_entries(void)
{
	return (Confentries);
}

/* print the config file */
static void
conf_print(FILE *cstream, FILE *tstream)
{
	struct confinfo *cp;
	char *exclude_opts = "PFfhnrvVw";
	const char *timestamp;

	if (tstream == NULL) {
		exclude_opts++;		/* -P option goes to config file */
	} else {
		(void) fprintf(tstream, gettext(
		    "# This file holds internal data for logadm(1M).\n"
		    "# Do not edit.\n"));
	}
	for (cp = Confinfo; cp; cp = cp->cf_next) {
		if (cp->cf_flags & CONFF_DELETED)
			continue;
		if (cp->cf_entry) {
			/* output timestamps to tstream */
			if (tstream != NULL && (timestamp =
			    opts_optarg(cp->cf_opts, "P")) != NULL) {
				opts_printword(cp->cf_entry, tstream);
				(void) fprintf(tstream, " -P ");
				opts_printword(timestamp, tstream);
				(void) fprintf(tstream, "\n");
			}
			if (cp->cf_flags & CONFF_TSONLY)
				continue;

			opts_printword(cp->cf_entry, cstream);
			if (cp->cf_opts)
				opts_print(cp->cf_opts, cstream, exclude_opts);
		}
		if (cp->cf_com) {
			if (cp->cf_entry)
				(void) fprintf(cstream, " ");
			(void) fprintf(cstream, "#%s", cp->cf_com);
		}
		(void) fprintf(cstream, "\n");
	}
}

#ifdef	TESTMODULE

/*
 * test main for conf module, usage: a.out conffile
 */
int
main(int argc, char *argv[])
{
	struct opts *opts;

	err_init(argv[0]);
	setbuf(stdout, NULL);
	opts_init(Opttable, Opttable_cnt);

	opts = opts_parse(NULL, NULL, 0);

	if (argc != 2)
		err(EF_RAW, "usage: %s conffile\n", argv[0]);

	conf_open(argv[1], argv[1], opts);

	printf("conffile <%s>:\n", argv[1]);
	conf_print(stdout, NULL);

	conf_close(opts);

	err_done(0);
	/* NOTREACHED */
	return (0);
}

#endif	/* TESTMODULE */