/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "config.h"

#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <locale.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <pwd.h>

#include <glib.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>

#include "gdm-common.h"

#ifndef HAVE_MKDTEMP
#include "mkdtemp.h"
#endif

const char *
gdm_make_temp_dir (char *template)
{
        return mkdtemp (template);
}

gboolean
gdm_is_version_unstable (void)
{
        char   **versions;
        gboolean unstable;

        unstable = FALSE;

        versions = g_strsplit (VERSION, ".", 3);
        if (versions && versions [0] && versions [1]) {
                int major;
                major = atoi (versions [1]);
                if ((major % 2) != 0) {
                        unstable = TRUE;
                }
        }
        g_strfreev (versions);

        return unstable;
}

gboolean
gdm_clear_close_on_exec_flag (int fd)
{
        int flags;

        if (fd < 0) {
                return FALSE;
        }

        flags = fcntl (fd, F_GETFD, 0);

        if (flags < 0) {
                return FALSE;
        }

        if ((flags & FD_CLOEXEC) != 0) {
                int status;

                status = fcntl (fd, F_SETFD, flags & ~FD_CLOEXEC);

                return status != -1;
        }

        return TRUE;
}

gboolean
gdm_get_pwent_for_name (const char     *name,
                        struct passwd **pwentp)
{
        struct passwd *pwent;

        do {
                errno = 0;
                pwent = getpwnam (name);
        } while (pwent == NULL && errno == EINTR);

        if (pwentp != NULL) {
                *pwentp = pwent;
        }

        return (pwent != NULL);
}

int
gdm_wait_on_and_disown_pid (int pid,
                            int timeout)
{
        int status;
        int ret;
        int num_tries;
        int flags;
        gboolean already_reaped;

        if (timeout > 0) {
                flags = WNOHANG;
                num_tries = 10 * timeout;
        } else {
                flags = 0;
                num_tries = 0;
        }
 wait_again:
        errno = 0;
        already_reaped = FALSE;
        ret = waitpid (pid, &status, flags);
        if (ret < 0) {
                if (errno == EINTR) {
                        goto wait_again;
                } else if (errno == ECHILD) {
                        already_reaped = TRUE;
                } else {
                        g_debug ("GdmCommon: waitpid () should not fail");
                }
        } else if (ret == 0) {
                num_tries--;

                if (num_tries > 0) {
                        g_usleep (G_USEC_PER_SEC / 10);
                } else {
                        char *path;
                        char *command;

                        path = g_strdup_printf ("/proc/%ld/cmdline", (long) pid);
                        if (g_file_get_contents (path, &command, NULL, NULL)) {;
                                g_warning ("GdmCommon: process (pid:%d, command '%s') isn't dying after %d seconds, now ignoring it.",
                                         (int) pid, command, timeout);
                                g_free (command);
                        } else {
                                g_warning ("GdmCommon: process (pid:%d) isn't dying after %d seconds, now ignoring it.",
                                         (int) pid, timeout);
                        }
                        g_free (path);

                        return 0;
                }
                goto wait_again;
        }

        g_debug ("GdmCommon: process (pid:%d) done (%s:%d)",
                 (int) pid,
                 already_reaped? "reaped earlier" :
                 WIFEXITED (status) ? "status"
                 : WIFSIGNALED (status) ? "signal"
                 : "unknown",
                 already_reaped? 1 :
                 WIFEXITED (status) ? WEXITSTATUS (status)
                 : WIFSIGNALED (status) ? WTERMSIG (status)
                 : -1);

        return status;
}

int
gdm_wait_on_pid (int pid)
{
    return gdm_wait_on_and_disown_pid (pid, 0);
}

int
gdm_signal_pid (int pid,
                int signal)
{
        int status = -1;

        /* perhaps block sigchld */
        g_debug ("GdmCommon: sending signal %d to process %d", signal, pid);
        errno = 0;
        status = kill (pid, signal);

        if (status < 0) {
                if (errno == ESRCH) {
                        g_warning ("Child process %d was already dead.",
                                   (int)pid);
                } else {
                        g_warning ("Couldn't kill child process %d: %s",
                                   pid,
                                   g_strerror (errno));
                }
        }

        /* perhaps unblock sigchld */

        return status;
}

/* hex conversion adapted from D-Bus */
/**
 * Appends a two-character hex digit to a string, where the hex digit
 * has the value of the given byte.
 *
 * @param str the string
 * @param byte the byte
 */
static void
_gdm_string_append_byte_as_hex (GString *str,
                                int      byte)
{
        const char hexdigits[16] = {
                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                'a', 'b', 'c', 'd', 'e', 'f'
        };

        str = g_string_append_c (str, hexdigits[(byte >> 4)]);

        str = g_string_append_c (str, hexdigits[(byte & 0x0f)]);
}

/**
 * Encodes a string in hex, the way MD5 and SHA-1 are usually
 * encoded. (Each byte is two hex digits.)
 *
 * @param source the string to encode
 * @param start byte index to start encoding
 * @param dest string where encoded data should be placed
 * @param insert_at where to place encoded data
 * @returns #TRUE if encoding was successful, #FALSE if no memory etc.
 */
gboolean
gdm_string_hex_encode (const GString *source,
                       int            start,
                       GString       *dest,
                       int            insert_at)
{
        GString             *result;
        const unsigned char *p;
        const unsigned char *end;
        gboolean             retval;

        g_return_val_if_fail (source != NULL, FALSE);
        g_return_val_if_fail (dest != NULL, FALSE);
        g_return_val_if_fail (source != dest, FALSE);
        g_return_val_if_fail (start >= 0, FALSE);
        g_return_val_if_fail (dest >= 0, FALSE);
        g_assert (start <= source->len);

        result = g_string_new (NULL);

        retval = FALSE;

        p = (const unsigned char*) source->str;
        end = p + source->len;
        p += start;

        while (p != end) {
                _gdm_string_append_byte_as_hex (result, *p);
                ++p;
        }

        dest = g_string_insert (dest, insert_at, result->str);

        retval = TRUE;

        g_string_free (result, TRUE);

        return retval;
}

/**
 * Decodes a string from hex encoding.
 *
 * @param source the string to decode
 * @param start byte index to start decode
 * @param end_return return location of the end of the hex data, or #NULL
 * @param dest string where decoded data should be placed
 * @param insert_at where to place decoded data
 * @returns #TRUE if decoding was successful, #FALSE if no memory.
 */
gboolean
gdm_string_hex_decode (const GString *source,
                       int            start,
                       int           *end_return,
                       GString       *dest,
                       int            insert_at)
{
        GString             *result;
        const unsigned char *p;
        const unsigned char *end;
        gboolean             retval;
        gboolean             high_bits;

        g_return_val_if_fail (source != NULL, FALSE);
        g_return_val_if_fail (dest != NULL, FALSE);
        g_return_val_if_fail (source != dest, FALSE);
        g_return_val_if_fail (start >= 0, FALSE);
        g_return_val_if_fail (dest >= 0, FALSE);

        g_assert (start <= source->len);

        result = g_string_new (NULL);

        retval = FALSE;

        high_bits = TRUE;
        p = (const unsigned char*) source->str;
        end = p + source->len;
        p += start;

        while (p != end) {
                unsigned int val;

                switch (*p) {
                case '0':
                        val = 0;
                        break;
                case '1':
                        val = 1;
                        break;
                case '2':
                        val = 2;
                        break;
                case '3':
                        val = 3;
                        break;
                case '4':
                        val = 4;
                        break;
                case '5':
                        val = 5;
                        break;
                case '6':
                        val = 6;
                        break;
                case '7':
                        val = 7;
                        break;
                case '8':
                        val = 8;
                        break;
                case '9':
                        val = 9;
                        break;
                case 'a':
                case 'A':
                        val = 10;
                        break;
                case 'b':
                case 'B':
                        val = 11;
                        break;
                case 'c':
                case 'C':
                        val = 12;
                        break;
                case 'd':
                case 'D':
                        val = 13;
                        break;
                case 'e':
                case 'E':
                        val = 14;
                        break;
                case 'f':
                case 'F':
                        val = 15;
                        break;
                default:
                        goto done;
                }

                if (high_bits) {
                        result = g_string_append_c (result, val << 4);
                } else {
                        int           len;
                        unsigned char b;

                        len = result->len;

                        b = result->str[len - 1];

                        b |= val;

                        result->str[len - 1] = b;
                }

                high_bits = !high_bits;

                ++p;
        }

 done:
        dest = g_string_insert (dest, insert_at, result->str);

        if (end_return) {
                *end_return = p - (const unsigned char*) source->str;
        }

        retval = TRUE;

        g_string_free (result, TRUE);

        return retval;
}

static gboolean
_fd_is_character_device (int fd)
{
        struct stat file_info;

        if (fstat (fd, &file_info) < 0) {
                return FALSE;
        }

        return S_ISCHR (file_info.st_mode);
}

static gboolean
_read_bytes (int      fd,
             char    *bytes,
             gsize    number_of_bytes,
             GError **error)
{
        size_t bytes_left_to_read;
        size_t total_bytes_read = 0;
        gboolean premature_eof;

        bytes_left_to_read = number_of_bytes;
        premature_eof = FALSE;
        do {
                size_t bytes_read = 0;

                errno = 0;
                bytes_read = read (fd, ((guchar *) bytes) + total_bytes_read,
                                   bytes_left_to_read);

                if (bytes_read > 0) {
                        total_bytes_read += bytes_read;
                        bytes_left_to_read -= bytes_read;
                } else if (bytes_read == 0) {
                        premature_eof = TRUE;
                        break;
                } else if ((errno != EINTR)) {
                        break;
                }
        } while (bytes_left_to_read > 0);

        if (premature_eof) {
                g_set_error (error,
                             G_FILE_ERROR,
                             G_FILE_ERROR_FAILED,
                             "No data available");

                return FALSE;
        } else if (bytes_left_to_read > 0) {
                g_set_error (error,
                             G_FILE_ERROR,
                             g_file_error_from_errno (errno),
                             "%s", g_strerror (errno));
                return FALSE;
        }

        return TRUE;
}

/**
 * Pulls a requested number of bytes from /dev/urandom
 *
 * @param size number of bytes to pull
 * @param error error if read fails
 * @returns The requested number of random bytes or #NULL if fail
 */

char *
gdm_generate_random_bytes (gsize    size,
                           GError **error)
{
        int fd;
        char *bytes;
        GError *read_error;

        /* We don't use the g_rand_* glib apis because they don't document
         * how much entropy they are seeded with, and it might be less
         * than the passed in size.
         */

        errno = 0;
        fd = open ("/dev/urandom", O_RDONLY);

        if (fd < 0) {
                g_set_error (error,
                             G_FILE_ERROR,
                             g_file_error_from_errno (errno),
                             "%s", g_strerror (errno));
                close (fd);
                return NULL;
        }

        if (!_fd_is_character_device (fd)) {
                g_set_error (error,
                             G_FILE_ERROR,
                             g_file_error_from_errno (ENODEV),
                             _("/dev/urandom is not a character device"));
                close (fd);
                return NULL;
        }

        bytes = g_malloc (size);
        read_error = NULL;
        if (!_read_bytes (fd, bytes, size, &read_error)) {
                g_propagate_error (error, read_error);
                g_free (bytes);
                close (fd);
                return NULL;
        }

        close (fd);
        return bytes;
}
