/**
 * Provides (read-only) memory-mapped I/O for ELF files.
 *
 * Reference: http://www.dwarfstd.org/
 *
 * Copyright: Copyright Digital Mars 2015 - 2018.
 * License:   $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
 * Authors:   Yazan Dabain, Martin Kinkelin
 * Source: $(DRUNTIMESRC core/elf/io.d)
 */

module core.internal.elf.io;

version (Posix):

import core.memory : pageSize;
import core.lifetime : move;
import core.sys.posix.fcntl;
import core.sys.posix.sys.mman;
import core.sys.posix.unistd;

version (linux)
{
    import core.sys.linux.link;
    version = LinuxOrBSD;
}
else version (FreeBSD)
{
    import core.sys.freebsd.sys.link_elf;
    version = LinuxOrBSD;
}
else version (DragonFlyBSD)
{
    import core.sys.dragonflybsd.sys.link_elf;
    version = LinuxOrBSD;
}

/**
 * File-based memory-mapped I/O (read-only).
 * Only supports ELF files with a byte-order matching the target platform's.
 * Params:
 *     Elf_Ehdr = Expected type of the ELF header (Elf{32,64}_Ehdr)
 *     Elf_Shdr = Expected type of the ELF section header (Elf{32,64}_Shdr)
 *     ELFCLASS = Expected ELF class (ELFCLASS{32,64})
 */
template ElfIO(Elf_Ehdr, Elf_Shdr, ubyte ELFCLASS)
{
    /**
     * ELF file (with memory-mapped ELF header).
     */
    struct ElfFile
    {
    @nogc nothrow:
        /**
         * Tries to open the specified file as ELF file matching the ElfIO
         * template parameters.
         * Returns: True on success.
         */
        static bool open(const(char)* path, out ElfFile file)
        {
            file = ElfFile(.open(path, O_RDONLY));
            return file.isValid();
        }

        /**
         * Constructs an instance based on the specified file descriptor.
         * Doesn't validate the file header.
         * The file is closed when destructing the instance.
         */
        this(int fd)
        {
            this.fd = fd;
            if (fd != -1)
            {
                // memory map header
                this.ehdr = TypedMMapRegion!Elf_Ehdr(fd, 0);
            }
        }

        @disable this(this);

        /// Closes the file.
        ~this()
        {
            if (fd != -1)
                close(fd);
        }

        private int fd = -1;
        /// Memory-mapped ELF header.
        TypedMMapRegion!Elf_Ehdr ehdr;

        /// Returns true if the ELF file header matches the ElfIO template parameters.
        bool isValid() const
        {
            enum EI_MAG0 = 0;
            enum EI_MAG1 = 1;
            enum EI_MAG2 = 2;
            enum EI_MAG3 = 3;
            enum EI_CLASS = 4;
            enum EI_DATA = 5;

            enum ELFMAG0 = 0x7f;
            enum ELFMAG1 = 'E';
            enum ELFMAG2 = 'L';
            enum ELFMAG3 = 'F';

            enum ELFCLASS32 = 1;
            enum ELFCLASS64 = 2;

            enum ELFDATA2LSB = 1;
            enum ELFDATA2MSB = 2;

            version (LittleEndian)   alias ELFDATA = ELFDATA2LSB;
            else version (BigEndian) alias ELFDATA = ELFDATA2MSB;
            else static assert(0, "unsupported byte order");

            if (fd == -1)
                return false;

            const ident = ehdr.e_ident;

            if (!(ident[EI_MAG0] == ELFMAG0 &&
                  ident[EI_MAG1] == ELFMAG1 &&
                  ident[EI_MAG2] == ELFMAG2 &&
                  ident[EI_MAG3] == ELFMAG3))
                return false;

            if (ident[EI_CLASS] != ELFCLASS)
                return false;

            // the file's byte order must match the target's
            if (ident[EI_DATA] != ELFDATA)
                return false;

            return true;
        }

        /**
         * Returns a struct to iterate over the named sections.
         * Examples:
         * --------------------
         * foreach (index, name, sectionHeader; elfFile.namedSections) ...
         * --------------------
         */
        NamedSections namedSections() const
        {
            return NamedSections(this);
        }

        /**
         * Tries to find the header of the section with the specified name.
         * Returns: True on success.
         */
        bool findSectionHeaderByName(const(char)[] sectionName, out ElfSectionHeader header) const
        {
            foreach (index, name, sectionHeader; namedSections)
            {
                if (name == sectionName)
                {
                    header = move(sectionHeader);
                    return true;
                }
            }
            return false;
        }
    }

    /// Enables iterating over an ELF file's (named) sections.
    struct NamedSections
    {
    @nogc nothrow:
        private const(ElfFile)* file;

        private this(ref const ElfFile file)
        {
            this.file = &file;
        }

        /// name: null-terminated
        alias Callback = int delegate(size_t index, const(char)[] name, ElfSectionHeader sectionHeader);

        ///
        int opApply(scope Callback dg)
        {
            const stringSectionHeader = ElfSectionHeader(*file, file.ehdr.e_shstrndx);
            const stringSection = ElfSection(*file, stringSectionHeader);

            foreach (i; 0 .. file.ehdr.e_shnum)
            {
                import core.stdc.string : strlen;

                auto sectionHeader = ElfSectionHeader(*file, i);
                auto sectionName = cast(const(char)*) (stringSection.data.ptr + sectionHeader.sh_name);
                const nameLen = strlen(sectionName);

                const r = dg(i, sectionName[0 .. nameLen], move(sectionHeader));
                if (r != 0)
                    return r;
            }

            return 0;
        }
    }

    /**
     * Memory-mapped ELF section header.
     */
    struct ElfSectionHeader
    {
    @nogc nothrow:
        /// Constructs a new instance based on the specified file and section index.
        this(ref const ElfFile file, size_t index)
        {
            assert(Elf_Shdr.sizeof == file.ehdr.e_shentsize);
            shdr = TypedMMapRegion!Elf_Shdr(
                file.fd,
                file.ehdr.e_shoff + index * Elf_Shdr.sizeof
            );
        }

        @disable this(this);

        alias shdr this;
        /// Memory-mapped section header.
        TypedMMapRegion!Elf_Shdr shdr;
    }

    /**
     * Memory-mapped ELF section data.
     */
    struct ElfSection
    {
    @nogc nothrow:
        /// Constructs a new instance based on the specified file and section header.
        this(ref const ElfFile file, ref const ElfSectionHeader shdr)
        {
            mappedRegion = TypedMMapRegion!void(file.fd, shdr.sh_offset, shdr.sh_size);
            size = shdr.sh_size;
        }

        @disable this(this);

        /// Returns the memory-mapped section data.
        /// The data is accessible as long as this ElfSection is alive.
        const(void)[] data() const
        {
            return mappedRegion.data[0 .. size];
        }

        alias data this;

    private:
        TypedMMapRegion!void mappedRegion;
        size_t size;
    }
}

/// ELF class for 32-bit ELF files.
enum ELFCLASS32 = 1;
/// ELF class for 64-bit ELF files.
enum ELFCLASS64 = 2;

// convenience aliases for the target platform
version (LinuxOrBSD)
{
    /// Native ELF header type.
    alias Elf_Ehdr = ElfW!"Ehdr";
    /// Native ELF section header type.
    alias Elf_Shdr = ElfW!"Shdr";

    /// Native ELF class.
    version (D_LP64) alias ELFCLASS = ELFCLASS64;
    else             alias ELFCLASS = ELFCLASS32;

    ///
    alias NativeElfIO = ElfIO!(Elf_Ehdr, Elf_Shdr, ELFCLASS);

    /// Native ELF file.
    alias ElfFile = NativeElfIO.ElfFile;
    /// Native ELF section header.
    alias ElfSectionHeader = NativeElfIO.ElfSectionHeader;
    /// Native ELF section.
    alias ElfSection = NativeElfIO.ElfSection;
}

private struct TypedMMapRegion(T)
{
@nogc nothrow:

    this(int fd, size_t offset, size_t length = 1)
    {
        const pageOffset = (offset / pageSize);
        const offsetDiff = offset - (pageOffset * pageSize);
        const mappedSize = (length * T.sizeof) + offsetDiff;
        const pageMapped = (mappedSize / pageSize) + !!(mappedSize % pageSize);
        this.region = MMapRegion(fd, pageOffset, pageMapped);
        if (this.region.data.ptr !is null)
            this.data = cast(const(T)*) (this.region.data.ptr + offsetDiff);
    }

    private MMapRegion region;

    public const(T)* data;
    alias data this;
}

private struct MMapRegion
{
    @nogc nothrow:

    /**
     * Instantiate an instance of this struct with the provided offset / length
     *
     * Params:
     *   fd = The file descriptor to map (likely obtained with `open`)
     *   pageOffset = The number of pages to offset the `fd` with.
     *                For example, if one wishes to skip the first 16 KiB of
     *                the file, the pageOffset will be 4 (for pageSize == 4kiB).
     *   pageCount = The number of pages to map
     */
    this(int fd, size_t pageOffset, size_t pageCount)
    {
        if (fd == -1)
            return;

        const offset = pageOffset * pageSize;
        const mappedSize = pageCount * pageSize;

        const ptr = cast(ubyte*) mmap(null, mappedSize, PROT_READ, MAP_PRIVATE, fd, offset);
        if (ptr !is MAP_FAILED)
            this.data = ptr[0 .. mappedSize];
    }

    @disable this(this);

    ~this()
    {
        if (this.data !is null)
            munmap(cast(void*) this.data.ptr, this.data.length);
        this.data = null;
    }

    public const(ubyte)[] data;
}

version (LinuxOrBSD)
unittest
{
    import core.internal.elf.dl, core.stdc.stdio;

    SharedObject exe = SharedObject.thisExecutable();

    ElfFile file;
    bool success = ElfFile.open(exe.name.ptr, file);
    assert(success, "cannot open ELF file");

    foreach (index, name, sectionHeader; file.namedSections)
    {
        printf("section %3d %-32s", cast(int) index, name.ptr);
        if (const offset = sectionHeader.shdr.sh_addr)
        {
            auto beg = exe.baseAddress + offset;
            printf("%p - %p\n", beg, beg + sectionHeader.shdr.sh_size);
        }
        else
        {
            printf("not mapped into memory\n");
        }
    }
}
