root/lib/libfigpar/figpar.c
/*-
 * Copyright (c) 2002-2015 Devin Teske <dteske@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.
 */

#include <sys/param.h>

#include <ctype.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "figpar.h"
#include "string_m.h"

struct figpar_config figpar_dummy_config = {0, NULL, {0}, NULL};

/*
 * Search for config option (struct figpar_config) in the array of config
 * options, returning the struct whose directive matches the given parameter.
 * If no match is found, a pointer to the static dummy array (above) is
 * returned.
 *
 * This is to eliminate dependency on the index position of an item in the
 * array, since the index position is more apt to be changed as code grows.
 */
struct figpar_config *
get_config_option(struct figpar_config options[], const char *directive)
{
        uint32_t n;

        /* Check arguments */
        if (options == NULL || directive == NULL)
                return (&figpar_dummy_config);

        /* Loop through the array, return the index of the first match */
        for (n = 0; options[n].directive != NULL; n++)
                if (strcmp(options[n].directive, directive) == 0)
                        return (&(options[n]));

        /* Re-initialize the dummy variable in case it was written to */
        figpar_dummy_config.directive   = NULL;
        figpar_dummy_config.type        = 0;
        figpar_dummy_config.action      = NULL;
        figpar_dummy_config.value.u_num = 0;

        return (&figpar_dummy_config);
}

/*
 * Parse the configuration file at `path' and execute the `action' call-back
 * functions for any directives defined by the array of config options (first
 * argument).
 *
 * For unknown directives that are encountered, you can optionally pass a
 * call-back function for the third argument to be called for unknowns.
 *
 * Returns zero on success; otherwise returns -1 and errno should be consulted.
*/
int
parse_config(struct figpar_config options[], const char *path,
    int (*unknown)(struct figpar_config *option, uint32_t line,
    char *directive, char *value), uint16_t processing_options)
{
        uint8_t bequals;
        uint8_t bsemicolon;
        uint8_t case_sensitive;
        uint8_t comment = 0;
        uint8_t end;
        uint8_t found;
        uint8_t have_equals = 0;
        uint8_t quote;
        uint8_t require_equals;
        uint8_t strict_equals;
        char p[2];
        char *directive;
        char *t;
        char *value;
        int error;
        int fd;
        ssize_t r = 1;
        uint32_t dsize;
        uint32_t line = 1;
        uint32_t n;
        uint32_t vsize;
        uint32_t x;
        off_t charpos;
        off_t curpos;
        char rpath[PATH_MAX];

        /* Sanity check: if no options and no unknown function, return */
        if (options == NULL && unknown == NULL)
                return (-1);

        /* Processing options */
        bequals = (processing_options & FIGPAR_BREAK_ON_EQUALS) == 0 ? 0 : 1;
        bsemicolon =
                (processing_options & FIGPAR_BREAK_ON_SEMICOLON) == 0 ? 0 : 1;
        case_sensitive =
                (processing_options & FIGPAR_CASE_SENSITIVE) == 0 ? 0 : 1;
        require_equals =
                (processing_options & FIGPAR_REQUIRE_EQUALS) == 0 ? 0 : 1;
        strict_equals =
                (processing_options & FIGPAR_STRICT_EQUALS) == 0 ? 0 : 1;

        /* Initialize strings */
        directive = value = 0;
        vsize = dsize = 0;

        /* Resolve the file path */
        if (realpath(path, rpath) == 0)
                return (-1);

        /* Open the file */
        if ((fd = open(rpath, O_RDONLY)) < 0)
                return (-1);

        /* Read the file until EOF */
        while (r != 0) {
                r = read(fd, p, 1);

                /* skip to the beginning of a directive */
                while (r != 0 && (isspace(*p) || *p == '#' || comment ||
                    (bsemicolon && *p == ';'))) {
                        if (*p == '#')
                                comment = 1;
                        else if (*p == '\n') {
                                comment = 0;
                                line++;
                        }
                        r = read(fd, p, 1);
                }
                /* Test for EOF; if EOF then no directive was found */
                if (r == 0) {
                        close(fd);
                        return (0);
                }

                /* Get the current offset */
                if ((curpos = lseek(fd, 0, SEEK_CUR)) == -1) {
                        close(fd);
                        return (-1);
                }
                curpos--;

                /* Find the length of the directive */
                for (n = 0; r != 0; n++) {
                        if (isspace(*p))
                                break;
                        if (bequals && *p == '=') {
                                have_equals = 1;
                                break;
                        }
                        if (bsemicolon && *p == ';')
                                break;
                        r = read(fd, p, 1);
                }

                /* Test for EOF, if EOF then no directive was found */
                if (n == 0 && r == 0) {
                        close(fd);
                        return (0);
                }

                /* Go back to the beginning of the directive */
                if (lseek(fd, curpos, SEEK_SET) == -1) {
                        close(fd);
                        return (-1);
                }

                /* Allocate and read the directive into memory */
                if (n > dsize) {
                        if ((directive = realloc(directive, n + 1)) == NULL) {
                                close(fd);
                                return (-1);
                        }
                        dsize = n;
                }
                r = read(fd, directive, n);

                /* Advance beyond the equals sign if appropriate/desired */
                if (bequals && *p == '=') {
                        if (lseek(fd, 1, SEEK_CUR) != -1)
                                r = read(fd, p, 1);
                        if (strict_equals && isspace(*p))
                                *p = '\n';
                }

                /* Terminate the string */
                directive[n] = '\0';

                /* Convert directive to lower case before comparison */
                if (!case_sensitive)
                        strtolower(directive);

                /* Move to what may be the start of the value */
                if (!(bsemicolon && *p == ';') &&
                    !(strict_equals && *p == '=')) {
                        while (r != 0 && isspace(*p) && *p != '\n')
                                r = read(fd, p, 1);
                }

                /* An equals sign may have stopped us, should we eat it? */
                if (r != 0 && bequals && *p == '=' && !strict_equals) {
                        have_equals = 1;
                        r = read(fd, p, 1);
                        while (r != 0 && isspace(*p) && *p != '\n')
                                r = read(fd, p, 1);
                }

                /* If no value, allocate a dummy value and jump to action */
                if (r == 0 || *p == '\n' || *p == '#' ||
                    (bsemicolon && *p == ';')) {
                        /* Initialize the value if not already done */
                        if (value == NULL && (value = malloc(1)) == NULL) {
                                close(fd);
                                return (-1);
                        }
                        value[0] = '\0';
                        goto call_function;
                }

                /* Get the current offset */
                if ((curpos = lseek(fd, 0, SEEK_CUR)) == -1) {
                        close(fd);
                        return (-1);
                }
                curpos--;

                /* Find the end of the value */
                quote = 0;
                end = 0;
                while (r != 0 && end == 0) {
                        /* Advance to the next character if we know we can */
                        if (*p != '\"' && *p != '#' && *p != '\n' &&
                            (!bsemicolon || *p != ';')) {
                                r = read(fd, p, 1);
                                continue;
                        }

                        /*
                         * If we get this far, we've hit an end-key
                         */

                        /* Get the current offset */
                        if ((charpos = lseek(fd, 0, SEEK_CUR)) == -1) {
                                close(fd);
                                return (-1);
                        }
                        charpos--;

                        /*
                         * Go back so we can read the character before the key
                         * to check if the character is escaped (which means we
                         * should continue).
                         */
                        if (lseek(fd, -2, SEEK_CUR) == -1) {
                                close(fd);
                                return (-1);
                        }
                        r = read(fd, p, 1);

                        /*
                         * Count how many backslashes there are (an odd number
                         * means the key is escaped, even means otherwise).
                         */
                        for (n = 1; *p == '\\'; n++) {
                                /* Move back another offset to read */
                                if (lseek(fd, -2, SEEK_CUR) == -1) {
                                        close(fd);
                                        return (-1);
                                }
                                r = read(fd, p, 1);
                        }

                        /* Move offset back to the key and read it */
                        if (lseek(fd, charpos, SEEK_SET) == -1) {
                                close(fd);
                                return (-1);
                        }
                        r = read(fd, p, 1);

                        /*
                         * If an even number of backslashes was counted meaning
                         * key is not escaped, we should evaluate what to do.
                         */
                        if ((n & 1) == 1) {
                                switch (*p) {
                                case '\"':
                                        /*
                                         * Flag current sequence of characters
                                         * to follow as being quoted (hashes
                                         * are not considered comments).
                                         */
                                        quote = !quote;
                                        break;
                                case '#':
                                        /*
                                         * If we aren't in a quoted series, we
                                         * just hit an inline comment and have
                                         * found the end of the value.
                                         */
                                        if (!quote)
                                                end = 1;
                                        break;
                                case '\n':
                                        /*
                                         * Newline characters must always be
                                         * escaped, whether inside a quoted
                                         * series or not, otherwise they
                                         * terminate the value.
                                         */
                                        end = 1;
                                case ';':
                                        if (!quote && bsemicolon)
                                                end = 1;
                                        break;
                                }
                        } else if (*p == '\n')
                                /* Escaped newline character. increment */
                                line++;

                        /* Advance to the next character */
                        r = read(fd, p, 1);
                }

                /* Get the current offset */
                if ((charpos = lseek(fd, 0, SEEK_CUR)) == -1) {
                        close(fd);
                        return (-1);
                }

                /* Get the length of the value */
                n = (uint32_t)(charpos - curpos);
                if (r != 0) /* more to read, but don't read ending key */
                        n--;

                /* Move offset back to the beginning of the value */
                if (lseek(fd, curpos, SEEK_SET) == -1) {
                        close(fd);
                        return (-1);
                }

                /* Allocate and read the value into memory */
                if (n > vsize) {
                        if ((value = realloc(value, n + 1)) == NULL) {
                                close(fd);
                                return (-1);
                        }
                        vsize = n;
                }
                r = read(fd, value, n);

                /* Terminate the string */
                value[n] = '\0';

                /* Cut trailing whitespace off by termination */
                t = value + n;
                while (isspace(*--t))
                        *t = '\0';

                /* Escape the escaped quotes (replaceall is in string_m.c) */
                x = strcount(value, "\\\""); /* in string_m.c */
                if (x != 0 && (n + x) > vsize) {
                        if ((value = realloc(value, n + x + 1)) == NULL) {
                                close(fd);
                                return (-1);
                        }
                        vsize = n + x;
                }
                if (replaceall(value, "\\\"", "\\\\\"") < 0) {
                        /* Replace operation failed for some unknown reason */
                        close(fd);
                        return (-1);
                }

                /* Remove all new line characters */
                if (replaceall(value, "\\\n", "") < 0) {
                        /* Replace operation failed for some unknown reason */
                        close(fd);
                        return (-1);
                }

                /* Resolve escape sequences */
                strexpand(value); /* in string_m.c */

call_function:
                /* Abort if we're seeking only assignments */
                if (require_equals && !have_equals)
                        return (-1);

                found = have_equals = 0; /* reset */

                /* If there are no options defined, call unknown and loop */
                if (options == NULL && unknown != NULL) {
                        error = unknown(NULL, line, directive, value);
                        if (error != 0) {
                                close(fd);
                                return (error);
                        }
                        continue;
                }

                /* Loop through the array looking for a match for the value */
                for (n = 0; options[n].directive != NULL; n++) {
                        error = fnmatch(options[n].directive, directive,
                            FNM_NOESCAPE);
                        if (error == 0) {
                                found = 1;
                                /* Call function for array index item */
                                if (options[n].action != NULL) {
                                        error = options[n].action(
                                            &options[n],
                                            line, directive, value);
                                        if (error != 0) {
                                                close(fd);
                                                return (error);
                                        }
                                }
                        } else if (error != FNM_NOMATCH) {
                                /* An error has occurred */
                                close(fd);
                                return (-1);
                        }
                }
                if (!found && unknown != NULL) {
                        /*
                         * No match was found for the value we read from the
                         * file; call function designated for unknown values.
                         */
                        error = unknown(NULL, line, directive, value);
                        if (error != 0) {
                                close(fd);
                                return (error);
                        }
                }
        }

        close(fd);
        return (0);
}