/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "jsvc.h"
#include <limits.h>
#include <glob.h>

/* Return the argument of a command line option */
static char *optional(int argc, char *argv[], int argi)
{

    argi++;
    if (argi >= argc)
        return NULL;
    if (argv[argi] == NULL)
        return NULL;
    if (argv[argi][0] == '-')
        return NULL;
    return strdup(argv[argi]);
}

static char *memstrcat(char *ptr, const char *str, const char *add)
{
    size_t nl = 1;
    int nas = ptr == NULL;
    if (ptr)
        nl += strlen(ptr);
    if (str)
        nl += strlen(str);
    if (add)
        nl += strlen(add);
    ptr = (char *)realloc(ptr, nl);
    if (ptr) {
        if (nas)
            *ptr = '\0';
        if (str)
            strcat(ptr, str);
        if (add)
            strcat(ptr, add);
    }
    return ptr;
}

static char *eval_ppath(char *strcp, const char *pattern)
{
    glob_t globbuf;
    char jars[PATH_MAX + 1];

    if (strlen(pattern) > (sizeof(jars) - 5)) {
        return memstrcat(strcp, pattern, NULL);
    }
    strcpy(jars, pattern);
    strcat(jars, ".jar");
    memset(&globbuf, 0, sizeof(glob_t));
    if (glob(jars, GLOB_ERR, NULL, &globbuf) == 0) {
        size_t n;
        for (n = 0; n < globbuf.gl_pathc - 1; n++) {
            strcp = memstrcat(strcp, globbuf.gl_pathv[n], ":");
            if (strcp == NULL) {
                globfree(&globbuf);
                return NULL;
            }
        }
        strcp = memstrcat(strcp, globbuf.gl_pathv[n], NULL);
        globfree(&globbuf);
    }
    return strcp;
}

#define JAVA_CLASSPATH      "-Djava.class.path="
/**
 * Call glob on each PATH like string path.
 * Glob is called only if the part ends with asterisk in which
 * case asterisk is replaced by *.jar when searching
 */
static char *eval_cpath(const char *cp)
{
    char *cpy = memstrcat(NULL, JAVA_CLASSPATH, cp);
    char *gcp = NULL;
    char *pos;
    char *ptr;

    if (!cpy)
        return NULL;
    ptr = cpy + sizeof(JAVA_CLASSPATH) - 1;;
    while ((pos = strchr(ptr, ':'))) {
        *pos = '\0';
        if (gcp)
            gcp = memstrcat(gcp, ":", NULL);
        else
            gcp = memstrcat(NULL, JAVA_CLASSPATH, NULL);
        if ((pos > ptr) && (*(pos - 1) == '*')) {
            if (!(gcp = eval_ppath(gcp, ptr))) {
                /* Error.
                 * Return the original string processed so far.
                 */
                return cpy;
            }
        }
        else
            gcp = memstrcat(gcp, ptr, NULL);
        ptr = pos + 1;
    }
    if (*ptr) {
        size_t end = strlen(ptr);
        if (gcp)
            gcp = memstrcat(gcp, ":", NULL);
        else
            gcp = memstrcat(NULL, JAVA_CLASSPATH, NULL);
        if (end > 0 && ptr[end - 1] == '*') {
            /* Last path elemet ends with star
             * Do a globbing.
             */
            gcp = eval_ppath(gcp, ptr);
        }
        else {
            /* Just add the part */
            gcp = memstrcat(gcp, ptr, NULL);
        }
    }
    /* Free the allocated copy */
    if (gcp) {
        free(cpy);
        return gcp;
    }
    else
        return cpy;
}

/* Parse command line arguments */
static arg_data *parse(int argc, char *argv[])
{
    arg_data *args = NULL;
    char *temp = NULL;
    char *cmnd = NULL;
    int x = 0;

    /* Create the default command line arguments */
    args = (arg_data *)malloc(sizeof(arg_data));
    args->pidf = "/var/run/jsvc.pid";  /* The default PID file */
    args->user = NULL;                 /* No user switching by default */
    args->dtch = true;                 /* Do detach from parent */
    args->vers = false;                /* Don't display version */
    args->help = false;                /* Don't display help */
    args->chck = false;                /* Don't do a check-only startup */
    args->stop = false;                /* Stop a running jsvc */
    args->wait = 0;                    /* Wait until jsvc has started the JVM */
    args->restarts = -1;               /* Infinite restarts by default */
    args->install = false;             /* Don't install as a service */
    args->remove = false;              /* Don't remove the installed service */
    args->service = false;             /* Don't run as a service */
    args->name = NULL;                 /* No VM version name */
    args->home = NULL;                 /* No default JAVA_HOME */
    args->onum = 0;                    /* Zero arguments, but let's have some room */
    args->clas = NULL;                 /* No class predefined */
    args->anum = 0;                    /* Zero class specific arguments but make room */
    args->cwd = "/";                   /* Use root as default */
    args->outfile = "/dev/null";       /* Swallow by default */
    args->errfile = "/dev/null";       /* Swallow by default */
    args->redirectstdin = true;        /* Redirect stdin to /dev/null by default */
    args->procname = "jsvc.exec";
#ifndef JSVC_UMASK
    args->umask = 0077;
#else
    args->umask = JSVC_UMASK;
#endif

    if (!(args->args = (char **)malloc(argc * sizeof(char *))))
        return NULL;
    if (!(args->opts = (char **)malloc(argc * sizeof(char *))))
        return NULL;

    /* Set up the command name */
    cmnd = strrchr(argv[0], '/');
    if (cmnd == NULL)
        cmnd = argv[0];
    else
        cmnd++;
    log_prog = strdup(cmnd);

    /* Iterate thru command line arguments */
    for (x = 1; x < argc; x++) {

        if (!strcmp(argv[x], "-cp") || !strcmp(argv[x], "-classpath")) {
            temp = optional(argc, argv, x++);
            if (temp == NULL) {
                log_error("Invalid classpath specified");
                return NULL;
            }
            args->opts[args->onum] = eval_cpath(temp);
            if (args->opts[args->onum] == NULL) {
                log_error("Invalid classpath specified");
                return NULL;
            }
            free(temp);
            args->onum++;

        }
        else if (!strcmp(argv[x], "-jvm")) {
            args->name = optional(argc, argv, x++);
            if (args->name == NULL) {
                log_error("Invalid Java VM name specified");
                return NULL;
            }
        }
        else if (!strcmp(argv[x], "-client")) {
            args->name = strdup("client");
        }
        else if (!strcmp(argv[x], "-server")) {
            args->name = strdup("server");
        }
        else if (!strcmp(argv[x], "-home") || !strcmp(argv[x], "-java-home")) {
            args->home = optional(argc, argv, x++);
            if (args->home == NULL) {
                log_error("Invalid Java Home specified");
                return NULL;
            }
        }
        else if (!strcmp(argv[x], "-user")) {
            args->user = optional(argc, argv, x++);
            if (args->user == NULL) {
                log_error("Invalid user name specified");
                return NULL;
            }
        }
        else if (!strcmp(argv[x], "-cwd")) {
            args->cwd = optional(argc, argv, x++);
            if (args->cwd == NULL) {
                log_error("Invalid working directory specified");
                return NULL;
            }
        }
        else if (!strcmp(argv[x], "-version")) {
            args->vers = true;
            args->dtch = false;
        }
        else if (!strcmp(argv[x], "-showversion")) {
            args->vershow = true;
        }
        else if (!strcmp(argv[x], "-?") || !strcmp(argv[x], "-help") || !strcmp(argv[x], "--help")) {
            args->help = true;
            args->dtch = false;
            return args;
        }
        else if (!strcmp(argv[x], "-X")) {
            log_error("Option -X currently unsupported");
            log_error("Please use \"java -X\" to see your extra VM options");
        }
        else if (!strcmp(argv[x], "-debug")) {
            log_debug_flag = true;
        }
        else if (!strcmp(argv[x], "-wait")) {
            temp = optional(argc, argv, x++);
            if (temp)
                args->wait = atoi(temp);
            if (args->wait < 10) {
                log_error("Invalid wait time specified (min=10)");
                return NULL;
            }
        }
        else if (!strcmp(argv[x], "-restarts")) {
            temp = optional(argc, argv, x++);
            if (temp)
                args->restarts = atoi(temp);
            if (args->restarts < -1) {
                log_error("Invalid max restarts [-1,0,...)");
                return NULL;
            }
        }
        else if (!strcmp(argv[x], "-umask")) {
            temp = optional(argc, argv, x++);
            if (temp == NULL) {
                log_error("Invalid umask specified");
                return NULL;
            }
            /* Parameter must be in octal */
            args->umask = (int)strtol(temp, NULL, 8);
            if (args->umask < 02) {
                log_error("Invalid umask specified (min=02)");
                return NULL;
            }
        }
        else if (!strcmp(argv[x], "-stop")) {
            args->stop = true;
        }
        else if (!strcmp(argv[x], "-check")) {
            args->chck = true;
            args->dtch = false;
        }
        else if (!strcmp(argv[x], "-nodetach")) {
            args->dtch = false;
        }
        else if (!strcmp(argv[x], "-keepstdin")) {
            args->redirectstdin = false;
        }
        else if (!strcmp(argv[x], "-service")) {
            args->service = true;
        }
        else if (!strcmp(argv[x], "-install")) {
            args->install = true;
        }
        else if (!strcmp(argv[x], "-remove")) {
            args->remove = true;
        }
        else if (!strcmp(argv[x], "-pidfile")) {
            args->pidf = optional(argc, argv, x++);
            if (args->pidf == NULL) {
                log_error("Invalid PID file specified");
                return NULL;
            }
        }
        else if (!strcmp(argv[x], "-outfile")) {
            args->outfile = optional(argc, argv, x++);
            if (args->outfile == NULL) {
                log_error("Invalid Output File specified");
                return NULL;
            }
        }
        else if (!strcmp(argv[x], "-errfile")) {
            args->errfile = optional(argc, argv, x++);
            if (args->errfile == NULL) {
                log_error("Invalid Error File specified");
                return NULL;
            }
        }
        else if (!strncmp(argv[x], "-verbose", 8)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strcmp(argv[x], "-D")) {
            log_error("Parameter -D must be followed by <name>=<value>");
            return NULL;
        }
        else if (!strncmp(argv[x], "-D", 2)) {
            temp = strchr(argv[x], '=');
            if (temp == argv[x] + 2) {
                log_error("A property name must be specified before '='");
                return NULL;
            }
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strncmp(argv[x], "-X", 2)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strncmp(argv[x], "-ea", 3)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strncmp(argv[x], "-enableassertions", 17)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strncmp(argv[x], "-da", 3)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strncmp(argv[x], "-disableassertions", 18)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strcmp(argv[x], "-esa")) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strcmp(argv[x], "-enablesystemassertions")) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strcmp(argv[x], "-dsa")) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strcmp(argv[x], "-disablesystemassertions")) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strcmp(argv[x], "-procname")) {
            args->procname = optional(argc, argv, x++);
            if (args->procname == NULL) {
                log_error("Invalid process name specified");
                return NULL;
            }
        }
        else if (!strncmp(argv[x], "-agentlib:", 10)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strncmp(argv[x], "-agentpath:", 11)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strncmp(argv[x], "-javaagent:", 11)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        /* Java 9 specific options */
        else if (!strncmp(argv[x], "--add-modules=", 14)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strncmp(argv[x], "--module-path=", 14)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strncmp(argv[x], "--upgrade-module-path=", 22)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strncmp(argv[x], "--add-reads=", 12)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strncmp(argv[x], "--add-exports=", 14)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strncmp(argv[x], "--add-opens=", 12)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strncmp(argv[x], "--limit-modules=", 16)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strncmp(argv[x], "--patch-module=", 15)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (!strncmp(argv[x], "--illegal-access=", 17)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        /* Java 11 specific options */
        else if (!strncmp(argv[x], "--enable-preview", 16)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        /* Java 17 specific options */
        else if (!strncmp(argv[x], "--enable-native-access=", 23)) {
            args->opts[args->onum++] = strdup(argv[x]);
        }
        else if (*argv[x] == '-') {
            log_error("Invalid option %s", argv[x]);
            return NULL;
        }
        else {
            args->clas = strdup(argv[x]);
            break;
        }
    }

    if (args->clas == NULL && args->remove == false) {
        log_error("No class specified");
        return NULL;
    }

    x++;
    while (x < argc) {
        args->args[args->anum++] = strdup(argv[x++]);
    }
    return args;
}

static const char *IsYesNo(bool par)
{
    if (par) {
        return "Yes";
    } else {
        return "No";
    }
}

static const char *IsTrueFalse(bool par)
{
    if (par) {
        return "True";
    } else {
        return "False";
    }
}

static const char *IsEnabledDisabled(bool par)
{
    if (par) {
        return "Enabled";
    } else {
        return "Disabled";
    }
}

/* Main entry point: parse command line arguments and dump them */
arg_data *arguments(int argc, char *argv[])
{
    arg_data *args = parse(argc, argv);
    int x = 0;

    if (args == NULL) {
        log_error("Cannot parse command line arguments");
        return NULL;
    }

    if (log_debug_flag == true) {
        log_debug("+-- DUMPING PARSED COMMAND AND ARGUMENTS ---------------");
        log_debug("| Executable:      %s", PRINT_NULL(argv[0]));
        log_debug("| Detach:          %s", IsTrueFalse(args->dtch));
        log_debug("| Show Version:    %s", IsYesNo(args->vers));
        log_debug("| Show Help:       %s", IsYesNo(args->help));
        log_debug("| Check Only:      %s", IsEnabledDisabled(args->chck));
        log_debug("| Stop:            %s", IsTrueFalse(args->stop));
        log_debug("| Wait:            %d", args->wait);
        log_debug("| Restarts:        %d", args->restarts);
        log_debug("| Run as service:  %s", IsYesNo(args->service));
        log_debug("| Install service: %s", IsYesNo(args->install));
        log_debug("| Remove service:  %s", IsYesNo(args->remove));
        log_debug("| JVM Name:        \"%s\"", PRINT_NULL(args->name));
        log_debug("| Java Home:       \"%s\"", PRINT_NULL(args->home));
        log_debug("| PID File:        \"%s\"", PRINT_NULL(args->pidf));
        log_debug("| User Name:       \"%s\"", PRINT_NULL(args->user));
        log_debug("| Extra Options:   %d", args->onum);
        for (x = 0; x < args->onum; x++) {
            log_debug("|   \"%s\"", args->opts[x]);
        }

        log_debug("| Class Invoked:   \"%s\"", PRINT_NULL(args->clas));
        log_debug("| Class Arguments: %d", args->anum);
        for (x = 0; x < args->anum; x++) {
            log_debug("|   \"%s\"", args->args[x]);
        }
        log_debug("+-------------------------------------------------------");
    }
    return args;
}
