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

#include <errno.h>
#include <stdio.h>
#include <sys/utsname.h>
#include <unistd.h>

#include "sd-id128.h"

#include "alloc-util.h"
#include "device-nodes.h"
#include "dropin.h"
#include "efivars.h"
#include "escape.h"
#include "fd-util.h"
#include "fileio.h"
#include "fstab-util.h"
#include "generator.h"
#include "id128-util.h"
#include "initrd-util.h"
#include "json.h"
#include "log.h"
#include "main-func.h"
#include "os-util.h"
#include "parse-util.h"
#include "path-util.h"
#include "proc-cmdline.h"
#include "special.h"
#include "string-util.h"
#include "unit-name.h"

static const char *arg_dest = NULL;
static char *arg_resume_device = NULL;
static char *arg_resume_options = NULL;
static char *arg_root_options = NULL;
static bool arg_noresume = false;
static uint64_t arg_resume_offset = 0;
static bool arg_resume_offset_set = false;

STATIC_DESTRUCTOR_REGISTER(arg_resume_device, freep);
STATIC_DESTRUCTOR_REGISTER(arg_resume_options, freep);
STATIC_DESTRUCTOR_REGISTER(arg_root_options, freep);

#if ENABLE_EFI
typedef struct EFIHibernateLocation {
        sd_id128_t uuid;
        uint64_t offset;
        const char *kernel_version;
        const char *id;
        const char *image_id;
        const char *version_id;
        const char *image_version;
} EFIHibernateLocation;
#endif

static int parse_proc_cmdline_item(const char *key, const char *value, void *data) {
        int r;

        if (proc_cmdline_key_streq(key, "resume")) {
                char *s;

                if (proc_cmdline_value_missing(key, value))
                        return 0;

                s = fstab_node_to_udev_node(value);
                if (!s)
                        return log_oom();

                free_and_replace(arg_resume_device, s);

        } else if (proc_cmdline_key_streq(key, "resume_offset")) {

                if (proc_cmdline_value_missing(key, value))
                        return 0;

                r = safe_atou64(value, &arg_resume_offset);
                if (r < 0)
                        return log_error_errno(r, "Failed to parse resume_offset=%s: %m", value);

                arg_resume_offset_set = true;

        } else if (proc_cmdline_key_streq(key, "resumeflags")) {

                if (proc_cmdline_value_missing(key, value))
                        return 0;

                if (!strextend_with_separator(&arg_resume_options, ",", value))
                        return log_oom();

        } else if (proc_cmdline_key_streq(key, "rootflags")) {

                if (proc_cmdline_value_missing(key, value))
                        return 0;

                if (!strextend_with_separator(&arg_root_options, ",", value))
                        return log_oom();

        } else if (proc_cmdline_key_streq(key, "noresume")) {
                if (value) {
                        log_warning("\"noresume\" kernel command line switch specified with an argument, ignoring.");
                        return 0;
                }

                arg_noresume = true;
        }

        return 0;
}

static int parse_efi_hibernate_location(void) {
        int r = 0;

#if ENABLE_EFI
        static const JsonDispatch dispatch_table[] = {
                { "uuid",                  JSON_VARIANT_STRING,   json_dispatch_id128,        offsetof(EFIHibernateLocation, uuid),           JSON_MANDATORY             },
                { "offset",                JSON_VARIANT_UNSIGNED, json_dispatch_uint64,       offsetof(EFIHibernateLocation, offset),         JSON_MANDATORY             },
                { "kernelVersion",         JSON_VARIANT_STRING,   json_dispatch_const_string, offsetof(EFIHibernateLocation, kernel_version), JSON_PERMISSIVE|JSON_DEBUG },
                { "osReleaseId",           JSON_VARIANT_STRING,   json_dispatch_const_string, offsetof(EFIHibernateLocation, id),             JSON_PERMISSIVE|JSON_DEBUG },
                { "osReleaseImageId",      JSON_VARIANT_STRING,   json_dispatch_const_string, offsetof(EFIHibernateLocation, image_id),       JSON_PERMISSIVE|JSON_DEBUG },
                { "osReleaseVersionId",    JSON_VARIANT_STRING,   json_dispatch_const_string, offsetof(EFIHibernateLocation, version_id),     JSON_PERMISSIVE|JSON_DEBUG },
                { "osReleaseImageVersion", JSON_VARIANT_STRING,   json_dispatch_const_string, offsetof(EFIHibernateLocation, image_version),  JSON_PERMISSIVE|JSON_DEBUG },
                {},
        };

        _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
        _cleanup_free_ char *location_str = NULL, *device = NULL, *id = NULL, *image_id = NULL,
                       *version_id = NULL, *image_version = NULL;
        struct utsname uts = {};
        EFIHibernateLocation location = {};

        r = efi_get_variable_string(EFI_SYSTEMD_VARIABLE(HibernateLocation), &location_str);
        if (r == -ENOENT) {
                log_debug_errno(r, "EFI variable HibernateLocation is not set, skipping.");
                return 0;
        }
        if (r < 0)
                return log_error_errno(r, "Failed to get EFI variable HibernateLocation: %m");

        r = json_parse(location_str, 0, &v, NULL, NULL);
        if (r < 0)
                return log_error_errno(r, "Failed to parse HibernateLocation JSON object: %m");

        r = json_dispatch(v, dispatch_table, NULL, JSON_LOG, &location);
        if (r < 0)
                return r;

        if (uname(&uts) < 0)
                log_warning_errno(errno, "Failed to get kernel info, ignoring: %m");

        r = parse_os_release(NULL,
                             "ID", &id,
                             "IMAGE_ID", &image_id,
                             "VERSION_ID", &version_id,
                             "IMAGE_VERSION", &image_version);
        if (r < 0)
                log_warning_errno(r, "Failed to parse os-release, ignoring: %m");

        if (!streq(uts.release, strempty(location.kernel_version)) ||
            !streq_ptr(id, location.id) ||
            !streq_ptr(image_id, location.image_id) ||
            !streq_ptr(version_id, location.version_id) ||
            !streq_ptr(image_version, location.image_version)) {

                log_notice("HibernateLocation system info doesn't match with current running system, not resuming from it.");
                return 0;
        }

        if (asprintf(&device, "/dev/disk/by-uuid/" SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(location.uuid)) < 0)
                return log_oom();

        if (!arg_resume_device) {
                arg_resume_device = TAKE_PTR(device);
                arg_resume_offset = location.offset;
        } else {
                if (!path_equal(arg_resume_device, device)) {
                        r = devnode_same(arg_resume_device, device);
                        if (r < 0)
                                log_debug_errno(r,
                                                "Failed to check if resume=%s is the same device as HibernateLocation device '%s', ignoring: %m",
                                                arg_resume_device, device);
                        if (r == 0)
                                log_warning("resume=%s doesn't match with HibernateLocation device '%s', proceeding anyway with resume=.",
                                            arg_resume_device, device);
                }

                if (arg_resume_offset != location.offset)
                        log_warning("resume_offset=%" PRIu64 " doesn't match with HibernateLocation offset %" PRIu64 ", proceeding anyway with resume_offset=.",
                                    arg_resume_offset, location.offset);
        }

        r = efi_set_variable(EFI_SYSTEMD_VARIABLE(HibernateLocation), NULL, 0);
        if (r < 0)
                log_warning_errno(r, "Failed to clear EFI variable HibernateLocation, ignoring: %m");
#endif

        return r;
}

static int process_resume(void) {
        _cleanup_free_ char *device_unit = NULL, *device_escaped = NULL;
        _cleanup_fclose_ FILE *f = NULL;
        int r;

        if (!arg_resume_device)
                return 0;

        r = unit_name_from_path(arg_resume_device, ".device", &device_unit);
        if (r < 0)
                return log_error_errno(r, "Failed to generate device unit name from path '%s': %m", arg_resume_device);

        r = write_drop_in(arg_dest, device_unit, 40, "device-timeout",
                          "# Automatically generated by systemd-hibernate-resume-generator\n\n"
                          "[Unit]\n"
                          "JobTimeoutSec=infinity\n");
        if (r < 0)
                log_warning_errno(r, "Failed to write device timeout drop-in, ignoring: %m");

        device_escaped = cescape(arg_resume_device);
        if (!device_escaped)
                return log_oom();

        r = generator_open_unit_file(arg_dest, NULL, SPECIAL_HIBERNATE_RESUME_SERVICE, &f);
        if (r < 0)
                return r;

        fprintf(f,
                "[Unit]\n"
                "Description=Resume from hibernation\n"
                "Documentation=man:systemd-hibernate-resume.service(8)\n"
                "DefaultDependencies=no\n"
                "BindsTo=%1$s\n"
                "Wants=local-fs-pre.target\n"
                "After=%1$s\n"
                "Before=local-fs-pre.target\n"
                "AssertPathExists=/etc/initrd-release\n"
                "\n"
                "[Service]\n"
                "Type=oneshot\n"
                "ExecStart=" ROOTLIBEXECDIR "/systemd-hibernate-resume %2$s %3$" PRIu64 "\n",
                device_unit,
                device_escaped,
                arg_resume_offset);

        r = fflush_and_check(f);
        if (r < 0)
                return log_error_errno(r, "Failed to create " SPECIAL_HIBERNATE_RESUME_SERVICE ": %m");

        r = generator_add_symlink(arg_dest, SPECIAL_SYSINIT_TARGET, "wants", SPECIAL_HIBERNATE_RESUME_SERVICE);
        if (r < 0)
                return r;

        r = generator_write_timeouts(arg_dest,
                                     arg_resume_device,
                                     arg_resume_device,
                                     arg_resume_options ?: arg_root_options,
                                     NULL);
        if (r < 0)
                return r;

        return 0;
}

static int run(const char *dest, const char *dest_early, const char *dest_late) {
        int r;

        arg_dest = ASSERT_PTR(dest);

        /* Don't even consider resuming outside of initrd. */
        if (!in_initrd()) {
                log_debug("Not running in an initrd, exiting.");
                return 0;
        }

        r = proc_cmdline_parse(parse_proc_cmdline_item, NULL, 0);
        if (r < 0)
                log_warning_errno(r, "Failed to parse kernel command line, ignoring: %m");

        if (arg_noresume) {
                log_info("Found \"noresume\" on the kernel command line, exiting.");
                return 0;
        }

        if (!arg_resume_device && arg_resume_offset_set)
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                       "Found resume_offset=%" PRIu64 " but resume= is unset, refusing.",
                                       arg_resume_offset);

        r = parse_efi_hibernate_location();
        if (r == -ENOMEM)
                return r;

        return process_resume();
}

DEFINE_MAIN_GENERATOR_FUNCTION(run);
