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

#include "sd-device.h"

#include "alloc-util.h"
#include "blkid-util.h"
#include "blockdev-util.h"
#include "build.h"
#include "chase.h"
#include "device-util.h"
#include "errno-util.h"
#include "fd-util.h"
#include "format-table.h"
#include "gpt.h"
#include "initrd-util.h"
#include "log.h"
#include "main-func.h"
#include "mountpoint-util.h"
#include "options.h"
#include "parse-argument.h"
#include "path-util.h"
#include "pretty-print.h"
#include "string-util.h"
#include "strv.h"
#include "utf8.h"
#include "xattr-util.h"

static char *arg_target = NULL;
static char *arg_root = NULL;

STATIC_DESTRUCTOR_REGISTER(arg_target, freep);
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);

static int help(void) {
        _cleanup_free_ char *link = NULL;
        _cleanup_(table_unrefp) Table *options = NULL;
        int r;

        r = terminal_urlify_man("systemd-validatefs@.service", "8", &link);
        if (r < 0)
                return log_oom();

        r = option_parser_get_help_table(&options);
        if (r < 0)
                return r;

        printf("%s [OPTIONS...] /path/to/mountpoint\n"
               "\n%sCheck file system validation constraints.%s\n"
               "\nOptions:\n",
               program_invocation_short_name,
               ansi_highlight(),
               ansi_normal());
        r = table_print_or_warn(options);
        if (r < 0)
                return r;

        printf("\nSee the %s for details.\n", link);
        return 0;
}

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

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

        OptionParser opts = { argc, argv };

        FOREACH_OPTION_OR_RETURN(c, &opts)
                switch (c) {
                OPTION_COMMON_HELP:
                        return help();

                OPTION_COMMON_VERSION:
                        return version();

                OPTION_LONG("root", "PATH|auto", "Operate relative to the specified path"):
                        if (streq(opts.arg, "auto"))
                                r = free_and_strdup_warn(&arg_root, in_initrd() ? "/sysroot" : NULL);
                        else {
                                if (!path_is_absolute(opts.arg))
                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                                               "--root= argument must be 'auto' or absolute path, got: %s", opts.arg);

                                r = parse_path_argument(opts.arg, /* suppress_root= */ true, &arg_root);
                        }
                        if (r < 0)
                                return r;
                        break;
                }

        char **args = option_parser_get_args(&opts);

        if (strv_length(args) != 1)
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                       "%s expects exactly one argument (the mount point).",
                                       program_invocation_short_name);

        arg_target = strdup(args[0]);
        if (!arg_target)
                return log_oom();

        if (arg_root && !path_startswith(arg_target, arg_root))
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                       "Specified path '%s' does not start with specified root '%s', refusing.",
                                       arg_target, arg_root);
        return 1;
}

typedef struct ValidateFields {
        sd_id128_t *gpt_type_uuid;
        size_t n_gpt_type_uuid;
        char **gpt_label;
        char **mount_point;
} ValidateFields;

static void validate_fields_done(ValidateFields *f) {
        assert(f);

        free(f->gpt_type_uuid);
        strv_free(f->gpt_label);
        strv_free(f->mount_point);
}

static char* validate_fields_gpt_type_uuid_as_string(const ValidateFields *f) {
        _cleanup_free_ char *joined = NULL;

        assert(f);

        FOREACH_ARRAY(u, f->gpt_type_uuid, f->n_gpt_type_uuid) {
                if (!strextend_with_separator(&joined, ", ", SD_ID128_TO_UUID_STRING(*u)))
                        return NULL;

                const char *id = gpt_partition_type_uuid_to_string(*u);
                if (id)
                        (void) strextend(&joined, " (", id, ")");
        }

        return TAKE_PTR(joined);
}

static int validate_fields_read(int fd, ValidateFields *ret) {
        _cleanup_(validate_fields_done) ValidateFields f = {};
        int r;

        assert(fd >= 0);
        assert(ret);

        _cleanup_strv_free_ char **l = NULL;
        r = getxattr_at_strv(fd, /* path= */ NULL, "user.validatefs.gpt_type_uuid", AT_EMPTY_PATH, &l);
        if (r < 0) {
                if (r != -ENODATA && !ERRNO_IS_NOT_SUPPORTED(r))
                        return log_error_errno(r, "Failed to read 'user.validatefs.gpt_type_uuid' xattr: %m");
        } else {
                STRV_FOREACH(i, l) {
                        if (!GREEDY_REALLOC(f.gpt_type_uuid, f.n_gpt_type_uuid+1))
                                return log_oom();

                        r = sd_id128_from_string(*i, f.gpt_type_uuid + f.n_gpt_type_uuid);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse 'user.validatefs.gpt_type_uuid' xattr: %s", *i);

                        f.n_gpt_type_uuid++;
                }
        }

        l = strv_free(l);
        r = getxattr_at_strv(fd, /* path= */ NULL, "user.validatefs.gpt_label", AT_EMPTY_PATH, &l);
        if (r < 0) {
                if (r != -ENODATA && !ERRNO_IS_NOT_SUPPORTED(r))
                        return log_error_errno(r, "Failed to read 'user.validatefs.gpt_label' xattr: %m");
        } else {
                STRV_FOREACH(i, l)
                        if (!utf8_is_valid(*i) ||
                            string_has_cc(*i, /* ok= */ NULL))
                                return log_error_errno(
                                                SYNTHETIC_ERRNO(EINVAL),
                                                "Extended attribute 'user.validatefs.gpt_label' contains invalid characters, refusing: %s", *i);

                f.gpt_label = TAKE_PTR(l);
        }

        l = strv_free(l);
        r = getxattr_at_strv(fd, /* path= */ NULL, "user.validatefs.mount_point", AT_EMPTY_PATH, &l);
        if (r < 0) {
                if (r != -ENODATA && !ERRNO_IS_NOT_SUPPORTED(r))
                        return log_error_errno(r, "Failed to read 'user.validatefs.mount_point' xattr: %m");
        } else {
                STRV_FOREACH(i, l)
                        if (!utf8_is_valid(*i) ||
                            string_has_cc(*i, /* ok= */ NULL) ||
                            !path_is_absolute(*i) ||
                            !path_is_normalized(*i))
                                return log_error_errno(
                                                SYNTHETIC_ERRNO(EINVAL),
                                                "Path listed in extended attribute 'user.validatefs.mount_point' is not a valid, normalized, absolute path or contains invalid characters, refusing: %s", *i);

                f.mount_point = TAKE_PTR(l);
        }

        r = f.n_gpt_type_uuid > 0 || !strv_isempty(f.gpt_label) || !strv_isempty(f.mount_point);
        *ret = TAKE_STRUCT(f);
        return r;
}

static int validate_mount_point(const char *path, const ValidateFields *f) {
        assert(path);
        assert(f);

        if (strv_isempty(f->mount_point))
                return 0;

        STRV_FOREACH(i, f->mount_point) {
                _cleanup_free_ char *jj = NULL;
                const char *j;

                if (arg_root) {
                        jj = path_join(arg_root, *i);
                        if (!jj)
                                return log_oom();

                        j = jj;
                } else
                        j = *i;

                if (path_equal(path, j))
                        return 0;
        }

        _cleanup_free_ char *joined = strv_join(f->mount_point, ", ");
        return log_error_errno(
                        SYNTHETIC_ERRNO(EPERM),
                        "File system is supposed to be mounted on one of %s only, but is mounted on %s, refusing.",
                        strna(joined), path);
}

static int validate_gpt_label(blkid_probe b, const ValidateFields *f) {
        assert(b);
        assert(f);

        if (strv_isempty(f->gpt_label))
                return 0;

        const char *v = NULL;
        (void) sym_blkid_probe_lookup_value(b, "PART_ENTRY_NAME", &v, /* len= */ NULL);

        if (strv_contains(f->gpt_label, strempty(v)))
                return 0;

        _cleanup_free_ char *joined = strv_join(f->gpt_label, "', '");
        return log_error_errno(
                        SYNTHETIC_ERRNO(EPERM),
                        "File system is supposed to be placed in a partition with labels '%s' only, but is placed in one labelled '%s', refusing.",
                        strna(joined), strempty(v));
}

static int validate_gpt_type(blkid_probe b, const ValidateFields *f) {
        assert(b);
        assert(f);

        if (f->n_gpt_type_uuid == 0)
                return 0;

        const char *v = NULL;
        (void) sym_blkid_probe_lookup_value(b, "PART_ENTRY_TYPE", &v, /* len= */ NULL);

        sd_id128_t id;
        if (!v || sd_id128_from_string(v, &id) < 0) {
                _cleanup_free_ char *joined = validate_fields_gpt_type_uuid_as_string(f);
                return log_error_errno(
                                SYNTHETIC_ERRNO(EPERM),
                                "File system is supposed to be placed in a partition of type UUIDs %s only, but has no type, refusing.",
                                strna(joined));
        }

        FOREACH_ARRAY(u, f->gpt_type_uuid, f->n_gpt_type_uuid)
                if (sd_id128_equal(*u, id))
                        return 0;

        _cleanup_free_ char *joined = validate_fields_gpt_type_uuid_as_string(f);
        return log_error_errno(
                        SYNTHETIC_ERRNO(EPERM),
                        "File system is supposed to be placed in a partition of type UUIDs %s only, but has type '%s', refusing.",
                        strna(joined), SD_ID128_TO_UUID_STRING(id));
}

static int validate_gpt_metadata_one(sd_device *d, const char *path, const ValidateFields *f) {
        int r;

        assert(d);
        assert(f);

        r = dlopen_libblkid(LOG_ERR);
        if (r < 0)
                return r;

        _cleanup_close_ int block_fd = sd_device_open(d, O_RDONLY|O_CLOEXEC|O_NONBLOCK);
        if (block_fd < 0)
                return log_error_errno(block_fd, "Failed to open block device backing '%s': %m", path);

        _cleanup_(blkid_free_probep) blkid_probe b = sym_blkid_new_probe();
        if (!b)
                return log_oom();

        errno = 0;
        r = sym_blkid_probe_set_device(b, block_fd, 0, 0);
        if (r != 0)
                return log_error_errno(errno_or_else(ENOMEM), "Failed to set up block device prober for '%s': %m", path);

        (void) sym_blkid_probe_enable_superblocks(b, 1);
        (void) sym_blkid_probe_set_superblocks_flags(b, BLKID_SUBLKS_TYPE|BLKID_SUBLKS_LABEL);
        (void) sym_blkid_probe_enable_partitions(b, 1);
        (void) sym_blkid_probe_set_partitions_flags(b, BLKID_PARTS_ENTRY_DETAILS);

        errno = 0;
        r = sym_blkid_do_safeprobe(b);
        if (r == _BLKID_SAFEPROBE_ERROR)
                return log_error_errno(errno_or_else(EIO), "Failed to probe block device of '%s': %m", path);
        if (r == _BLKID_SAFEPROBE_AMBIGUOUS)
                return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), "Found multiple file system labels on block device '%s'.", path);
        if (r == _BLKID_SAFEPROBE_NOT_FOUND)
                return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), "Found no file system label on block device '%s'.", path);

        assert(r == _BLKID_SAFEPROBE_FOUND);

        const char *v = NULL;
        (void) sym_blkid_probe_lookup_value(b, "PART_ENTRY_SCHEME", &v, /* len= */ NULL);
        if (!streq_ptr(v, "gpt"))
                return log_error_errno(SYNTHETIC_ERRNO(EPERM), "File system is supposed to be on a GPT partition table, but is not, refusing.");

        r = validate_gpt_label(b, f);
        if (r < 0)
                return r;

        r = validate_gpt_type(b, f);
        if (r < 0)
                return r;

        return 0;
}

static int validate_gpt_metadata(int fd, const char *path, const ValidateFields *f) {
        int r;

        assert(fd >= 0);
        assert(path);
        assert(f);

        if (strv_isempty(f->gpt_label) && f->n_gpt_type_uuid == 0)
                return 0;

        _cleanup_(sd_device_unrefp) sd_device *d = NULL;
        r = block_device_new_from_fd(fd, BLOCK_DEVICE_LOOKUP_BACKING, &d);
        if (r < 0)
                return log_error_errno(r, "Failed to find block device backing '%s': %m", path);

        /* Now validate all subordinate devices individually. */
        bool have_slaves = false;
        const char *suffix;
        FOREACH_DEVICE_CHILD_WITH_SUFFIX(d, child, suffix) {
                if (!path_startswith(suffix, "slaves"))
                        continue;

                have_slaves = true;

                r = validate_gpt_metadata_one(child, path, f);
                if (r < 0)
                        return r;
        }

        /* If this device has no subordinate devices, then validate the device itself instead */
        if (!have_slaves) {
                r = validate_gpt_metadata_one(d, path, f);
                if (r < 0)
                        return r;
        }

        return 0;
}

static int validate_fields_check(int fd, const char *path, const ValidateFields *f) {
        int r;

        assert(fd >= 0);
        assert(path);
        assert(f);

        r = validate_mount_point(path, f);
        if (r < 0)
                return r;

        r = validate_gpt_metadata(fd, path, f);
        if (r < 0)
                return r;

        log_info("File system '%s' passed validation constraints, proceeding.", path);
        return 0;
}

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

        log_setup();

        r = parse_argv(argc, argv);
        if (r <= 0)
                return r;

        _cleanup_free_ char *resolved = NULL;
        _cleanup_close_ int target_fd = chase_and_open(arg_target, arg_root, CHASE_MUST_BE_DIRECTORY, O_DIRECTORY|O_CLOEXEC, &resolved);
        if (target_fd < 0)
                return log_error_errno(target_fd, "Failed to open directory '%s': %m", arg_target);

        r = is_mount_point_at(target_fd, /* path= */ NULL, /* flags= */ 0);
        if (r < 0)
                return log_error_errno(r, "Failed to determine whether '%s' is a mount point: %m", resolved);
        if (!r)
                return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "Directory '%s' is not a mount point.", resolved);

        _cleanup_(validate_fields_done) ValidateFields f = {};
        r = validate_fields_read(target_fd, &f);
        if (r < 0)
                return r;
        if (r == 0) {
                log_info("File system '%s' has no validation constraints set, not validating.", resolved);
                return 0;
        }

        return validate_fields_check(target_fd, resolved, &f);
}

DEFINE_MAIN_FUNCTION(run);
