/*
 * Copyright (c) 2020-2022, the SerenityOS developers.
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/DeprecatedString.h>
#include <AK/LexicalPath.h>
#include <AK/StringBuilder.h>
#include <AK/StringView.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/File.h>
#include <LibCore/MappedFile.h>
#include <LibCore/System.h>
#include <LibELF/DynamicLoader.h>
#include <LibELF/DynamicObject.h>
#include <LibELF/Image.h>
#include <LibELF/Validation.h>
#include <LibMain/Main.h>
#include <ctype.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

static char const* object_program_header_type_to_string(ElfW(Word) type)
{
    switch (type) {
    case PT_NULL:
        return "NULL";
    case PT_LOAD:
        return "LOAD";
    case PT_DYNAMIC:
        return "DYNAMIC";
    case PT_INTERP:
        return "INTERP";
    case PT_NOTE:
        return "NOTE";
    case PT_SHLIB:
        return "SHLIB";
    case PT_PHDR:
        return "PHDR";
    case PT_TLS:
        return "TLS";
    case PT_LOOS:
        return "LOOS";
    case PT_HIOS:
        return "HIOS";
    case PT_LOPROC:
        return "LOPROC";
    case PT_HIPROC:
        return "HIPROC";
    case PT_GNU_EH_FRAME:
        return "GNU_EH_FRAME";
    case PT_GNU_RELRO:
        return "GNU_RELRO";
    case PT_GNU_STACK:
        return "GNU_STACK";
    case PT_OPENBSD_RANDOMIZE:
        return "OPENBSD_RANDOMIZE";
    case PT_OPENBSD_WXNEEDED:
        return "OPENBSD_WXNEEDED";
    case PT_OPENBSD_BOOTDATA:
        return "OPENBSD_BOOTDATA";
    default:
        return "(?)";
    }
}

static char const* object_section_header_type_to_string(ElfW(Word) type)
{
    switch (type) {
    case SHT_NULL:
        return "NULL";
    case SHT_PROGBITS:
        return "PROGBITS";
    case SHT_SYMTAB:
        return "SYMTAB";
    case SHT_STRTAB:
        return "STRTAB";
    case SHT_RELA:
        return "RELA";
    case SHT_HASH:
        return "HASH";
    case SHT_DYNAMIC:
        return "DYNAMIC";
    case SHT_NOTE:
        return "NOTE";
    case SHT_NOBITS:
        return "NOBITS";
    case SHT_REL:
        return "REL";
    case SHT_SHLIB:
        return "SHLIB";
    case SHT_DYNSYM:
        return "DYNSYM";
    case SHT_NUM:
        return "NUM";
    case SHT_INIT_ARRAY:
        return "INIT_ARRAY";
    case SHT_FINI_ARRAY:
        return "FINI_ARRAY";
    case SHT_PREINIT_ARRAY:
        return "PREINIT_ARRAY";
    case SHT_GROUP:
        return "GROUP";
    case SHT_SYMTAB_SHNDX:
        return "SYMTAB_SHNDX";
    case SHT_RELR:
        return "RELR";
    case SHT_LOOS:
        return "SOOS";
    case SHT_SUNW_dof:
        return "SUNW_dof";
    case SHT_GNU_LIBLIST:
        return "GNU_LIBLIST";
    case SHT_SUNW_move:
        return "SUNW_move";
    case SHT_SUNW_syminfo:
        return "SUNW_syminfo";
    case SHT_SUNW_verdef:
        return "SUNW_verdef";
    case SHT_SUNW_verneed:
        return "SUNW_verneed";
    case SHT_SUNW_versym: // or SHT_HIOS
        return "SUNW_versym";
    case SHT_LOPROC:
        return "LOPROC";
    case SHT_HIPROC:
        return "HIPROC";
    case SHT_LOUSER:
        return "LOUSER";
    case SHT_HIUSER:
        return "HIUSER";
    case SHT_GNU_HASH:
        return "GNU_HASH";
    default:
        return "(?)";
    }
}

static char const* object_symbol_type_to_string(ElfW(Word) type)
{
    switch (type) {
    case STT_NOTYPE:
        return "NOTYPE";
    case STT_OBJECT:
        return "OBJECT";
    case STT_FUNC:
        return "FUNC";
    case STT_SECTION:
        return "SECTION";
    case STT_FILE:
        return "FILE";
    case STT_TLS:
        return "TLS";
    case STT_GNU_IFUNC:
        return "IFUNC";
    case STT_LOPROC:
        return "LOPROC";
    case STT_HIPROC:
        return "HIPROC";
    default:
        return "(?)";
    }
}

static char const* object_symbol_binding_to_string(ElfW(Word) type)
{
    switch (type) {
    case STB_LOCAL:
        return "LOCAL";
    case STB_GLOBAL:
        return "GLOBAL";
    case STB_WEAK:
        return "WEAK";
    case STB_NUM:
        return "NUM";
    case STB_LOPROC:
        return "LOPROC";
    case STB_HIPROC:
        return "HIPROC";
    default:
        return "(?)";
    }
}

static char const* object_relocation_type_to_string(ElfW(Word) type)
{
    switch (type) {
#if ARCH(x86_64)
    case R_X86_64_NONE:
        return "R_X86_64_NONE";
    case R_X86_64_64:
        return "R_X86_64";
    case R_X86_64_GLOB_DAT:
        return "R_x86_64_GLOB_DAT";
    case R_X86_64_JUMP_SLOT:
        return "R_X86_64_JUMP_SLOT";
    case R_X86_64_RELATIVE:
        return "R_X86_64_RELATIVE";
    case R_X86_64_TPOFF64:
        return "R_X86_64_TPOFF64";
#endif
    default:
        return "(?)";
    }
}

ErrorOr<int> serenity_main(Main::Arguments arguments)
{
    TRY(Core::System::pledge("stdio rpath"));

    DeprecatedString path {};
    static bool display_all = false;
    static bool display_elf_header = false;
    static bool display_program_headers = false;
    static bool display_section_headers = false;
    static bool display_headers = false;
    static bool display_symbol_table = false;
    static bool display_dynamic_symbol_table = false;
    static bool display_core_notes = false;
    static bool display_relocations = false;
    static bool display_unwind_info = false;
    static bool display_dynamic_section = false;
    static bool display_hardening = false;
    StringView string_dump_section {};

    Core::ArgsParser args_parser;
    args_parser.add_option(display_all, "Display all", "all", 'a');
    args_parser.add_option(display_elf_header, "Display ELF header", "file-header", 'h');
    args_parser.add_option(display_program_headers, "Display program headers", "program-headers", 'l');
    args_parser.add_option(display_section_headers, "Display section headers", "section-headers", 'S');
    args_parser.add_option(display_headers, "Equivalent to: -h -l -S -s -r -d -n -u -c", "headers", 'e');
    args_parser.add_option(display_symbol_table, "Display the symbol table", "syms", 's');
    args_parser.add_option(display_dynamic_symbol_table, "Display the dynamic symbol table", "dyn-syms", '\0');
    args_parser.add_option(display_dynamic_section, "Display the dynamic section", "dynamic", 'd');
    args_parser.add_option(display_core_notes, "Display core notes", "notes", 'n');
    args_parser.add_option(display_relocations, "Display relocations", "relocs", 'r');
    args_parser.add_option(display_unwind_info, "Display unwind info", "unwind", 'u');
    args_parser.add_option(display_hardening, "Display security hardening info", "checksec", 'c');
    args_parser.add_option(string_dump_section, "Display the contents of a section as strings", "string-dump", 'p', "section-name");
    args_parser.add_positional_argument(path, "ELF path", "path");
    args_parser.parse(arguments);

    if (arguments.argc < 3) {
        args_parser.print_usage(stderr, arguments.argv[0]);
        return Error::from_errno(EINVAL);
    }

    if (display_headers) {
        display_elf_header = true;
        display_program_headers = true;
        display_section_headers = true;
    }

    if (display_all) {
        display_elf_header = true;
        display_program_headers = true;
        display_section_headers = true;
        display_dynamic_symbol_table = true;
        display_dynamic_section = true;
        display_core_notes = true;
        display_relocations = true;
        display_unwind_info = true;
        display_symbol_table = true;
        display_hardening = true;
    }

    path = LexicalPath::absolute_path(TRY(Core::System::getcwd()), path);

    auto file_or_error = Core::MappedFile::map(path);

    if (file_or_error.is_error()) {
        warnln("Unable to map file {}: {}", path, file_or_error.error());
        return -1;
    }

    auto elf_image_data = file_or_error.value()->bytes();
    ELF::Image elf_image(elf_image_data);

    if (!elf_image.is_valid()) {
        warnln("File is not a valid ELF object");
        return -1;
    }

    StringBuilder interpreter_path_builder;
    auto result_or_error = ELF::validate_program_headers(*(const ElfW(Ehdr)*)elf_image_data.data(), elf_image_data.size(), elf_image_data, &interpreter_path_builder);
    if (result_or_error.is_error() || !result_or_error.value()) {
        warnln("Invalid ELF headers");
        return -1;
    }
    auto interpreter_path = interpreter_path_builder.string_view();

    auto& header = *reinterpret_cast<const ElfW(Ehdr)*>(elf_image_data.data());

    RefPtr<ELF::DynamicObject> object = nullptr;

    if (elf_image.is_dynamic()) {
        if (interpreter_path.is_empty()) {
            interpreter_path = "/usr/lib/Loader.so"sv;
            warnln("Warning: Dynamic ELF object has no interpreter path. Using: {}", interpreter_path);
        }

        auto interpreter_file_or_error = Core::MappedFile::map(interpreter_path);

        if (interpreter_file_or_error.is_error()) {
            warnln("Unable to map interpreter file {}: {}", interpreter_path, interpreter_file_or_error.error());
        } else {
            auto interpreter_image_data = interpreter_file_or_error.value()->bytes();

            ELF::Image interpreter_image(interpreter_image_data);

            if (!interpreter_image.is_valid()) {
                warnln("ELF interpreter image is invalid");
            }
        }

        int fd = TRY(Core::System::open(path, O_RDONLY));
        auto result = ELF::DynamicLoader::try_create(fd, path);
        if (result.is_error()) {
            outln("{}", result.error().text);
            return 1;
        }
        auto& loader = result.value();
        if (!loader->is_valid()) {
            outln("{} is not a valid ELF dynamic shared object!", path);
            return 1;
        }

        object = loader->map();
        if (!object) {
            outln("Failed to map dynamic ELF object {}", path);
            return 1;
        }
    }

    if (display_elf_header) {
        outln("ELF header:");

        out("  Magic:                             ");
        for (char i : StringView { header.e_ident, sizeof(header.e_ident) }) {
            if (isprint(i)) {
                out("{:c} ", i);
            } else {
                out("{:02x} ", i);
            }
        }
        outln();

        outln("  Type:                              {} ({})", header.e_type, ELF::Image::object_file_type_to_string(header.e_type).value_or("(?)"sv));
        outln("  Machine:                           {} ({})", header.e_machine, ELF::Image::object_machine_type_to_string(header.e_machine).value_or("(?)"sv));
        outln("  Version:                           {:#x}", header.e_version);
        outln("  Entry point address:               {:#x}", header.e_entry);
        outln("  Start of program headers:          {} (bytes into file)", header.e_phoff);
        outln("  Start of section headers:          {} (bytes into file)", header.e_shoff);
        outln("  Flags:                             {:#x}", header.e_flags);
        outln("  Size of this header:               {} (bytes)", header.e_ehsize);
        outln("  Size of program headers:           {} (bytes)", header.e_phentsize);
        outln("  Number of program headers:         {}", header.e_phnum);
        outln("  Size of section headers:           {} (bytes)", header.e_shentsize);
        outln("  Number of section headers:         {}", header.e_shnum);
        outln("  Section header string table index: {}", header.e_shstrndx);
        outln();
    }

    auto addr_padding = "        ";

    if (display_section_headers) {
        if (!display_all) {
            outln("There are {} section headers, starting at offset {:#x}:", header.e_shnum, header.e_shoff);
            outln();
        }

        if (!elf_image.section_count()) {
            outln("There are no sections in this file.");
        } else {
            outln("Section Headers:");
            outln("  Name                Type            Address{}    Offset{}     Size{}       Flags", addr_padding, addr_padding, addr_padding);

            elf_image.for_each_section([](const ELF::Image::Section& section) {
                out("  {:19} ", section.name());
                out("{:15} ", object_section_header_type_to_string(section.type()));
                out("{:p} ", section.address());
                out("{:p} ", section.offset());
                out("{:p} ", section.size());
                out("{}", section.flags());
                outln();
            });
        }
        outln();
    }

    if (display_program_headers) {
        if (!display_all) {
            outln("ELF file type is {} ({})", header.e_type, ELF::Image::object_file_type_to_string(header.e_type).value_or("(?)"sv));
            outln("Entry point {:#x}\n", header.e_entry);
            outln("There are {} program headers, starting at offset {}", header.e_phnum, header.e_phoff);
            outln();
        }

        if (!elf_image.program_header_count()) {
            outln("There are no program headers in this file.");
        } else {
            outln("Program Headers:");
            outln("  Type           Offset{}     VirtAddr{}   PhysAddr{}   FileSiz{}    MemSiz{}     Flg  Align",
                addr_padding, addr_padding, addr_padding, addr_padding, addr_padding);

            elf_image.for_each_program_header([](const ELF::Image::ProgramHeader& program_header) {
                out("  ");
                out("{:14} ", object_program_header_type_to_string(program_header.type()));
                out("{:p} ", program_header.offset());
                out("{:p} ", program_header.vaddr().as_ptr());
                out("{:p} ", program_header.vaddr().as_ptr()); // FIXME: assumes PhysAddr = VirtAddr
                out("{:p} ", program_header.size_in_image());
                out("{:p} ", program_header.size_in_memory());
                out("{:04x} ", program_header.flags());
                out("{:p}", program_header.alignment());
                outln();

                if (program_header.type() == PT_INTERP)
                    outln("      [Interpreter: {}]", program_header.raw_data());
            });
        }

        // TODO: Display section to segment mapping
        outln();
    }

    if (display_dynamic_section) {
        auto found_dynamic_section = false;
        if (elf_image.is_dynamic()) {
            elf_image.for_each_section([&found_dynamic_section](const ELF::Image::Section& section) {
                if (section.name() != ELF_DYNAMIC)
                    return IterationDecision::Continue;

                found_dynamic_section = true;

                if (section.entry_count()) {
                    outln("Dynamic section '{}' at offset {:#08x} contains {} entries.", section.name().to_deprecated_string(), section.offset(), section.entry_count());
                } else {
                    outln("Dynamic section '{}' at offset {:#08x} contains zero entries.", section.name().to_deprecated_string(), section.offset());
                }

                return IterationDecision::Break;
            });

            Vector<DeprecatedString> libraries;
            object->for_each_needed_library([&libraries](StringView entry) {
                libraries.append(DeprecatedString::formatted("{}", entry));
            });

            auto library_index = 0;
            outln("  Tag        Type              Name / Value");
            object->for_each_dynamic_entry([&library_index, &libraries, &object](const ELF::DynamicObject::DynamicEntry& entry) {
                out("  {:#08x} ", entry.tag());
                out("{:17} ", ELF::DynamicObject::name_for_dtag(entry.tag()));

                if (entry.tag() == DT_NEEDED) {
                    outln("Shared library: {}", libraries[library_index]);
                    library_index++;
                } else if (entry.tag() == DT_RPATH) {
                    outln("Library rpath: {}", object->rpath());
                } else if (entry.tag() == DT_RUNPATH) {
                    outln("Library runpath: {}", object->runpath());
                } else if (entry.tag() == DT_SONAME) {
                    outln("Library soname: {}", object->soname());
                } else {
                    outln("{:#08x}", entry.val());
                }
            });
        }

        if (!found_dynamic_section)
            outln("No dynamic section in this file.");

        outln();
    }

    if (display_relocations) {
        if (elf_image.is_dynamic()) {
            if (!object->relocation_section().entry_count()) {
                outln("Relocation section '{}' at offset {:#08x} contains zero entries:", object->relocation_section().name(), object->relocation_section().offset());
            } else {
                outln("Relocation section '{}' at offset {:#08x} contains {} entries:", object->relocation_section().name(), object->relocation_section().offset(), object->relocation_section().entry_count());
                outln("  Offset{}      Type                Sym Value{}   Sym Name", addr_padding, addr_padding);
                object->relocation_section().for_each_relocation([](const ELF::DynamicObject::Relocation& reloc) {
                    out("  {:p} ", reloc.offset());
                    out(" {:18} ", object_relocation_type_to_string(reloc.type()));
                    out(" {:p} ", reloc.symbol().value());
                    out(" {}", reloc.symbol().name());
                    outln();
                });
            }
            outln();

            if (!object->plt_relocation_section().entry_count()) {
                outln("Relocation section '{}' at offset {:#08x} contains zero entries:", object->plt_relocation_section().name(), object->plt_relocation_section().offset());
            } else {
                outln("Relocation section '{}' at offset {:#08x} contains {} entries:", object->plt_relocation_section().name(), object->plt_relocation_section().offset(), object->plt_relocation_section().entry_count());
                outln("  Offset{}      Type                Sym Value{}   Sym Name", addr_padding, addr_padding);
                object->plt_relocation_section().for_each_relocation([](const ELF::DynamicObject::Relocation& reloc) {
                    out("  {:p} ", reloc.offset());
                    out(" {:18} ", object_relocation_type_to_string(reloc.type()));
                    out(" {:p} ", reloc.symbol().value());
                    out(" {}", reloc.symbol().name());
                    outln();
                });
            }

            outln();

            size_t relr_count = 0;
            object->for_each_relr_relocation([&relr_count](auto) { ++relr_count; });
            if (relr_count != 0) {
                outln("Relocation section '.relr.dyn' at offset {:#08x} contains {} entries:", object->relr_relocation_section().offset(), object->relr_relocation_section().entry_count());
                outln("{:>8x} offsets", relr_count);
                object->for_each_relr_relocation([](auto offset) { outln("{:p}", offset); });
            }
        } else {
            outln("No relocations in this file.");
        }

        outln();
    }

    if (display_unwind_info) {
        // TODO: Unwind info
        outln("Decoding of unwind sections for machine type {} is not supported.", ELF::Image::object_machine_type_to_string(header.e_machine).value_or("?"sv));
        outln();
    }

    if (display_core_notes) {
        auto found_notes = false;
        elf_image.for_each_program_header([&found_notes](const ELF::Image::ProgramHeader& program_header) {
            if (program_header.type() != PT_NOTE)
                return;

            found_notes = true;

            outln("Displaying notes section '{}' at offset {:#08x} of length {:#08x}:", object_program_header_type_to_string(program_header.type()), program_header.offset(), program_header.size_in_image());

            // FIXME: Parse CORE notes. Notes are in JSON format on SerenityOS, but vary between systems.
            outln("{}", program_header.raw_data());
        });

        if (!found_notes)
            outln("No core notes in this file.");

        outln();
    }

    if (display_dynamic_symbol_table || display_symbol_table) {
        auto found_dynamic_symbol_table = false;

        if (elf_image.is_dynamic()) {
            elf_image.for_each_section([&found_dynamic_symbol_table](const ELF::Image::Section& section) {
                if (section.name() != ELF_DYNSYM)
                    return IterationDecision::Continue;

                found_dynamic_symbol_table = true;

                if (!section.entry_count()) {
                    outln("Symbol table '{}' contains zero entries.", ELF_DYNSYM);
                } else {
                    outln("Symbol table '{}' contains {} entries.", ELF_DYNSYM, section.entry_count());
                }

                return IterationDecision::Break;
            });

            if (object->symbol_count()) {
                // FIXME: Add support for init/fini/start/main sections
                outln("   Num: Value{}      Size{}       Type     Bind     Name", addr_padding, addr_padding);
                object->for_each_symbol([](const ELF::DynamicObject::Symbol& sym) {
                    out("  {:>4}: ", sym.index());
                    out("{:p} ", sym.value());
                    out("{:p} ", sym.size());
                    out("{:8} ", object_symbol_type_to_string(sym.type()));
                    out("{:8} ", object_symbol_binding_to_string(sym.bind()));
                    out("{}", sym.name());
                    outln();
                });
            }
        }

        if (!found_dynamic_symbol_table)
            outln("No dynamic symbol information for this file.");

        outln();
    }

    if (display_symbol_table) {
        if (elf_image.symbol_count()) {
            outln("Symbol table '{}' contains {} entries:", ELF_SYMTAB, elf_image.symbol_count());
            outln("   Num: Value{}      Size{}       Type     Bind     Name", addr_padding, addr_padding);

            elf_image.for_each_symbol([](const ELF::Image::Symbol& sym) {
                out("  {:>4}: ", sym.index());
                out("{:p} ", sym.value());
                out("{:p} ", sym.size());
                out("{:8} ", object_symbol_type_to_string(sym.type()));
                out("{:8} ", object_symbol_binding_to_string(sym.bind()));
                out("{}", sym.name());
                outln();
            });
        } else {
            outln("Symbol table '{}' contains zero entries.", ELF_SYMTAB);
        }
        outln();
    }

    if (display_hardening) {
        outln("Security Hardening:");
        outln("RELRO         Stack Canary NX           PIE          RPATH        RUNPATH      Symbols      ");

        bool relro = false;
        elf_image.for_each_program_header([&relro](const ELF::Image::ProgramHeader& program_header) {
            if (program_header.type() == PT_GNU_RELRO) {
                relro = true;
                return IterationDecision::Break;
            }
            return IterationDecision::Continue;
        });

        bool full_relro = false;
        if (relro) {
            object->for_each_dynamic_entry([&full_relro](const ELF::DynamicObject::DynamicEntry& entry) {
                if (entry.tag() == DT_BIND_NOW) {
                    full_relro = true;
                    return IterationDecision::Break;
                }
                return IterationDecision::Continue;
            });
            if (full_relro)
                out("\033[0;32m{:13}\033[0m ", "Full RELRO");
            else
                out("\033[0;33m{:13}\033[0m ", "Partial RELRO");
        } else {
            out("\033[0;31m{:13}\033[0m ", "No RELRO");
        }

        bool canary = false;
        elf_image.for_each_symbol([&canary](const ELF::Image::Symbol& sym) {
            if (sym.name() == "__stack_chk_fail" || sym.name() == "__intel_security_cookie") {
                canary = true;
                return IterationDecision::Break;
            }
            return IterationDecision::Continue;
        });

        if (canary)
            out("\033[0;32m{:12}\033[0m ", "Canary found");
        else
            out("\033[0;31m{:12}\033[0m ", "No canary");

        bool nx = false;
        elf_image.for_each_program_header([&nx](const ELF::Image::ProgramHeader& program_header) {
            if (program_header.type() == PT_GNU_STACK) {
                if (program_header.flags() & PF_X)
                    nx = false;
                else
                    nx = true;
                return IterationDecision::Break;
            }
            return IterationDecision::Continue;
        });

        if (nx)
            out("\033[0;32m{:12}\033[0m ", "NX enabled");
        else
            out("\033[0;31m{:12}\033[0m ", "NX disabled");

        bool pie = false;
        if (header.e_type == ET_REL || header.e_type == ET_DYN)
            pie = true;

        if (pie)
            out("\033[0;32m{:12}\033[0m ", "PIE enabled");
        else
            out("\033[0;31m{:12}\033[0m ", "No PIE");

        StringView rpath;
        if (elf_image.is_dynamic())
            rpath = object->rpath();

        if (rpath.is_empty())
            out("\033[0;32m{:12}\033[0m ", "No RPATH");
        else
            out("\033[0;31m{:12}\033[0m ", rpath);

        StringView runpath;
        if (elf_image.is_dynamic())
            runpath = object->runpath();

        if (runpath.is_empty())
            out("\033[0;32m{:12}\033[0m ", "No RUNPATH");
        else
            out("\033[0;31m{:12}\033[0m ", runpath);

        out("{} symbols", elf_image.symbol_count());
        outln();
    }

    if (!string_dump_section.is_null()) {
        auto maybe_section = elf_image.lookup_section(string_dump_section);
        if (maybe_section.has_value()) {
            outln("String dump of section \'{}\':", string_dump_section);
            StringView data(maybe_section->raw_data(), maybe_section->size());
            data.for_each_split_view('\0', SplitBehavior::Nothing, [&data](auto string) {
                auto offset = string.characters_without_null_termination() - data.characters_without_null_termination();
                outln("[{:6x}] {}", offset, string);
            });
        } else {
            warnln("Could not find section \'{}\'", string_dump_section);
            return 1;
        }
    }
    return 0;
}
