/*
 * options.c -- Option parsing
 *
 * (c) Copyright IBM Corporation 2014, 2015.
 *
 * Author: Stefan Berger <stefanb@us.ibm.com>
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 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.
 *
 * Neither the names of the IBM Corporation nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
 * HOLDER 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 "config.h"

#include "sys_dependencies.h"

#include <limits.h>
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#include <pwd.h>
#include <grp.h>

#include "options.h"

static void
option_error_set(char **error, const char *format, ...)
{
    va_list ap;
    int ret;

    va_start(ap, format);
    ret = vasprintf(error, format, ap);
    va_end(ap);

    (void)ret;
}

/*
 * option_value_add
 * Add a option's value that was parsed following a template to the collection
 * of option values.
 * @ovs: OptionValues where to add the given value
 * @optdesc: the template to use for parsing this option
 * @val: the value to parses as a datatype given in optdesc
 * @error: Pointer to a pointer for an error message
 *
 * Returns 0 on success, -1 on error.
 */
static int
option_value_add(OptionValues *ovs, const OptionDesc optdesc, const char *val,
                 char **error)
{
    int ret = 0;
    char *endptr = NULL;
    long int li;
    long unsigned int lui;
    struct passwd *passwd;
    struct group *group;
    size_t idx = ovs->n_options;
    void *tmp;

    tmp = realloc(ovs->options, (idx + 1) * sizeof(*ovs->options));
    if (!tmp) {
        free(ovs->options);
        option_error_set(error, "Out of memory");
        return -1;
    }
    ovs->options = tmp;

    ovs->n_options = idx + 1;
    ovs->options[idx].type = optdesc.type;
    ovs->options[idx].name = optdesc.name;

    switch (optdesc.type) {
    case OPT_TYPE_STRING:
        ovs->options[idx].u.string = strdup(val);
        if (!ovs->options[idx].u.string) {
            option_error_set(error, "Out of memory");
            return -1;
        }
        break;
    case OPT_TYPE_INT:
        li = strtol(val, &endptr, 10);
        if (*endptr != '\0') {
            option_error_set(error, "invalid number '%s'", val);
            return -1;
        }
        if (li < INT_MIN || li > INT_MAX) {
            option_error_set(error, "number %li outside valid range", li);
        }
        ovs->options[idx].u.integer = li;

        break;
    case OPT_TYPE_UINT:
        lui = strtol(val, &endptr, 10);
        if (*endptr != '\0') {
            option_error_set(error, "invalid number '%s'", val);
            return -1;
        }
        if (lui > UINT_MAX) {
            option_error_set(error, "number %li outside valid range", lui);
        }
        ovs->options[idx].u.uinteger = lui;

        break;
    case OPT_TYPE_BOOLEAN:
        if (!strcasecmp(val, "true") || !strcasecmp(val, "1")) {
            ovs->options[idx].u.boolean = true;
        } else {
            ovs->options[idx].u.boolean = false;
        }
        break;
    case OPT_TYPE_MODE_T:
        lui = strtol(val, &endptr, 8);
        if (*endptr != '\0') {
            option_error_set(error, "invalid mode type '%s'", val);
            return -1;
        }
        if (lui > 0777) {
            option_error_set(error, "mode %s is invalid", val);
            return -1;
        }
        ovs->options[idx].u.mode = (mode_t)lui;

        break;
    case OPT_TYPE_UID_T:
        lui = strtol(val, &endptr, 10);
        if (*endptr == '\0') {
            if (lui > UINT_MAX) {
                option_error_set(error, "uid %s outside valid range", val);
                return -1;
            }
        } else {
            /* try as a string */
            passwd = getpwnam(val);
            if (!passwd) {
                option_error_set(error, "User '%s' does not exist.\n", val);
                return -1;
            }
            lui = passwd->pw_uid;
        }
        ovs->options[idx].u.uid = (uid_t)lui;

        break;
    case OPT_TYPE_GID_T:
        lui = strtol(val, &endptr, 10);
        if (*endptr == '\0') {
            if (lui > UINT_MAX) {
                option_error_set(error, "gid %s outside valid range", val);
                return -1;
            }
        } else {
            /* try as a string */
            group = getgrnam(val);
            if (!group) {
                option_error_set(error, "Group '%s' does not exist.\n", val);
                return -1;
            }
            lui = group->gr_gid;
        }
        ovs->options[idx].u.gid = (uid_t)lui;

        break;
    }

    return ret;
}

/*
 * options_parse:
 * Parse the string of options following the template; return the
 * parsed Options or an error string.
 * @opts: string containing the comma separated options to parse
 * @optdesc: template to follow when parsing individual options
 * @error: Pointer to a pointer for holding an error message
 *
 * Returns the parse options, types, and values in OptionValues.
 */
OptionValues *
options_parse(char *opts, const OptionDesc optdesc[], char **error)
{
    char *saveptr;
    char *tok;
    int i;
    OptionValues *ovs = calloc(sizeof(OptionValues), 1);
    bool found;
    char *opts_bak;

    if (!ovs) {
        option_error_set(error, "Out of memory.");
        return NULL;
    }

    opts_bak = strdup(opts);
    if (!opts_bak) {
        option_error_set(error, "Out of memory.");
        goto error;
    }

    saveptr = opts_bak; /* make coverity happy */

    tok = strtok_r(opts_bak, ",", &saveptr);
    while (tok) {
        size_t toklen = strlen(tok);

        found = false;
        i = 0;
        while (optdesc[i].name) {
            size_t len = strlen(optdesc[i].name);

            if (toklen > len + 1 && tok[len] == '=' &&
                !strncmp(optdesc[i].name, tok, len)) {
                const char *v = &tok[len + 1];

                if (option_value_add(ovs, optdesc[i], v, error) < 0)
                    goto error;
                found = true;
                break;
            } else if (!strcmp(optdesc[i].name, tok)) {
                if (option_value_add(ovs, optdesc[i], "true", error) < 0)
                    goto error;
                found = true;
                break;
            }
            i++;
        }
        if (!found) {
            option_error_set(error, "Unknown option '%s'", tok);
            goto error;
        }

        tok = strtok_r(NULL, ",", &saveptr);
    }

    free(opts_bak);

    return ovs;

error:
    free(opts_bak);

    option_values_free(ovs);
    return NULL;
}

/*
 * option_values_free
 * Free the option values
 * @ovs: OptionValues structure to free; may be NULL
 */
void
option_values_free(OptionValues *ovs)
{
    size_t i;

    if (!ovs)
        return;

    for (i = 0; i < ovs->n_options; i++) {
        switch (ovs->options[i].type) {
        case OPT_TYPE_STRING:
            free(ovs->options[i].u.string);
            break;
        case OPT_TYPE_INT:
        case OPT_TYPE_UINT:
        case OPT_TYPE_BOOLEAN:
        case OPT_TYPE_MODE_T:
        case OPT_TYPE_UID_T:
        case OPT_TYPE_GID_T:
            break;
        }
    }
    free(ovs->options);
    free(ovs);
}

/*
 * Given the name of a string option, return the value it received when it
 * was parsed.
 * @ovs: The OptionValues
 * @name: the name of the option
 * @def: the default value
 *
 * Returns the parsed value or the default value if none was parsed;
 * If the value is of different type than a string, NULL is returned.
 */
const char *
option_get_string(OptionValues *ovs, const char *name, const char *def)
{
    size_t i;

    for (i = 0; i < ovs->n_options; i++) {
        if (!strcmp(name, ovs->options[i].name)) {
            if (ovs->options[i].type == OPT_TYPE_STRING)
                return ovs->options[i].u.string;
            return NULL;
        }
    }

    return def;
}

/*
 * Given the name of an int option, return the value it received when it
 * was parsed.
 * @ovs: The OptionValues
 * @name: the name of the option
 * @def: the default value
 *
 * Returns the parsed value or the default value if none was parsed
 * If the value is of different type than an integer, -1 is returned.
 */
int
option_get_int(OptionValues *ovs, const char *name, int def)
{
    size_t i;

    for (i = 0; i < ovs->n_options; i++) {
        if (!strcmp(name, ovs->options[i].name)) {
            if (ovs->options[i].type == OPT_TYPE_INT)
                return ovs->options[i].u.integer;
            return -1;
        }
    }

    return def;
}

/*
 * Given the name of an uint option, return the value it received when it
 * was parsed.
 * @ovs: The OptionValues
 * @name: the name of the option
 * @def: the default value
 *
 * Returns the parsed value or the default value if none was parsed
 * If the value is of different type than an integer, ~0 is returned.
 */
unsigned int
option_get_uint(OptionValues *ovs, const char *name, unsigned int def)
{
    size_t i;

    for (i = 0; i < ovs->n_options; i++) {
        if (!strcmp(name, ovs->options[i].name)) {
            if (ovs->options[i].type == OPT_TYPE_UINT)
                return ovs->options[i].u.uinteger;
            return ~0;
        }
    }

    return def;
}

/*
 * Given the name of a boolean option, return the value it received when it
 * was parsed.
 * @ovs: The OptionValues
 * @name: the name of the option
 * @def: the default value
 *
 * Returns the parsed value or the default value if none was parsed
 * If the value is of different type than a boolean, false is returned.
 */
bool
option_get_bool(OptionValues *ovs, const char *name, bool def)
{
    size_t i;

    for (i = 0; i < ovs->n_options; i++) {
        if (!strcmp(name, ovs->options[i].name)) {
            if (ovs->options[i].type == OPT_TYPE_BOOLEAN)
                return ovs->options[i].u.boolean;
            return false;
        }
    }

    return def;
}

/*
 * Given the name of a mode_t (chmod) option, return the value it received when it
 * was parsed.
 * @ovs: The OptionValues
 * @name: the name of the option
 * @def: the default value
 *
 * Returns the parsed value or the default value if none was parsed
 * If the value is of different type than a mode_t, ~0 is returned.
 */
mode_t
option_get_mode_t(OptionValues *ovs, const char *name, mode_t def)
{
    size_t i;

    for (i = 0; i < ovs->n_options; i++) {
        if (!strcmp(name, ovs->options[i].name)) {
            if (ovs->options[i].type == OPT_TYPE_MODE_T)
                return ovs->options[i].u.mode;
            return ~0;
        }
    }

    return def;
}

/*
 * Given the name of a uid_t (chown) option, return the value it received when it
 * was parsed.
 * @ovs: The OptionValues
 * @name: the name of the option
 * @def: the default value
 *
 * Returns the parsed value or the default value if none was parsed
 * If the value is of different type than a uid_t, -1 is returned.
 */
uid_t
option_get_uid_t(OptionValues *ovs, const char *name, uid_t def)
{
    size_t i;

    for (i = 0; i < ovs->n_options; i++) {
        if (!strcmp(name, ovs->options[i].name)) {
            if (ovs->options[i].type == OPT_TYPE_UID_T)
                return ovs->options[i].u.uid;
            return -1;
        }
    }

    return def;
}

/*
 * Given the name of a gid_t (chown) option, return the value it received when it
 * was parsed.
 * @ovs: The OptionValues
 * @name: the name of the option
 * @def: the default value
 *
 * Returns the parsed value or the default value if none was parsed
 * If the value is of different type than a uid_t, -1 is returned.
 */
gid_t
option_get_gid_t(OptionValues *ovs, const char *name, gid_t def)
{
    size_t i;

    for (i = 0; i < ovs->n_options; i++) {
        if (!strcmp(name, ovs->options[i].name)) {
            if (ovs->options[i].type == OPT_TYPE_GID_T)
                return ovs->options[i].u.gid;
            return -1;
        }
    }

    return def;
}
