/*
 * Copyright (C) 2015 Red Hat, Inc.
 *
 * SPDX-License-Identifier: LGPL-2.0+
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <https://www.gnu.org/licenses/>.
 */

#include "config.h"
#include "libglnx.h"
#include <glib.h>
#include <stdlib.h>
#include <gio/gio.h>
#include <string.h>

#include <ostree.h>
#include "ostree-libarchive-private.h"
#include <archive.h>
#include <archive_entry.h>

typedef struct {
  OstreeRepo *repo;
  int fd;
  int fd_empty;
  char *tmpd;
  GError *skip_all;
} TestData;

static void
test_data_init (TestData *td)
{
  GError *error = NULL;
  g_autoptr(OtAutoArchiveWrite) a = archive_write_new ();
  struct archive_entry *ae;
  uid_t uid = getuid ();
  gid_t gid = getgid ();

  td->tmpd = g_mkdtemp (g_strdup ("/var/tmp/test-libarchive-import-XXXXXX"));
  g_assert_cmpint (0, ==, chdir (td->tmpd));

  td->fd = openat (AT_FDCWD, "foo.tar.gz", O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC, 0644);
  (void) unlink ("foo.tar.gz");

  g_assert_no_error (error);
  g_assert (td->fd >= 0);
  
  g_assert_cmpint (0, ==, archive_write_set_format_pax (a));
  g_assert_cmpint (0, ==, archive_write_add_filter_gzip (a));
  g_assert_cmpint (0, ==, archive_write_open_fd (a, td->fd));

  ae = archive_entry_new ();
  archive_entry_set_pathname (ae, "/");
  archive_entry_set_mode (ae, S_IFDIR | 0755);
  archive_entry_set_uid (ae, uid);
  archive_entry_set_gid (ae, gid);
  g_assert_cmpint (0, ==, archive_write_header (a, ae));
  archive_entry_free (ae);

  ae = archive_entry_new ();
  archive_entry_set_pathname (ae, "/file");
  archive_entry_set_mode (ae, S_IFREG | 0777);
  archive_entry_set_uid (ae, uid);
  archive_entry_set_gid (ae, gid);
  archive_entry_set_size (ae, 4);
  g_assert_cmpint (0, ==, archive_write_header (a, ae));
  g_assert_cmpint (4, ==, archive_write_data (a, "foo\n", 4));
  archive_entry_free (ae);

  ae = archive_entry_new ();
  archive_entry_set_pathname (ae, "/devnull");
  archive_entry_set_mode (ae, S_IFCHR | 0777);
  archive_entry_set_uid (ae, uid);
  archive_entry_set_gid (ae, gid);
  archive_entry_set_devmajor (ae, 1);
  archive_entry_set_devminor (ae, 3);
  g_assert_cmpint (0, ==, archive_write_header (a, ae));
  archive_entry_free (ae);

  ae = archive_entry_new ();
  archive_entry_set_pathname (ae, "/anotherfile");
  archive_entry_set_mode (ae, S_IFREG | 0777);
  archive_entry_set_uid (ae, uid);
  archive_entry_set_gid (ae, gid);
  archive_entry_set_size (ae, 4);
  archive_entry_xattr_add_entry (ae, "user.a_key", "my value", 8);
  archive_entry_xattr_add_entry (ae, "user.key2", "", 0);
  archive_entry_xattr_add_entry (ae, "user.b_key", "contains\0nuls", 14);
  g_assert_cmpint (0, ==, archive_write_header (a, ae));
  g_assert_cmpint (4, ==, archive_write_data (a, "bar\n", 4));
  archive_entry_free (ae);

  ae = archive_entry_new ();
  archive_entry_set_pathname (ae, "/etc");
  archive_entry_set_mode (ae, S_IFDIR | 0755);
  archive_entry_set_uid (ae, uid);
  archive_entry_set_gid (ae, gid);
  g_assert_cmpint (0, ==, archive_write_header (a, ae));
  archive_entry_free (ae);

  ae = archive_entry_new ();
  archive_entry_set_pathname (ae, "/etc/file");
  archive_entry_set_mode (ae, S_IFREG | 0777);
  archive_entry_set_uid (ae, uid);
  archive_entry_set_gid (ae, gid);
  archive_entry_set_size (ae, 4);
  g_assert_cmpint (0, ==, archive_write_header (a, ae));
  g_assert_cmpint (4, ==, archive_write_data (a, "bar\n", 4));
  archive_entry_free (ae);

  g_assert_cmpint (ARCHIVE_OK, ==, archive_write_close (a));

  td->fd_empty = openat (AT_FDCWD, "empty.tar.gz", O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC, 0644);
  g_assert (td->fd_empty >= 0);
  (void) unlink ("empty.tar.gz");

  g_assert_cmpint (ARCHIVE_OK, ==, archive_write_free (a));
  a = archive_write_new ();
  g_assert (a);

  g_assert_cmpint (0, ==, archive_write_set_format_pax (a));
  g_assert_cmpint (0, ==, archive_write_add_filter_gzip (a));
  g_assert_cmpint (0, ==, archive_write_open_fd (a, td->fd_empty));
  g_assert_cmpint (ARCHIVE_OK, ==, archive_write_close (a));

  { g_autoptr(GFile) repopath = g_file_new_for_path ("repo");
    td->repo = ostree_repo_new (repopath);

    g_assert_cmpint (0, ==, mkdir ("repo", 0755));

    ostree_repo_create (td->repo, OSTREE_REPO_MODE_BARE_USER, NULL, &error);

    /* G_IO_ERROR_NOT_SUPPORTED probably means no extended attribute support */
    if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
      g_propagate_prefixed_error (&td->skip_all, g_steal_pointer (&error),
                                  "Unable to set up repository: ");
    else
      g_assert_no_error (error);
  }
}

static gboolean
spawn_cmdline (const char *cmd, GError **error)
{
  int estatus;
  if (!g_spawn_command_line_sync (cmd, NULL, NULL, &estatus, error))
    return FALSE;
  if (!g_spawn_check_exit_status (estatus, error))
    return FALSE;
  return TRUE;
}

static void
test_archive_setup (int fd, struct archive *a)
{
  g_assert_cmpint (0, ==, lseek (fd, 0, SEEK_SET));
  g_assert_cmpint (0, ==, archive_read_support_format_all (a));
  g_assert_cmpint (0, ==, archive_read_support_filter_all (a));
  g_assert_cmpint (0, ==, archive_read_open_fd (a, fd, 8192));
}

static void
test_libarchive_noautocreate_empty (gconstpointer data)
{
  TestData *td = (void*)data;
  GError *error = NULL;
  g_autoptr(OtAutoArchiveRead) a = archive_read_new ();
  OstreeRepoImportArchiveOptions opts = { 0, };
  glnx_unref_object OstreeMutableTree *mtree = ostree_mutable_tree_new ();

  if (td->skip_all != NULL)
    {
      g_test_skip (td->skip_all->message);
      return;
    }

  test_archive_setup (td->fd_empty, a);

  (void)ostree_repo_import_archive_to_mtree (td->repo, &opts, a, mtree, NULL, NULL, &error);
  g_assert_no_error (error);
  g_assert (ostree_mutable_tree_get_metadata_checksum (mtree) == NULL);
}

static void
test_libarchive_autocreate_empty (gconstpointer data)
{
  TestData *td = (void*)data;
  g_autoptr(GError) error = NULL;
  g_autoptr(OtAutoArchiveRead) a = archive_read_new ();
  OstreeRepoImportArchiveOptions opts = { 0, };
  glnx_unref_object OstreeMutableTree *mtree = ostree_mutable_tree_new ();

  if (td->skip_all != NULL)
    {
      g_test_skip (td->skip_all->message);
      return;
    }

  opts.autocreate_parents = 1;

  test_archive_setup (td->fd_empty, a);

  (void)ostree_repo_import_archive_to_mtree (td->repo, &opts, a, mtree, NULL, NULL, &error);
  g_assert_no_error (error);
  g_assert (ostree_mutable_tree_get_metadata_checksum (mtree) != NULL);
}

static void
test_libarchive_error_device_file (gconstpointer data)
{
  TestData *td = (void*)data;
  g_autoptr(GError) error = NULL;
  g_autoptr(OtAutoArchiveRead) a = archive_read_new ();
  OstreeRepoImportArchiveOptions opts = { 0, };
  glnx_unref_object OstreeMutableTree *mtree = ostree_mutable_tree_new ();

  if (td->skip_all != NULL)
    {
      g_test_skip (td->skip_all->message);
      return;
    }

  test_archive_setup (td->fd, a);

  (void)ostree_repo_import_archive_to_mtree (td->repo, &opts, a, mtree, NULL, NULL, &error);
  g_assert (error != NULL);
  g_clear_error (&error);
}

static gboolean
skip_if_no_xattr (TestData *td)
{
  /* /var/tmp might actually be a tmpfs */
  if (setxattr (td->tmpd, "user.test-xattr-support", "yes", 4, 0) != 0)
    {
      int saved_errno = errno;
      g_autofree gchar *message
        = g_strdup_printf ("unable to setxattr on \"%s\": %s",
                           td->tmpd, g_strerror (saved_errno));
      g_test_skip (message);
      return TRUE;
    }

  return FALSE;
}

static gboolean
import_write_and_ref (OstreeRepo      *repo,
                      OstreeRepoImportArchiveOptions *opts,
                      struct archive  *a,
                      const char      *ref,
                      OstreeRepoCommitModifier *modifier,
                      GError         **error)
{
  g_autoptr(OstreeMutableTree) mtree = ostree_mutable_tree_new ();

  if (!ostree_repo_prepare_transaction (repo, NULL, NULL, error))
    return FALSE;

  if (!ostree_repo_import_archive_to_mtree (repo, opts, a, mtree, modifier,
                                            NULL, error))
    return FALSE;

  g_autoptr(GFile) root = NULL;
  if (!ostree_repo_write_mtree (repo, mtree, &root, NULL, error))
    return FALSE;

  g_autofree char *commit_checksum = NULL;
  if (!ostree_repo_write_commit (repo, NULL, "", "", NULL,
                                 OSTREE_REPO_FILE (root),
                                 &commit_checksum, NULL, error))
    return FALSE;

  ostree_repo_transaction_set_ref (repo, NULL, ref, commit_checksum);

  if (!ostree_repo_commit_transaction (repo, NULL, NULL, error))
    return FALSE;

  return TRUE;
}

static void
test_libarchive_ignore_device_file (gconstpointer data)
{
  TestData *td = (void*)data;
  g_autoptr(GError) error = NULL;
  g_autoptr(OtAutoArchiveRead) a = archive_read_new ();
  OstreeRepoImportArchiveOptions opts = { 0, };

  if (skip_if_no_xattr (td))
    goto out;

  if (td->skip_all != NULL)
    {
      g_test_skip (td->skip_all->message);
      goto out;
    }

  test_archive_setup (td->fd, a);

  opts.ignore_unsupported_content = TRUE;

  if (!import_write_and_ref (td->repo, &opts, a, "foo", NULL, &error))
    goto out;

  /* check contents */
  if (!spawn_cmdline ("ostree --repo=repo ls foo file", &error))
    goto out;

  if (!spawn_cmdline ("ostree --repo=repo ls foo anotherfile", &error))
    goto out;

  if (!spawn_cmdline ("ostree --repo=repo ls foo /etc/file", &error))
    goto out;

  if (spawn_cmdline ("ostree --repo=repo ls foo devnull", &error))
    g_assert_not_reached ();
  g_assert (error != NULL);
  g_clear_error (&error);

 out:
  g_assert_no_error (error);
}

static gboolean
check_ostree_convention (GError *error)
{
  if (!spawn_cmdline ("ostree --repo=repo ls bar file", &error))
    return FALSE;

  if (!spawn_cmdline ("ostree --repo=repo ls bar anotherfile", &error))
    return FALSE;

  if (!spawn_cmdline ("ostree --repo=repo ls bar /usr/etc/file", &error))
    return FALSE;

  if (spawn_cmdline ("ostree --repo=repo ls bar /etc/file", &error))
    g_assert_not_reached ();
  g_assert (error != NULL);
  g_clear_error (&error);

  if (spawn_cmdline ("ostree --repo=repo ls bar devnull", &error))
    g_assert_not_reached ();
  g_assert (error != NULL);
  g_clear_error (&error);

  return TRUE;
}

static void
test_libarchive_ostree_convention (gconstpointer data)
{
  TestData *td = (void*)data;
  GError *error = NULL;
  g_autoptr(OtAutoArchiveRead) a = archive_read_new ();
  OstreeRepoImportArchiveOptions opts = { 0, };

  if (skip_if_no_xattr (td))
    goto out;

  if (td->skip_all != NULL)
    {
      g_test_skip (td->skip_all->message);
      goto out;
    }

  test_archive_setup (td->fd, a);

  opts.autocreate_parents = TRUE;
  opts.use_ostree_convention = TRUE;
  opts.ignore_unsupported_content = TRUE;

  if (!import_write_and_ref (td->repo, &opts, a, "bar", NULL, &error))
    goto out;

  if (!check_ostree_convention (error))
    goto out;

 out:
  g_assert_no_error (error);
}

static GVariant*
xattr_cb (OstreeRepo  *repo,
          const char  *path,
          GFileInfo   *file_info,
          gpointer     user_data)
{
  g_auto(GVariantBuilder) builder;
  g_variant_builder_init (&builder, (GVariantType*)"a(ayay)");
  if (strcmp (path, "/anotherfile") == 0)
    g_variant_builder_add (&builder, "(@ay@ay)",
                           g_variant_new_bytestring ("user.data"),
                           g_variant_new_bytestring ("mydata"));
  return g_variant_ref_sink (g_variant_builder_end (&builder));
}

static void
test_libarchive_xattr_callback (gconstpointer data)
{
  TestData *td = (void*)data;
  GError *error = NULL;
  g_autoptr(OtAutoArchiveRead) a = archive_read_new ();
  OstreeRepoImportArchiveOptions opts = { 0 };
  g_autoptr(OstreeRepoCommitModifier) modifier = NULL;
  char buf[7] = { 0 };

  if (skip_if_no_xattr (td))
    goto out;

  if (td->skip_all != NULL)
    {
      g_test_skip (td->skip_all->message);
      goto out;
    }

  modifier = ostree_repo_commit_modifier_new (0, NULL, NULL, NULL);
  ostree_repo_commit_modifier_set_xattr_callback (modifier, xattr_cb,
                                                  NULL, NULL);

  test_archive_setup (td->fd, a);

  opts.ignore_unsupported_content = TRUE;

  if (!import_write_and_ref (td->repo, &opts, a, "baz", modifier, &error))
    goto out;

  /* check contents */
  if (!spawn_cmdline ("ostree --repo=repo checkout baz baz-checkout", &error))
    goto out;

  g_assert_cmpint (0, >, getxattr ("baz-checkout/file", "user.data", NULL, 0));
  g_assert_cmpint (ENODATA, ==, errno);

  if (getxattr ("baz-checkout/anotherfile", "user.data", buf, sizeof buf) < 0)
    {
      glnx_set_prefix_error_from_errno (&error, "%s", "getxattr");
      goto out;
    }

  g_assert_cmpstr (buf, ==, "mydata");

 out:
  g_assert_no_error (error);
}

static void
test_libarchive_xattr_import (gconstpointer data)
{
  TestData *td = (void*)data;
  GError *error = NULL;
  g_autoptr(OtAutoArchiveRead) a = archive_read_new ();
  OstreeRepoImportArchiveOptions opts = { 0 };
  char buf[15] = { 0 };

  if (skip_if_no_xattr (td))
    goto out;

  if (td->skip_all != NULL)
    {
      g_test_skip (td->skip_all->message);
      goto out;
    }

  test_archive_setup (td->fd, a);

  opts.ignore_unsupported_content = TRUE;

  if (!import_write_and_ref (td->repo, &opts, a, "baz", NULL, &error))
    goto out;

  /* check contents */
  if (!spawn_cmdline ("ostree --repo=repo checkout baz import-checkout", NULL))
    goto out;

  spawn_cmdline ("ostree --repo=repo ls -R -X baz", NULL);
  spawn_cmdline ("attr -l import-checkout/anotherfile", NULL);

  ssize_t x = getxattr ("import-checkout/anotherfile", "user.a_key", buf, sizeof buf - 1);
  g_assert_cmpint (x, ==, 8);
  g_assert_cmpstr (buf, ==, "my value");

  x = getxattr ("import-checkout/anotherfile", "user.key2", buf, sizeof buf - 1);
  g_assert_cmpint (x, ==, 0);

  x = getxattr ("import-checkout/anotherfile", "user.b_key", buf, sizeof buf - 1);
  g_assert_cmpint (x, ==, 14);
  g_assert (memcmp(buf, "contains\0nuls", 14) == 0);

 out:
  g_assert_no_error (error);
}

static void
test_libarchive_xattr_import_skip_xattr (gconstpointer data)
{
  TestData *td = (void*)data;
  GError *error = NULL;
  g_autoptr(OtAutoArchiveRead) a = archive_read_new ();
  OstreeRepoImportArchiveOptions opts = { 0 };
  g_autoptr(OstreeRepoCommitModifier) modifier = NULL;

  if (skip_if_no_xattr (td))
    goto out;

  if (td->skip_all != NULL)
    {
      g_test_skip (td->skip_all->message);
      goto out;
    }

  test_archive_setup (td->fd, a);

  opts.ignore_unsupported_content = TRUE;

  modifier = ostree_repo_commit_modifier_new (
      OSTREE_REPO_COMMIT_MODIFIER_FLAGS_SKIP_XATTRS, NULL, NULL, NULL);
  if (!import_write_and_ref (td->repo, &opts, a, "baz", modifier, &error))
    goto out;

  /* check contents */
  if (!spawn_cmdline ("ostree --repo=repo checkout baz import-checkout", NULL))
    goto out;

  ssize_t n_attrs = listxattr ("import-checkout/anotherfile", NULL, 0);
  g_assert_cmpint (n_attrs, ==, 0);

 out:
  g_assert_no_error (error);
}

static GVariant*
path_cb (OstreeRepo  *repo,
         const char  *path,
         GFileInfo   *file_info,
         gpointer     user_data)
{
  if (strcmp (path, "/etc/file") == 0)
    *(gboolean*)user_data = TRUE;
  return NULL;
}

static void
entry_pathname_test_helper (gconstpointer data, gboolean on)
{
  TestData *td = (void*)data; GError *error = NULL;
  g_autoptr(OtAutoArchiveRead) a = archive_read_new ();
  OstreeRepoImportArchiveOptions opts = { 0, };
  OstreeRepoCommitModifier *modifier = NULL;
  gboolean met_etc_file = FALSE;

  if (skip_if_no_xattr (td))
    goto out;

  if (td->skip_all != NULL)
    {
      g_test_skip (td->skip_all->message);
      goto out;
    }

  modifier = ostree_repo_commit_modifier_new (0, NULL, NULL, NULL);
  ostree_repo_commit_modifier_set_xattr_callback (modifier, path_cb,
                                                  NULL, &met_etc_file);

  test_archive_setup (td->fd, a);

  opts.autocreate_parents = TRUE;
  opts.use_ostree_convention = TRUE;
  opts.ignore_unsupported_content = TRUE;
  opts.callback_with_entry_pathname = on;

  if (!import_write_and_ref (td->repo, &opts, a, "bar", modifier, &error))
    goto out;

  /* the flag shouldn't have any effect on the final tree */
  if (!check_ostree_convention (error))
    goto out;

  if (!on && met_etc_file)
    {
      g_set_error_literal (&error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "Received callback with /etc/file");
      goto out;
    }

  if (on && !met_etc_file)
    {
      g_set_error_literal (&error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "Did not receive callback with /etc/file");
      goto out;
    }

  ostree_repo_commit_modifier_unref (modifier);
 out:
  g_assert_no_error (error);
}

static void
test_libarchive_no_use_entry_pathname (gconstpointer data)
{
  entry_pathname_test_helper (data, FALSE);
}

static void
test_libarchive_use_entry_pathname (gconstpointer data)
{
  entry_pathname_test_helper (data, TRUE);
}

static void
test_libarchive_selinux (gconstpointer data)
{
  TestData *td = (void*)data;
  GError *error = NULL;
  g_autoptr(OtAutoArchiveRead) a = archive_read_new ();
  OstreeRepoImportArchiveOptions opts = { 0 };
  glnx_unref_object OstreeSePolicy *sepol = NULL;
  g_autoptr(OstreeRepoCommitModifier) modifier = NULL;
  char buf[64] = { 0 };

  if (skip_if_no_xattr (td))
    goto out;
  if (getenv ("container"))
    {
      // FIXME dedup this with libtest.sh have_selinux_relabel
      g_test_skip ("skip in containers for now");
      goto out;
    }

  if (td->skip_all != NULL)
    {
      g_test_skip (td->skip_all->message);
      goto out;
    }

  {
    glnx_unref_object GFile *root = g_file_new_for_path ("/");

    sepol = ostree_sepolicy_new (root, NULL, NULL);
  }

  if (sepol == NULL || ostree_sepolicy_get_name (sepol) == NULL)
    {
      g_test_skip ("SELinux disabled");
      goto out;
    }

  modifier = ostree_repo_commit_modifier_new (0, NULL, NULL, NULL);
  ostree_repo_commit_modifier_set_sepolicy (modifier, sepol);

  test_archive_setup (td->fd, a);

  opts.ignore_unsupported_content = TRUE;

  if (!import_write_and_ref (td->repo, &opts, a, "bob", modifier, &error))
    goto out;

  /* check contents */
  if (!spawn_cmdline ("ostree --repo=repo checkout bob bob-checkout", &error))
    goto out;

  if (getxattr ("bob-checkout/etc", "security.selinux", buf, sizeof buf) < 0)
    {
      glnx_set_prefix_error_from_errno (&error, "%s", "getxattr");
      goto out;
    }

  buf[(sizeof buf) - 1] = '\0';
  g_assert_cmpstr (buf, ==, "system_u:object_r:etc_t:s0");

 out:
  g_assert_no_error (error);
}

int main (int argc, char **argv)
{
  TestData td = {NULL,};
  int r;

  test_data_init (&td);

  g_test_init (&argc, &argv, NULL);

  g_test_add_data_func ("/libarchive/noautocreate-empty", &td, test_libarchive_noautocreate_empty);
  g_test_add_data_func ("/libarchive/autocreate-empty", &td, test_libarchive_autocreate_empty);
  g_test_add_data_func ("/libarchive/error-device-file", &td, test_libarchive_error_device_file);
  g_test_add_data_func ("/libarchive/ignore-device-file", &td, test_libarchive_ignore_device_file);
  g_test_add_data_func ("/libarchive/ostree-convention", &td, test_libarchive_ostree_convention);
  g_test_add_data_func ("/libarchive/xattr-import", &td, test_libarchive_xattr_import);
  g_test_add_data_func ("/libarchive/xattr-import-skip-xattr", &td, test_libarchive_xattr_import_skip_xattr);
  g_test_add_data_func ("/libarchive/xattr-callback", &td, test_libarchive_xattr_callback);
  g_test_add_data_func ("/libarchive/no-use-entry-pathname", &td, test_libarchive_no_use_entry_pathname);
  g_test_add_data_func ("/libarchive/use-entry-pathname", &td, test_libarchive_use_entry_pathname);
  g_test_add_data_func ("/libarchive/selinux", &td, test_libarchive_selinux);

  r = g_test_run();

  g_clear_object (&td.repo);
  if (td.tmpd && g_getenv ("TEST_SKIP_CLEANUP") == NULL)
    (void) glnx_shutil_rm_rf_at (AT_FDCWD, td.tmpd, NULL, NULL);
  g_free (td.tmpd);
  return r;
}
