/*
 * Copyright (C) 2014  ABRT team
 * Copyright (C) 2014  RedHat Inc
 *
 * 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.
 */
#include "libabrt.h"
#include "abrt-journal.h"

#define ABRT_JOURNAL_WATCH_STATE_FILE VAR_STATE"/abrt-dump-journal-core.state"

/*
 * A journal message is a set of key value pairs in the following format:
 *   FIELD_NAME=${binary data}
 *
 * A journal message contains many fields useful in syslog but ABRT doesn't
 * need all of them. So the following list defines mapping between journal
 * fields and ABRT problem items.
 *
 * ABRT goes through the list and for each item reads journal field called
 * 'item.name' and saves its contents in $DUMP_DIRECTORY/'item.file'.
 */
struct field_mapping {
    const char *name;
    const char *file;
} fields [] = {
    { .name = "COREDUMP_EXE",               .file = FILENAME_EXECUTABLE, },
    { .name = "COREDUMP_CMDLINE",           .file = FILENAME_CMDLINE, },
    { .name = "COREDUMP_PROC_STATUS",       .file = FILENAME_PROC_PID_STATUS, },
    { .name = "COREDUMP_PROC_MAPS",         .file = FILENAME_MAPS, },
    { .name = "COREDUMP_PROC_LIMITS",       .file = FILENAME_LIMITS, },
    { .name = "COREDUMP_PROC_CGROUP",       .file = FILENAME_CGROUP, },
    { .name = "COREDUMP_ENVIRON",           .file = FILENAME_ENVIRON, },
    { .name = "COREDUMP_CWD",               .file = FILENAME_PWD, },
    { .name = "COREDUMP_ROOT",              .file = FILENAME_ROOTDIR, },
    { .name = "COREDUMP_OPEN_FDS",          .file = FILENAME_OPEN_FDS, },
    { .name = "COREDUMP_UID",               .file = FILENAME_UID, },
    //{ .name = "COREDUMP_GID",               .file = FILENAME_GID, },
    { .name = "COREDUMP_PID",               .file = FILENAME_PID, },
    { .name = "COREDUMP_PROC_MOUNTINFO",    .file = FILENAME_MOUNTINFO, },
};

/*
 * Something like 'struct problem_data' but optimized for copying data from
 * journald to ABRT.
 *
 * 'struct problem_data' allocates a new memory for every single item and I
 * found that very inefficient in this case.
 *
 * The following structure holds data that we already retreived from journald
 * so we won't need to retrieve the data again.
 *
 * Why we retrieve data before we store them? Because we do some checking
 * before we start saving data in ABRT. We check whether the signal is one of
 * those we are interested in or whether the executable crashes too often to
 * ignore the current crash ...
 */
struct crash_info
{
    abrt_journal_t *ci_journal;

    int ci_signal_no;
    const char *ci_signal_name;
    char *ci_executable_path;          ///< /full/path/to/executable
    const char *ci_executable_name;    ///< executable
    uid_t ci_uid;
    pid_t ci_pid;

    struct field_mapping *ci_mapping;
    size_t ci_mapping_items;
};

/*
 * ABRT watch core configuration
 */
typedef struct
{
    const char *awc_dump_location;
    int awc_throttle;
}
abrt_watch_core_conf_t;


/*
 * A helper structured holding executable name and its last occurrence time.
 *
 * It is rather an array than a queue. Ii uses statically allocated array and
 * the head member points the next position for creating a new entry (the next
 * position may be already occupied and in such case the data shall be released).
 */
struct occurrence_queue
{
    int oq_head;       ///< the first empty index
    unsigned oq_size;  ///< size of the queue

    struct last_occurrence
    {
        unsigned oqlc_stamp;
        char *oqlc_executable;
    } oq_occurrences[8];

} s_queue = {
    .oq_head = -1,
    .oq_size = 8,
};

static unsigned
abrt_journal_get_last_occurrence(const char *executable)
{
    if (s_queue.oq_head < 0)
        return 0;

    unsigned index = s_queue.oq_head == 0 ? s_queue.oq_size - 1 : s_queue.oq_head - 1;
    for (unsigned i = 0; i < s_queue.oq_size; ++i)
    {
        if (s_queue.oq_occurrences[index].oqlc_executable == NULL)
            break;

        if (strcmp(executable, s_queue.oq_occurrences[index].oqlc_executable) == 0)
            return s_queue.oq_occurrences[index].oqlc_stamp;

        if (index-- == 0)
            index = s_queue.oq_size - 1;
    }

    return 0;
}

static void
abrt_journal_update_occurrence(const char *executable, unsigned ts)
{
    if (s_queue.oq_head < 0)
        s_queue.oq_head = 0;
    else
    {
        unsigned index = s_queue.oq_head == 0 ? s_queue.oq_size - 1 : s_queue.oq_head - 1;
        for (unsigned i = 0; i < s_queue.oq_size; ++i)
        {
            if (s_queue.oq_occurrences[index].oqlc_executable == NULL)
                break;

            if (strcmp(executable, s_queue.oq_occurrences[index].oqlc_executable) == 0)
            {
                /* Enhancement: move this entry right behind head */
                s_queue.oq_occurrences[index].oqlc_stamp = ts;
                return;
            }

            if (index-- == 0)
                index = s_queue.oq_size - 1;
        }
    }

    s_queue.oq_occurrences[s_queue.oq_head].oqlc_stamp = ts;
    free(s_queue.oq_occurrences[s_queue.oq_head].oqlc_executable);
    s_queue.oq_occurrences[s_queue.oq_head].oqlc_executable = xstrdup(executable);

    if (++s_queue.oq_head >= s_queue.oq_size)
        s_queue.oq_head = 0;

    return;
}

/*
 * Converts a journal message into an intermediate ABRT problem (struct crash_info).
 *
 * Refuses to create the problem in the following cases:
 * - the crashed executable has 'abrt' prefix
 * - the signals is not fatal (see signal_is_fatal())
 * - the journal message misses one of the following fields
 *   - COREDUMP_SIGNAL
 *   - COREDUMP_EXE
 *   - COREDUMP_UID
 *   - COREDUMP_PROC_STATUS
 * - if any data does not have an expected format
 */
static int
abrt_journal_core_retrieve_information(abrt_journal_t *journal, struct crash_info *info)
{
    if (abrt_journal_get_int_field(journal, "COREDUMP_SIGNAL", &(info->ci_signal_no)) != 0)
    {
        log_info("Failed to get signal number from journal message");
        return -EINVAL;
    }

    if (!signal_is_fatal(info->ci_signal_no, &(info->ci_signal_name)))
    {
        log_info("Signal '%d' is not fatal: ignoring crash", info->ci_signal_no);
        return 1;
    }

    info->ci_executable_path = abrt_journal_get_string_field(journal, "COREDUMP_EXE", NULL);
    if (info->ci_executable_path == NULL)
    {
        log_notice("Could not get crashed 'executable'.");
        return -ENOENT;
    }

    info->ci_executable_name = strrchr(info->ci_executable_path, '/');
    if (info->ci_executable_name == NULL)
    {
        info->ci_executable_name = info->ci_executable_path;
    }
    else if(strncmp(++(info->ci_executable_name), "abrt", 4) == 0)
    {
        error_msg("Ignoring crash of ABRT executable '%s'", info->ci_executable_path);
        return 1;
    }

    if (abrt_journal_get_unsigned_field(journal, "COREDUMP_UID", &(info->ci_uid)))
    {
        log_info("Failed to get UID from journal message");
        return -EINVAL;
    }

    /* This is not fatal, the pid is used only in dumpdir name */
    if (abrt_journal_get_int_field(journal, "COREDUMP_PID", &(info->ci_pid)))
    {
        log_notice("Failed to get PID from journal message.");
        info->ci_pid = getpid();
    }

    char *proc_status = abrt_journal_get_string_field(journal, "COREDUMP_PROC_STATUS", NULL);
    if (proc_status == NULL)
    {
        log_info("Failed to get /proc/[pid]/status from journal message");
        return -ENOENT;
    }

    uid_t tmp_fsuid = get_fsuid(proc_status);
    if (tmp_fsuid < 0)
        return -EINVAL;

    if (tmp_fsuid != info->ci_uid)
    {
        /* use root for suided apps unless it's explicitly set to UNSAFE */
        info->ci_uid = (dump_suid_policy() != DUMP_SUID_UNSAFE) ? 0 : tmp_fsuid;
    }

    return 0;
}

/*
 * Initializes ABRT problem directory and save the relevant journal message
 * fileds in that directory.
 */
static int
save_systemd_coredump_in_dump_directory(struct dump_dir *dd, struct crash_info *info)
{
    char coredump_path[PATH_MAX + 1] = { '\0' };
    if (coredump_path != abrt_journal_get_string_field(info->ci_journal, "COREDUMP_FILENAME", coredump_path))
        log_debug("Processing coredumpctl entry without a real file");

    const size_t len = strlen(coredump_path);
    if (   (len >= 3
            && coredump_path[len - 3] == '.'
            && coredump_path[len - 2] == 'x'
            && coredump_path[len - 1] == 'z')
        || (len >= 4
            && coredump_path[len - 4] == '.'
            && coredump_path[len - 3] == 'l'
            && coredump_path[len - 2] == 'z'
            && coredump_path[len - 1] == '4'))
    {
        if (dd_copy_file_unpack(dd, FILENAME_COREDUMP, coredump_path))
            return -1;
    }
    else if (len > 0)
    {
        if (dd_copy_file(dd, FILENAME_COREDUMP, coredump_path))
            return -1;
    }
    else
    {
        const char *data = NULL;
        size_t data_len = 0;
        int r = abrt_journal_get_field(info->ci_journal, "COREDUMP", (const void **)&data, &data_len);
        if (r < 0)
        {
            log_info("Ignoring coredumpctl entry without core dump file.");
            return -1;
        }

        dd_save_binary(dd, FILENAME_COREDUMP, data, data_len);
    }

    dd_save_text(dd, FILENAME_ABRT_VERSION, VERSION);
    dd_save_text(dd, FILENAME_TYPE, "CCpp");
    dd_save_text(dd, FILENAME_ANALYZER, "abrt-journal-core");

    char *reason;
    if (info->ci_signal_name == NULL)
        reason = xasprintf("%s killed by signal %d", info->ci_executable_name, info->ci_signal_no);
    else
        reason = xasprintf("%s killed by SIG%s", info->ci_executable_name, info->ci_signal_name);

    dd_save_text(dd, FILENAME_REASON, reason);
    free(reason);

    char *cursor = NULL;
    if (abrt_journal_get_cursor(info->ci_journal, &cursor) == 0)
        dd_save_text(dd, "journald_cursor", cursor);
    free(cursor);

    const char *data = NULL;
    size_t data_len = 0;

    if (!abrt_journal_get_field(info->ci_journal, "COREDUMP_CONTAINER_CMDLINE", (const void **)&data, &data_len))
    {
        dd_save_binary(dd, FILENAME_CONTAINER_CMDLINE, data, data_len);
    }

    for (size_t i = 0; i < info->ci_mapping_items; ++i)
    {
        const char *data;
        size_t data_len;
        struct field_mapping *f = info->ci_mapping + i;

        if (abrt_journal_get_field(info->ci_journal, f->name, (const void **)&data, &data_len))
        {
            log_info("systemd-coredump journald message misses field: '%s'", f->name);
            continue;
        }

        dd_save_binary(dd, f->file, data, data_len);
    }

    return 0;
}

static int
abrt_journal_core_to_abrt_problem(struct crash_info *info, const char *dump_location)
{
    struct dump_dir *dd = create_dump_dir_ext(dump_location, "ccpp", info->ci_pid, /*fs owner*/0,
            (save_data_call_back)save_systemd_coredump_in_dump_directory, info);

    if (dd != NULL)
    {
        char *path = xstrdup(dd->dd_dirname);
        dd_close(dd);
        notify_new_path(path);
        log_debug("ABRT daemon has been notified about directory: '%s'", path);
        free(path);
    }

    return dd == NULL;
}

/*
 * Creates an abrt problem from a journal message
 */
static int
abrt_journal_dump_core(abrt_journal_t *journal, const char *dump_location)
{
    struct crash_info info = { 0 };
    info.ci_journal = journal;
    info.ci_mapping = fields;
    info.ci_mapping_items = sizeof(fields)/sizeof(*fields);

    /* Compatibility hack, a watch's callback gets the journal already moved
     * to a next message. */
    abrt_journal_next(journal);

    /* This the watch call back mentioned in the comment above. We use the
     * following function also in abrt_journal_watch_cores(). */
    int r = abrt_journal_core_retrieve_information(journal, &info);
    if (r != 0)
    {
        if (r < 0)
            error_msg(_("Failed to obtain all required information from journald"));

        goto dump_cleanup;
    }

    r = abrt_journal_core_to_abrt_problem(&info, dump_location);

dump_cleanup:
    if (info.ci_executable_path != NULL)
        free(info.ci_executable_path);

    return r;
}

/*
 * A function called when a new journal core is detected.
 *
 * The function retrieves information from journal, checks the last occurrence
 * time of the crashed executable and if there was no recent occurrence creates
 * an ABRT problem from the journal message. Finally updates the last occurrence
 * time.
 */
static void
abrt_journal_watch_cores(abrt_journal_watch_t *watch, void *user_data)
{
    const abrt_watch_core_conf_t *conf = (const abrt_watch_core_conf_t *)user_data;

    struct crash_info info = { 0 };
    info.ci_journal = abrt_journal_watch_get_journal(watch);
    info.ci_mapping = fields;
    info.ci_mapping_items = sizeof(fields)/sizeof(*fields);

    int r = abrt_journal_core_retrieve_information(abrt_journal_watch_get_journal(watch), &info);
    if (r)
    {
        if (r < 0)
            error_msg(_("Failed to obtain all required information from journald"));

        goto watch_cleanup;
    }

    // do not dump too often
    //   ignore crashes of a single executable appearing in THROTTLE s (keep last 10 executable)
    const unsigned current = time(NULL);
    const unsigned last = abrt_journal_get_last_occurrence(info.ci_executable_path);

    if (current < last)
    {
        error_msg("BUG: current time stamp lower than an old one");

        if (g_verbose > 2)
            abort();

        goto watch_cleanup;
    }

    const unsigned sub = current - last;
    if (sub < conf->awc_throttle)
    {
        /* We don't want to update the counter here. */
        error_msg(_("Not saving repeating crash after %ds (limit is %ds)"), sub, conf->awc_throttle);
        goto watch_cleanup;
    }

    if (abrt_journal_core_to_abrt_problem(&info, conf->awc_dump_location))
    {
        error_msg(_("Failed to save detect problem data in abrt database"));
        goto watch_cleanup;
    }

    abrt_journal_update_occurrence(info.ci_executable_path, current);

watch_cleanup:
    abrt_journal_save_current_position(info.ci_journal, ABRT_JOURNAL_WATCH_STATE_FILE);

    if (info.ci_executable_path != NULL)
        free(info.ci_executable_path);

    return;
}

static void
watch_journald(abrt_journal_t *journal, abrt_watch_core_conf_t *conf)
{
    abrt_journal_watch_t *watch = NULL;
    if (abrt_journal_watch_new(&watch, journal, abrt_journal_watch_cores, (void *)conf) < 0)
        error_msg_and_die(_("Failed to initialize systemd-journal watch"));

    abrt_journal_watch_run_sync(watch);
    abrt_journal_watch_free(watch);
}

int
main(int argc, char *argv[])
{
    /* I18n */
    setlocale(LC_ALL, "");
#if ENABLE_NLS
    bindtextdomain(PACKAGE, LOCALEDIR);
    textdomain(PACKAGE);
#endif

    abrt_init(argv);

    /* Can't keep these strings/structs static: _() doesn't support that */
    const char *program_usage_string = _(
        "& [-vsf] [-e]/[-c CURSOR] [-t INT]/[-T] [-d DIR]/[-D]\n"
        "\n"
        "Extract coredumps from systemd-journal\n"
        "\n"
        "-c and -e options conflicts because both specifies the first read message.\n"
        "\n"
        "-e is useful only for -f because the following of journal starts by reading \n"
        "the entire journal if the last seen possition is not available.\n"
        "\n"
        "The last seen position is saved in "ABRT_JOURNAL_WATCH_STATE_FILE"\n"
    );
    enum {
        OPT_v = 1 << 0,
        OPT_s = 1 << 1,
        OPT_d = 1 << 2,
        OPT_D = 1 << 3,
        OPT_c = 1 << 4,
        OPT_e = 1 << 5,
        OPT_t = 1 << 6,
        OPT_T = 1 << 7,
        OPT_f = 1 << 8,
    };

    char *cursor = NULL;
    char *dump_location = NULL;
    int throttle = 0;

    /* Keep enum above and order of options below in sync! */
    struct options program_options[] = {
        OPT__VERBOSE(&g_verbose),
        OPT_BOOL(  's', NULL, NULL, _("Log to syslog")),
        OPT_STRING('d', NULL, &dump_location, "DIR", _("Create new problem directory in DIR for every coredump")),
        OPT_BOOL(  'D', NULL, NULL, _("Same as -d DumpLocation, DumpLocation is specified in abrt.conf")),
        OPT_STRING('c', NULL, &cursor, "CURSOR", _("Start reading systemd-journal from the CURSOR position")),
        OPT_BOOL(  'e', NULL, NULL, _("Start reading systemd-journal from the end")),
        OPT_INTEGER('t', NULL, &throttle, _("Throttle problem directory creation to 1 per INT second")),
        OPT_BOOL(  'T', NULL, NULL, _("Same as -t INT, INT is specified in plugins/CCpp.conf")),
        OPT_BOOL(  'f', NULL, NULL, _("Follow systemd-journal from the last seen position (if available)")),
        OPT_END()
    };
    unsigned opts = parse_opts(argc, argv, program_options, program_usage_string);

    export_abrt_envvars(0);

    if ((opts & OPT_s) || getenv("ABRT_SYSLOG"))
        logmode = LOGMODE_JOURNAL;

    if ((opts & OPT_c) && (opts & OPT_e))
        error_msg_and_die(_("You need to specify either -c CURSOR or -e"));

    /* Initialize ABRT configuration */
    load_abrt_conf();

    if (opts & OPT_D)
    {
        if (opts & OPT_d)
            show_usage_and_die(program_usage_string, program_options);
        dump_location = g_settings_dump_location;
    }

    {   /* Load CCpp.conf */
        map_string_t *settings = new_map_string();
        load_abrt_plugin_conf_file("CCpp.conf", settings);
        const char *value;

        /* The following commented out lines are not supported by
         * abrt-dump-journal-core but are supported by abrt-hook-ccpp */
        //value = get_map_string_item_or_NULL(settings, "MakeCompatCore");
        //setting_MakeCompatCore = value && string_to_bool(value);
        //value = get_map_string_item_or_NULL(settings, "SaveBinaryImage");
        //setting_SaveBinaryImage = value && string_to_bool(value);
        //value = get_map_string_item_or_NULL(settings, "SaveFullCore");
        //setting_SaveFullCore = value ? string_to_bool(value) : true;

        value = get_map_string_item_or_NULL(settings, "VerboseLog");
        if (value)
            g_verbose = xatoi_positive(value);

        free_map_string(settings);
    }

    /* systemd-coredump creates journal messages with SYSLOG_IDENTIFIER equals
     * 'systemd-coredump' and we are interested only in the systemd-coredump
     * messages.
     *
     * Of cores, it is possible to override this when need while debugging.
     */
    const char *const env_journal_filter = getenv("ABRT_DUMP_JOURNAL_CORE_DEBUG_FILTER");
    GList *coredump_journal_filter = NULL;
    coredump_journal_filter = g_list_append(coredump_journal_filter,
           (env_journal_filter ? (gpointer)env_journal_filter : (gpointer)"SYSLOG_IDENTIFIER=systemd-coredump"));

    abrt_journal_t *journal = NULL;
    if (abrt_journal_new(&journal))
        error_msg_and_die(_("Cannot open systemd-journal"));

    if (abrt_journal_set_journal_filter(journal, coredump_journal_filter) < 0)
        error_msg_and_die(_("Cannot filter systemd-journal to systemd-coredump data only"));

    g_list_free(coredump_journal_filter);

    if ((opts & OPT_e) && abrt_journal_seek_tail(journal) < 0)
        error_msg_and_die(_("Cannot seek to the end of journal"));

    if (cursor && abrt_journal_set_cursor(journal, cursor))
        error_msg_and_die(_("Failed to set systemd-journal cursor '%s'"), cursor);

    if ((opts & OPT_f))
    {
        if (!cursor && !(opts & OPT_e))
        {
            abrt_journal_restore_position(journal, ABRT_JOURNAL_WATCH_STATE_FILE);

            /* The stored position has already been seen, so move to the next one. */
            abrt_journal_next(journal);
        }

        abrt_watch_core_conf_t conf = {
            .awc_dump_location = dump_location,
            .awc_throttle = throttle,
        };

        watch_journald(journal, &conf);

        abrt_journal_save_current_position(journal, ABRT_JOURNAL_WATCH_STATE_FILE);
    }
    else
        abrt_journal_dump_core(journal, dump_location);

    abrt_journal_free(journal);
    free_abrt_conf_data();

    return EXIT_SUCCESS;
}
