/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once

#include "efi.h"
#include "memory-util.h"

#if SD_BOOT

#include "proto/file-io.h"

/* This is provided by the linker. */
extern uint8_t __executable_start[];

DISABLE_WARNING_REDUNDANT_DECLS;
void free(void *p); /* NOLINT (readability-redundant-declaration,readability-inconsistent-declaration-parameter-name) */
REENABLE_WARNING;

static inline void freep(void *p) {
        free(*(void **) p);
}

#define _cleanup_free_ _cleanup_(freep)

_malloc_ _alloc_(1) _returns_nonnull_ _warn_unused_result_
void *xmalloc(size_t size);

_malloc_ _alloc_(1) _returns_nonnull_ _warn_unused_result_
static inline void *xcalloc(size_t size) {
        void *t = xmalloc(size);
        memzero(t, size);
        return t;
}

_malloc_ _alloc_(1, 2) _returns_nonnull_ _warn_unused_result_
static inline void *xcalloc_multiply(size_t n, size_t size) {
        assert_se(MUL_ASSIGN_SAFE(&size, n));
        return xcalloc(size);
}

_malloc_ _alloc_(1, 2) _returns_nonnull_ _warn_unused_result_
static inline void *xmalloc_multiply(size_t n, size_t size) {
        assert_se(MUL_ASSIGN_SAFE(&size, n));
        return xmalloc(size);
}

/* Use malloc attribute as this never returns p like userspace realloc. */
_malloc_ _alloc_(3) _returns_nonnull_ _warn_unused_result_
static inline void *xrealloc(void *p, size_t old_size, size_t new_size) {
        assert(p || old_size == 0);

        void *t = xmalloc(new_size);

        size_t size = MIN(old_size, new_size);
        if (size > 0) {
                assert(p);
                memcpy(t, p, size);
        }

        free(p);
        return t;
}

_malloc_ _alloc_(2) _returns_nonnull_ _warn_unused_result_
static inline void* xmemdup(const void *p, size_t l) {
        return memcpy(xmalloc(l), p, l);
}

#define xnew(type, n) ((type *) xmalloc_multiply((n), sizeof(type)))
#define xnew0(type, n) ((type *) xcalloc_multiply((n), sizeof(type)))

bool free_and_xstrdup16(char16_t **p, const char16_t *s);

typedef struct {
        EFI_PHYSICAL_ADDRESS addr;
        size_t n_pages;
} Pages;

static inline void cleanup_pages(Pages *p) {
        if (p->n_pages == 0)
                return;
#ifdef EFI_DEBUG
        assert_se(BS->FreePages(p->addr, p->n_pages) == EFI_SUCCESS);
#else
        (void) BS->FreePages(p->addr, p->n_pages);
#endif
}

#define _cleanup_pages_ _cleanup_(cleanup_pages)

static inline Pages xmalloc_pages(
                EFI_ALLOCATE_TYPE type, EFI_MEMORY_TYPE memory_type, size_t n_pages, EFI_PHYSICAL_ADDRESS addr) {
        assert_se(BS->AllocatePages(type, memory_type, n_pages, &addr) == EFI_SUCCESS);
        return (Pages) {
                .addr = addr,
                .n_pages = n_pages,
        };
}

Pages xmalloc_aligned_pages(
                EFI_ALLOCATE_TYPE type,
                EFI_MEMORY_TYPE memory_type,
                size_t n_pages,
                size_t alignment,
                EFI_PHYSICAL_ADDRESS addr);

static inline Pages xmalloc_initrd_pages(size_t n_pages) {
        /* The original native x86 boot protocol of the Linux kernel was not 64bit safe, hence we try to
         * allocate memory for the initrds below the 4G boundary on x86, since we don't know early enough
         * which protocol we'll use to ultimately boot the kernel. This restriction is somewhat obsolete,
         * since these days we generally prefer the kernel's newer EFI entrypoint instead, which has no such
         * limitations. There's a good chance that for large allocations we won't be successful, hence
         * immediately fallback to an unrestricted allocation. On other architectures we do not bother with
         * any restriction on this, in particular as some of them don't even have RAM mapped to such low
         * addresses. */

#if defined(__i386__) || defined(__x86_64__)
        EFI_PHYSICAL_ADDRESS addr = UINT32_MAX; /* Below 4G boundary. */
        if (BS->AllocatePages(
                        AllocateMaxAddress,
                        EfiLoaderData,
                        EFI_SIZE_TO_PAGES(n_pages),
                        &addr) == EFI_SUCCESS)
                return (Pages) {
                        .addr = addr,
                        .n_pages = EFI_SIZE_TO_PAGES(n_pages),
                };
#endif
        return xmalloc_pages(
                        AllocateAnyPages,
                        EfiLoaderData,
                        EFI_SIZE_TO_PAGES(n_pages),
                        0 /* Ignored. */);
}

void convert_efi_path(char16_t *path);
char16_t *xstr8_to_path(const char *str8);
char16_t *mangle_stub_cmdline(char16_t *cmdline);

EFI_STATUS chunked_read(EFI_FILE *file, size_t *size, void *buf);
EFI_STATUS file_read(EFI_FILE *dir, const char16_t *name, uint64_t offset, size_t size, char **ret, size_t *ret_size);
EFI_STATUS load_file_from_simple_filesystem(const EFI_DEVICE_PATH *device_path, char **file_buffer, size_t *file_size);
EFI_STATUS file_handle_read(EFI_FILE *handle, uint64_t offset, size_t size, char **ret, size_t *ret_size);

static inline void file_closep(EFI_FILE **handle) {
        if (!*handle)
                return;

        (*handle)->Close(*handle);
}

#define _cleanup_file_close_ _cleanup_(file_closep)

static inline void unload_imagep(EFI_HANDLE *image) {
        if (*image)
                (void) BS->UnloadImage(*image);
}

/* Note that GUID is evaluated multiple times! */
#define GUID_FORMAT_STR "%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X"
#define GUID_FORMAT_VAL(g) (g).Data1, (g).Data2, (g).Data3, (g).Data4[0], (g).Data4[1], \
        (g).Data4[2], (g).Data4[3], (g).Data4[4], (g).Data4[5], (g).Data4[6], (g).Data4[7]

void set_attribute_safe(size_t attr);
void print_at(size_t x, size_t y, size_t attr, const char16_t *str);
void clear_screen(size_t attr);

typedef int (*compare_pointer_func_t)(const void *a, const void *b);
void sort_pointer_array(void **array, size_t n_members, compare_pointer_func_t compare);

EFI_STATUS get_file_info(EFI_FILE *handle, EFI_FILE_INFO **ret, size_t *ret_size);
EFI_STATUS get_volume_ro(EFI_FILE *handle, bool *ret);
EFI_STATUS readdir(EFI_FILE *handle, EFI_FILE_INFO **buffer, size_t *buffer_size);

bool is_ascii(const char16_t *f);

char16_t **strv_free(char16_t **l);

DEFINE_TRIVIAL_CLEANUP_FUNC(char16_t**, strv_free);

#define _cleanup_strv_free_ _cleanup_(strv_freep)

EFI_STATUS open_directory(EFI_FILE *root_dir, const char16_t *path, EFI_FILE **ret);

/* Conversion between EFI_PHYSICAL_ADDRESS and pointers is not obvious. The former is always 64-bit, even on
 * 32-bit archs. And gcc complains if we cast a pointer to an integer of a different size. Hence let's do the
 * conversion indirectly: first into uintptr_t and then extended to EFI_PHYSICAL_ADDRESS. */
static inline EFI_PHYSICAL_ADDRESS POINTER_TO_PHYSICAL_ADDRESS(const void *p) {
        return (EFI_PHYSICAL_ADDRESS) (uintptr_t) p;
}

static inline void *PHYSICAL_ADDRESS_TO_POINTER(EFI_PHYSICAL_ADDRESS addr) {
        /* On 32-bit systems the address might not be convertible (as pointers are 32-bit but
         * EFI_PHYSICAL_ADDRESS 64-bit) */
        assert(addr <= UINTPTR_MAX);
        return (void *) (uintptr_t) addr;
}

/* If EFI_DEBUG, print our name and version and also report the address of the image base so a debugger can
 * be attached. See debug-sd-boot.sh for how this can be done. */
void notify_debugger(const char *identity, bool wait);

/* On x86 the compiler assumes a different incoming stack alignment than what we get.
 * This will cause long long variables to be misaligned when building with
 * '-mlong-double' (for correct struct layouts). Normally, the compiler realigns the
 * stack itself on entry, but we have to do this ourselves here as the compiler does
 * not know that this is our entry point. */
#ifdef __i386__
#  define _realign_stack_ __attribute__((force_align_arg_pointer))
#else
#  define _realign_stack_
#endif

#define DEFINE_EFI_MAIN_FUNCTION(func, identity, wait_for_debugger)                    \
        EFI_SYSTEM_TABLE *ST;                                                          \
        EFI_BOOT_SERVICES *BS;                                                         \
        EFI_RUNTIME_SERVICES *RT;                                                      \
        _realign_stack_                                                                \
        EFIAPI EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table);  \
        EFIAPI EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table) { \
                ST = system_table;                                                     \
                BS = system_table->BootServices;                                       \
                RT = system_table->RuntimeServices;                                    \
                __stack_chk_guard_init();                                              \
                notify_debugger((identity), (wait_for_debugger));                      \
                EFI_STATUS err = func(image);                                          \
                log_wait();                                                            \
                return err;                                                            \
        }

#if defined(__i386__) || defined(__x86_64__)
void beep(unsigned beep_count);
#else
static inline void beep(unsigned beep_count) {}
#endif

EFI_STATUS open_volume(EFI_HANDLE device, EFI_FILE **ret_file);

static inline bool efi_guid_equal(const EFI_GUID *a, const EFI_GUID *b) {
        return memcmp(a, b, sizeof(EFI_GUID)) == 0;
}

void *find_configuration_table(const EFI_GUID *guid);

char16_t *get_extra_dir(const EFI_DEVICE_PATH *file_path);

#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
#  define be16toh(x) __builtin_bswap16(x)
#  define be32toh(x) __builtin_bswap32(x)
#  define le16toh(x) (x)
#  define le32toh(x) (x)
#else
#  error "Unexpected byte order in EFI mode?"
#endif

#define bswap_16(x) __builtin_bswap16(x)
#define bswap_32(x) __builtin_bswap32(x)

#else

#include "alloc-util.h"

#define xnew0(type, n) ASSERT_PTR(new0(type, n))

#endif

char16_t *url_replace_last_component(const char16_t *url, const char16_t *filename);

static inline bool EFI_STATUS_IS_WRITE_REFUSED(EFI_STATUS status) {
        return IN_SET(status, EFI_WRITE_PROTECTED, EFI_DEVICE_ERROR, EFI_ACCESS_DENIED);
}
