/*
 * Create a blob with a Multiboot2 image for the ARMv8 platform
 *
 * This tool reads menu.lst, loads a boot driver, a CPU kernel and modules,
 * and assemble them into a Multiboot2 image. Adds also relocation info for
 * the driver and the kernel.
 *
 * Copyright (c) 2016, ETH Zurich.
 * All rights reserved.
 *
 * This file is distributed under the terms in the attached LICENSE file.
 * If you do not find this file, copies can be found by writing to:
 * ETH Zurich D-INFK, Universitaetstrasse 6, CH-8092 Zurich. Attn: Systems Group.
 */

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

#include <sys/stat.h>
#include <sys/types.h>

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <libelf.h>
#include <limits.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* We need to be able to parse menu.lst files, create multiboot images. */
#include "../../include/grubmenu.h"
#include "../../include/multiboot2.h"
#include "blob.h"

#define DBG(format, ...) printf(format, ## __VA_ARGS__)

/* Keep physical addresses and kernel virtual addresses separated, as far as
 * possible. */
typedef uint64_t kvaddr_t;
typedef uint64_t paddr_t;

/*** A Linear Memory Allocator ***/
static paddr_t phys_alloc_start = 0;

static size_t round_up(size_t x, size_t y)
{
    size_t z = x + (y - 1);
    return z - (z % y);
}

/* Advance the allocator to an address with the given alignment. */
static paddr_t align_alloc(paddr_t align)
{
    phys_alloc_start = round_up(phys_alloc_start, align);
    return phys_alloc_start;
}

/* Allocate an aligned block. */
static paddr_t phys_alloc(size_t size, size_t align)
{
    align_alloc(align);
    paddr_t addr = phys_alloc_start;
    phys_alloc_start += size;
    return addr;
}

/*** Failure Handling ***/

static void fail(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
    exit(EXIT_FAILURE);
}

static void fail_errno(const char *fmt, ...)
{
    char s[1024];

    va_list ap;
    va_start(ap, fmt);
    vsnprintf(s, 1024, fmt, ap);
    va_end(ap);

    perror(s);
    exit(EXIT_FAILURE);
}

static void fail_elf(const char *s)
{
    fprintf(stderr, "%s: %s\n", s, elf_errmsg(elf_errno()));
    exit(EXIT_FAILURE);
}

static void join_paths(char *dst, const char *src1, const char *src2)
{
    strcpy(dst, src1);
    dst[strlen(src1)] = '/';
    strcpy(dst + strlen(src1) + 1, src2);
}

struct ram_region {
    uint64_t base;
    uint64_t npages;
    void *buffer;
};

struct loaded_module {
    void *data;
    paddr_t paddr;
    size_t len, size;
    const char *shortname;
};

struct loaded_image {
    struct ram_region segment;

    size_t loaded_size;
    paddr_t loaded_paddr;
    kvaddr_t loaded_vaddr;

    paddr_t entry;
    const char *extrasym_name;
    void *extrasym_ptr;

    void *shdrs, *symtab, *strtab, *shstrtab;
    size_t shdrs_size, symtab_size, strtab_size, shstrtab_size;
    size_t shdrs_entsize, symtab_entsize;

    unsigned no_relocations;
    struct Blob_relocation *relocations;
};


/* Load an ELF file as a raw data blob. */
void raw_load(const char *path, struct loaded_module *m)
{
    struct stat mstat;

    if (stat(path, &mstat))
        fail_errno("stat: %s", path);

    size_t data_len = mstat.st_size;
    m->len = round_up(data_len, BASE_PAGE_SIZE);
    m->size = data_len;
    m->data = calloc(m->len, 1);
    if (!m->data)
        fail_errno("calloc");
    m->paddr = phys_alloc(m->len, BASE_PAGE_SIZE);

    printf("Allocated 0x%zx at PA %016zx for %s (%zd)\n", m->len, m->paddr,
           path, data_len);

    int fd = open(path, O_RDONLY);
    if (fd < 0)
        fail_errno("open: %s", path);
    size_t read_len = read(fd, m->data, data_len);
    if (read_len != data_len)
        fail_errno("fread");
    close(fd);
}

/*** Multiboot ***/

#define ROUND_UP(x, y) (((x) + ((y) - 1)) & ~((y) - 1))
#define ALIGN(x) ROUND_UP((x), sizeof(uintptr_t))

/* Create the multiboot header, using only *physical* addresses. */
void *create_multiboot_info(struct menu_lst *menu,
                            struct loaded_module *modules,
                            size_t * mb_size, paddr_t * mb_base,
                            paddr_t entry)
{
    size_t size;
    unsigned i;
    void *cursor;

    /* Calculate the boot information size. */
    /* Multiboot2 information data structure */
    size = 8;
    /* cpu driver command line */
    size += ALIGN(sizeof(struct multiboot_tag_string)
                  + strlen(menu->kernel.args) + 1);
    // /* Boot driver module tag, including command line and ELF image */
    size += ALIGN(sizeof(struct multiboot_tag_module_64)
                  + strlen(menu->boot_driver.path) + 2);
    // /* CPU driver module tag, including command line and ELF image */
    size += ALIGN(sizeof(struct multiboot_tag_module_64)
                  + strlen(menu->kernel.path) + strlen(menu->kernel.args) +
                  2);
    /* All other modules */
    for (i = 0; i < menu->nmodules; i++) {
        size += ALIGN(sizeof(struct multiboot_tag_module_64)
                      + strlen(menu->modules[i].path) +
                      strlen(menu->modules[i].args) + 2);
    }
#define MEM_MAP_SIZE (1<<13)
    /* EFI memory map */
    size += ALIGN(sizeof(struct multiboot_tag_efi_mmap) + MEM_MAP_SIZE);
    // END tag
    size += ALIGN(sizeof(struct multiboot_tag));

    size_t allocated_size = round_up(size, BASE_PAGE_SIZE);
    /* Allocate target addresses. */
    paddr_t base = phys_alloc(size, BASE_PAGE_SIZE);
    *mb_size = allocated_size;
    *mb_base = base;

    /* Allocate our host buffer. */
    void *mb = calloc(allocated_size, 1);
    if (!mb)
        fail_errno("calloc");

    cursor = mb;
    /* Skip the information structure for now */
    cursor += 8;

    /* Add the boot command line */
    {
        struct multiboot_tag_string *bootcmd =
            (struct multiboot_tag_string *) cursor;
        bootcmd->type = MULTIBOOT_TAG_TYPE_CMDLINE;
        bootcmd->size = ALIGN(sizeof(struct multiboot_tag_string)
                             + strlen(menu->kernel.path) +
                             strlen(menu->kernel.args) + 2);
        sprintf(bootcmd->string, "%s %s", menu->kernel.path,
                menu->kernel.args);
        cursor += bootcmd->size;
    }

    /* Add the boot driver module. */
    {
        struct multiboot_tag_module_64 *boot_driver =
            (struct multiboot_tag_module_64 *) cursor;

        boot_driver->type = MULTIBOOT_TAG_TYPE_MODULE_64;
        boot_driver->size = ALIGN(sizeof(struct multiboot_tag_module_64)
                                  + strlen(menu->boot_driver.path) + 2);
        boot_driver->mod_start = (multiboot_uint64_t) modules[0].paddr;
        boot_driver->mod_end =
            (multiboot_uint64_t) (modules[0].paddr + modules[0].size - 1);
        sprintf(boot_driver->cmdline, "%s", menu->boot_driver.path);
        cursor += boot_driver->size;
    }
    /* Add the kernel module. */
    {
        struct multiboot_tag_module_64 *kernel =
            (struct multiboot_tag_module_64 *) cursor;

        kernel->type = MULTIBOOT_TAG_TYPE_MODULE_64;
        kernel->size = ALIGN(sizeof(struct multiboot_tag_module_64)
                             + strlen(menu->kernel.path) +
                             strlen(menu->kernel.args) + 2);
        kernel->mod_start = (multiboot_uint64_t) modules[1].paddr;
        kernel->mod_end =
            (multiboot_uint64_t) (modules[1].paddr + modules[1].size - 1);
        sprintf(kernel->cmdline, "%s %s", menu->kernel.path,
                menu->kernel.args);
        cursor += kernel->size;
    }
    /* Add the remaining modules */
    for (i = 0; i < menu->nmodules; i++) {
        struct multiboot_tag_module_64 *module =
            (struct multiboot_tag_module_64 *) cursor;

        module->type = MULTIBOOT_TAG_TYPE_MODULE_64;
        module->size = ALIGN(sizeof(struct multiboot_tag_module_64)
                             + strlen(menu->modules[i].path) +
                             strlen(menu->modules[i].args) + 2);
        module->mod_start = (multiboot_uint64_t) modules[i + 2].paddr;
        module->mod_end =
            (multiboot_uint64_t) (modules[i + 2].paddr +
                                  modules[i + 2].size - 1);
        sprintf(module->cmdline, "%s %s", menu->modules[i].path,
                menu->modules[i].args);
        cursor += module->size;
    }
    /* Add the EFI MMAP tag */
    {
        struct multiboot_tag_efi_mmap *mmap_tag =
            (struct multiboot_tag_efi_mmap *) cursor;
        mmap_tag->type = MULTIBOOT_TAG_TYPE_EFI_MMAP;
        cursor += sizeof(struct multiboot_tag_efi_mmap);
    }
    return mb;
}

int relocate_elf(struct ram_region *segment, Elf * elf,
                 Elf64_Phdr * phdr, size_t phnum, size_t shnum,
                 unsigned *no_relocations,
                 struct Blob_relocation **relocations)
{
    size_t i;

    *no_relocations = 0;

    /* Search for relocaton sections. */
    for (i = 0; i < shnum; i++) {
        Elf_Scn *scn = elf_getscn(elf, i);
        if (!scn) {
            printf("elf_getscn: %s\n", elf_errmsg(elf_errno()));
            return -1;
        }

        Elf64_Shdr *shdr = elf64_getshdr(scn);
        if (!shdr) {
            printf("elf64_getshdr: %s\n", elf_errmsg(elf_errno()));
            return -1;
        }
        if (shdr->sh_type == SHT_DYNAMIC) {
            int relocations_size;
            Elf_Data *data = elf_getdata(scn, NULL);
            Elf64_Dyn *dt = (Elf64_Dyn *) data->d_buf;
            for (; dt->d_tag && dt->d_tag != DT_RELACOUNT; dt++) {
            }
            assert(dt->d_tag == DT_RELACOUNT);
            *no_relocations = dt->d_un.d_val;
            relocations_size =
                round_up(*no_relocations * sizeof(struct Blob_relocation),
                         BASE_PAGE_SIZE);
            *relocations = malloc(relocations_size);
        } else if (shdr->sh_type == SHT_RELA) {
            if (shdr->sh_info != 0) {
                printf("I expected global relocations, but got"
                       " section-specific ones.\n");
                return -1;
            }

            /* Hardcoded for one loadable segment.
               XXX: seems to be not always the case for some ARMv8 builids.
             */
            //ASSERT(phnum == 1);

            Elf64_Addr segment_elf_base = phdr[0].p_vaddr;
            Elf64_Addr segment_load_base = segment->base;
            Elf64_Sxword segment_delta =
                segment_load_base - segment_elf_base;

            /* Walk the section data descriptors. */
            Elf_Data *reldata;
            for (reldata = elf_getdata(scn, NULL);
                 reldata; reldata = elf_getdata(scn, reldata)) {
                size_t rsize;
                if (shdr->sh_type == SHT_REL)
                    rsize = sizeof(Elf64_Rel);
                else
                    rsize = sizeof(Elf64_Rela);

                size_t nrel = reldata->d_size / rsize;

                /* Iterate through the relocations. */
                size_t i;
                for (i = 0; i < nrel; i++) {
                    void *reladdr = reldata->d_buf + i * rsize;
                    Elf64_Addr offset;
                    Elf64_Xword sym, type;
                    Elf64_Sxword addend;

                    assert(shdr->sh_type == SHT_RELA);
                    Elf64_Rela *rel = reladdr;

                    offset = rel->r_offset;
                    sym = ELF64_R_SYM(rel->r_info);
                    type = ELF64_R_TYPE(rel->r_info);
                    addend = rel->r_addend;

                    assert(type == R_AARCH64_RELATIVE);
                    if (sym != 0) {
                        printf("Relocation references a"
                               " dynamic symbol, which is"
                               " unsupported.\n");
                        return -1;
                    }

                    /* Delta(S) + A */
                    (*relocations)[i].offset = offset;
                    (*relocations)[i].addend = addend;
                }
            }
        }
    }

    return 0;
}

/* Load and relocate an ELF, with the given offset between the physical
 * address at which it is loaded, and the virtual address at which it
 * executes.  For the boot driver, the offset is zero.  Return a variety of
 * information about the loaded image. */
static void load(struct loaded_module *module, uint32_t vp_offset,
                 struct loaded_image *image, int save_sections)
{
    int i;
    /* Open the ELF. */
    Elf *elf = elf_memory(module->data, module->size);
    if (!elf)
        fail_elf("elf_begin");

    /* Grab the unrelocated entry address from the header. */
    Elf64_Ehdr *ehdr = elf64_getehdr(elf);
    if (!ehdr)
        fail_elf("elf64_getehdr");
    image->entry = ehdr->e_entry;

    /* Grab the program headers i.e. the list of loadable segments. */
    size_t phnum;
    if (elf_getphdrnum(elf, &phnum))
        fail_elf("elf_getphnum");

    Elf64_Phdr *phdr = elf64_getphdr(elf);
    if (!phdr)
        fail_elf("elf_getphdr");

    DBG("%zd program segments.\n", phnum);

    /* Grab the raw ELF data. */
    size_t elfsize;
    void *elfdata = elf_rawfile(elf, &elfsize);
    if (!elfdata)
        fail_elf("elf_rawfile");

    /* Count the loadable segments, to allocate the region list. */
    size_t nloadsegs = 0;
    for (i = 0; i < phnum; i++) {
        if (phdr[i].p_type == PT_LOAD)
            nloadsegs++;
    }

    for (i = 0; i < phnum; i++) {
        printf
            ("Segment %d load address %zx, offset %zx, file size %zx, memory size %zx\n",
             i, phdr[i].p_vaddr, phdr[i].p_offset, phdr[i].p_filesz,
             phdr[i].p_memsz);
        if (phdr[i].p_type != PT_LOAD)
            continue;

        unsigned p_pages =
            round_up(phdr[i].p_memsz, BASE_PAGE_SIZE) / BASE_PAGE_SIZE;
        void *p_buf;

        paddr_t pa = phys_alloc(phdr[i].p_memsz, BASE_PAGE_SIZE);
        p_buf = calloc(p_pages * BASE_PAGE_SIZE, 1);
        assert(p_buf);

        image->segment.buffer = p_buf;
        image->segment.base = pa;
        image->segment.npages = p_pages;

        memcpy(p_buf, module->data + phdr[i].p_offset, phdr[i].p_filesz);
    }

    size_t shnum;
    int status;
    status = elf_getshdrnum(elf, &shnum);
    if (status) {
        printf("elf_getshdrnum: %s\n", elf_errmsg(elf_errno()));
        assert(0);
    }

    status =
        relocate_elf(&image->segment, elf, phdr, phnum, shnum,
                     &image->no_relocations, &image->relocations);
    if (status) {
        printf("Relocation failed.\n");
        assert(0);
    }
    elf_end(elf);
}


int main(int argc, char *argv[])
{
    char pathbuf[PATH_MAX + 1];

    // if(argc != 6) usage(argv[0]);

    const char *menu_lst = argv[1],
        *outfile = argv[2], *buildroot = argv[3];

    errno = 0;

    printf("ARMv8 Static Bootloader\n");

    /* Read the menu.lst file. */
    printf("Reading boot configuration from %s\n", menu_lst);
    struct menu_lst *menu = read_menu_lst(menu_lst);

    struct loaded_module *modules =
        calloc(menu->nmodules + 2, sizeof(struct loaded_module));
    if (!modules)
        fail_errno("calloc");

    // create the Blob
    paddr_t base = phys_alloc(sizeof(struct Blob), BASE_PAGE_SIZE);
    printf("Blob info struct at PA %016lx\n", base);

    // Load the boot driver
    join_paths(pathbuf, buildroot, menu->boot_driver.path);
    raw_load(pathbuf, modules);

    /* Use the filename as a short identifier. */
    const char *lastslash = strrchr(menu->boot_driver.path, '/');
    if (lastslash) {
        modules[0].shortname = lastslash + 1;
    } else {
        modules[0].shortname = "";
    }
    // Load the kernel
    join_paths(pathbuf, buildroot, menu->kernel.path);
    raw_load(pathbuf, modules + 1);

    /* Use the filename as a short identifier. */
    lastslash = strrchr(menu->kernel.path, '/');
    if (lastslash) {
        modules[1].shortname = lastslash + 1;
    } else {
        modules[1].shortname = "";
    }

    /*** Load the modules. ***/

    for (size_t i = 0; i < menu->nmodules; i++) {
        join_paths(pathbuf, buildroot, menu->modules[i].path);
        raw_load(pathbuf, modules + i + 2);

        /* Use the filename as a short identifier. */
        lastslash = strrchr(menu->modules[i].path, '/');
        if (lastslash) {
            modules[i + 2].shortname = lastslash + 1;
        } else {
            modules[i + 2].shortname = "";
        }
    }

    if (elf_version(EV_CURRENT) == EV_NONE)
        fail("ELF library version out of date.\n");
    /*** Load the boot driver. ***/

    /* Load and relocate it. */
    struct loaded_image bd_image[2];
    bd_image[0].extrasym_name = "boot_arguments";
    load(modules, 0, bd_image, 1);
    load(modules + 1, 0, bd_image + 1, 1);

    printf("Boot driver entry point: PA %08zx\n", bd_image[0].entry);
    printf("CPU driver entry point: PA %08zx\n", bd_image[1].entry);

    paddr_t pa, endpa;
    struct Blob blob;

    memset(blob.data, 0, sizeof(blob.data));
    blob.magic = 0x12345678fedcba90;

    pa = phys_alloc(bd_image[0].no_relocations *
                    sizeof(struct Blob_relocation), BASE_PAGE_SIZE);
    printf("Boot relocations PA %016zx,%d\n", pa,
           bd_image[0].no_relocations);
    blob.boot_driver_relocations = pa;
    blob.boot_driver_relocations_count = bd_image[0].no_relocations;
    blob.boot_driver_segment = bd_image[0].segment.base;
    blob.boot_driver_segment_size = bd_image[0].segment.npages * BASE_PAGE_SIZE;
    blob.boot_driver_entry = (uint64_t)bd_image[0].entry;

    pa = phys_alloc(bd_image[1].no_relocations *
                    sizeof(struct Blob_relocation), BASE_PAGE_SIZE);
    printf("Kernel relocations PA %016zx,%d\n", pa,
           bd_image[1].no_relocations);
    blob.cpu_driver_relocations = pa;
    blob.cpu_driver_relocations_count = bd_image[1].no_relocations;
    blob.cpu_driver_segment = bd_image[1].segment.base;
    blob.cpu_driver_segment_size = bd_image[1].segment.npages * BASE_PAGE_SIZE;
    blob.cpu_driver_entry = (uint64_t)bd_image[1].entry;

    /*** Create the multiboot info header. ***/
    size_t mb_size, size;
    paddr_t mb_base;
    void *mb_image =
        create_multiboot_info(menu, modules, &mb_size, &mb_base,
                              bd_image[1].entry);

    endpa = phys_alloc(BASE_PAGE_SIZE, BASE_PAGE_SIZE);
    printf("Final PA %016zx\n", endpa);

    blob.multiboot = mb_base;
    blob.multiboot_size = mb_size;

    blob.modules = modules[0].paddr;
    for (size_t i = 0; i < menu->nmodules + 2; i++) {
        blob.modules_size += modules[i].len;
    }

    size_t r;
    FILE *fp = fopen(outfile, "wb");
    assert(fp >= 0);
    // write the blob info
    r = fwrite(&blob, 1, BASE_PAGE_SIZE, fp);
    assert(r == BASE_PAGE_SIZE);
    // write the modules
    for (size_t i = 0; i < menu->nmodules + 2; i++) {
        r = fwrite(modules[i].data, 1, modules[i].len, fp);
        assert(r == modules[i].len);
    }
    // write the boot driver's ELF section
    r = fwrite(bd_image[0].segment.buffer, 1,
              bd_image[0].segment.npages * BASE_PAGE_SIZE, fp);
    assert(r == bd_image[0].segment.npages * BASE_PAGE_SIZE);
    // write the kernel's ELF section
    r = fwrite(bd_image[1].segment.buffer, 1,
              bd_image[1].segment.npages * BASE_PAGE_SIZE, fp);
    assert(r == bd_image[1].segment.npages * BASE_PAGE_SIZE);
    // write the boot driver's relocations
    size =
        round_up(bd_image[0].no_relocations *
                 sizeof(struct Blob_relocation), BASE_PAGE_SIZE);
    r = fwrite(bd_image[0].relocations, 1, size, fp);
    assert(r == size);
    // write the kernel's relocations
    size =
        round_up(bd_image[1].no_relocations *
                 sizeof(struct Blob_relocation), BASE_PAGE_SIZE);
    r = fwrite(bd_image[1].relocations, 1, size, fp);
    assert(r == size);
    // write the multiboot info
    r = fwrite(mb_image, 1, mb_size, fp);
    assert(r == mb_size);
    fclose(fp);

    return 0;
}
