/*
   config.c
  
   All modifications to the original source file are:
    - Copyright (C) 2025 Aitor Cuadrado Zubizarreta <aitor_czr@gnuinos.org> 
 
   Original copyright and license text produced below.
*/

/*
   vdev: a virtual device manager for *nix
   Copyright (C) 2015  Jude Nelson

   This program is dual-licensed: you can redistribute it and/or modify
   it under the terms of the GNU General Public License version 3 or later as 
   published by the Free Software Foundation. For the terms of this 
   license, see LICENSE.GPLv3+ or <http://www.gnu.org/licenses/>.

   You are free to use this program under the terms of the GNU General
   Public License, but WITHOUT ANY WARRANTY; without even the implied 
   warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
   See the GNU General Public License for more details.

   Alternatively, you are free to use this program under the terms of the 
   Internet Software Consortium License, but WITHOUT ANY WARRANTY; without
   even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
   For the terms of this license, see LICENSE.ISC or 
   <http://www.isc.org/downloads/software-support-policy/isc-license/>.
*/


#include "config.h"

#define INI_MAX_LINE 4096
#define INI_STOP_ON_FIRST_ERROR 1

#include "ini.h"
#include <limits.h>

// FUSE reserved options
static const char *FUSE_OPT_S = "-s";
static const char *FUSE_OPT_O = "-o";
static const char *FUSE_OPT_D = "-d";
static const char *FUSE_OPT_F = "-f";

// ini parser callback 
// return 1 on parsed (WARNING: masks OOM)
// return 0 on not parsed
static int vdev_config_ini_parser(void *userdata, char const *section, 
                                  char const *name, char const *value)
{
    struct vdev_config *conf = (struct vdev_config*)userdata;
    bool success = false;
    int rc = 0;
   
    if (!strcmp(section, VDEV_CONFIG_NAME)) {
        
        if (!strcmp(name, VDEV_CONFIG_ACLS)) {
     
            if (!conf->acls_dir)
                /* save this */ 
                conf->acls_dir = vdev_strdup_or_null(value);
            return 1;
        }
      
        if (!strcmp(name, VDEV_CONFIG_ACTIONS)) {
     
            if (!conf->acts_dir)
                /* save this */ 
                conf->acts_dir = vdev_strdup_or_null(value);
     
            return 1;
        }
      
        if (!strcmp(name, VDEV_CONFIG_HELPERS)) {
     
            if (!conf->helpers_dir)
                /* save this */ 
                conf->helpers_dir = vdev_strdup_or_null(value);
     
            return 1;
        }
      
        if (!strcmp(name, VDEV_CONFIG_DEFAULT_MODE)) {
      
            char *tmp = NULL;
            conf->default_mode = (mode_t)strtoul(value, &tmp, 8);
     
            if (*tmp != '\0') {
                fprintf(stderr, "Invalid value '%s' for '%s'\n", value, name);
                return 0;
            } else {
                return 1;
            }
        }
      
        if (!strcmp(name, VDEV_CONFIG_DEFAULT_POLICY)) {
     
            conf->default_policy = strcasecmp(value, "allow") ? 1 : 0;
            return 1;
        }
      
        if (!strcmp(name, VDEV_CONFIG_PIDFILE_PATH)) {
     
            if (!conf->pidfile_path)
                conf->pidfile_path = vdev_strdup_or_null(value);
     
            return 1;
        }
      
        if (!strcmp(name, VDEV_CONFIG_LOGFILE_PATH)) {
     
            if (!conf->logfile_path)
                conf->logfile_path = vdev_strdup_or_null(value);
     
            return 1;
        }
      
        if (!strcmp(name, VDEV_CONFIG_LOG_LEVEL)) {
     
            if (!strcasecmp(value, "debug")) {
        
                conf->debug_level = VDEV_LOGLEVEL_DEBUG;
                conf->error_level = VDEV_LOGLEVEL_WARN;
            
            } else if (!strcasecmp(value, "info")) {
        
                conf->debug_level = VDEV_LOGLEVEL_INFO;
                conf->error_level = VDEV_LOGLEVEL_WARN;
            
            } else if (!strcasecmp(value, "warn") || !strcasecmp(value, "warning")) {
        
                conf->debug_level = VDEV_LOGLEVEL_NONE;
                conf->error_level = VDEV_LOGLEVEL_WARN;
                
            } else if (!strcasecmp(value, "error") || !strcasecmp(value, "critical")) {
        
                conf->debug_level = VDEV_LOGLEVEL_NONE;
                conf->error_level = VDEV_LOGLEVEL_ERROR;
                
            } else {
                fprintf(stderr, "Unrecognized value '%s' for '%s'\n", value, name);
                return 0;
            }
     
            return 1;
        }
      
        if (!strcmp(name, VDEV_CONFIG_MOUNTPOINT)) {
     
            if (!conf->mountpoint)
                conf->mountpoint = vdev_strdup_or_null(value);
     
            return 1;
        }
      
        if (!strcmp(name, VDEV_CONFIG_COLDPLUG_ONLY)) {

            if (!strcasecmp(value, "true")) {
                conf->coldplug_only = true;
                return 1;
            }
            if (!strcasecmp(value, "false")) {
                conf->coldplug_only = false;
                return 1;
            }
            conf->coldplug_only = (bool)vdev_parse_uint64(value, &success);
            if (!success) {
                fprintf(stderr, "Invalid value '%s' for '%s'\n", value, name);
                return 0;
            }
            return 1;
        }

        if (!strcmp(name, VDEV_CONFIG_QUEUE_TRACKING_FLAG)) {
            uint64_t qf = vdev_parse_uint64(value, &success);
            if (!success || qf > INT_MAX) {
                fprintf(stderr, "Invalid value '%s' for '%s' (must be non-negative integer)\n",
                        value, name);
                return 0;
            }
            conf->queue_tracking_flag = (int)qf;
            return 1;
        }
      
        if (!strcmp(name, VDEV_CONFIG_PRESEED)) {
            
            if (!conf->preseed_path)
                conf->preseed_path = vdev_strdup_or_null(value);
            return 1;
        }
        return 1;
    }
   
    if (!strcmp(section, VDEV_OS_CONFIG_NAME)) {
      
        /* OS-specific config value */
        rc = vdev_params_add(&conf->os_config, name, value);
        if (rc != 0)
            return 0;
        else
            return 1;
    }
   
    fprintf(stderr, "Unrecognized field '%s'\n", name);
    return 1;
}


// config sanity check 
int vdev_config_sanity_check(struct vdev_config *conf)
{
    int rc = 0;
   
    if (!conf->acls_dir) {
        fprintf(stderr, "[ERROR]: missing acls\n");
        rc = -EINVAL;
    }
   
    if (!conf->acts_dir) {
        fprintf(stderr, "[ERROR]: missing actions\n");
        rc = -EINVAL;
    }
   
    if (!conf->mountpoint) {
        fprintf(stderr, "[ERROR]: missing mountpoint\n");
        rc = -EINVAL;
    }
   
    return rc;
}


// convert a number between 0 and 16 to its hex representation 
// hex must have at least 2 characters
// always succeeds 
void vdev_bin_to_hex(unsigned char num, char *hex)
{
    unsigned char upper = num >> 4;
    unsigned char lower = num & 0xf;
   
    if (upper < 10)
        hex[0] = upper + '0';
    else
        hex[0] = upper + 'A';
   
    if (lower < 10)
        hex[1] = lower + '0';
    else
        hex[1] = lower + 'A';
}

// generate an instance nonce 
// NOTE: not thread-safe, since it uses mrand48(3) (can't use /dev/urandom, since it doesn't exist yet)
// always succeeds 
static void vdev_config_make_instance_nonce(struct vdev_config *conf)
{
    char instance[VDEV_CONFIG_INSTANCE_NONCE_LEN];
   
    /* generate an instance nonce */ 
    for (int i = 0; i < VDEV_CONFIG_INSTANCE_NONCE_LEN; i++) 
        instance[i] = (char)mrand48();
   
    memset(conf->instance_str, 0, VDEV_CONFIG_INSTANCE_NONCE_STRLEN);
   
    for (int i = 0; i < VDEV_CONFIG_INSTANCE_NONCE_LEN; i++)
        vdev_bin_to_hex((unsigned char)instance[i], &conf->instance_str[2 * i]);
}
   

// initialize a config 
// always succeeds
int vdev_config_init(struct vdev_config *conf)
{
    memset(conf, 0, sizeof(struct vdev_config));
    conf->queue_tracking_flag = 0;
    return 0;
}

// load from a file, by path
// return on on success
// return -errno on failure to open 
int vdev_config_load(char const *path, struct vdev_config *conf)
{
    FILE *f = NULL;
    int rc = 0;
   
    f = fopen(path, "r");
    if (f == NULL)
        return -errno;
   
    rc = vdev_config_load_file(f, conf);
   
    fclose(f);
   
    if (rc == 0)
        vdev_config_make_instance_nonce(conf);
        
    return rc;
}

// load from a file
// return 0 on success
// return -errno on failure to load
int vdev_config_load_file(FILE *file, struct vdev_config *conf)
{
    int rc = 0;
   
    rc = ini_parse_file(file, vdev_config_ini_parser, conf);
    if (rc != 0) {
        vdev_error("ini_parse_file(config) rc = %d\n", rc);
        vdev_config_free(conf);
        return rc;
    }

    // convert paths 
    rc = vdev_config_fullpaths(conf);
    if (rc != 0) {
        vdev_error("vdev_config_fullpaths: %s\n", strerror(-rc));
        vdev_config_free(conf);
    }
    return rc;
}

// free a config
// always succeeds
int vdev_config_free(struct vdev_config *conf)
{
    if (conf->acls_dir) {
        free(conf->acls_dir);
        conf->acls_dir = NULL;
    }
   
    if (conf->acts_dir) {
        free(conf->acts_dir);
        conf->acts_dir = NULL;
    }
   
    if (conf->os_config) {
        vdev_params_free(conf->os_config);
        conf->os_config = NULL;
    }
   
    if (conf->helpers_dir) {
        free(conf->helpers_dir);
        conf->helpers_dir = NULL;
    }
   
    if (conf->config_path) {
        free(conf->config_path);
        conf->config_path = NULL;
    }
   
    if (conf->mountpoint) {
        free(conf->mountpoint);
        conf->mountpoint = NULL;
    }
   
    if (conf->preseed_path) {
        free(conf->preseed_path);
        conf->preseed_path = NULL;
    }
   
    if (conf->logfile_path) {
        free(conf->logfile_path);
        conf->logfile_path = NULL;
    }
   
    if (conf->pidfile_path) {
        free(conf->pidfile_path);
        conf->pidfile_path = NULL;
    }
   
    return 0;
}


// convert all paths in the config to absolute paths 
// return 0 on success 
// return -ENOMEM on OOM 
// return -ERANGE if cwd is too long
int vdev_config_fullpaths(struct vdev_config *conf)
{
    char **need_fullpath[] = {
        &conf->config_path,
        &conf->acls_dir,
        &conf->acts_dir,
        &conf->helpers_dir,
        &conf->pidfile_path,
        &conf->logfile_path,
        &conf->preseed_path,
        NULL
    };
   
    char cwd_buf[PATH_MAX + 1];
    memset(cwd_buf, 0, PATH_MAX + 1);
   
    char *tmp = getcwd(cwd_buf, PATH_MAX);
    if (!tmp) {
        vdev_error("Current working directory exceeds %u bytes\n", PATH_MAX);
        return -ERANGE;
    }
   
    for (int i = 0; need_fullpath[i] != NULL; i++) {
        if (need_fullpath[i] != NULL && (*need_fullpath[i]) != NULL) {
            if (*(need_fullpath[i])[0] != '/') {
                /* relative path */ 
                char *new_path = vdev_fullpath(cwd_buf, *(need_fullpath)[i], NULL);
                if (new_path == NULL)
                    return -ENOMEM;
        
                free(*(need_fullpath[i]));
                *(need_fullpath[i]) = new_path;
            }
        }
    }
   
    return 0;
}



// print usage statement 
int vdev_config_usage(char const *progname)
{
    fprintf(stderr, "\
\
Usage: %s [options] mountpoint\n\
Options include:\n\
\n\
   -c, --config-file CONFIG_FILE\n\
          Path to the config file to use.\n\
          \n\
   -v, --verbose-level VERBOSITY\n\
          Set the level of verbose output.  Valid values are\n\
          positive integers.  Larger integers lead to more\n\
          debugging information.\n\
          \n\
   -l, --log-file LOGFILE_PATH\n\
          Path to which to log information.\n\
          Pass 'syslog' to log to syslog, instead of a logfile.\n\
          \n\
   -1, --once\n\
          Exit once all existant devices have been processed.\n\
          \n\
   -C, --coldplug-only\n\
          Coldplug only (sysfs seed; exit when drained).\n\
          Without this flag, vdevd is a hotplug listener (netlink).\n\
          \n\
   -f, --foreground\n\
          Run in the foreground; do not daemonize.\n\
          \n\
   -p, --pidfile PATH\n\
          Write the PID of the daemon to PATH.\n\
", progname);
  
    return 0;
}

// get the mountpoint option, by taking the last argument that wasn't an optarg
static int vdev_config_get_mountpoint_from_fuse(int fuse_argc, 
                                                char **fuse_argv, char **ret_mountpoint)
{
    *ret_mountpoint = realpath(fuse_argv[fuse_argc - 1], NULL);
    if (*ret_mountpoint == NULL) {
        int rc = -errno;
        printf("No mountpoint, rc = %d\n", rc);
      
        for (int i = 0; i < fuse_argc; i++)
            printf("argv[%d]: '%s'\n", i, fuse_argv[i]);
      
        return -EINVAL;
    }
    return 0;
}


// parse command-line options from argv.
// fill in fuse_argv with fuse-specific options.
// config must be initialized; this method simply augments it 
// return 0 on success 
// return -1 on unrecognized option 
int vdev_config_load_from_args(struct vdev_config *config, 
                               int argc, char **argv, int *fuse_argc, char **fuse_argv)
{
    static struct option vdev_options[] = {
        {"config-file",     required_argument,   0, 'c'},
        {"verbose-level",   required_argument,   0, 'v'},
        {"logfile",         required_argument,   0, 'l'},
        {"pidfile",         required_argument,   0, 'p'},
        {"once",            no_argument,         0, '1'},
        {"coldplug-only",   no_argument,         0, 'C'},
        {"foreground",      no_argument,         0, 'f'},
        {0, 0, 0, 0}
    };

    int rc = 0;
    int opt_index = 0;
    int c = 0;
    int fuse_optind = 0;
 
    char const *optstr = "c:v:l:o:fC1p:ds";
  
    if (fuse_argv) { 
        fuse_argv[fuse_optind] = argv[0];
        fuse_optind++;
    }
   
    config->sync = false;
   
    while (rc == 0 && c != -1) {
        c = getopt_long(argc, argv, optstr, vdev_options, &opt_index);
        if (c == -1)
            break;
      
        switch (c) {
        case 'c':
            if (config->config_path)
                free(config->config_path);
            config->config_path = vdev_strdup_or_null(optarg);
            break;
        case 'l':
            if (config->logfile_path)
                free(config->logfile_path);
            config->logfile_path = vdev_strdup_or_null(optarg);
            break;
        case 'p':
            if (config->pidfile_path)
                free(config->pidfile_path);
            config->pidfile_path = vdev_strdup_or_null(optarg);
            break;
        case 'v':
        {
            long debug_level = 0;
            char *tmp = NULL;
            debug_level = strtol(optarg, &tmp, 10);
            if (*tmp != '\0') {
                fprintf(stderr, "Invalid argument for -d\n");
                rc = -1;
            } else {
                config->debug_level = debug_level;
            }
            break;
        }
        case 'C':
        case '1':
            config->coldplug_only = true;
            break;
        case 's':
            /* FUSE option */ 
            if (fuse_argv) {
                fuse_argv[fuse_optind] = (char*)FUSE_OPT_S;
                fuse_optind++;
            }
            break;
        case 'd':
            /* FUSE option */ 
            if (fuse_argv) {
                fuse_argv[fuse_optind] = (char*)FUSE_OPT_D;
                fuse_optind++;
            }
            break;
        case 'f':
            /* FUSE option */ 
            if (fuse_argv) {
                fuse_argv[fuse_optind] = (char*)FUSE_OPT_F;
                fuse_optind++;
            }
            config->foreground = true;
            break;
        case 'o':
            /* FUSE option */ 
            if (fuse_argv) {
                fuse_argv[fuse_optind] = (char*)FUSE_OPT_O;
                fuse_optind++;
                fuse_argv[fuse_optind] = optarg;
                fuse_optind++;
            }
            break;
        default:
            fprintf(stderr, "Unrecognized option -%c\n", c);
            rc = -1;
            break;
        }
    }
   
    if (rc != 0)
        return rc;
   
    if (fuse_argv) {
        /* copy over non-option arguments to fuse_argv */ 
        for (int i = optind; i < argc; i++) {      
            fuse_argv[fuse_optind] = argv[i];
            fuse_optind++;
        }
   
        *fuse_argc = fuse_optind;
   
        /* parse FUSE args to get the mountpoint */ 
        rc = vdev_config_get_mountpoint_from_fuse(*fuse_argc, fuse_argv, 
                                                  &config->mountpoint);
    }
    else {
        /* extract mountpoint */
        config->mountpoint = realpath(argv[optind], NULL);
        if (config->mountpoint == NULL) {
            rc = -errno;
            fprintf(stderr, "Failed to evaluate '%s': %s\n", 
                            argv[optind], strerror(-rc));
        }
    }
    return rc;
}

