/*
 * Copyright © 2018 Collabora Ltd.
 * SPDX-License-Identifier: MIT
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge,
 * publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <config.h>

#include <dbus/dbus.h>
#include "dbus/dbus-sysdeps.h"
#include "test-utils-glib.h"

#ifdef G_OS_UNIX
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "dbus/dbus-sysdeps-unix.h"
#endif

typedef struct
{
  size_t n_fds;
  int *fds;
} Fixture;

static void
setup (Fixture *f G_GNUC_UNUSED,
       gconstpointer context G_GNUC_UNUSED)
{
}

/* In an unfortunate clash of terminology, GPid is actually a process
 * handle (a HANDLE, vaguely analogous to a file descriptor) on Windows.
 * For _dbus_command_for_pid() we need an actual process ID. */
#ifdef G_OS_UNIX
# define NO_PROCESS 0
# ifndef G_PID_FORMAT
#   define G_PID_FORMAT "i"
# endif

static unsigned long
get_pid (pid_t process)
{
  return process;
}

static void
terminate_and_wait (pid_t process)
{
  int ret;

  kill (process, SIGTERM);

  do
    {
      ret = waitpid (process, NULL, 0);
    }
  while (ret == -1 && errno == EINTR);
}
#else
# define NO_PROCESS NULL
# ifndef G_PID_FORMAT
#   define G_PID_FORMAT "p"
# endif

static unsigned long
get_pid (HANDLE process)
{
  return GetProcessId (process);
}

static void
terminate_and_wait (HANDLE process)
{
  TerminateProcess (process, 1);
  WaitForSingleObject (process, INFINITE);
}
#endif

static void
test_command_for_pid (Fixture *f,
                      gconstpointer context G_GNUC_UNUSED)
{
  gchar *argv[] = { NULL, NULL, NULL };
  GError *error = NULL;
  GPid process = NO_PROCESS;
  unsigned long pid;
  DBusError d_error = DBUS_ERROR_INIT;
  DBusString string;

  argv[0] = test_get_helper_executable ("test-sleep-forever" DBUS_EXEEXT);

  if (argv[0] == NULL)
    {
      g_test_skip ("DBUS_TEST_EXEC not set");
      return;
    }

  argv[1] = g_strdup ("bees");

  if (!g_spawn_async (NULL, argv, NULL,
                      (G_SPAWN_STDOUT_TO_DEV_NULL |
                       G_SPAWN_STDERR_TO_DEV_NULL |
                       G_SPAWN_DO_NOT_REAP_CHILD),
                      NULL, NULL,
                      &process, &error))
    g_error ("Unable to run %s: %s", argv[0], error->message);

  pid = get_pid (process);
  g_test_message ("Process ID of process handle %" G_PID_FORMAT ": %lu",
                  process, pid);

  if (!_dbus_string_init (&string))
    g_error ("out of memory");

  if (_dbus_command_for_pid (pid, &string, strlen (argv[0]) + 1024, &d_error))
    {
      gchar *expected;

      g_test_message ("Process %lu: \"%s\"", pid,
                      _dbus_string_get_const_data (&string));

      expected = g_strdup_printf ("%s %s", argv[0], argv[1]);

      g_assert_cmpstr (_dbus_string_get_const_data (&string), ==,
                       expected);
      g_assert_cmpuint (_dbus_string_get_length (&string), ==,
                        strlen (expected));
      g_free (expected);
    }
  else
    {
      g_test_message ("Unable to get command for process %lu: %s: %s",
                      pid, d_error.name, d_error.message);
      g_assert_nonnull (d_error.name);
      g_assert_nonnull (d_error.message);
      dbus_error_free (&d_error);
    }

  if (!_dbus_string_set_length (&string, 0))
    g_error ("out of memory");

  /* Test truncation */
  if (_dbus_command_for_pid (pid, &string, 10, NULL))
    {
      gchar *expected;

      g_test_message ("Process %lu (truncated): \"%s\"", pid,
                      _dbus_string_get_const_data (&string));

      expected = g_strdup_printf ("%s %s", argv[0], argv[1]);
      expected[10] = '\0';

      g_assert_cmpstr (_dbus_string_get_const_data (&string), ==,
                       expected);
      g_assert_cmpuint (_dbus_string_get_length (&string), ==, 10);
      g_free (expected);
    }
  else
    {
      g_test_message ("Unable to get command for process %lu", pid);
    }

  if (process != NO_PROCESS)
    terminate_and_wait (process);

  _dbus_string_free (&string);
  g_spawn_close_pid (process);
  g_free (argv[1]);
  g_free (argv[0]);
}

#ifdef G_OS_UNIX
static gboolean
check_valid_fd (int fd,
                gboolean *close_on_exec)
{
  int flags = fcntl (fd, F_GETFD);

  if (flags < 0)
    {
      int saved_errno = errno;

      g_assert_cmpint (saved_errno, ==, EBADF);
      return FALSE;
    }

  if (close_on_exec != NULL)
    *close_on_exec = ((flags & FD_CLOEXEC) != 0);

  return TRUE;
}

static void
fixture_open_some_fds (Fixture *f)
{
  const size_t n = 50;  /* must be divisible by 2, otherwise arbitrary */
  size_t i;
  const char *error_str = NULL;

  /* Ensure that fds[] will not contain 0, 1 or 2 */
  if (!_dbus_ensure_standard_fds (0, &error_str))
    g_error ("_dbus_ensure_standard_fds: %s (%s)",
             error_str, g_strerror (errno));

  g_assert_cmpuint (f->n_fds, ==, 0);
  g_assert_null (f->fds);
  f->n_fds = n;
  f->fds = g_new0 (int, n);

  for (i = 0; i < n; i++)
    f->fds[i] = -1;

  for (i = 0; i < n; i += 2)
    {
      gboolean cloexec = TRUE;

      if (pipe (&f->fds[i]) < 0)
        g_error ("pipe: %s", g_strerror (errno));

      g_assert_cmpint (f->fds[i], >=, 3);
      g_assert_cmpint (f->fds[i + 1], >=, 3);
      _dbus_fd_clear_close_on_exec (f->fds[i]);
      _dbus_fd_clear_close_on_exec (f->fds[i + 1]);
      g_assert_true (check_valid_fd (f->fds[i], &cloexec));
      g_assert_false (cloexec);
      g_assert_true (check_valid_fd (f->fds[i + 1], &cloexec));
      g_assert_false (cloexec);
    }
}

static void
test_close_all (Fixture *f,
                gconstpointer context G_GNUC_UNUSED)
{
  size_t i;

  fixture_open_some_fds (f);

  for (i = 0; i < f->n_fds; i++)
    g_assert_true (check_valid_fd (f->fds[i], NULL));

  _dbus_close_all ();

  for (i = 0; i < f->n_fds; i++)
    {
      g_assert_false (check_valid_fd (f->fds[i], NULL));
      /* Don't re-close the fd in teardown */
      f->fds[i] = -1;
    }
}

static void
test_set_all_close_on_exec (Fixture *f,
                            gconstpointer context G_GNUC_UNUSED)
{
  size_t i;

  fixture_open_some_fds (f);

  for (i = 0; i < f->n_fds; i++)
    {
      gboolean cloexec = TRUE;

      g_assert_true (check_valid_fd (f->fds[i], &cloexec));
      g_assert_false (cloexec);
    }

  _dbus_fd_set_all_close_on_exec ();

  for (i = 0; i < f->n_fds; i++)
    {
      gboolean cloexec = FALSE;

      g_assert_true (check_valid_fd (f->fds[i], &cloexec));
      g_assert_true (cloexec);
    }
}
#endif

static void
teardown (Fixture *f G_GNUC_UNUSED,
          gconstpointer context G_GNUC_UNUSED)
{
#ifdef G_OS_UNIX
  size_t i;

  for (i = 0; i < f->n_fds; i++)
    {
      if (f->fds[i] >= 0)
        _dbus_close (f->fds[i], NULL);
    }

  g_free (f->fds);
#endif
}

int
main (int argc,
      char **argv)
{
  int ret;

  test_init (&argc, &argv);

  g_test_add ("/sysdeps/command_for_pid",
              Fixture, NULL, setup, test_command_for_pid, teardown);

#ifdef G_OS_UNIX
  g_test_add ("/sysdeps/close_all",
              Fixture, NULL, setup, test_close_all, teardown);
  g_test_add ("/sysdeps/set_all_close_on_exec",
              Fixture, NULL, setup, test_set_all_close_on_exec, teardown);
#endif

  ret = g_test_run ();
  dbus_shutdown ();
  return ret;
}
