/* based on plugin.c
 *   by Alex Chadwick
 *
 * Copyright (C) 2014, Alex Chadwick
 * Modified 2018, Maschell
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include "PluginInformation.h"
#include <utils/logger.h>
#include <dynamic_libs/os_types.h>
#include <libelf.h>
#include <string.h>
#include <sys/types.h>
#include <wups.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <utils/utils.h>
#include "ElfTools.h"

bool PluginInformation::checkFileExtenstion(const char * path) {
    if(path == NULL) {
        return false;
    }
    const char *extension;

    /* find the file extension */
    extension = strrchr(path, '.');
    if (extension == NULL) {
        extension = strchr(path, '\0');
    } else {
        extension++;
    }

    if(extension == NULL) {
        return false;
    }

    if (strcmp(extension, "mod") == 0 ||
            strcmp(extension, "o") == 0 ||
            strcmp(extension, "a") == 0 ||
            strcmp(extension, "elf") == 0) {
        return true;
    }
    return false;
}

bool PluginInformation::openAndParseElf() {
    bool result = false;
    int32_t fd = -1;
    Elf *elf = NULL;

    /* check for compile errors */
    if (elf_version(EV_CURRENT) == EV_NONE) {
        goto exit_error;
    }

    fd = open(getPath().c_str(), O_RDONLY, 0);

    if (fd == -1) {
        DEBUG_FUNCTION_LINE("failed to open '%s' \n", getPath().c_str());
        goto exit_error;
    }

    elf = elf_begin(fd, ELF_C_READ, NULL);

    if (elf == NULL) {
        DEBUG_FUNCTION_LINE("elf was NULL\n");
        goto exit_error;
    }

    switch (elf_kind(elf)) {
    case ELF_K_AR:
        /* TODO */
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Archives not yet supported.\n", getPath().c_str());
        goto exit_error;
    case ELF_K_ELF:
        result = this->parseElf(elf);
        break;
    default:
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Invalid ELF file.\n", getPath().c_str());
        goto exit_error;
    }

exit_error:
    if (elf != NULL) {
        elf_end(elf);
    }
    if (fd != -1) {
        close(fd);
    }
    return result;
}

bool PluginInformation::parseElf( Elf *elf) {
    bool res = false;
    Elf_Scn *scn;
    Elf32_Ehdr *ehdr;
    char *ident;
    size_t shstrndx, sz, symtab_count, symtab_strndx;
    Elf32_Sym *symtab = NULL;

    size_t cur_size = 0;

    const char * path_c = getPath().c_str();

    if(elf == NULL) {
        goto exit_error;
    }
    if(elf_kind(elf) != ELF_K_ELF) {
        goto exit_error;
    }

    ident = elf_getident(elf, &sz);

    if (ident == NULL) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Invalid ELF header.\n", path_c);
        goto exit_error;
    }
    if (sz < 7) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Invalid ELF header.\n", path_c);
        goto exit_error;
    }
    if (ident[4] != ELFCLASS32) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Not 32 bit ELF.\n", path_c);
        goto exit_error;
    }
    if (ident[5] != ELFDATA2MSB) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Not Big Endian.\n", path_c);
        goto exit_error;
    }
    if (ident[6] != EV_CURRENT) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Unknown ELF version.\n", path_c);
        goto exit_error;
    }

    ehdr = elf32_getehdr(elf);

    if (ehdr == NULL) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Invalid ELF header\n", path_c);
        goto exit_error;
    }
    /*if (ehdr->e_type != ET_REL) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Not relocatable ELF.\n", path_c);
        goto exit_error;
    }*/
    if (ehdr->e_machine != EM_PPC) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Architecture not EM_PPC.\n", path_c);
        goto exit_error;
    }
    if (ehdr->e_version != EV_CURRENT) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Unknown ELF version.\n", path_c);
        goto exit_error;
    }

    if (!ElfTools::loadElfSymtab(elf, &symtab, &symtab_count, &symtab_strndx)) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Couldn't parse symtab.\n", path_c);
        goto exit_error;
    }

    if(symtab == NULL) {
        goto exit_error;
    }

    if(!metadataRead(elf, symtab, symtab_count, symtab_strndx)) {
        goto exit_error;
    }

    if (elf_getshdrstrndx(elf, &shstrndx) != 0) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Couldn't find shdrstndx.\n", path_c);
        goto exit_error;
    }

    for (scn = elf_nextscn(elf, NULL); scn != NULL; scn = elf_nextscn(elf, scn)) {
        Elf32_Shdr *shdr;

        shdr = elf32_getshdr(scn);
        if (shdr == NULL) {
            continue;
        }

        if ((shdr->sh_type == SHT_PROGBITS || shdr->sh_type == SHT_NOBITS) &&
                (shdr->sh_flags & SHF_ALLOC)) {

            const char *name;

            name = elf_strptr(elf, shstrndx, shdr->sh_name);
            if (name == NULL) {
                continue;
            }

            if (strcmp(name, ".wups.meta") == 0) {
                continue;
            } else if (strcmp(name, ".wups.load") == 0) {
                cur_size +=
                    shdr->sh_size / sizeof(wups_loader_entry_t) * 6*4;
            } else if (strcmp(name, ".wups.hooks") == 0) {
                cur_size +=
                    shdr->sh_size / sizeof(wups_loader_hook_t) * 2*4;
            } else {
                cur_size += shdr->sh_size;
                /* add alignment padding to size */
                if (shdr->sh_addralign > 3) {
                    /* roundup to multiple of sh_addralign  */
                    cur_size += (-cur_size & (shdr->sh_addralign - 1));
                } else {
                    /* roundup to multiple of 4 */
                    cur_size += (-cur_size & 3);
                }
            }
        }
    }

    /* roundup to multiple of 4 */
    cur_size += (-cur_size & 3);

    this->setSize(cur_size);

    res = true;
exit_error:
    if (symtab != NULL) {
        free(symtab);
    }

    return res;
}

bool PluginInformation::metadataRead(Elf *elf, Elf32_Sym *symtab, size_t symtab_count, size_t symtab_strndx) {
    char *metadata = NULL, *metadata_cur, *metadata_end;
    const char *name, *author, *version, *license, *wups, *buildtimestamp, *description;

    Elf_Scn *scn;
    size_t shstrndx;

    if (elf_getshdrstrndx(elf, &shstrndx) != 0) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Couldn't find shstrndx\n", path);
        goto exit_error;
    }

    for (scn = elf_nextscn(elf, NULL); scn != NULL; scn = elf_nextscn(elf, scn)) {
        Elf32_Shdr *shdr;
        const char *name;

        shdr = elf32_getshdr(scn);
        if (shdr == NULL) {
            continue;
        }

        name = elf_strptr(elf, shstrndx, shdr->sh_name);
        if (name == NULL) {
            continue;
        }

        if (strcmp(name, ".wups.meta") == 0) {
            if (shdr->sh_size == 0) {
                continue;
            }

            if (metadata != NULL) {
                continue;
            }

            metadata = (char*) malloc(shdr->sh_size);

            if (metadata == NULL) {
                continue;
            }

            if (!ElfTools::elfLoadSection(elf, scn, shdr, metadata)) {
                DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Couldn't load .wups.meta.\n", path);
                goto exit_error;
            }

            ElfTools::elfLoadSymbols(elf_ndxscn(scn), metadata, symtab, symtab_count);

            if (!ElfTools::elfLink(elf, elf_ndxscn(scn), metadata, symtab, symtab_count, symtab_strndx, false)) {
                DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - .wups.meta contains invalid relocations.\n", path);
                goto exit_error;
            }

            metadata_end = metadata + shdr->sh_size;
            metadata_end[-1] = '\0';
        }
    }

    if (metadata == NULL) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Not a WUPS plugin file.\n", path);
        goto exit_error;
    }

    name = NULL;
    author = NULL;
    version = NULL;
    license = NULL;
    wups = NULL;
    buildtimestamp = NULL;
    description = NULL;

    for (metadata_cur = metadata; metadata_cur < metadata_end; metadata_cur = strchr(metadata_cur, '\0') + 1) {

        char *eq;

        if(metadata_cur < metadata || metadata_cur >= metadata_end) {
            goto exit_error;
        }

        if (*metadata_cur == '\0') {
            continue;
        }

        eq = strchr(metadata_cur, '=');
        if (eq == NULL) {
            continue;
        }

        if (strncmp(metadata_cur, "name", eq - metadata_cur) == 0) {
            if (name != NULL) {
                DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Multiple WUPS_PLUGIN_NAME declarations.\n", path);
                goto exit_error;
            }
            name = eq + 1;
        } else if (strncmp(metadata_cur, "author", eq - metadata_cur) == 0) {
            if (author != NULL) {
                DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Multiple WUPS_PLUGIN_AUTHOR declarations.\n", path);
                goto exit_error;
            }
            author = eq + 1;
        } else if (strncmp(metadata_cur, "version", eq - metadata_cur) == 0) {
            if (version != NULL) {
                DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Multiple WUPS_PLUGIN_VERSION declarations.\n", path);
                goto exit_error;
            }
            version = eq + 1;
        } else if (strncmp(metadata_cur, "license", eq - metadata_cur) == 0) {
            if (license != NULL) {
                DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Multiple WUPS_PLUGIN_LICENSE declarations.\n", path);
                goto exit_error;
            }
            license = eq + 1;
        } else if (strncmp(metadata_cur, "buildtimestamp", eq - metadata_cur) == 0) {
            if (buildtimestamp != NULL) {
                DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Multiple WUPS_PLUGIN_TIMESTAMP declarations.\n", path);
                goto exit_error;
            }
            buildtimestamp = eq + 1;
        } else if (strncmp(metadata_cur, "description", eq - metadata_cur) == 0) {
            if (description != NULL) {
                DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Multiple WUPS_PLUGIN_LICENSE declarations.\n", path);
                goto exit_error;
            }
            description = eq + 1;
        } else if (strncmp(metadata_cur, "wups", eq - metadata_cur) == 0) {
            if (wups != NULL) {
                DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Multiple WUPS_PLUGIN_NAME declarations.\n", path);
                goto exit_error;
            }
            wups = eq + 1;
        }
    }

    if (description == NULL) {
        description = "";
    }

    if (wups == NULL || strcmp(wups, "0.1") != 0) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Unrecognised WUPS version.\n", path);
        goto exit_error;
    }
    if (buildtimestamp == NULL) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Couldn't find buildtimestamp.\n", path);
        goto exit_error;
    }
    if (name == NULL) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Missing WUPS_PLUGIN_NAME declaration.\n",path);
        goto exit_error;
    }
    if (author == NULL) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Missing WUPS_PLUGIN_AUTHOR declaration.\n", path);
        goto exit_error;
    }
    if (version == NULL) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Missing WUPS_PLUGIN_VERSION declaration.\n", path);
        goto exit_error;
    }
    if (license == NULL) {
        DEBUG_FUNCTION_LINE("Warning: Ignoring '%s' - Missing WUPS_PLUGIN_LICENSE declaration.\n", path);
        goto exit_error;
    }

    this->setName(name);
    this->setAuthor(author);
    this->setVersion(version);
    this->setLicense(license);
    this->setBuildTimestamp(buildtimestamp);
    this->setDescription(description);

    return true;

exit_error:

    if (metadata != NULL) {
        free(metadata);
    }
    return false;
}
