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

#include "efi-efivars.h"
#include "efi-string.h"
#include "ticks.h"
#include "util.h"

EFI_STATUS efivar_set_raw(const EFI_GUID *vendor, const char16_t *name, const void *buf, size_t size, uint32_t flags) {
        assert(vendor);
        assert(name);
        assert(buf || size == 0);

        flags |= EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS;
        return RT->SetVariable((char16_t *) name, (EFI_GUID *) vendor, flags, size, (void *) buf);
}

EFI_STATUS efivar_set_str16(const EFI_GUID *vendor, const char16_t *name, const char16_t *value, uint32_t flags) {
        assert(vendor);
        assert(name);

        return efivar_set_raw(vendor, name, value, value ? strsize16(value) : 0, flags);
}

EFI_STATUS efivar_set_uint64_str16(const EFI_GUID *vendor, const char16_t *name, uint64_t i, uint32_t flags) {
        assert(vendor);
        assert(name);

        _cleanup_free_ char16_t *str = xasprintf("%" PRIu64, i);
        return efivar_set_str16(vendor, name, str, flags);
}

EFI_STATUS efivar_set_uint32_le(const EFI_GUID *vendor, const char16_t *name, uint32_t value, uint32_t flags) {
        uint8_t buf[4];

        assert(vendor);
        assert(name);

        buf[0] = (uint8_t)(value >> 0U & 0xFF);
        buf[1] = (uint8_t)(value >> 8U & 0xFF);
        buf[2] = (uint8_t)(value >> 16U & 0xFF);
        buf[3] = (uint8_t)(value >> 24U & 0xFF);

        return efivar_set_raw(vendor, name, buf, sizeof(buf), flags);
}

EFI_STATUS efivar_set_uint64_le(const EFI_GUID *vendor, const char16_t *name, uint64_t value, uint32_t flags) {
        uint8_t buf[8];

        assert(vendor);
        assert(name);

        buf[0] = (uint8_t)(value >> 0U & 0xFF);
        buf[1] = (uint8_t)(value >> 8U & 0xFF);
        buf[2] = (uint8_t)(value >> 16U & 0xFF);
        buf[3] = (uint8_t)(value >> 24U & 0xFF);
        buf[4] = (uint8_t)(value >> 32U & 0xFF);
        buf[5] = (uint8_t)(value >> 40U & 0xFF);
        buf[6] = (uint8_t)(value >> 48U & 0xFF);
        buf[7] = (uint8_t)(value >> 56U & 0xFF);

        return efivar_set_raw(vendor, name, buf, sizeof(buf), flags);
}

EFI_STATUS efivar_unset(const EFI_GUID *vendor, const char16_t *name, uint32_t flags) {
        EFI_STATUS err;

        assert(vendor);
        assert(name);

        /* We could be wiping a non-volatile variable here and the spec makes no guarantees that won't incur
         * in an extra write (and thus wear out). So check and clear only if needed. */
        err = efivar_get_raw(vendor, name, NULL, NULL);
        if (err == EFI_SUCCESS)
                return efivar_set_raw(vendor, name, NULL, 0, flags);

        return err;
}

EFI_STATUS efivar_get_str16(const EFI_GUID *vendor, const char16_t *name, char16_t **ret) {
        _cleanup_free_ char16_t *buf = NULL;
        EFI_STATUS err;
        char16_t *val;
        size_t size;

        assert(vendor);
        assert(name);

        err = efivar_get_raw(vendor, name, (void**) &buf, &size);
        if (err != EFI_SUCCESS)
                return err;

        /* Make sure there are no incomplete characters in the buffer */
        if ((size % sizeof(char16_t)) != 0)
                return EFI_INVALID_PARAMETER;

        if (!ret)
                return EFI_SUCCESS;

        /* Return buffer directly if it happens to be NUL terminated already */
        if (size >= sizeof(char16_t) && buf[size / sizeof(char16_t) - 1] == 0) {
                *ret = TAKE_PTR(buf);
                return EFI_SUCCESS;
        }

        /* Make sure a terminating NUL is available at the end */
        val = xmalloc(size + sizeof(char16_t));

        memcpy(val, buf, size);
        val[size / sizeof(char16_t) - 1] = 0; /* NUL terminate */

        *ret = val;
        return EFI_SUCCESS;
}

EFI_STATUS efivar_get_uint64_str16(const EFI_GUID *vendor, const char16_t *name, uint64_t *ret) {
        EFI_STATUS err;

        assert(vendor);
        assert(name);

        _cleanup_free_ char16_t *val = NULL;
        err = efivar_get_str16(vendor, name, &val);
        if (err != EFI_SUCCESS)
                return err;

        uint64_t u;
        if (!parse_number16(val, &u, NULL))
                return EFI_INVALID_PARAMETER;

        if (ret)
                *ret = u;
        return EFI_SUCCESS;
}

EFI_STATUS efivar_get_uint32_le(const EFI_GUID *vendor, const char16_t *name, uint32_t *ret) {
        _cleanup_free_ uint8_t *buf = NULL;
        size_t size;
        EFI_STATUS err;

        assert(vendor);
        assert(name);

        err = efivar_get_raw(vendor, name, (void**) &buf, &size);
        if (err != EFI_SUCCESS)
                return err;

        if (size != sizeof(uint32_t))
                return EFI_BUFFER_TOO_SMALL;

        if (ret)
                *ret = (uint32_t) buf[0] << 0U | (uint32_t) buf[1] << 8U | (uint32_t) buf[2] << 16U |
                        (uint32_t) buf[3] << 24U;

        return EFI_SUCCESS;
}

EFI_STATUS efivar_get_uint64_le(const EFI_GUID *vendor, const char16_t *name, uint64_t *ret) {
        _cleanup_free_ uint8_t *buf = NULL;
        size_t size;
        EFI_STATUS err;

        assert(vendor);
        assert(name);

        err = efivar_get_raw(vendor, name, (void**) &buf, &size);
        if (err != EFI_SUCCESS)
                return err;

        if (size != sizeof(uint64_t))
                return EFI_BUFFER_TOO_SMALL;

        if (ret)
                *ret = (uint64_t) buf[0] << 0U | (uint64_t) buf[1] << 8U | (uint64_t) buf[2] << 16U |
                        (uint64_t) buf[3] << 24U | (uint64_t) buf[4] << 32U | (uint64_t) buf[5] << 40U |
                        (uint64_t) buf[6] << 48U | (uint64_t) buf[7] << 56U;

        return EFI_SUCCESS;
}

EFI_STATUS efivar_get_raw(const EFI_GUID *vendor, const char16_t *name, void **ret_data, size_t *ret_size) {
        EFI_STATUS err;

        assert(vendor);
        assert(name);

        size_t size = 0;
        err = RT->GetVariable((char16_t *) name, (EFI_GUID *) vendor, NULL, &size, NULL);
        if (err != EFI_BUFFER_TOO_SMALL)
                return err;

        _cleanup_free_ void *buf = xmalloc(size);
        err = RT->GetVariable((char16_t *) name, (EFI_GUID *) vendor, NULL, &size, buf);
        if (err != EFI_SUCCESS)
                return err;

        if (ret_data)
                *ret_data = TAKE_PTR(buf);
        if (ret_size)
                *ret_size = size;

        return EFI_SUCCESS;
}

EFI_STATUS efivar_get_boolean_u8(const EFI_GUID *vendor, const char16_t *name, bool *ret) {
        _cleanup_free_ uint8_t *b = NULL;
        size_t size;
        EFI_STATUS err;

        assert(vendor);
        assert(name);

        err = efivar_get_raw(vendor, name, (void**) &b, &size);
        if (err != EFI_SUCCESS)
                return err;

        if (ret)
                *ret = *b > 0;

        return EFI_SUCCESS;
}

void efivar_set_time_usec(const EFI_GUID *vendor, const char16_t *name, uint64_t usec) {
        assert(vendor);
        assert(name);

        if (usec == 0)
                usec = time_usec();
        if (usec == 0)
                return;

        _cleanup_free_ char16_t *str = xasprintf("%" PRIu64, usec);
        efivar_set_str16(vendor, name, str, 0);
}

uint64_t get_os_indications_supported(void) {
        uint64_t osind;
        EFI_STATUS err;

        /* Returns the supported OS indications. If we can't acquire it, returns a zeroed out mask, i.e. no
         * supported features. */

        err = efivar_get_uint64_le(MAKE_GUID_PTR(EFI_GLOBAL_VARIABLE), u"OsIndicationsSupported", &osind);
        if (err != EFI_SUCCESS)
                return 0;

        return osind;
}
