/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <fcntl.h>
#include <getopt.h>
#include <unistd.h>

#include "argv-util.h"
#include "build.h"
#include "chase.h"
#include "errno-util.h"
#include "extract-word.h"
#include "fd-util.h"
#include "format-table.h"
#include "image-policy.h"
#include "main-func.h"
#include "mount-util.h"
#include "mountpoint-util.h"
#include "mstack.h"
#include "parse-argument.h"
#include "pretty-print.h"
#include "string-util.h"

static enum {
        ACTION_INSPECT,
        ACTION_MOUNT,
        ACTION_UMOUNT,
} arg_action = ACTION_INSPECT;
static char *arg_what = NULL;
static char *arg_where = NULL;
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
static PagerFlags arg_pager_flags = 0;
static int arg_legend = true;
static MStackFlags arg_mstack_flags = 0;
static bool arg_rmdir = false;
static ImagePolicy *arg_image_policy = NULL;
static ImageFilter *arg_image_filter = NULL;

STATIC_DESTRUCTOR_REGISTER(arg_what, freep);
STATIC_DESTRUCTOR_REGISTER(arg_where, freep);
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
STATIC_DESTRUCTOR_REGISTER(arg_image_filter, image_filter_freep);

static int help(void) {
        _cleanup_free_ char *link = NULL;
        int r;

        r = terminal_urlify_man("systemd-mstack", "1", &link);
        if (r < 0)
                return log_oom();

        printf("%1$s [OPTIONS...] WHAT\n"
               "%1$s [OPTIONS...] --mount WHAT WHERE\n"
               "%1$s [OPTIONS...] --umount WHERE\n"
               "\n%5$sInspect or apply mount stack.%6$s\n\n"
               "%3$sOptions:%4$s\n"
               "     --no-pager               Do not pipe output into a pager\n"
               "     --no-legend              Do not print the column headers\n"
               "     --json=pretty|short|off  Generate JSON output\n"
               "  -r --read-only              Mount read-only\n"
               "     --mkdir                  Make mount directory before mounting, if missing\n"
               "     --rmdir                  Remove mount directory after unmounting\n"
               "     --image-policy=POLICY\n"
               "                              Specify image dissection policy\n"
               "     --image-filter=FILTER\n"
               "                              Specify image dissection filter\n"
               "\n%3$sCommands:%4$s\n"
               "  -h --help                   Show this help\n"
               "     --version                Show package version\n"
               "  -m --mount                  Mount the mstack to the specified directory\n"
               "  -M                          Shortcut for --mount --mkdir\n"
               "  -u --umount                 Unmount the image from the specified directory\n"
               "  -U                          Shortcut for --umount --rmdir\n"
               "\nSee the %2$s for details.\n",
               program_invocation_short_name,
               link,
               ansi_underline(), ansi_normal(),
               ansi_highlight(), ansi_normal());

        return 0;
}

static int parse_argv(int argc, char *argv[]) {

        enum {
                ARG_VERSION = 0x100,
                ARG_NO_PAGER,
                ARG_NO_LEGEND,
                ARG_JSON,
                ARG_MKDIR,
                ARG_RMDIR,
                ARG_IMAGE_POLICY,
                ARG_IMAGE_FILTER,
        };

        static const struct option options[] = {
                { "help",         no_argument,       NULL, 'h'              },
                { "version",      no_argument,       NULL, ARG_VERSION      },
                { "no-pager",     no_argument,       NULL, ARG_NO_PAGER     },
                { "no-legend",    no_argument,       NULL, ARG_NO_LEGEND    },
                { "mount",        no_argument,       NULL, 'm'              },
                { "umount",       no_argument,       NULL, 'u'              },
                { "json",         required_argument, NULL, ARG_JSON         },
                { "read-only",    no_argument,       NULL, 'r'              },
                { "mkdir",        no_argument,       NULL, ARG_MKDIR        },
                { "rmdir",        no_argument,       NULL, ARG_RMDIR        },
                { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY },
                { "image-filter", required_argument, NULL, ARG_IMAGE_FILTER },
                {}
        };

        int c, r;

        assert(argc >= 0);
        assert(argv);

        while ((c = getopt_long(argc, argv, "hmMuUr", options, NULL)) >= 0) {

                switch (c) {

                case 'h':
                        return help();

                case ARG_VERSION:
                        return version();

                case ARG_NO_PAGER:
                        arg_pager_flags |= PAGER_DISABLE;
                        break;

                case ARG_NO_LEGEND:
                        arg_legend = false;
                        break;

                case ARG_JSON:
                        r = parse_json_argument(optarg, &arg_json_format_flags);
                        if (r <= 0)
                                return r;

                        break;

                case 'r':
                        arg_mstack_flags |= MSTACK_RDONLY;
                        break;

                case ARG_IMAGE_POLICY:
                        r = parse_image_policy_argument(optarg, &arg_image_policy);
                        if (r < 0)
                                return r;
                        break;

                case ARG_IMAGE_FILTER: {
                        _cleanup_(image_filter_freep) ImageFilter *f = NULL;
                        r = image_filter_parse(optarg, &f);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse image filter expression: %s", optarg);

                        image_filter_free(arg_image_filter);
                        arg_image_filter = TAKE_PTR(f);
                        break;
                }

                case ARG_MKDIR:
                        arg_mstack_flags |= MSTACK_MKDIR;
                        break;

                case ARG_RMDIR:
                        arg_rmdir = true;
                        break;

                case 'm':
                        arg_action = ACTION_MOUNT;
                        break;

                case 'M':
                        /* Shortcut combination of --mkdir + --mount */
                        arg_action = ACTION_MOUNT;
                        arg_mstack_flags |= MSTACK_MKDIR;
                        break;

                case 'u':
                        arg_action = ACTION_UMOUNT;
                        break;

                case 'U':
                        /* Shortcut combination of --rmdir + --umount */
                        arg_action = ACTION_UMOUNT;
                        arg_rmdir = true;
                        break;

                case '?':
                        return -EINVAL;

                default:
                        assert_not_reached();
                }
        }

        switch (arg_action) {

        case ACTION_INSPECT:
                if (optind + 1 != argc)
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected one argument.");

                r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_what);
                if (r < 0)
                        return r;

                break;

        case ACTION_MOUNT:
                if (optind + 2 != argc)
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected two arguments.");

                r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_what);
                if (r < 0)
                        return r;

                r = parse_path_argument(argv[optind+1], /* suppress_root= */ false, &arg_where);
                if (r < 0)
                        return r;

                break;

        case ACTION_UMOUNT:
                if (optind + 1 != argc)
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected one argument.");

                r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_where);
                if (r < 0)
                        return r;

                break;

        default:
                assert_not_reached();
        }

        return 1;
}

static int parse_argv_as_mount_helper(int argc, char *argv[]) {
        const char *options = NULL;
        bool fake = false;
        int c, r;

        /* Implements util-linux "external helper" command line interface, as per mount(8) man page. */

        while ((c = getopt(argc, argv, "sfnvN:o:t:")) >= 0) {
                switch (c) {

                case 'f':
                        fake = true;
                        break;

                case 'o':
                        options = optarg;
                        break;

                case 't':
                        if (!streq(optarg, "mstack"))
                                log_debug("Unexpected file system type '%s', ignoring.", optarg);
                        break;

                case 's': /* sloppy mount options */
                case 'n': /* aka --no-mtab */
                case 'v': /* aka --verbose */
                        log_debug("Ignoring option -%c, not implemented.", c);
                        break;

                case 'N': /* aka --namespace= */
                        return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Option -%c is not implemented, refusing.", c);

                case '?':
                        return -EINVAL;
                }
        }

        if (optind + 2 != argc)
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                       "Expected an image file path and target directory as only argument.");

        for (const char *p = options;;) {
                _cleanup_free_ char *word = NULL;

                r = extract_first_word(&p, &word, ",", EXTRACT_KEEP_QUOTE);
                if (r < 0)
                        return log_error_errno(r, "Failed to extract mount option: %m");
                if (r == 0)
                        break;

                if (streq(word, "ro"))
                        SET_FLAG(arg_mstack_flags, MSTACK_RDONLY, true);
                else if (streq(word, "rw"))
                        SET_FLAG(arg_mstack_flags, MSTACK_RDONLY, false);
                else
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                               "Unknown mount option '%s'.", word);
        }

        if (fake)
                return 0;

        r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_what);
        if (r < 0)
                return r;

        r = parse_path_argument(argv[optind+1], /* suppress_root= */ false, &arg_where);
        if (r < 0)
                return r;

        arg_action = ACTION_MOUNT;
        return 1;
}

static int inspect_mstack(void) {
        _cleanup_(mstack_freep) MStack *mstack = NULL;
        int r;

        assert(arg_what);

        r = mstack_load(arg_what, /* dir_fd= */ -EBADF, &mstack);
        if (r < 0)
                return log_debug_errno(r, "Failed to load .mstack/ directory '%s': %m", arg_what);

        _cleanup_(table_unrefp) Table *t = NULL;

        t = table_new("type", "name", "image", "what", "where", "sort");
        if (!t)
                return log_oom();

        table_set_ersatz_string(t, TABLE_ERSATZ_DASH);

        FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) {
                _cleanup_free_ char *w = NULL;
                r = fd_get_path(m->what_fd, &w);
                if (r < 0)
                        return log_error_errno(r, "Failed to get path of what file descriptor: %m");

                r = table_add_many(
                                t,
                                TABLE_STRING, mstack_mount_type_to_string(m->mount_type),
                                TABLE_STRING, m->what,
                                TABLE_STRING, image_type_to_string(m->image_type),
                                TABLE_PATH, w,
                                TABLE_PATH, m->where ?: ((mstack->root_mount && mstack->root_mount != m) ? "/usr" : "/"),
                                TABLE_STRING, m->sort_key);
                if (r < 0)
                        return table_log_add_error(r);
        }

        return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
}

static int mount_mstack(void) {
        int r;

        assert(arg_what);
        assert(arg_where);

        r = mstack_apply(
                        arg_what,
                        /* dir_fd= */ -EBADF,
                        arg_where,
                        /* temp_mount_dir= */ NULL,  /* auto-create temporary directory */
                        /* mountfsd_link= */ NULL,
                        /* userns_fd= */ -EBADF,
                        arg_image_policy,
                        arg_image_filter,
                        arg_mstack_flags,
                        /* ret_root_fd= */ NULL);
         if (r < 0)
                 return log_error_errno(r, "Failed to apply .mstack/ directory '%s': %m", arg_what);

         return 0;
}

static int umount_mstack(void) {
        int r;

        assert(arg_where);

        _cleanup_free_ char *canonical = NULL;
        _cleanup_close_ int fd = chase_and_open(arg_where, /* root= */ NULL, /* chase_flags= */ 0, O_DIRECTORY, &canonical);
        if (fd == -ENOTDIR)
                return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "'%s' is not a directory", arg_where);
        if (fd < 0)
                return log_error_errno(fd, "Failed to resolve path '%s': %m", arg_where);

        r = is_mount_point_at(fd, /* path= */ NULL, /* flags= */ 0);
        if (r == 0)
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "'%s' is not a mount point", canonical);
        if (r < 0)
                return log_error_errno(r, "Failed to determine whether '%s' is a mount point: %m", canonical);

        fd = safe_close(fd);

        r = umount_recursive(canonical, 0);
        if (r < 0)
                return log_error_errno(r, "Failed to unmount '%s': %m", canonical);

        if (arg_rmdir) {
                r = RET_NERRNO(rmdir(canonical));
                if (r < 0)
                        return log_error_errno(r, "Failed to remove mount directory '%s': %m", canonical);
        }

        return 0;
}

static int run(int argc, char *argv[]) {
        int r;

        log_setup();

        if (invoked_as(argv, "mount.mstack"))
                r = parse_argv_as_mount_helper(argc, argv);
        else
                r = parse_argv(argc, argv);
        if (r <= 0)
                return r;

        switch (arg_action) {

        case ACTION_INSPECT:
                return inspect_mstack();

        case ACTION_MOUNT:
                return mount_mstack();

        case ACTION_UMOUNT:
                return umount_mstack();

        default:
                assert_not_reached();
        }
}

DEFINE_MAIN_FUNCTION(run);
