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

#include "chid.h"
#include "devicetree.h"
#include "pe.h"
#include "util.h"

#define DOS_FILE_MAGIC "MZ"
#define PE_FILE_MAGIC  "PE\0\0"
#define MAX_SECTIONS 96

#if defined(__i386__)
#  define TARGET_MACHINE_TYPE 0x014CU
#  define TARGET_MACHINE_TYPE_COMPATIBILITY 0x8664U
#elif defined(__x86_64__)
#  define TARGET_MACHINE_TYPE 0x8664U
#elif defined(__aarch64__)
#  define TARGET_MACHINE_TYPE 0xAA64U
#elif defined(__arm__)
#  define TARGET_MACHINE_TYPE 0x01C2U
#elif defined(__riscv) && __riscv_xlen == 32
#  define TARGET_MACHINE_TYPE 0x5032U
#elif defined(__riscv) && __riscv_xlen == 64
#  define TARGET_MACHINE_TYPE 0x5064U
#elif defined(__loongarch__) && __loongarch_grlen == 32
#  define TARGET_MACHINE_TYPE 0x6232U
#elif defined(__loongarch__) && __loongarch_grlen == 64
#  define TARGET_MACHINE_TYPE 0x6264U
#else
#  error Unknown EFI arch
#endif

#ifndef TARGET_MACHINE_TYPE_COMPATIBILITY
#  define TARGET_MACHINE_TYPE_COMPATIBILITY 0
#endif

typedef struct DosFileHeader {
        uint8_t  Magic[2];
        uint16_t LastSize;
        uint16_t nBlocks;
        uint16_t nReloc;
        uint16_t HdrSize;
        uint16_t MinAlloc;
        uint16_t MaxAlloc;
        uint16_t ss;
        uint16_t sp;
        uint16_t Checksum;
        uint16_t ip;
        uint16_t cs;
        uint16_t RelocPos;
        uint16_t nOverlay;
        uint16_t reserved[4];
        uint16_t OEMId;
        uint16_t OEMInfo;
        uint16_t reserved2[10];
        uint32_t ExeHeader;
} _packed_ DosFileHeader;

typedef struct CoffFileHeader {
        uint16_t Machine;
        uint16_t NumberOfSections;
        uint32_t TimeDateStamp;
        uint32_t PointerToSymbolTable;
        uint32_t NumberOfSymbols;
        uint16_t SizeOfOptionalHeader;
        uint16_t Characteristics;
} _packed_ CoffFileHeader;

#define OPTHDR32_MAGIC 0x10B /* PE32  OptionalHeader */
#define OPTHDR64_MAGIC 0x20B /* PE32+ OptionalHeader */

typedef struct PeOptionalHeader {
        uint16_t Magic;
        uint8_t  LinkerMajor;
        uint8_t  LinkerMinor;
        uint32_t SizeOfCode;
        uint32_t SizeOfInitializedData;
        uint32_t SizeOfUninitializeData;
        uint32_t AddressOfEntryPoint;
        uint32_t BaseOfCode;
        union {
                struct { /* PE32 */
                        uint32_t BaseOfData;
                        uint32_t ImageBase32;
                };
                uint64_t ImageBase64; /* PE32+ */
        };
        uint32_t SectionAlignment;
        uint32_t FileAlignment;
        uint16_t MajorOperatingSystemVersion;
        uint16_t MinorOperatingSystemVersion;
        uint16_t MajorImageVersion;
        uint16_t MinorImageVersion;
        uint16_t MajorSubsystemVersion;
        uint16_t MinorSubsystemVersion;
        uint32_t Win32VersionValue;
        uint32_t SizeOfImage;
        uint32_t SizeOfHeaders;
        uint32_t CheckSum;
        uint16_t Subsystem;
        uint16_t DllCharacteristics;
        /* fields with different sizes for 32/64 omitted */
} _packed_ PeOptionalHeader;

typedef struct PeFileHeader {
        uint8_t  Magic[4];
        CoffFileHeader FileHeader;
        PeOptionalHeader OptionalHeader;
} _packed_ PeFileHeader;

#define SECTION_TABLE_BYTES_MAX (16U * 1024U * 1024U)

static bool verify_dos(const DosFileHeader *dos) {
        assert(dos);

        DISABLE_WARNING_TYPE_LIMITS;
        return memcmp(dos->Magic, DOS_FILE_MAGIC, STRLEN(DOS_FILE_MAGIC)) == 0 &&
                dos->ExeHeader >= sizeof(DosFileHeader) &&
                (size_t) dos->ExeHeader <= SIZE_MAX - sizeof(PeFileHeader);
        REENABLE_WARNING;
}

static bool verify_pe(
                const DosFileHeader *dos,
                const PeFileHeader *pe,
                bool allow_compatibility) {

        assert(dos);
        assert(pe);

        return memcmp(pe->Magic, PE_FILE_MAGIC, STRLEN(PE_FILE_MAGIC)) == 0 &&
                (pe->FileHeader.Machine == TARGET_MACHINE_TYPE ||
                 (allow_compatibility && pe->FileHeader.Machine == TARGET_MACHINE_TYPE_COMPATIBILITY)) &&
                pe->FileHeader.NumberOfSections > 0 &&
                pe->FileHeader.NumberOfSections <= MAX_SECTIONS &&
                IN_SET(pe->OptionalHeader.Magic, OPTHDR32_MAGIC, OPTHDR64_MAGIC) &&
                pe->FileHeader.SizeOfOptionalHeader < SIZE_MAX - (dos->ExeHeader + offsetof(PeFileHeader, OptionalHeader));
}

static size_t section_table_offset(const DosFileHeader *dos, const PeFileHeader *pe) {
        assert(dos);
        assert(pe);

        return dos->ExeHeader + offsetof(PeFileHeader, OptionalHeader) + pe->FileHeader.SizeOfOptionalHeader;
}

static bool pe_section_name_equal(const char *a, const char *b) {

        if (a == b)
                return true;
        if (!a != !b)
                return false;

        /* Compares up to 8 characters of a and b i.e. the name size limit in the PE section header */

        for (size_t i = 0; i < sizeof_field(PeSectionHeader, Name); i++) {
                if (a[i] != b[i])
                        return false;

                if (a[i] == 0) /* Name is shorter than 8 */
                        return true;
        }

        return true;
}

static bool pe_use_this_dtb(
                const void *dtb,
                size_t dtb_size,
                const void *base,
                const Device *device,
                size_t section_nb) {

        assert(dtb);

        EFI_STATUS err;

        err = devicetree_match(dtb, dtb_size);
        if (err == EFI_SUCCESS)
                return true;
        if (err != EFI_UNSUPPORTED)
                return false;

        /* There's nothing to match against if firmware does not provide DTB and there is no .hwids section */
        if (!device || !base)
                return false;

        const char *compatible = device_get_compatible(base, device);
        if (!compatible)
                return false;

        err = devicetree_match_by_compatible(dtb, dtb_size, compatible);
        if (err == EFI_SUCCESS)
                return true;
        if (err == EFI_INVALID_PARAMETER)
                log_error_status(err, "Found bad DT blob in PE section %zu", section_nb);
        return false;
}

static void pe_locate_sections_internal(
                const PeSectionHeader section_table[],
                size_t n_section_table,
                const char *const section_names[],
                size_t validate_base,
                const void *device_table,
                const Device *device,
                PeSectionVector sections[]) {

        assert(section_table || n_section_table == 0);
        assert(section_names);
        assert(sections);

        /* Searches for the sections listed in 'sections[]' within the section table. Validates the resulted
         * data. If 'validate_base' is non-zero also takes base offset when loaded into memory into account for
         * checking for overflows. */

        for (size_t i = 0; section_names[i]; i++)
                FOREACH_ARRAY(j, section_table, n_section_table) {

                        if (!pe_section_name_equal((const char*) j->Name, section_names[i]))
                                continue;

                        /* Overflow check: ignore sections that are impossibly large, relative to the file
                         * address for the section. */
                        size_t size_max = SIZE_MAX - j->PointerToRawData;
                        if ((size_t) j->SizeOfRawData > size_max)
                                continue;

                        /* Overflow check: ignore sections that are impossibly large, given the virtual
                         * address for the section */
                        size_max = SIZE_MAX - j->VirtualAddress;
                        if ((size_t) j->VirtualSize > size_max)
                                continue;

                        /* 2nd overflow check: ignore sections that are impossibly large also taking the
                         * loaded base into account. */
                        if (validate_base != 0) {
                                if (validate_base > size_max)
                                        continue;
                                size_max -= validate_base;

                                if (j->VirtualAddress > size_max)
                                        continue;
                        }

                        /* Special handling for .dtbauto sections compared to plain .dtb */
                        if (pe_section_name_equal(section_names[i], ".dtbauto")) {
                                /* .dtbauto sections require validate_base for matching */
                                if (!validate_base)
                                        break;
                                if (!pe_use_this_dtb(
                                                  (const uint8_t *) SIZE_TO_PTR(validate_base) + j->VirtualAddress,
                                                  j->VirtualSize,
                                                  device_table,
                                                  device,
                                                  (PTR_TO_SIZE(j) - PTR_TO_SIZE(section_table)) / sizeof(*j)))
                                        continue;
                        }

                        /* At this time, the sizes and offsets have been validated. Store them away */
                        sections[i] = (PeSectionVector) {
                                .memory_size = j->VirtualSize,
                                .memory_offset = j->VirtualAddress,
                                /* VirtualSize can be bigger than SizeOfRawData when the section requires
                                 * uninitialized data. It can also be smaller than SizeOfRawData when there's
                                 * no need for uninitialized data as SizeOfRawData is aligned to
                                 * FileAlignment and VirtualSize isn't. The actual data that's read from disk
                                 * is the minimum of these two fields. */
                                .file_size = MIN(j->SizeOfRawData, j->VirtualSize),
                                .file_offset = j->PointerToRawData,
                        };

                        /* First matching section wins, ignore the rest */
                        break;
                }
}

static bool looking_for_dtbauto(const char *const section_names[]) {
        assert(section_names);

        for (size_t i = 0; section_names[i]; i++)
                if (pe_section_name_equal(section_names[i], ".dtbauto"))
                        return true;
         return false;
}

static void pe_locate_sections(
                const PeSectionHeader section_table[],
                size_t n_section_table,
                const char *const section_names[],
                size_t validate_base,
                PeSectionVector sections[]) {

        if (!looking_for_dtbauto(section_names))
                return pe_locate_sections_internal(
                                  section_table,
                                  n_section_table,
                                  section_names,
                                  validate_base,
                                  /* device_base */ NULL,
                                  /* device */ NULL,
                                  sections);

        /* It doesn't make sense not to provide validate_base here */
        assert(validate_base != 0);

        const void *hwids = NULL;
        const Device *device = NULL;

        if (!firmware_devicetree_exists()) {
                /* Find HWIDs table and search for the current device */
                PeSectionVector hwids_section = {};

                pe_locate_sections_internal(
                                section_table,
                                n_section_table,
                                (const char *const[]) { ".hwids", NULL },
                                validate_base,
                                /* device_table */ NULL,
                                /* device */ NULL,
                                &hwids_section);

                if (PE_SECTION_VECTOR_IS_SET(&hwids_section)) {
                        hwids = (const uint8_t *) SIZE_TO_PTR(validate_base) + hwids_section.memory_offset;

                        EFI_STATUS err = chid_match(hwids, hwids_section.memory_size, &device);
                        if (err != EFI_SUCCESS) {
                                log_error_status(err, "HWID matching failed, no DT blob will be selected: %m");
                                hwids = NULL;
                        }
                }
        }

        return pe_locate_sections_internal(
                            section_table,
                            n_section_table,
                            section_names,
                            validate_base,
                            hwids,
                            device,
                            sections);
}

static uint32_t get_compatibility_entry_address(const DosFileHeader *dos, const PeFileHeader *pe) {
        /* The kernel may provide alternative PE entry points for different PE architectures. This allows
         * booting a 64-bit kernel on 32-bit EFI that is otherwise running on a 64-bit CPU. The locations of any
         * such compat entry points are located in a special PE section. */

        assert(dos);
        assert(pe);

        static const char *const section_names[] = { ".compat", NULL };
        PeSectionVector vector = {};
        pe_locate_sections(
                        (const PeSectionHeader *) ((const uint8_t *) dos + section_table_offset(dos, pe)),
                        pe->FileHeader.NumberOfSections,
                        section_names,
                        PTR_TO_SIZE(dos),
                        &vector);

        if (!PE_SECTION_VECTOR_IS_SET(&vector)) /* not found */
                return 0;

        typedef struct {
                uint8_t type;
                uint8_t size;
                uint16_t machine_type;
                uint32_t entry_point;
        } _packed_ LinuxPeCompat1;

        size_t addr = vector.memory_offset, size = vector.memory_size;

        while (size >= sizeof(LinuxPeCompat1) && addr % alignof(LinuxPeCompat1) == 0) {
                const LinuxPeCompat1 *compat = (const LinuxPeCompat1 *) ((const uint8_t *) dos + addr);

                if (compat->type == 0 || compat->size == 0 || compat->size > size)
                        break;

                if (compat->type == 1 &&
                    compat->size >= sizeof(LinuxPeCompat1) &&
                    compat->machine_type == TARGET_MACHINE_TYPE)
                        return compat->entry_point;

                addr += compat->size;
                size -= compat->size;
        }

        return 0;
}

EFI_STATUS pe_kernel_info(const void *base, uint32_t *ret_compat_address, size_t *ret_size_in_memory) {
        assert(base);
        assert(ret_compat_address);

        const DosFileHeader *dos = (const DosFileHeader *) base;
        if (!verify_dos(dos))
                return EFI_LOAD_ERROR;

        const PeFileHeader *pe = (const PeFileHeader *) ((const uint8_t *) base + dos->ExeHeader);
        if (!verify_pe(dos, pe, /* allow_compatibility= */ true))
                return EFI_LOAD_ERROR;

        /* When allocating we need to also consider the virtual/uninitialized data sections, so parse it out
         * of the SizeOfImage field in the PE header and return it */
        if (ret_size_in_memory)
                *ret_size_in_memory = pe->OptionalHeader.SizeOfImage;

        /* Support for LINUX_INITRD_MEDIA_GUID was added in kernel stub 1.0. */
        if (pe->OptionalHeader.MajorImageVersion < 1)
                return EFI_UNSUPPORTED;

        if (pe->FileHeader.Machine == TARGET_MACHINE_TYPE) {
                *ret_compat_address = 0;
                return EFI_SUCCESS;
        }

        uint32_t compat_address = get_compatibility_entry_address(dos, pe);
        if (compat_address == 0)
                /* Image type not supported and no compat entry found. */
                return EFI_UNSUPPORTED;

        *ret_compat_address = compat_address;
        return EFI_SUCCESS;
}

EFI_STATUS pe_section_table_from_base(
                const void *base,
                const PeSectionHeader **ret_section_table,
                size_t *ret_n_section_table) {

        assert(base);
        assert(ret_section_table);
        assert(ret_n_section_table);

        const DosFileHeader *dos = (const DosFileHeader*) base;
        if (!verify_dos(dos))
                return EFI_LOAD_ERROR;

        const PeFileHeader *pe = (const PeFileHeader*) ((const uint8_t*) base + dos->ExeHeader);
        if (!verify_pe(dos, pe, /* allow_compatibility= */ false))
                return EFI_LOAD_ERROR;

        *ret_section_table = (const PeSectionHeader*) ((const uint8_t*) base + section_table_offset(dos, pe));
        *ret_n_section_table = pe->FileHeader.NumberOfSections;

        return EFI_SUCCESS;
}

EFI_STATUS pe_memory_locate_sections(
                const void *base,
                const char *const section_names[],
                PeSectionVector sections[]) {

        EFI_STATUS err;

        assert(base);
        assert(section_names);
        assert(sections);

        const PeSectionHeader *section_table;
        size_t n_section_table;
        err = pe_section_table_from_base(base, &section_table, &n_section_table);
        if (err != EFI_SUCCESS)
                return err;

        pe_locate_sections(
                        section_table,
                        n_section_table,
                        section_names,
                        PTR_TO_SIZE(base),
                        sections);

        return EFI_SUCCESS;
}

EFI_STATUS pe_section_table_from_file(
                EFI_FILE *handle,
                PeSectionHeader **ret_section_table,
                size_t *ret_n_section_table) {

        EFI_STATUS err;
        size_t len;

        assert(handle);
        assert(ret_section_table);
        assert(ret_n_section_table);

        DosFileHeader dos;
        len = sizeof(dos);
        err = handle->Read(handle, &len, &dos);
        if (err != EFI_SUCCESS)
                return err;
        if (len != sizeof(dos) || !verify_dos(&dos))
                return EFI_LOAD_ERROR;

        err = handle->SetPosition(handle, dos.ExeHeader);
        if (err != EFI_SUCCESS)
                return err;

        PeFileHeader pe;
        len = sizeof(pe);
        err = handle->Read(handle, &len, &pe);
        if (err != EFI_SUCCESS)
                return err;
        if (len != sizeof(pe) || !verify_pe(&dos, &pe, /* allow_compatibility= */ false))
                return EFI_LOAD_ERROR;

        DISABLE_WARNING_TYPE_LIMITS;
        if ((size_t) pe.FileHeader.NumberOfSections > SIZE_MAX / sizeof(PeSectionHeader))
                return EFI_OUT_OF_RESOURCES;
        REENABLE_WARNING;
        size_t n_section_table = (size_t) pe.FileHeader.NumberOfSections;
        if (n_section_table * sizeof(PeSectionHeader) > SECTION_TABLE_BYTES_MAX)
                return EFI_OUT_OF_RESOURCES;

        _cleanup_free_ PeSectionHeader *section_table = xnew(PeSectionHeader, n_section_table);
        if (!section_table)
                return EFI_OUT_OF_RESOURCES;

        err = handle->SetPosition(handle, section_table_offset(&dos, &pe));
        if (err != EFI_SUCCESS)
                return err;

        len = n_section_table * sizeof(PeSectionHeader);
        err = handle->Read(handle, &len, section_table);
        if (err != EFI_SUCCESS)
                return err;
        if (len != n_section_table * sizeof(PeSectionHeader))
                return EFI_LOAD_ERROR;

        *ret_section_table = TAKE_PTR(section_table);
        *ret_n_section_table = n_section_table;
        return EFI_SUCCESS;
}

static const PeSectionHeader* pe_section_table_find_profile_start(
                const PeSectionHeader *section_table,
                size_t n_section_table,
                unsigned profile) {

        assert(section_table || n_section_table == 0);

        if (profile == UINT_MAX) /* base profile? that starts at the beginning */
                return section_table;

        unsigned current_profile = UINT_MAX;
        FOREACH_ARRAY(p, section_table, n_section_table) {

                if (!pe_section_name_equal((const char*) p->Name, ".profile"))
                        continue;

                if (current_profile == UINT_MAX)
                        current_profile = 0;
                else
                        current_profile++;

                if (current_profile == profile) /* Found our profile! */
                        return p;
        }

        /* We reached the end of the table? Then this section does not exist */
        return NULL;
}

static size_t pe_section_table_find_profile_length(
                const PeSectionHeader *section_table,
                size_t n_section_table,
                const PeSectionHeader *start,
                unsigned profile) {

        assert(section_table);
        assert(n_section_table > 0);
        assert(start >= section_table);
        assert(start < section_table + n_section_table);

        /* Look for the next .profile (or the end of the table), this is where the sections for this
         * profile end. The base profile does not start with a .profile, the others do, hence conditionally
         * skip over the first entry. */
        const PeSectionHeader *e;
        if (profile == UINT_MAX) /* Base profile */
                e = start;
        else {
                assert(pe_section_name_equal((const char *) start->Name, ".profile"));
                e = start + 1;
        }

        for (; e < section_table + n_section_table; e++)
                if (pe_section_name_equal((const char*) e->Name, ".profile"))
                        return e - start;

        return (section_table + n_section_table) - start;
}

EFI_STATUS pe_locate_profile_sections(
                const PeSectionHeader section_table[],
                size_t n_section_table,
                const char* const section_names[],
                unsigned profile,
                size_t validate_base,
                PeSectionVector sections[]) {

        assert(section_table || n_section_table == 0);
        assert(section_names);
        assert(sections);

        /* Now scan through the section table until we skipped over the right number of .profile sections */
        const PeSectionHeader *p = pe_section_table_find_profile_start(section_table, n_section_table, profile);
        if (!p)
                return EFI_NOT_FOUND;

        /* Look for the next .profile (or the end of the table), this is where the sections for this
         * profile end. */
        size_t n = pe_section_table_find_profile_length(section_table, n_section_table, p, profile);

        /* And now parse everything between the start and end of our profile */
        pe_locate_sections(
                        p,
                        n,
                        section_names,
                        validate_base,
                        sections);

        return EFI_SUCCESS;
}
