/*-
 * Copyright (c) 2003-2008 Tim Kientzle
 * 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(S) ``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(S) 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.
 */

/*
 * Command line parser for tar.
 */

#include "bsdtar_platform.h"
__FBSDID("$FreeBSD$");

#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#endif
#ifdef HAVE_STRING_H
#include <string.h>
#endif

#include "bsdtar.h"
#include "err.h"

/*
 * Short options for tar.  Please keep this sorted.
 */
static const char *short_options
    = "Bb:C:cf:HhI:JjkLlmnOoPpqrSs:T:tUuvW:wX:xyZz";

/*
 * Long options for tar.  Please keep this list sorted.
 *
 * The symbolic names for options that lack a short equivalent are
 * defined in bsdtar.h.  Also note that so far I've found no need
 * to support optional arguments to long options.  That would be
 * a small change to the code below.
 */

static struct option {
    const char *name;
    int required;      /* 1 if this option requires an argument. */
    int equivalent;    /* Equivalent short option. */
} tar_longopts[] = {
    { "absolute-paths",       0, 'P' },
    { "append",               0, 'r' },
    { "block-size",           1, 'b' },
    { "bunzip2",              0, 'j' },
    { "bzip",                 0, 'j' },
    { "bzip2",                0, 'j' },
    { "cd",                   1, 'C' },
    { "check-links",          0, OPTION_CHECK_LINKS },
    { "chroot",               0, OPTION_CHROOT },
    { "compress",             0, 'Z' },
    { "confirmation",         0, 'w' },
    { "create",               0, 'c' },
    { "dereference",      0, 'L' },
    { "directory",            1, 'C' },
    { "exclude",              1, OPTION_EXCLUDE },
    { "exclude-from",         1, 'X' },
    { "extract",              0, 'x' },
    { "fast-read",            0, 'q' },
    { "file",                 1, 'f' },
    { "files-from",           1, 'T' },
    { "format",               1, OPTION_FORMAT },
    { "options",              1, OPTION_OPTIONS },
    { "gunzip",               0, 'z' },
    { "gzip",                 0, 'z' },
    { "help",                 0, OPTION_HELP },
    { "include",              1, OPTION_INCLUDE },
    { "interactive",          0, 'w' },
    { "insecure",             0, 'P' },
    { "keep-newer-files",     0, OPTION_KEEP_NEWER_FILES },
    { "keep-old-files",       0, 'k' },
    { "list",                 0, 't' },
    { "lzma",                 0, OPTION_LZMA },
    { "modification-time",    0, 'm' },
    { "newer",        1, OPTION_NEWER_CTIME },
    { "newer-ctime",      1, OPTION_NEWER_CTIME },
    { "newer-ctime-than",     1, OPTION_NEWER_CTIME_THAN },
    { "newer-mtime",      1, OPTION_NEWER_MTIME },
    { "newer-mtime-than",     1, OPTION_NEWER_MTIME_THAN },
    { "newer-than",       1, OPTION_NEWER_CTIME_THAN },
    { "nodump",               0, OPTION_NODUMP },
    { "norecurse",            0, 'n' },
    { "no-recursion",         0, 'n' },
    { "no-same-owner",    0, OPTION_NO_SAME_OWNER },
    { "no-same-permissions",  0, OPTION_NO_SAME_PERMISSIONS },
    { "null",         0, OPTION_NULL },
    { "numeric-owner",    0, OPTION_NUMERIC_OWNER },
    { "one-file-system",      0, OPTION_ONE_FILE_SYSTEM },
    { "posix",        0, OPTION_POSIX },
    { "preserve-permissions", 0, 'p' },
    { "read-full-blocks",     0, 'B' },
    { "same-owner",           0, OPTION_SAME_OWNER },
    { "same-permissions",     0, 'p' },
    { "strip-components",     1, OPTION_STRIP_COMPONENTS },
    { "to-stdout",            0, 'O' },
    { "totals",       0, OPTION_TOTALS },
    { "uncompress",           0, 'Z' },
    { "unlink",       0, 'U' },
    { "unlink-first",     0, 'U' },
    { "update",               0, 'u' },
    { "use-compress-program", 1, OPTION_USE_COMPRESS_PROGRAM },
    { "verbose",              0, 'v' },
    { "version",              0, OPTION_VERSION },
    { "xz",                   0, 'J' },
    { NULL, 0, 0 }
};

/*
 * This getopt implementation has two key features that common
 * getopt_long() implementations lack.  Apart from those, it's a
 * straightforward option parser, considerably simplified by not
 * needing to support the wealth of exotic getopt_long() features.  It
 * has, of course, been shamelessly tailored for bsdtar.  (If you're
 * looking for a generic getopt_long() implementation for your
 * project, I recommend Gregory Pietsch's public domain getopt_long()
 * implementation.)  The two additional features are:
 *
 * Old-style tar arguments: The original tar implementation treated
 * the first argument word as a list of single-character option
 * letters.  All arguments follow as separate words.  For example,
 *    tar xbf 32 /dev/tape
 * Here, the "xbf" is three option letters, "32" is the argument for
 * "b" and "/dev/tape" is the argument for "f".  We support this usage
 * if the first command-line argument does not begin with '-'.  We
 * also allow regular short and long options to follow, e.g.,
 *    tar xbf 32 /dev/tape -P --format=pax
 *
 * -W long options: There's an obscure GNU convention (only rarely
 * supported even there) that allows "-W option=argument" as an
 * alternative way to support long options.  This was supported in
 * early bsdtar as a way to access long options on platforms that did
 * not support getopt_long() and is preserved here for backwards
 * compatibility.  (Of course, if I'd started with a custom
 * command-line parser from the beginning, I would have had normal
 * long option support on every platform so that hack wouldn't have
 * been necessary.  Oh, well.  Some mistakes you just have to live
 * with.)
 *
 * TODO: We should be able to use this to pull files and intermingled
 * options (such as -C) from the command line in write mode.  That
 * will require a little rethinking of the argument handling in
 * bsdtar.c.
 *
 * TODO: If we want to support arbitrary command-line options from -T
 * input (as GNU tar does), we may need to extend this to handle option
 * words from sources other than argv/arc.  I'm not really sure if I
 * like that feature of GNU tar, so it's certainly not a priority.
 */

int
bsdtar_getopt(struct bsdtar *bsdtar)
{
    enum { state_start = 0, state_old_tar, state_next_word,
           state_short, state_long };
    static int state = state_start;
    static char *opt_word;

    const struct option *popt, *match = NULL, *match2 = NULL;
    const char *p, *long_prefix = "--";
    size_t optlength;
    int opt = '?';
    int required = 0;

    bsdtar->optarg = NULL;

    /* First time through, initialize everything. */
    if (state == state_start) {
        /* Skip program name. */
        ++bsdtar->argv;
        --bsdtar->argc;
        if (*bsdtar->argv == NULL)
            return (-1);
        /* Decide between "new style" and "old style" arguments. */
        if (bsdtar->argv[0][0] == '-') {
            state = state_next_word;
        } else {
            state = state_old_tar;
            opt_word = *bsdtar->argv++;
            --bsdtar->argc;
        }
    }

    /*
     * We're parsing old-style tar arguments
     */
    if (state == state_old_tar) {
        /* Get the next option character. */
        opt = *opt_word++;
        if (opt == '\0') {
            /* New-style args can follow old-style. */
            state = state_next_word;
        } else {
            /* See if it takes an argument. */
            p = strchr(short_options, opt);
            if (p == NULL)
                return ('?');
            if (p[1] == ':') {
                bsdtar->optarg = *bsdtar->argv;
                if (bsdtar->optarg == NULL) {
                    lafe_warnc(0,
                        "Option %c requires an argument",
                        opt);
                    return ('?');
                }
                ++bsdtar->argv;
                --bsdtar->argc;
            }
        }
    }

    /*
     * We're ready to look at the next word in argv.
     */
    if (state == state_next_word) {
        /* No more arguments, so no more options. */
        if (bsdtar->argv[0] == NULL)
            return (-1);
        /* Doesn't start with '-', so no more options. */
        if (bsdtar->argv[0][0] != '-')
            return (-1);
        /* "--" marks end of options; consume it and return. */
        if (strcmp(bsdtar->argv[0], "--") == 0) {
            ++bsdtar->argv;
            --bsdtar->argc;
            return (-1);
        }
        /* Get next word for parsing. */
        opt_word = *bsdtar->argv++;
        --bsdtar->argc;
        if (opt_word[1] == '-') {
            /* Set up long option parser. */
            state = state_long;
            opt_word += 2; /* Skip leading '--' */
        } else {
            /* Set up short option parser. */
            state = state_short;
            ++opt_word;  /* Skip leading '-' */
        }
    }

    /*
     * We're parsing a group of POSIX-style single-character options.
     */
    if (state == state_short) {
        /* Peel next option off of a group of short options. */
        opt = *opt_word++;
        if (opt == '\0') {
            /* End of this group; recurse to get next option. */
            state = state_next_word;
            return bsdtar_getopt(bsdtar);
        }

        /* Does this option take an argument? */
        p = strchr(short_options, opt);
        if (p == NULL)
            return ('?');
        if (p[1] == ':')
            required = 1;

        /* If it takes an argument, parse that. */
        if (required) {
            /* If arg is run-in, opt_word already points to it. */
            if (opt_word[0] == '\0') {
                /* Otherwise, pick up the next word. */
                opt_word = *bsdtar->argv;
                if (opt_word == NULL) {
                    lafe_warnc(0,
                        "Option -%c requires an argument",
                        opt);
                    return ('?');
                }
                ++bsdtar->argv;
                --bsdtar->argc;
            }
            if (opt == 'W') {
                state = state_long;
                long_prefix = "-W "; /* For clearer errors. */
            } else {
                state = state_next_word;
                bsdtar->optarg = opt_word;
            }
        }
    }

    /* We're reading a long option, including -W long=arg convention. */
    if (state == state_long) {
        /* After this long option, we'll be starting a new word. */
        state = state_next_word;

        /* Option name ends at '=' if there is one. */
        p = strchr(opt_word, '=');
        if (p != NULL) {
            optlength = (size_t)(p - opt_word);
            bsdtar->optarg = (char *)(uintptr_t)(p + 1);
        } else {
            optlength = strlen(opt_word);
        }

        /* Search the table for an unambiguous match. */
        for (popt = tar_longopts; popt->name != NULL; popt++) {
            /* Short-circuit if first chars don't match. */
            if (popt->name[0] != opt_word[0])
                continue;
            /* If option is a prefix of name in table, record it.*/
            if (strncmp(opt_word, popt->name, optlength) == 0) {
                match2 = match; /* Record up to two matches. */
                match = popt;
                /* If it's an exact match, we're done. */
                if (strlen(popt->name) == optlength) {
                    match2 = NULL; /* Forget the others. */
                    break;
                }
            }
        }

        /* Fail if there wasn't a unique match. */
        if (match == NULL) {
            lafe_warnc(0,
                "Option %s%s is not supported",
                long_prefix, opt_word);
            return ('?');
        }
        if (match2 != NULL) {
            lafe_warnc(0,
                "Ambiguous option %s%s (matches --%s and --%s)",
                long_prefix, opt_word, match->name, match2->name);
            return ('?');
        }

        /* We've found a unique match; does it need an argument? */
        if (match->required) {
            /* Argument required: get next word if necessary. */
            if (bsdtar->optarg == NULL) {
                bsdtar->optarg = *bsdtar->argv;
                if (bsdtar->optarg == NULL) {
                    lafe_warnc(0,
                        "Option %s%s requires an argument",
                        long_prefix, match->name);
                    return ('?');
                }
                ++bsdtar->argv;
                --bsdtar->argc;
            }
        } else {
            /* Argument forbidden: fail if there is one. */
            if (bsdtar->optarg != NULL) {
                lafe_warnc(0,
                    "Option %s%s does not allow an argument",
                    long_prefix, match->name);
                return ('?');
            }
        }
        return (match->equivalent);
    }

    return (opt);
}
