/* MemProf -- memory profiler and leak detector
 * Copyright 1999, 2000, 2001, Red Hat, Inc.
 * Copyright 2002, Kristian Rietveld
 *
 * Sysprof -- Sampling, systemwide CPU profiler
 * Copyright 2004, 2005, 2006, 2007, Soeren Sandmann
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

/* Most interesting code in this file is lifted from bfdutils.c
 * and process.c from Memprof,
 */
#include "config.h"

#include <glib.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdint.h>

#include "binfile.h"
#include "elfparser.h"

struct bin_file_t
{
    int         ref_count;

    GList *     elf_files;

    char *      filename;

    char *      undefined_name;

    gulong      text_offset;

    gboolean    inode_check;
    ino_t       inode;
};

static ino_t
read_inode (const char *filename)
{
    struct stat statbuf;

    if (strcmp (filename, "[vdso]") == 0)
        return (ino_t)0;

    if (stat (filename, &statbuf) < 0)
        return (ino_t)-1;

    return statbuf.st_ino;
}

static gboolean
already_warned (const char *name)
{
    static GPtrArray *warnings;
    guint i;

    if (!warnings)
        warnings = g_ptr_array_new ();

    for (i = 0; i < warnings->len; ++i)
    {
        if (strcmp (warnings->pdata[i], name) == 0)
            return TRUE;
    }

    g_ptr_array_add (warnings, g_strdup (name));

    return FALSE;
}

static ElfParser *
get_build_id_file (ElfParser          *elf,
                   const char * const *debug_dirs)
{
    const char *build_id;
    GList *tries = NULL, *list;
    char *init, *rest;
    ElfParser *result = NULL;
    char *tmp;

    build_id = elf_parser_get_build_id (elf);

    if (!build_id)
        return NULL;

    if (strlen (build_id) < 4)
        return NULL;

    init = g_strndup (build_id, 2);
    rest = g_strdup_printf ("%s%s", build_id + 2, ".debug");

    if (debug_dirs)
      {
        for (guint i = 0; debug_dirs[i]; i++)
          {
            tmp = g_build_filename (debug_dirs[i], ".build-id", init, rest, NULL);
            tries = g_list_append (tries, tmp);
          }
      }

    for (list = tries; list != NULL; list = list->next)
    {
        char *name = list->data;
        ElfParser *parser = elf_parser_new (name, NULL);

        if (parser)
        {
            const char *file_id = elf_parser_get_build_id (parser);

            if (file_id && strcmp (build_id, file_id) == 0)
            {
                result = parser;
                break;
            }

            elf_parser_free (parser);
        }
    }

    g_list_foreach (tries, (GFunc)g_free, NULL);
    g_list_free (tries);

    g_free (init);
    g_free (rest);

    return result;
}

static ElfParser *
get_debuglink_file (ElfParser            *elf,
                    const char           *filename,
                    char                **new_name,
                    const gchar * const  *debug_dirs)
{
    const char *basename;
    guint32 crc32;
    ElfParser *result = NULL;
    const char *build_id;
    char *dir;
    const char *files;
    const char *prefix = "";

    if (!elf)
        return NULL;

    basename = elf_parser_get_debug_link (elf, &crc32);

    build_id = elf_parser_get_build_id (elf);

#if 0
    g_print ("   debug link for %s is %s\n", filename, basename);
#endif

    if (!basename)
        return NULL;

    dir = g_path_get_dirname (filename);

    /* Flatpak paths have the form of ".../files/bin" or ".../files/lib/x86_64-linux-gnu". */
    files = g_strrstr (dir, "/files/");
    if (files)
      prefix = files + sizeof ("/files/") - 1;

    for (guint i = 0; debug_dirs[i]; i++)
    {
        /* Most files from Flatpak will be from .Platform, which usually has a prefix like "lib/x86_64-linux-gnu"
         * but in the debug dir the files are under "usr/lib/x86_64-linux-gnu", so try with "usr" first. */
        g_autofree char *name = g_build_filename (debug_dirs[i], "usr", prefix, basename, NULL);
        ElfParser *parser = elf_parser_new (name, NULL);
        guint32 file_crc;
        const char *file_build_id;

        if (!parser)
        {
            /* Files from Flatpak com.example.App.Debug have prefixes like "bin" or "lib",
             * and they don't need the "usr" for the debug dir, so try without "usr". */
            g_free (name);
            name = g_build_filename (debug_dirs[i], prefix, basename, NULL);
            parser = elf_parser_new (name, NULL);
        }

        if (parser)
        {
            /* If both files have build ids, and they don't match,
             * there is no point computing a CRC32 that we know
             * will fail
             */
            file_build_id = elf_parser_get_build_id (parser);
            if (build_id && file_build_id && strcmp (build_id, file_build_id) != 0)
                goto skip;

            file_crc = elf_parser_get_crc32 (parser);

            if (file_crc == crc32)
            {
                result = parser;
                *new_name = g_steal_pointer (&name);
                break;
            }
            else
            {
                if (!already_warned (name))
                {
                    g_print ("warning: %s has wrong crc %x, %s has crc %x)\n",
                             name, file_crc, filename, crc32);
                }
            }

        skip:
            elf_parser_free (parser);
        }
    }

    g_free (dir);

    return result;
}

static GList *
get_debug_binaries (GList               *files,
                    ElfParser           *elf,
                    const char          *filename,
                    const gchar * const *debug_dirs)
{
    ElfParser *build_id_file;
    GHashTable *seen_names;
    GList *free_us = NULL;

    build_id_file = get_build_id_file (elf, debug_dirs);

    if (build_id_file)
        return g_list_prepend (files, build_id_file);

    /* .gnu_debuglink is actually a chain of debuglinks, and
     * there have been real-world cases where following it was
     * necessary to get useful debug information.
     */
    seen_names = g_hash_table_new (g_str_hash, g_str_equal);

    while (elf)
    {
        char *debug_name;

        if (g_hash_table_lookup (seen_names, filename))
            break;

        g_hash_table_insert (seen_names, (char *)filename, (char *)filename);

        elf = get_debuglink_file (elf, filename, &debug_name, debug_dirs);

        if (elf)
        {
            files = g_list_prepend (files, elf);
            free_us = g_list_prepend (free_us, debug_name);
            filename = debug_name;
        }
    }

    g_list_foreach (free_us, (GFunc)g_free, NULL);
    g_list_free (free_us);

    g_hash_table_destroy (seen_names);

    return files;
}

#ifdef __linux__
G_GNUC_PRINTF (1, 2)
static char **
get_lines (const char *format,
           ...)
{
    va_list args;
    char *filename;
    char **result = NULL;
    char *contents;

    va_start (args, format);
    filename = g_strdup_vprintf (format, args);
    va_end (args);

    if (g_file_get_contents (filename, &contents, NULL, NULL))
    {
        result = g_strsplit (contents, "\n", -1);

        g_free (contents);
    }

    g_free (filename);

    return result;
}
#endif

static const uint8_t *
get_vdso_bytes (size_t *length)
{
#ifdef __linux__
    static const uint8_t *bytes = NULL;
    static size_t n_bytes = 0;
    static gboolean has_data;

    if (!has_data)
    {
        char **lines = get_lines ("/proc/%d/maps", getpid());
        int i;

        for (i = 0; lines[i] != NULL; ++i)
        {
            char file[256];
            gulong start;
            gulong end;
            int count = sscanf (
                lines[i], "%lx-%lx %*15s %*x %*x:%*x %*u %255s",
                &start, &end, file);

            if (count == 3 && strcmp (file, "[vdso]") == 0)
            {
                n_bytes = end - start;

                /* Dup the memory here so that valgrind will only
                 * report one 1 byte invalid read instead of
                 * a ton when the elf parser scans the vdso
                 *
                 * The reason we get a spurious invalid read from
                 * valgrind is that we are getting the address directly
                 * from /proc/maps, and valgrind knows that its mmap()
                 * wrapper never returned that address. But since it
                 * is a legal mapping, it is legal to read it.
                 */
                bytes = g_memdup2 ((uint8_t *)start, n_bytes);

                has_data = TRUE;
            }
        }
    }

    if (length)
        *length = n_bytes;

    return bytes;
#else
    if (length)
        *length = 0;
    return NULL;
#endif
}

bin_file_t *
bin_file_new (const char          *filename,
              const gchar * const *debug_dirs)
{
    const gchar *real_filename = filename;
    ElfParser *elf = NULL;
    bin_file_t *bf;

    bf = g_new0 (bin_file_t, 1);

    bf->inode_check = FALSE;
    bf->filename = g_strdup (filename);
    bf->undefined_name = g_strdup_printf ("In file %s", real_filename);
    bf->ref_count = 1;
    bf->elf_files = NULL;

    if (strcmp (filename, "[vdso]") == 0)
    {
        const guint8 *vdso_bytes;
        gsize length;

        vdso_bytes = get_vdso_bytes (&length);

        if (vdso_bytes)
            elf = elf_parser_new_from_data (vdso_bytes, length);
    }
    else
    {
        elf = elf_parser_new (filename, NULL);
    }

    if (elf)
    {
        /* We need the text offset of the actual binary, not the
         * (potential) debug binaries
         */
        bf->text_offset = elf_parser_get_text_offset (elf);

        bf->elf_files = get_debug_binaries (bf->elf_files, elf, filename, debug_dirs);
        bf->elf_files = g_list_append (bf->elf_files, elf);

        bf->inode = read_inode (filename);
    }

    return bf;
}

void
bin_file_free (bin_file_t *bin_file)
{
    if (--bin_file->ref_count == 0)
    {
        g_list_foreach (bin_file->elf_files, (GFunc)elf_parser_free, NULL);
        g_list_free (bin_file->elf_files);

        g_free (bin_file->filename);
        g_free (bin_file->undefined_name);
        g_free (bin_file);
    }
}

const bin_symbol_t *
bin_file_lookup_symbol (bin_file_t *bin_file,
                        gulong      address)
{
    GList *list;

#if 0
    g_print ("-=-=-=- \n");

    g_print ("bin file lookup lookup %d\n", address);
#endif

    address -= bin_file->text_offset;

#if 0
    g_print ("lookup %lx in %s\n", address, bin_file->filename);
#endif

    for (list = bin_file->elf_files; list != NULL; list = list->next)
    {
        ElfParser *elf = list->data;
        const ElfSym *sym = elf_parser_lookup_symbol (elf, address);

        if (sym)
        {
#if 0
            g_print ("found  %lx => %s\n", address,
                     bin_symbol_get_name (bin_file, (const bin_symbol_t *)sym));
#endif
            return (const bin_symbol_t *)sym;
        }
    }

#if 0
    g_print ("%lx undefined in %s (textoffset %lx)\n",
             address + bin_file->text_offset,
             bin_file->filename,
             bin_file->text_offset);
#endif

    return (const bin_symbol_t *)bin_file->undefined_name;
}

gboolean
bin_file_check_inode (bin_file_t *bin_file,
                      ino_t       inode)
{
    if (bin_file->inode == inode)
        return TRUE;

    if (!bin_file->elf_files)
        return FALSE;

    if (!bin_file->inode_check)
    {
        g_print ("warning: Inode mismatch for %s (disk: %"G_GUINT64_FORMAT", memory: %"G_GUINT64_FORMAT")\n",
                 bin_file->filename, (guint64)bin_file->inode, (guint64)inode);

        bin_file->inode_check = TRUE;
    }

    return FALSE;
}

static const ElfSym *
get_elf_sym (bin_file_t          *file,
             const bin_symbol_t  *symbol,
             ElfParser          **elf_ret)
{
    GList *list;

    for (list = file->elf_files; list != NULL; list = list->next)
    {
        const ElfSym *sym = (const ElfSym *)symbol;
        ElfParser *elf = list->data;

        if (elf_parser_owns_symbol (elf, sym))
        {
            *elf_ret = elf;
            return sym;
        }
    }

    g_critical ("Internal error: unrecognized symbol pointer");

    *elf_ret = NULL;
    return NULL;
}

const char *
bin_symbol_get_name (bin_file_t         *file,
                     const bin_symbol_t *symbol)
{
    if (file->undefined_name == (char *)symbol)
    {
        return file->undefined_name;
    }
    else
    {
        ElfParser *elf;
        const ElfSym *sym;

        sym = get_elf_sym (file, symbol, &elf);

        return elf_parser_get_sym_name (elf, sym);
    }
}

gulong
bin_symbol_get_address (bin_file_t         *file,
                        const bin_symbol_t *symbol)
{
    if (file->undefined_name == (char *)symbol)
    {
        return 0x0;
    }
    else
    {
        ElfParser *elf;
        const ElfSym *sym;

        sym = get_elf_sym (file, symbol, &elf);

        return elf_parser_get_sym_address (elf, sym);
    }
}

void
bin_symbol_get_address_range (bin_file_t         *file,
                              const bin_symbol_t *symbol,
                              gulong             *begin,
                              gulong             *end)
{
    if (file->undefined_name == (char *)symbol)
    {
        *begin = 0;
        *end = 0;
    }
    else
    {
        ElfParser *elf;
        const ElfSym *sym;

        sym = get_elf_sym (file, symbol, &elf);
        elf_parser_get_sym_address_range (elf, sym, begin, end);
    }
}
