/*
 * SPDX-License-Identifier: ISC
 *
 * Copyright (c) 2004-2005, 2010-2015, 2017-2018
 *	Todd C. Miller <Todd.Miller@sudo.ws>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <config.h>

#include <errno.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_STDBOOL_H
# include <stdbool.h>
#else
# include <compat/stdbool.h>
#endif /* HAVE_STDBOOL_H */
#include <unistd.h>
#ifndef HAVE_GETADDRINFO
# include <compat/getaddrinfo.h>
#endif

#include <sudo_compat.h>
#include <sudo_fatal.h>
#include <sudo_gettext.h>
#include <sudo_queue.h>
#include <sudo_util.h>
#include <sudo_plugin.h>

struct sudo_fatal_callback {
    SLIST_ENTRY(sudo_fatal_callback) entries;
    void (*func)(void);
};
SLIST_HEAD(sudo_fatal_callback_list, sudo_fatal_callback);

static struct sudo_fatal_callback_list callbacks = SLIST_HEAD_INITIALIZER(&callbacks);
static sudo_conv_t sudo_warn_conversation;
static sudo_warn_setlocale_t sudo_warn_setlocale;
static sudo_warn_setlocale_t sudo_warn_setlocale_prev;

static void warning(const char * restrict errstr, const char * restrict fmt, va_list ap);

static void
do_cleanup(void)
{
    struct sudo_fatal_callback *cb;

    /* Run callbacks, removing them from the list as we go. */
    while ((cb = SLIST_FIRST(&callbacks)) != NULL) {
	SLIST_REMOVE_HEAD(&callbacks, entries);
	cb->func();
	free(cb);
    }
}

sudo_noreturn void
sudo_fatal_nodebug_v1(const char * restrict fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    warning(strerror(errno), fmt, ap);
    va_end(ap);
    do_cleanup();
    exit(EXIT_FAILURE);
}

sudo_noreturn void
sudo_fatalx_nodebug_v1(const char * restrict fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    warning(NULL, fmt, ap);
    va_end(ap);
    do_cleanup();
    exit(EXIT_FAILURE);
}

sudo_noreturn void
sudo_vfatal_nodebug_v1(const char * restrict fmt, va_list ap)
{
    warning(strerror(errno), fmt, ap);
    do_cleanup();
    exit(EXIT_FAILURE);
}

sudo_noreturn void
sudo_vfatalx_nodebug_v1(const char * restrict fmt, va_list ap)
{
    warning(NULL, fmt, ap);
    do_cleanup();
    exit(EXIT_FAILURE);
}

void
sudo_warn_nodebug_v1(const char * restrict fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    warning(strerror(errno), fmt, ap);
    va_end(ap);
}

void
sudo_warnx_nodebug_v1(const char * restrict fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    warning(NULL, fmt, ap);
    va_end(ap);
}

void
sudo_vwarn_nodebug_v1(const char * restrict fmt, va_list ap)
{
    warning(strerror(errno), fmt, ap);
}

void
sudo_vwarnx_nodebug_v1(const char * restrict fmt, va_list ap)
{
    warning(NULL, fmt, ap);
}

sudo_noreturn void
sudo_gai_fatal_nodebug_v1(int errnum, const char * restrict fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    warning(gai_strerror(errnum), fmt, ap);
    va_end(ap);
    do_cleanup();
    exit(EXIT_FAILURE);
}

sudo_noreturn void
sudo_gai_vfatal_nodebug_v1(int errnum, const char * restrict fmt, va_list ap)
{
    warning(gai_strerror(errnum), fmt, ap);
    do_cleanup();
    exit(EXIT_FAILURE);
}

void
sudo_gai_warn_nodebug_v1(int errnum, const char * restrict fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    warning(gai_strerror(errnum), fmt, ap);
    va_end(ap);
}

void
sudo_gai_vwarn_nodebug_v1(int errnum, const char * restrict fmt, va_list ap)
{
    warning(gai_strerror(errnum), fmt, ap);
}

static void
warning(const char * restrict errstr, const char * restrict fmt, va_list ap)
{
    int cookie;
    const int saved_errno = errno;

    /* Set user locale if setter was specified. */
    if (sudo_warn_setlocale != NULL)
	sudo_warn_setlocale(false, &cookie);

    if (sudo_warn_conversation != NULL) {
	struct sudo_conv_message msgs[6];
	char static_buf[1024], *buf = static_buf;
	int nmsgs = 0;

	/* Use conversation function. */
        msgs[nmsgs].msg_type = SUDO_CONV_ERROR_MSG;
	msgs[nmsgs++].msg = getprogname();
        if (fmt != NULL) {
		va_list ap2;
		int buflen;

		/* Use static buffer if possible, else dynamic. */
		va_copy(ap2, ap);
		buflen = vsnprintf(static_buf, sizeof(static_buf), fmt, ap2);
		va_end(ap2);
		if (buflen >= ssizeof(static_buf)) {
		    /* Not enough room in static buf, allocate dynamically. */
		    if (vasprintf(&buf, fmt, ap) == -1)
			buf = static_buf;
		}
		if (buflen > 0) {
		    msgs[nmsgs].msg_type = SUDO_CONV_ERROR_MSG;
		    msgs[nmsgs++].msg = ": ";
		    msgs[nmsgs].msg_type = SUDO_CONV_ERROR_MSG;
		    msgs[nmsgs++].msg = buf;
		}
        }
        if (errstr != NULL) {
	    msgs[nmsgs].msg_type = SUDO_CONV_ERROR_MSG;
	    msgs[nmsgs++].msg = ": ";
	    msgs[nmsgs].msg_type = SUDO_CONV_ERROR_MSG;
	    msgs[nmsgs++].msg = errstr;
        }
	msgs[nmsgs].msg_type = SUDO_CONV_ERROR_MSG;
	msgs[nmsgs++].msg = "\n";
	sudo_warn_conversation(nmsgs, msgs, NULL, NULL);
	if (buf != static_buf)
	    free(buf);
    } else {
	/* Write to the standard error. */
        fputs(getprogname(), stderr);
        if (fmt != NULL) {
                fputs(": ", stderr);
                vfprintf(stderr, fmt, ap);
        }
        if (errstr != NULL) {
            fputs(": ", stderr);
            fputs(errstr, stderr);
        }
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
        if (sudo_term_is_raw(fileno(stderr)))
            putc('\r', stderr);
#endif
        putc('\n', stderr);
    }

    /* Restore old locale as needed. */
    if (sudo_warn_setlocale != NULL)
	sudo_warn_setlocale(true, &cookie);

    /* Do not clobber errno. */
    errno = saved_errno;
}

/*
 * Register a callback to be run when sudo_fatal()/sudo_fatalx() is called.
 */
int
sudo_fatal_callback_register_v1(sudo_fatal_callback_t func)
{
    struct sudo_fatal_callback *cb;

    /* Do not register the same callback twice.  */
    SLIST_FOREACH(cb, &callbacks, entries) {
	if (func == cb->func)
	    return -1;		/* dupe! */
    }

    /* Allocate and insert new callback. */
    cb = malloc(sizeof(*cb));
    if (cb == NULL)
	return -1;
    cb->func = func;
    SLIST_INSERT_HEAD(&callbacks, cb, entries);

    return 0;
}

/*
 * Deregister a sudo_fatal()/sudo_fatalx() callback.
 */
int
sudo_fatal_callback_deregister_v1(sudo_fatal_callback_t func)
{
    struct sudo_fatal_callback *cb, *prev = NULL;

    /* Search for callback and remove if found, dupes are not allowed. */
    SLIST_FOREACH(cb, &callbacks, entries) {
	if (cb->func == func) {
	    if (prev == NULL)
		SLIST_REMOVE_HEAD(&callbacks, entries);
	    else
		SLIST_REMOVE_AFTER(prev, entries);
	    free(cb);
	    return 0;
	}
	prev = cb;
    }

    return -1;
}

/*
 * Set the conversation function to use for output instead of the
 * standard error.  If conv is NULL, switch back to standard error.
 */
void
sudo_warn_set_conversation_v1(sudo_conv_t conv)
{
    sudo_warn_conversation = conv;
}

/*
 * Set the locale function so the plugin can use a non-default
 * locale for user warnings.
 */
void
sudo_warn_set_locale_func_v1(sudo_warn_setlocale_t func)
{
    sudo_warn_setlocale_prev = sudo_warn_setlocale;
    sudo_warn_setlocale = func;
}

#ifdef HAVE_LIBINTL_H
char *
sudo_warn_gettext_v1(const char *domainname, const char *msgid)
{
    int cookie;
    char *msg;

    /* Set user locale if setter was specified. */
    if (sudo_warn_setlocale != NULL)
	sudo_warn_setlocale(false, &cookie);

    msg = dgettext(domainname, msgid);

    /* Restore old locale as needed. */
    if (sudo_warn_setlocale != NULL)
	sudo_warn_setlocale(true, &cookie);

    return msg;
}
#endif /* HAVE_LIBINTL_H */
