/*
 * Copyright (C) 2016 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 <stdlib.h>
#include <stdbool.h>
#include <gio/gio.h>
#include <string.h>
#include <err.h>

#include "libglnx.h"
#include "libostreetest.h"

static void
test_repo_is_not_system (gconstpointer data)
{
  OstreeRepo *repo = (void*)data;
  g_assert (!ostree_repo_is_system (repo));
}

static GBytes *
input_stream_to_bytes (GInputStream *input)
{
  g_autoptr(GOutputStream) mem_out_stream = NULL;
  g_autoptr(GError) error = NULL;

  if (input == NULL)
    return g_bytes_new (NULL, 0);

  mem_out_stream = g_memory_output_stream_new (NULL, 0, g_realloc, g_free);
  g_output_stream_splice (mem_out_stream,
                          input,
                          G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
                          NULL,
                          &error);
  g_assert_no_error (error);

  return g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (mem_out_stream));
}

static void
test_raw_file_to_archive_stream (gconstpointer data)
{
  OstreeRepo *repo = OSTREE_REPO (data);
  g_autofree gchar *commit_checksum = NULL;
  g_autoptr(GHashTable) reachable = NULL;
  g_autoptr(GError) error = NULL;
  /* branch name of the test repository, see setup_test_repository in libtest.sh */
  const gchar *rev = "test2";
  GHashTableIter iter;
  GVariant *serialized_object;
  guint checks = 0;

  ostree_repo_resolve_rev (repo,
                           rev,
                           FALSE,
                           &commit_checksum,
                           &error);
  g_assert_no_error (error);
  ostree_repo_traverse_commit (repo,
                               commit_checksum,
                               -1,
                               &reachable,
                               NULL,
                               &error);
  g_assert_no_error (error);
  g_hash_table_iter_init (&iter, reachable);
  while (g_hash_table_iter_next (&iter, (gpointer*)&serialized_object, NULL))
    {
      const gchar *object_checksum;
      OstreeObjectType object_type;
      g_autoptr(GInputStream) input = NULL;
      g_autoptr(GFileInfo) info = NULL;
      g_autoptr(GVariant) xattrs = NULL;
      g_autoptr(GBytes) input_bytes = NULL;
      g_autoptr(GInputStream) mem_input = NULL;
      g_autoptr(GInputStream) zlib_stream = NULL;
      g_autoptr(GBytes) zlib_bytes = NULL;
      g_autoptr(GInputStream) mem_zlib = NULL;
      g_autoptr(GInputStream) input2 = NULL;
      g_autoptr(GFileInfo) info2 = NULL;
      g_autoptr(GVariant) xattrs2 = NULL;
      g_autoptr(GBytes) input2_bytes = NULL;

      ostree_object_name_deserialize (serialized_object, &object_checksum, &object_type);
      if (object_type != OSTREE_OBJECT_TYPE_FILE)
        continue;

      ostree_repo_load_file (repo,
                             object_checksum,
                             &input,
                             &info,
                             &xattrs,
                             NULL,
                             &error);
      g_assert_no_error (error);

      input_bytes = input_stream_to_bytes (input);
      /* This is to simulate NULL input received from
       * ostree_repo_load_file. Instead of creating the mem_input
       * variable, I could also rewind the input stream and pass it to
       * the function below, but this would assume that the input
       * stream implements either the GSeekable or
       * GFileDescriptorBased interface. */
      if (input != NULL)
        mem_input = g_memory_input_stream_new_from_bytes (input_bytes);
      ostree_raw_file_to_archive_z2_stream (mem_input,
                                            info,
                                            xattrs,
                                            &zlib_stream,
                                            NULL,
                                            &error);
      g_assert_no_error (error);

      zlib_bytes = input_stream_to_bytes (zlib_stream);
      mem_zlib = g_memory_input_stream_new_from_bytes (zlib_bytes);
      ostree_content_stream_parse (FALSE,
                                   mem_zlib,
                                   g_bytes_get_size (zlib_bytes),
                                   FALSE,
                                   &input2,
                                   &info2,
                                   &xattrs2,
                                   NULL,
                                   &error);
      g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED);
      g_clear_error (&error);

      g_seekable_seek (G_SEEKABLE (mem_zlib),
                       0,
                       G_SEEK_SET,
                       NULL,
                       &error);
      g_assert_no_error (error);

      ostree_content_stream_parse (TRUE,
                                   mem_zlib,
                                   g_bytes_get_size (zlib_bytes),
                                   FALSE,
                                   &input2,
                                   &info2,
                                   &xattrs2,
                                   NULL,
                                   &error);
      g_assert_no_error (error);

      input2_bytes = input_stream_to_bytes (input2);
      g_assert_true (g_bytes_equal (input_bytes, input2_bytes));
      g_assert_true (g_variant_equal (xattrs, xattrs2));
      /* TODO: Not sure how to compare fileinfos */
      ++checks;
    }
  /* to make sure we really tested the function */
  g_assert_cmpint (checks, >, 0);
}

static gboolean hi_content_stream_new (GInputStream **out_stream,
                                       guint64       *out_length,
                                       GError **error)
{
  static const char hi[] = "hi";
  g_autoptr(GMemoryInputStream) hi_memstream = (GMemoryInputStream*)g_memory_input_stream_new_from_data (hi, sizeof(hi)-1, NULL);
  g_autoptr(GFileInfo) finfo = g_file_info_new ();
  g_file_info_set_attribute_uint32 (finfo, "standard::type", G_FILE_TYPE_REGULAR);
  g_file_info_set_attribute_boolean (finfo, "standard::is-symlink", FALSE);
  g_file_info_set_attribute_uint32 (finfo, "unix::uid", 0);
  g_file_info_set_attribute_uint32 (finfo, "unix::gid", 0);
  g_file_info_set_attribute_uint32 (finfo, "unix::mode", S_IFREG|0644);
  return ostree_raw_file_to_content_stream ((GInputStream*)hi_memstream, finfo, NULL, out_stream, out_length, NULL, error);
}

static void
test_validate_remotename (void)
{
  const char *valid[] = {"foo", "hello-world"};
  const char *invalid[] = {"foo/bar", ""};
  for (guint i = 0; i < G_N_ELEMENTS(valid); i++)
    {
      g_autoptr(GError) error = NULL;
      g_assert (ostree_validate_remote_name (valid[i], &error));
      g_assert_no_error (error);
    }
  for (guint i = 0; i < G_N_ELEMENTS(invalid); i++)
    {
      g_autoptr(GError) error = NULL;
      g_assert (!ostree_validate_remote_name (invalid[i], &error));
      g_assert (error != NULL);
    }
}

static void
test_object_writes (gconstpointer data)
{
  OstreeRepo *repo = OSTREE_REPO (data);
  g_autoptr(GError) error = NULL;

  static const char hi_sha256[] = "2301b5923720c3edc1f0467addb5c287fd5559e3e0cd1396e7f1edb6b01be9f0";

  /* Successful content write */
  { g_autoptr(GInputStream) hi_memstream = NULL;
    guint64 len;
    hi_content_stream_new (&hi_memstream, &len, &error);
    g_assert_no_error (error);
    g_autofree guchar *csum = NULL;
    (void)ostree_repo_write_content (repo, hi_sha256, hi_memstream, len, &csum,
                                     NULL, &error);
    g_assert_no_error (error);
  }

  /* Invalid content write */
  { g_autoptr(GInputStream) hi_memstream = NULL;
    guint64 len;
    hi_content_stream_new (&hi_memstream, &len, &error);
    g_assert_no_error (error);
    g_autofree guchar *csum = NULL;
    static const char invalid_hi_sha256[] = "cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe";
    g_assert (!ostree_repo_write_content (repo, invalid_hi_sha256, hi_memstream, len, &csum,
                                          NULL, &error));
    g_assert (error);
    g_assert (strstr (error->message, "Corrupted file object"));
  }
}

static gboolean
impl_test_break_hardlink (int tmp_dfd,
                          const char *path,
                          GError **error)
{
  const char *linkedpath = glnx_strjoina (path, ".link");
  struct stat orig_stbuf;
  if (!glnx_fstatat (tmp_dfd, path, &orig_stbuf, AT_SYMLINK_NOFOLLOW, error))
    return FALSE;

  /* Calling ostree_break_hardlink() should be a noop */
  struct stat stbuf;
  if (!ostree_break_hardlink (tmp_dfd, path, TRUE, NULL, error))
    return FALSE;
  if (!glnx_fstatat (tmp_dfd, path, &stbuf, AT_SYMLINK_NOFOLLOW, error))
    return FALSE;

  g_assert_cmpint (orig_stbuf.st_dev, ==, stbuf.st_dev);
  g_assert_cmpint (orig_stbuf.st_ino, ==, stbuf.st_ino);

  if (linkat (tmp_dfd, path, tmp_dfd, linkedpath, 0) < 0)
    return glnx_throw_errno_prefix (error, "linkat");

  if (!ostree_break_hardlink (tmp_dfd, path, TRUE, NULL, error))
    return FALSE;
  if (!glnx_fstatat (tmp_dfd, path, &stbuf, AT_SYMLINK_NOFOLLOW, error))
    return FALSE;
  /* This file should be different */
  g_assert_cmpint (orig_stbuf.st_dev, ==, stbuf.st_dev);
  g_assert_cmpint (orig_stbuf.st_ino, !=, stbuf.st_ino);
  /* But this one is still the same */
  if (!glnx_fstatat (tmp_dfd, linkedpath, &stbuf, AT_SYMLINK_NOFOLLOW, error))
    return FALSE;
  g_assert_cmpint (orig_stbuf.st_dev, ==, stbuf.st_dev);
  g_assert_cmpint (orig_stbuf.st_ino, ==, stbuf.st_ino);

  (void) unlinkat (tmp_dfd, path, 0);
  (void) unlinkat (tmp_dfd, linkedpath, 0);

  return TRUE;
}

static void
test_break_hardlink (void)
{
  int tmp_dfd = AT_FDCWD;
  g_autoptr(GError) error = NULL;

  /* Regular file */
  const char hello_hardlinked_content[] = "hello hardlinked content";
  glnx_file_replace_contents_at (tmp_dfd, "test-hardlink",
                                 (guint8*)hello_hardlinked_content,
                                 strlen (hello_hardlinked_content),
                                 GLNX_FILE_REPLACE_NODATASYNC,
                                 NULL, &error);
  g_assert_no_error (error);
  (void)impl_test_break_hardlink (tmp_dfd, "test-hardlink", &error);
  g_assert_no_error (error);

  /* Symlink */
  if (symlinkat ("some-path", tmp_dfd, "test-symhardlink") < 0)
    err (1, "symlinkat");
  (void)impl_test_break_hardlink (tmp_dfd, "test-symhardlink", &error);
  g_assert_no_error (error);
}

static GVariant*
xattr_cb (OstreeRepo  *repo,
          const char  *path,
          GFileInfo   *file_info,
          gpointer     user_data)
{
  GVariant *xattr = user_data;
  if (g_str_equal (path, "/baz/cow"))
    return g_variant_ref (xattr);
  return NULL;
}

/* check that using a devino cache doesn't cause us to ignore xattr callbacks */
static void
test_devino_cache_xattrs (void)
{
  g_autoptr(GError) error = NULL;
  gboolean ret = FALSE;

  g_autoptr(GFile) repo_path = g_file_new_for_path ("repo");

  /* re-initialize as bare */
  ret = ot_test_run_libtest ("setup_test_repository bare", &error);
  g_assert_no_error (error);
  g_assert (ret);

  gboolean can_relabel;
  ret = ot_check_relabeling (&can_relabel, &error);
  g_assert_no_error (error);
  g_assert (ret);

  gboolean has_user_xattrs;
  ret = ot_check_user_xattrs (&has_user_xattrs, &error);
  g_assert_no_error (error);
  g_assert (ret);

  /* we need both because we're bare and our tests target user xattrs */
  if (!can_relabel || !has_user_xattrs)
    {
      g_test_skip ("this test requires full xattr support");
      return;
    }

  g_autoptr(OstreeRepo) repo = ostree_repo_new (repo_path);
  ret = ostree_repo_open (repo, NULL, &error);
  g_assert_no_error (error);
  g_assert (ret);

  g_autofree char *csum = NULL;
  ret = ostree_repo_resolve_rev (repo, "test2", FALSE, &csum, &error);
  g_assert_no_error (error);
  g_assert (ret);

  g_autoptr(OstreeRepoDevInoCache) cache = ostree_repo_devino_cache_new ();

  OstreeRepoCheckoutAtOptions options = {0,};
  options.no_copy_fallback = TRUE;
  options.devino_to_csum_cache = cache;
  ret = ostree_repo_checkout_at (repo, &options, AT_FDCWD, "checkout", csum, NULL, &error);
  g_assert_no_error (error);
  g_assert (ret);

  g_autoptr(OstreeMutableTree) mtree = ostree_mutable_tree_new ();
  g_autoptr(OstreeRepoCommitModifier) modifier =
    ostree_repo_commit_modifier_new (0, NULL, NULL, NULL);
  ostree_repo_commit_modifier_set_devino_cache (modifier, cache);

  g_auto(GVariantBuilder) builder;
  g_variant_builder_init (&builder, (GVariantType*)"a(ayay)");
  g_variant_builder_add (&builder, "(@ay@ay)",
                         g_variant_new_bytestring ("user.myattr"),
                         g_variant_new_bytestring ("data"));
  g_autoptr(GVariant) orig_xattrs = g_variant_ref_sink (g_variant_builder_end (&builder));

  ret = ostree_repo_prepare_transaction (repo, NULL, NULL, &error);
  g_assert_no_error (error);
  g_assert (ret);

  ostree_repo_commit_modifier_set_xattr_callback (modifier, xattr_cb, NULL, orig_xattrs);
  ret = ostree_repo_write_dfd_to_mtree (repo, AT_FDCWD, "checkout",
                                        mtree, modifier, NULL, &error);
  g_assert_no_error (error);
  g_assert (ret);

  g_autoptr(GFile) root = NULL;
  ret = ostree_repo_write_mtree (repo, mtree, &root, NULL, &error);
  g_assert_no_error (error);
  g_assert (ret);

  /* now check that the final xattr matches */
  g_autoptr(GFile) baz_child = g_file_get_child (root, "baz");
  g_autoptr(GFile) cow_child = g_file_get_child (baz_child, "cow");

  g_autoptr(GVariant) xattrs = NULL;
  ret = ostree_repo_file_get_xattrs (OSTREE_REPO_FILE (cow_child), &xattrs, NULL, &error);
  g_assert_no_error (error);
  g_assert (ret);

  gboolean found_xattr = FALSE;
  gsize n = g_variant_n_children (xattrs);
  for (gsize i = 0; i < n; i++)
    {
      const guint8* name;
      const guint8* value;
      g_variant_get_child (xattrs, i, "(^&ay^&ay)", &name, &value);

      if (g_str_equal ((const char*)name, "user.myattr"))
        {
          g_assert_cmpstr ((const char*)value, ==, "data");
          found_xattr = TRUE;
          break;
        }
    }

  g_assert (found_xattr);

  OstreeRepoTransactionStats stats;
  ret = ostree_repo_commit_transaction (repo, &stats, NULL, &error);
  g_assert_no_error (error);
  g_assert (ret);

  /* we should only have had to checksum /baz/cow */
  g_assert_cmpint (stats.content_objects_written, ==, 1);
}

/* https://github.com/ostreedev/ostree/issues/1721
 * We should be able to commit large metadata objects now.
 */
static void
test_big_metadata (void)
{
  g_autoptr(GError) error = NULL;
  gboolean ret = FALSE;

  g_autoptr(GFile) repo_path = g_file_new_for_path ("repo");

  /* init as bare-user-only so we run everywhere */
  ret = ot_test_run_libtest ("setup_test_repository bare-user-only", &error);
  g_assert_no_error (error);
  g_assert (ret);

  g_autoptr(OstreeRepo) repo = ostree_repo_new (repo_path);
  ret = ostree_repo_open (repo, NULL, &error);
  g_assert_no_error (error);
  g_assert (ret);

  g_autoptr(GFile) object_to_commit = NULL;
  ret = ostree_repo_read_commit (repo, "test2", &object_to_commit, NULL, NULL, &error);
  g_assert_no_error (error);
  g_assert (ret);

  g_autoptr(OstreeMutableTree) mtree = ostree_mutable_tree_new ();
  ret = ostree_repo_write_directory_to_mtree (repo, object_to_commit, mtree, NULL,
                                              NULL, &error);
  g_assert_no_error (error);
  g_assert (ret);

  const size_t len = 20 * 1024 * 1024;
  g_assert_cmpint (len, >, OSTREE_MAX_METADATA_SIZE);
  g_autofree char *large_buf = g_malloc (len);
  memset (large_buf, 0x42, len);
  g_autoptr(GVariantBuilder) builder =
    g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
  g_autofree char *commit_checksum = NULL;
  g_variant_builder_add (builder, "{sv}", "large-value",
                         g_variant_new_fixed_array ((GVariantType*)"y",
                                                    large_buf, len, sizeof (char)));
  ret = ostree_repo_write_commit (repo, NULL, NULL, NULL, g_variant_builder_end (builder),
                                  OSTREE_REPO_FILE (object_to_commit), &commit_checksum, NULL, &error);
  g_assert_no_error (error);
  g_assert (ret);
}

static void
compare_xattrs (GVariant *orig, GVariant *new)
{
  g_assert_cmpint (g_variant_n_children (orig) + 1, ==, g_variant_n_children (new));
  GVariant *kp, *vp;
  GVariantIter iter;
  g_variant_iter_init (&iter, new);
  bool found = false;
  while (g_variant_iter_loop (&iter, "(@ay@ay)", &kp, &vp))
    {
      const char *k = g_variant_get_bytestring (kp);
      if (!g_str_equal (k, "user.ostreetesting"))
        continue;
      g_assert_cmpint (g_variant_get_size (vp), ==, 4);
      found = true;
    }
  g_assert (found);
}

static void
test_read_xattrs (void)
{
  g_autoptr(GError) local_error = NULL;
  GError **error = &local_error;

  g_auto(GLnxTmpDir) tmpd = { 0, };
  // Use /var/tmp to hope we get xattr support
  glnx_mkdtempat (AT_FDCWD, "/var/tmp/ostree-xattrs-test.XXXXXX", 0700, &tmpd, error);
  g_assert_no_error (local_error);

  const char value[] = "foo";

  {
    g_autoptr(GVariant) current_xattrs = ostree_fs_get_all_xattrs (tmpd.fd, NULL, error);
    g_assert_no_error (local_error);
  
    int r = fsetxattr (tmpd.fd, "user.ostreetesting", value, sizeof (value), 0);
    g_assert_cmpint (r, ==, 0);
  
    g_autoptr(GVariant) new_xattrs = ostree_fs_get_all_xattrs (tmpd.fd, NULL, error);
    g_assert_no_error (local_error);
  
    compare_xattrs (current_xattrs, new_xattrs);
  }

  {
    if (symlinkat ("nosuchtarget", tmpd.fd, "somelink") < 0)
      glnx_throw_errno_prefix (error, "symlinkat");
    g_assert_no_error (local_error);

    g_autoptr(GVariant) current_xattrs = ostree_fs_get_all_xattrs_at (tmpd.fd, "somelink", NULL, error);
    g_assert_no_error (local_error);
    (void) current_xattrs;

    // OK, can't do user. xattrs on symlinks unfortunately.

    // char pathbuf[PATH_MAX];
    // snprintf (pathbuf, sizeof (pathbuf), "/proc/self/fd/%d/%s", tmpd.fd, "somelink");
    // int r = lsetxattr (pathbuf, "user.ostreetesting", value, sizeof (value), 0);
    // if (r < 0)
    //   glnx_throw_errno_prefix (error, "lsetxattr");
    // g_assert_no_error (local_error);
  
    // g_autoptr(GVariant) new_xattrs = ostree_fs_get_all_xattrs_at (tmpd.fd, "somelink", NULL, error);
    // g_assert_no_error (local_error);
  
    // compare_xattrs (current_xattrs, new_xattrs);
  }
}

int main (int argc, char **argv)
{
  g_autoptr(GError) error = NULL;
  glnx_unref_object OstreeRepo *repo = NULL;

  g_test_init (&argc, &argv, NULL);

  repo = ot_test_setup_repo (NULL, &error);
  if (!repo)
    goto out;

  g_test_add_data_func ("/repo-not-system", repo, test_repo_is_not_system);
  g_test_add_data_func ("/raw-file-to-archive-stream", repo, test_raw_file_to_archive_stream);
  g_test_add_data_func ("/objectwrites", repo, test_object_writes);
  g_test_add_func ("/xattrs-devino-cache", test_devino_cache_xattrs);
  g_test_add_func ("/break-hardlink", test_break_hardlink);
  g_test_add_func ("/remotename", test_validate_remotename);
  g_test_add_func ("/big-metadata", test_big_metadata);
  g_test_add_func ("/read-xattrs", test_read_xattrs);

  return g_test_run();
 out:
  if (error)
    g_error ("%s", error->message);
  return 1;
}
