/*
 * Copyright © 2019-2020 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 <stdlib.h>
#include <unistd.h>

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

#include "glib-backports.h"
#include "libglnx/libglnx.h"

#include "resolve-in-sysroot.h"
#include "test-utils.h"
#include "utils.h"

typedef struct
{
  TestsOpenFdSet old_fds;
} Fixture;

static void
setup (Fixture *f,
       gconstpointer context)
{
  f->old_fds = tests_check_fd_leaks_enter ();
}

static void
teardown (Fixture *f,
          gconstpointer context)
{
  tests_check_fd_leaks_leave (f->old_fds);
}

static gboolean
fd_same_as_rel_path_nofollow (int fd,
                              int dfd,
                              const gchar *path)
{
  GStatBuf fd_buffer, path_buffer;

  return (fstat (fd, &fd_buffer) == 0
          && fstatat (dfd, path, &path_buffer, AT_SYMLINK_NOFOLLOW) == 0
          && fd_buffer.st_dev == path_buffer.st_dev
          && fd_buffer.st_ino == path_buffer.st_ino);
}

typedef struct
{
  const char *name;
  const char *target;
} Symlink;

typedef enum
{
  RESOLVE_ERROR_DOMAIN_NONE,
  RESOLVE_ERROR_DOMAIN_GIO,
} ResolveErrorDomain;

typedef enum
{
  RESOLVE_CALL_FLAGS_IGNORE_PATH = (1 << 0),
  RESOLVE_CALL_FLAGS_NONE = 0
} ResolveCallFlags;

typedef struct
{
  struct
  {
    const char *path;
    PvResolveFlags flags;
    ResolveCallFlags test_flags;
  } call;
  struct
  {
    const char *path;
    int code;
  } expect;
} ResolveTest;

static void
test_resolve_in_sysroot (Fixture *f,
                         gconstpointer context)
{
  static const char * const prepare_dirs[] =
  {
    "a/b/c/d/e",
    "a/b2/c2/d2/e2",
  };
  static const Symlink prepare_symlinks[] =
  {
    { "a/b/symlink_to_c", "c" },
    { "a/b/symlink_to_b2", "../b2" },
    { "a/b/symlink_to_c2", "../../a/b2/c2" },
    { "a/b/abs_symlink_to_run", "/run" },
    { "a/b/long_symlink_to_dev", "../../../../../../../../../../../dev" },
    { "x", "create_me" },
  };
  static const ResolveTest tests[] =
  {
    { { "a/b/c/d" }, { "a/b/c/d" } },
    {
      { "a/b/c/d", PV_RESOLVE_FLAGS_NONE, RESOLVE_CALL_FLAGS_IGNORE_PATH },
      { "a/b/c/d" },
    },
    { { "a/b/c/d", PV_RESOLVE_FLAGS_MKDIR_P }, { "a/b/c/d" } },
    {
      { "a/b/c/d", PV_RESOLVE_FLAGS_MKDIR_P, RESOLVE_CALL_FLAGS_IGNORE_PATH },
      { "a/b/c/d" },
    },
    { { "create_me" }, { NULL, G_IO_ERROR_NOT_FOUND } },
    {
      { "create_me", PV_RESOLVE_FLAGS_NONE, RESOLVE_CALL_FLAGS_IGNORE_PATH },
      { NULL, G_IO_ERROR_NOT_FOUND }
    },
    { { "a/b/c/d", PV_RESOLVE_FLAGS_MKDIR_P }, { "a/b/c/d" } },
    { { "a/b///////.////./././///././c/d" }, { "a/b/c/d" } },
    { { "/a/b///////.////././../b2////././c2/d2" }, { "a/b2/c2/d2" } },
    { { "a/b/c/d/e/f" }, { NULL, G_IO_ERROR_NOT_FOUND } },
    { { "a/b/c/d/e/f", PV_RESOLVE_FLAGS_MKDIR_P }, { "a/b/c/d/e/f" } },
    { { "a/b/c/d/e/f" }, { "a/b/c/d/e/f" } },
    { { "a/b/c/d/e/f", PV_RESOLVE_FLAGS_MKDIR_P }, { "a/b/c/d/e/f" } },
    { { "a3/b3/c3" }, { NULL, G_IO_ERROR_NOT_FOUND } },
    { { "a3/b3/c3", PV_RESOLVE_FLAGS_MKDIR_P }, { "a3/b3/c3" } },
    { { "a/b/symlink_to_c" }, { "a/b/c" } },
    { { "a/b/symlink_to_c/d" }, { "a/b/c/d" } },
    {
      { "a/b/symlink_to_c/d", PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK },
      { "a/b/c/d" }
    },
    {
      { "a/b/symlink_to_c/d", PV_RESOLVE_FLAGS_REJECT_SYMLINKS },
      { NULL, G_IO_ERROR_TOO_MANY_LINKS }
    },
    { { "a/b/symlink_to_b2" }, { "a/b2" } },
    { { "a/b/symlink_to_c2" }, { "a/b2/c2" } },
    { { "a/b/abs_symlink_to_run" }, { NULL, G_IO_ERROR_NOT_FOUND } },
    {
      { "a/b/abs_symlink_to_run", PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK },
      { "a/b/abs_symlink_to_run" }
    },
    { { "run" }, { NULL, G_IO_ERROR_NOT_FOUND } },    /* Wasn't created yet */
    { { "a/b/abs_symlink_to_run", PV_RESOLVE_FLAGS_MKDIR_P }, { "run" } },
    { { "a/b/abs_symlink_to_run/host" }, { NULL, G_IO_ERROR_NOT_FOUND } },
    { { "a/b/abs_symlink_to_run/host", PV_RESOLVE_FLAGS_MKDIR_P }, { "run/host" } },
    { { "a/b/long_symlink_to_dev" }, { NULL, G_IO_ERROR_NOT_FOUND } },
    { { "a/b/long_symlink_to_dev/shm" }, { NULL, G_IO_ERROR_NOT_FOUND } },
    { { "a/b/long_symlink_to_dev/shm", PV_RESOLVE_FLAGS_MKDIR_P }, { "dev/shm" } },
    { { "a/b/../b2/c2/../c3", PV_RESOLVE_FLAGS_MKDIR_P }, { "a/b2/c3" } },
    { { "x" }, { NULL, G_IO_ERROR_NOT_FOUND } },
    { { "x", PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK }, { "x" } },
    /* This is a bit odd: unlike mkdir -p, we create targets for dangling
     * symlinks. It's easier to do this than not, and for pressure-vessel's
     * use-case it probably even makes more sense than not. */
    { { "x/y" }, { NULL, G_IO_ERROR_NOT_FOUND } },
    { { "x/y", PV_RESOLVE_FLAGS_MKDIR_P }, { "create_me/y" } },
  };
  g_autoptr(GError) error = NULL;
  g_auto(GLnxTmpDir) tmpdir = { FALSE };
  gsize i;

  glnx_mkdtemp ("test-XXXXXX", 0700, &tmpdir, &error);
  g_assert_no_error (error);

  for (i = 0; i < G_N_ELEMENTS (prepare_dirs); i++)
    {
      const char *it = prepare_dirs[i];

      glnx_shutil_mkdir_p_at (tmpdir.fd, it, 0700, NULL, &error);
      g_assert_no_error (error);
    }

  for (i = 0; i < G_N_ELEMENTS (prepare_symlinks); i++)
    {
      const Symlink *it = &prepare_symlinks[i];

      if (symlinkat (it->target, tmpdir.fd, it->name) != 0)
        g_error ("symlinkat %s: %s", it->name, g_strerror (errno));
    }

  for (i = 0; i < G_N_ELEMENTS (tests); i++)
    {
      const ResolveTest *it = &tests[i];
      glnx_autofd int fd = -1;
      g_autofree gchar *path = NULL;
      gchar **out_path;
      g_autoptr(GString) description = g_string_new ("");
      TestsOpenFdSet old_fds;

      old_fds = tests_check_fd_leaks_enter ();

      if (it->call.flags & PV_RESOLVE_FLAGS_MKDIR_P)
        g_string_append (description, " (creating directories)");

      if (it->call.flags & PV_RESOLVE_FLAGS_KEEP_FINAL_SYMLINK)
        g_string_append (description, " (not following final symlink)");

      if (it->call.flags & PV_RESOLVE_FLAGS_REJECT_SYMLINKS)
        g_string_append (description, " (not following any symlink)");

      g_test_message ("%" G_GSIZE_FORMAT ": Resolving %s%s",
                      i, it->call.path, description->str);

      if (it->call.test_flags & RESOLVE_CALL_FLAGS_IGNORE_PATH)
        out_path = NULL;
      else
        out_path = &path;

      fd = pv_resolve_in_sysroot (tmpdir.fd, it->call.path,
                                  it->call.flags, out_path, &error);

      if (it->expect.path != NULL)
        {
          g_assert_no_error (error);
          g_assert_cmpint (fd, >=, 0);

          if (out_path != NULL)
            g_assert_cmpstr (*out_path, ==, it->expect.path);

          g_assert_true (fd_same_as_rel_path_nofollow (fd, tmpdir.fd,
                                                       it->expect.path));
        }
      else
        {
          g_assert_error (error, G_IO_ERROR, it->expect.code);
          g_assert_cmpint (fd, ==, -1);

          if (out_path != NULL)
            g_assert_cmpstr (*out_path, ==, NULL);

          g_clear_error (&error);
        }

      glnx_close_fd (&fd);
      tests_check_fd_leaks_leave (old_fds);
    }
}

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

  g_test_init (&argc, &argv, NULL);
  g_test_add ("/resolve-in-sysroot", Fixture, NULL,
              setup, test_resolve_in_sysroot, teardown);

  return g_test_run ();
}