root/usr/src/cmd/logadm/conf.c
/*
 * 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 = NULL;

                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 (timescan && 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(8).\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

int Debug;

/*
 * 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]);

        (void) 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 */