/*
 * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include <sys/stat.h>
#include <ctype.h>

#ifdef DEBUG_ARGFILE
  #ifndef NO_JNI
    #define NO_JNI
  #endif
  #define JLI_ReportMessage(...) printf(__VA_ARGS__)
  #define JDK_JAVA_OPTIONS "JDK_JAVA_OPTIONS"
  int IsWhiteSpaceOption(const char* name) { return 1; }
#else
  #include "java.h"
  #include "jni.h"
#endif

#include "jli_util.h"
#include "emessages.h"

#define MAX_ARGF_SIZE 0x7fffffffL

static char* clone_substring(const char *begin, size_t len) {
    char *rv = (char *) JLI_MemAlloc(len + 1);
    memcpy(rv, begin, len);
    rv[len] = '\0';
    return rv;
}

enum STATE {
    FIND_NEXT,
    IN_COMMENT,
    IN_QUOTE,
    IN_ESCAPE,
    SKIP_LEAD_WS,
    IN_TOKEN
};

typedef struct {
    enum STATE state;
    const char* cptr;
    const char* eob;
    char quote_char;
    JLI_List parts;
} __ctx_args;

#define NOT_FOUND -1
static int firstAppArgIndex = NOT_FOUND;

static jboolean expectingNoDashArg = JNI_FALSE;
// Initialize to 1, as the first argument is the app name and not preprocessed
static size_t argsCount = 1;
static jboolean stopExpansion = JNI_FALSE;
static jboolean relaunch = JNI_FALSE;

/*
 * Prototypes for internal functions.
 */
static jboolean expand(JLI_List args, const char *str, const char *var_name);

JNIEXPORT void JNICALL
JLI_InitArgProcessing(jboolean hasJavaArgs, jboolean disableArgFile) {
    // No expansion for relaunch
    if (argsCount != 1) {
        relaunch = JNI_TRUE;
        stopExpansion = JNI_TRUE;
        argsCount = 1;
    } else {
        stopExpansion = disableArgFile;
    }

    expectingNoDashArg = JNI_FALSE;

    // for tools, this value remains 0 all the time.
    firstAppArgIndex = hasJavaArgs ? 0: NOT_FOUND;
}

JNIEXPORT int JNICALL
JLI_GetAppArgIndex() {
    // Will be 0 for tools
    return firstAppArgIndex;
}

static void checkArg(const char *arg) {
    size_t idx = 0;
    argsCount++;

    // All arguments arrive here must be a launcher argument,
    // ie. by now, all argfile expansions must have been performed.
    if (*arg == '-') {
        expectingNoDashArg = JNI_FALSE;
        if (IsWhiteSpaceOption(arg)) {
            // expect an argument
            expectingNoDashArg = JNI_TRUE;

            if (JLI_StrCmp(arg, "-jar") == 0 ||
                JLI_StrCmp(arg, "--module") == 0 ||
                JLI_StrCmp(arg, "-m") == 0) {
                // This is tricky, we do expect NoDashArg
                // But that is considered main class to stop expansion
                expectingNoDashArg = JNI_FALSE;
                // We can not just update the idx here because if -jar @file
                // still need expansion of @file to get the argument for -jar
            }
        } else if (JLI_StrCmp(arg, "--disable-@files") == 0) {
            stopExpansion = JNI_TRUE;
        }
    } else {
        if (!expectingNoDashArg) {
            // this is main class, argsCount is index to next arg
            idx = argsCount;
        }
        expectingNoDashArg = JNI_FALSE;
    }
    // only update on java mode and not yet found main class
    if (firstAppArgIndex == NOT_FOUND && idx != 0) {
        firstAppArgIndex = (int) idx;
    }
}

/*
       [\n\r]   +------------+                        +------------+ [\n\r]
      +---------+ IN_COMMENT +<------+                | IN_ESCAPE  +---------+
      |         +------------+       |                +------------+         |
      |    [#]       ^               |[#]                 ^     |            |
      |   +----------+               |                [\\]|     |[^\n\r]     |
      v   |                          |                    |     v            |
+------------+ [^ \t\n\r\f]  +------------+['"]>      +------------+         |
| FIND_NEXT  +-------------->+ IN_TOKEN   +-----------+ IN_QUOTE   +         |
+------------+               +------------+   <[quote]+------------+         |
  |   ^                          |                       |  ^   ^            |
  |   |               [ \t\n\r\f]|                 [\n\r]|  |   |[^ \t\n\r\f]v
  |   +--------------------------+-----------------------+  |  +--------------+
  |                       ['"]                              |  | SKIP_LEAD_WS |
  +---------------------------------------------------------+  +--------------+
*/
static char* nextToken(__ctx_args *pctx) {
    const char* nextc = pctx->cptr;
    const char* const eob = pctx->eob;
    const char* anchor = nextc;
    char *token;

    for (; nextc < eob; nextc++) {
        register char ch = *nextc;

        // Skip white space characters
        if (pctx->state == FIND_NEXT || pctx->state == SKIP_LEAD_WS) {
            while (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t' || ch == '\f') {
                nextc++;
                if (nextc >= eob) {
                    return NULL;
                }
                ch = *nextc;
            }
            pctx->state = (pctx->state == FIND_NEXT) ? IN_TOKEN : IN_QUOTE;
            anchor = nextc;
        // Deal with escape sequences
        } else if (pctx->state == IN_ESCAPE) {
            // concatenation directive
            if (ch == '\n' || ch == '\r') {
                pctx->state = SKIP_LEAD_WS;
            } else {
            // escaped character
                char* escaped = (char*) JLI_MemAlloc(2 * sizeof(char));
                escaped[1] = '\0';
                switch (ch) {
                    case 'n':
                        escaped[0] = '\n';
                        break;
                    case 'r':
                        escaped[0] = '\r';
                        break;
                    case 't':
                        escaped[0] = '\t';
                        break;
                    case 'f':
                        escaped[0] = '\f';
                        break;
                    default:
                        escaped[0] = ch;
                        break;
                }
                JLI_List_add(pctx->parts, escaped);
                pctx->state = IN_QUOTE;
            }
            // anchor to next character
            anchor = nextc + 1;
            continue;
        // ignore comment to EOL
        } else if (pctx->state == IN_COMMENT) {
            while (ch != '\n' && ch != '\r') {
                nextc++;
                if (nextc >= eob) {
                    return NULL;
                }
                ch = *nextc;
            }
            anchor = nextc + 1;
            pctx->state = FIND_NEXT;
            continue;
        }

        assert(pctx->state != IN_ESCAPE);
        assert(pctx->state != FIND_NEXT);
        assert(pctx->state != SKIP_LEAD_WS);
        assert(pctx->state != IN_COMMENT);

        switch(ch) {
            case ' ':
            case '\t':
            case '\f':
                if (pctx->state == IN_QUOTE) {
                    continue;
                }
                // fall through
            case '\n':
            case '\r':
                if (pctx->parts->size == 0) {
                    token = clone_substring(anchor, nextc - anchor);
                } else {
                    JLI_List_addSubstring(pctx->parts, anchor, nextc - anchor);
                    token = JLI_List_combine(pctx->parts);
                    JLI_List_free(pctx->parts);
                    pctx->parts = JLI_List_new(4);
                }
                pctx->cptr = nextc + 1;
                pctx->state = FIND_NEXT;
                return token;
            case '#':
                if (pctx->state == IN_QUOTE) {
                    continue;
                }
                pctx->state = IN_COMMENT;
                anchor = nextc + 1;
                break;
            case '\\':
                if (pctx->state != IN_QUOTE) {
                    continue;
                }
                JLI_List_addSubstring(pctx->parts, anchor, nextc - anchor);
                pctx->state = IN_ESCAPE;
                // anchor after backslash character
                anchor = nextc + 1;
                break;
            case '\'':
            case '"':
                if (pctx->state == IN_QUOTE && pctx->quote_char != ch) {
                    // not matching quote
                    continue;
                }
                // partial before quote
                if (anchor != nextc) {
                    JLI_List_addSubstring(pctx->parts, anchor, nextc - anchor);
                }
                // anchor after quote character
                anchor = nextc + 1;
                if (pctx->state == IN_TOKEN) {
                    pctx->quote_char = ch;
                    pctx->state = IN_QUOTE;
                } else {
                    pctx->state = IN_TOKEN;
                }
                break;
            default:
                break;
        }
    }

    assert(nextc == eob);
    // Only need partial token, not comment or whitespaces
    if (pctx->state == IN_TOKEN || pctx->state == IN_QUOTE) {
        if (anchor < nextc) {
            // not yet return until end of stream, we have part of a token.
            JLI_List_addSubstring(pctx->parts, anchor, nextc - anchor);
        }
    }
    return NULL;
}

static JLI_List readArgFile(FILE *file) {
    char buf[4096];
    JLI_List rv;
    __ctx_args ctx;
    size_t size;
    char *token;

    ctx.state = FIND_NEXT;
    ctx.parts = JLI_List_new(4);
    // initialize to avoid -Werror=maybe-uninitialized issues from gcc 7.3 onwards.
    ctx.quote_char = '"';

    /* arbitrarily pick 8, seems to be a reasonable number of arguments */
    rv = JLI_List_new(8);

    while (!feof(file)) {
        size = fread(buf, sizeof(char), sizeof(buf), file);
        if (ferror(file)) {
            JLI_List_free(rv);
            return NULL;
        }

        /* nextc is next character to read from the buffer
         * eob is the end of input
         * token is the copied token value, NULL if no a complete token
         */
        ctx.cptr = buf;
        ctx.eob = buf + size;
        token = nextToken(&ctx);
        while (token != NULL) {
            checkArg(token);
            JLI_List_add(rv, token);
            token = nextToken(&ctx);
        }
    }

    // remaining partial token
    if (ctx.state == IN_TOKEN || ctx.state == IN_QUOTE) {
        if (ctx.parts->size != 0) {
            token = JLI_List_combine(ctx.parts);
            checkArg(token);
            JLI_List_add(rv, token);
        }
    }
    JLI_List_free(ctx.parts);

    return rv;
}

/*
 * if the arg represent a file, that is, prefix with a single '@',
 * return a list of arguments from the file.
 * otherwise, return NULL.
 */
static JLI_List expandArgFile(const char *arg) {
    FILE *fptr;
    struct stat st;
    JLI_List rv;

    /* failed to access the file */
    if (stat(arg, &st) != 0) {
        JLI_ReportMessage(CFG_ERROR6, arg);
        exit(1);
    }

    if (st.st_size > MAX_ARGF_SIZE) {
        JLI_ReportMessage(CFG_ERROR10, MAX_ARGF_SIZE);
        exit(1);
    }

    fptr = fopen(arg, "r");
    /* arg file cannot be openned */
    if (fptr == NULL) {
        JLI_ReportMessage(CFG_ERROR6, arg);
        exit(1);
    }

    rv = readArgFile(fptr);
    fclose(fptr);

    /* error occurred reading the file */
    if (rv == NULL) {
        JLI_ReportMessage(DLL_ERROR4, arg);
        exit(1);
    }

    return rv;
}

/*
 * expand a string into a list of words separated by whitespace.
 */
static JLI_List expandArg(const char *arg) {
    JLI_List rv;

    /* arbitrarily pick 8, seems to be a reasonable number of arguments */
    rv = JLI_List_new(8);

    expand(rv, arg, NULL);

    return rv;
}

JNIEXPORT JLI_List JNICALL
JLI_PreprocessArg(const char *arg, jboolean expandSourceOpt) {
    JLI_List rv;

    if (firstAppArgIndex > 0) {
        // In user application arg, no more work.
        return NULL;
    }

    if (stopExpansion) {
        // still looking for user application arg
        checkArg(arg);
        return NULL;
    }

    if (expandSourceOpt
            && JLI_StrCCmp(arg, "--source") == 0
            && JLI_StrChr(arg, ' ') != NULL) {
        return expandArg(arg);
    }

    if (arg[0] != '@') {
        checkArg(arg);
        return NULL;
    }

    if (arg[1] == '\0') {
        // @ by itself is an argument
        checkArg(arg);
        return NULL;
    }

    arg++;
    if (arg[0] == '@') {
        // escaped @argument
        rv = JLI_List_new(1);
        checkArg(arg);
        JLI_List_add(rv, JLI_StringDup(arg));
    } else {
        rv = expandArgFile(arg);
    }
    return rv;
}

int isTerminalOpt(char *arg) {
    return JLI_StrCmp(arg, "-jar") == 0 ||
           JLI_StrCmp(arg, "-m") == 0 ||
           JLI_StrCmp(arg, "--module") == 0 ||
           JLI_StrCmp(arg, "--dry-run") == 0 ||
           JLI_StrCmp(arg, "-h") == 0 ||
           JLI_StrCmp(arg, "-?") == 0 ||
           JLI_StrCmp(arg, "-help") == 0 ||
           JLI_StrCmp(arg, "--help") == 0 ||
           JLI_StrCmp(arg, "-X") == 0 ||
           JLI_StrCmp(arg, "--help-extra") == 0 ||
           JLI_StrCmp(arg, "-version") == 0 ||
           JLI_StrCmp(arg, "--version") == 0 ||
           JLI_StrCmp(arg, "-fullversion") == 0 ||
           JLI_StrCmp(arg, "--full-version") == 0;
}

JNIEXPORT jboolean JNICALL
JLI_AddArgsFromEnvVar(JLI_List args, const char *var_name) {
    char *env = getenv(var_name);

    if (firstAppArgIndex == 0) {
        // Not 'java', return
        return JNI_FALSE;
    }

    if (relaunch) {
        return JNI_FALSE;
    }

    if (NULL == env) {
        return JNI_FALSE;
    }

    JLI_ReportMessage(ARG_INFO_ENVVAR, var_name, env);
    return expand(args, env, var_name);
}

/*
 * Expand a string into a list of args.
 * If the string is the result of looking up an environment variable,
 * var_name should be set to the name of that environment variable,
 * for use if needed in error messages.
 */

static jboolean expand(JLI_List args, const char *str, const char *var_name) {
    jboolean inEnvVar = (var_name != NULL);

    char *p, *arg;
    char quote;
    JLI_List argsInFile;

    // This is retained until the process terminates as it is saved as the args
    p = JLI_MemAlloc(JLI_StrLen(str) + 1);
    while (*str != '\0') {
        while (*str != '\0' && isspace(*str)) {
            str++;
        }

        // Trailing space
        if (*str == '\0') {
            break;
        }

        arg = p;
        while (*str != '\0' && !isspace(*str)) {
            if (inEnvVar && (*str == '"' || *str == '\'')) {
                quote = *str++;
                while (*str != quote && *str != '\0') {
                    *p++ = *str++;
                }

                if (*str == '\0') {
                    JLI_ReportMessage(ARG_ERROR8, var_name);
                    exit(1);
                }
                str++;
            } else {
                *p++ = *str++;
            }
        }

        *p++ = '\0';

        argsInFile = JLI_PreprocessArg(arg, JNI_FALSE);

        if (NULL == argsInFile) {
            if (isTerminalOpt(arg)) {
                if (inEnvVar) {
                    JLI_ReportMessage(ARG_ERROR9, arg, var_name);
                } else {
                    JLI_ReportMessage(ARG_ERROR15, arg);
                }
                exit(1);
            }
            JLI_List_add(args, arg);
        } else {
            size_t cnt, idx;
            char *argFile = arg;
            cnt = argsInFile->size;
            for (idx = 0; idx < cnt; idx++) {
                arg = argsInFile->elements[idx];
                if (isTerminalOpt(arg)) {
                    if (inEnvVar) {
                        JLI_ReportMessage(ARG_ERROR10, arg, argFile, var_name);
                    } else {
                        JLI_ReportMessage(ARG_ERROR16, arg, argFile);
                    }
                    exit(1);
                }
                JLI_List_add(args, arg);
            }
            // Shallow free, we reuse the string to avoid copy
            JLI_MemFree(argsInFile->elements);
            JLI_MemFree(argsInFile);
        }
        /*
         * Check if main-class is specified after argument being checked. It
         * must always appear after expansion, as a main-class could be specified
         * indirectly into environment variable via an @argfile, and it must be
         * caught now.
         */
        if (firstAppArgIndex != NOT_FOUND) {
            if (inEnvVar) {
                JLI_ReportMessage(ARG_ERROR11, var_name);
            } else {
                JLI_ReportMessage(ARG_ERROR17);
            }
            exit(1);
        }

        assert (*str == '\0' || isspace(*str));
    }

    return JNI_TRUE;
}

#ifdef DEBUG_ARGFILE
/*
 * Stand-alone sanity test, build with following command line
 * $ CC -DDEBUG_ARGFILE -DNO_JNI -g args.c jli_util.c
 */

void fail(char *expected, char *actual, size_t idx) {
    printf("FAILED: Token[%lu] expected to be <%s>, got <%s>\n", idx, expected, actual);
    exit(1);
}

void test_case(char *case_data, char **tokens, size_t cnt_tokens) {
    size_t actual_cnt;
    char *token;
    __ctx_args ctx;

    actual_cnt = 0;

    ctx.state = FIND_NEXT;
    ctx.parts = JLI_List_new(4);
    ctx.cptr = case_data;
    ctx.eob = case_data + strlen(case_data);

    printf("Test case: <%s>, expected %lu tokens.\n", case_data, cnt_tokens);

    for (token = nextToken(&ctx); token != NULL; token = nextToken(&ctx)) {
        // should not have more tokens than expected
        if (actual_cnt >= cnt_tokens) {
            printf("FAILED: Extra token detected: <%s>\n", token);
            exit(2);
        }
        if (JLI_StrCmp(token, tokens[actual_cnt]) != 0) {
            fail(tokens[actual_cnt], token, actual_cnt);
        }
        actual_cnt++;
    }

    char* last = NULL;
    if (ctx.parts->size != 0) {
        last = JLI_List_combine(ctx.parts);
    }
    JLI_List_free(ctx.parts);

    if (actual_cnt >= cnt_tokens) {
        // same number of tokens, should have nothing left to parse
        if (last != NULL) {
            if (*last != '#') {
                printf("Leftover detected: %s", last);
                exit(2);
            }
        }
    } else {
        if (JLI_StrCmp(last, tokens[actual_cnt]) != 0) {
            fail(tokens[actual_cnt], last, actual_cnt);
        }
        actual_cnt++;
    }
    if (actual_cnt != cnt_tokens) {
        printf("FAILED: Number of tokens not match, expected %lu, got %lu\n",
            cnt_tokens, actual_cnt);
        exit(3);
    }

    printf("PASS\n");
}

#define DO_CASE(name) \
    test_case(name[0], name + 1, sizeof(name)/sizeof(char*) - 1)

int main(int argc, char** argv) {
    size_t i, j;

    char* case1[] = { "-version -cp \"c:\\\\java libs\\\\one.jar\" \n",
        "-version", "-cp", "c:\\java libs\\one.jar" };
    DO_CASE(case1);

    // note the open quote at the end
    char* case2[] = { "com.foo.Panda \"Furious 5\"\fand\t'Shi Fu' \"escape\tprison",
        "com.foo.Panda", "Furious 5", "and", "Shi Fu", "escape\tprison"};
    DO_CASE(case2);

    char* escaped_chars[] = { "escaped chars testing \"\\a\\b\\c\\f\\n\\r\\t\\v\\9\\6\\23\\82\\28\\377\\477\\278\\287\"",
        "escaped", "chars", "testing", "abc\f\n\r\tv96238228377477278287"};
    DO_CASE(escaped_chars);

    char* mixed_quote[]  = { "\"mix 'single quote' in double\" 'mix \"double quote\" in single' partial\"quote me\"this",
        "mix 'single quote' in double", "mix \"double quote\" in single", "partialquote methis"};
    DO_CASE(mixed_quote);

    char* comments[]  = { "line one #comment\n'line #2' #rest are comment\r\n#comment on line 3\nline 4 #comment to eof",
        "line", "one", "line #2", "line", "4"};
    DO_CASE(comments);

    char* open_quote[] = { "This is an \"open quote \n    across line\n\t, note for WS.",
        "This", "is", "an", "open quote ", "across", "line", ",", "note", "for", "WS." };
    DO_CASE(open_quote);

    char* escape_in_open_quote[] = { "Try \"this \\\\\\\\ escape\\n double quote \\\" in open quote",
        "Try", "this \\\\ escape\n double quote \" in open quote" };
    DO_CASE(escape_in_open_quote);

    char* quote[] = { "'-Dmy.quote.single'='Property in single quote. Here a double quote\" Add some slashes \\\\/'",
        "-Dmy.quote.single=Property in single quote. Here a double quote\" Add some slashes \\/" };
    DO_CASE(quote);

    char* multi[] = { "\"Open quote to \n  new \"line \\\n\r   third\\\n\r\\\tand\ffourth\"",
        "Open quote to ", "new", "line third\tand\ffourth" };
    DO_CASE(multi);

    char* escape_quote[] = { "c:\\\"partial quote\"\\lib",
        "c:\\partial quote\\lib" };
    DO_CASE(escape_quote);

    if (argc > 1) {
        for (i = 0; i < argc; i++) {
            JLI_List tokens = JLI_PreprocessArg(argv[i], JNI_FALSE);
            if (NULL != tokens) {
                for (j = 0; j < tokens->size; j++) {
                    printf("Token[%lu]: <%s>\n", (unsigned long) j, tokens->elements[j]);
                }
            }
        }
    }
}

#endif // DEBUG_ARGFILE
