/*
 * 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 "ostree-kernel-args-private.h"
#include "otutil.h"

static gboolean
check_string_existance (OstreeKernelArgs *karg, const char *string_to_find)
{
  g_auto (GStrv) string_list = ostree_kernel_args_to_strv (karg);
  return g_strv_contains ((const char *const *)string_list, string_to_find);
}

static gboolean
kernel_args_entry_value_equal (gconstpointer data, gconstpointer value)
{
  const OstreeKernelArgsEntry *e = data;
  return g_strcmp0 (_ostree_kernel_args_entry_get_value (e), value) == 0;
}

static gboolean
kernel_args_entry_key_equal (gconstpointer data, gconstpointer key)
{
  const OstreeKernelArgsEntry *e = data;
  return g_strcmp0 (_ostree_kernel_args_entry_get_key (e), key) == 0;
}

static void
test_kargs_delete (void)
{
  g_autoptr (GError) error = NULL;
  gboolean ret;
  __attribute__ ((cleanup (ostree_kernel_args_cleanup))) OstreeKernelArgs *karg
      = ostree_kernel_args_new ();

  ostree_kernel_args_append (karg, "single_key=test");
  ostree_kernel_args_append (karg, "test=firstval");
  ostree_kernel_args_append (karg, "test=secondval");
  ostree_kernel_args_append (karg, "test=");
  ostree_kernel_args_append (karg, "test");

  /* Delete a non-existant key should fail */
  ret = ostree_kernel_args_delete (karg, "non_existant_key", &error);
  g_assert (!ret);
  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED);
  g_clear_error (&error);

  /* Delete a key with multiple values when only specifying key should work if a no-value
   * variant exists */
  ret = ostree_kernel_args_delete (karg, "test", &error);
  g_assert_no_error (error);
  g_assert (ret);
  g_assert (!check_string_existance (karg, "test"));

  /* Trying again now should fail since there are only kargs with various values */
  ret = ostree_kernel_args_delete (karg, "test", &error);
  g_assert (!ret);
  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED);
  g_clear_error (&error);

  /* Delete a key with a non existant value should fail */
  ret = ostree_kernel_args_delete (karg, "test=non_existant_value", &error);
  g_assert (!ret);
  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED);
  g_clear_error (&error);

  /* Delete a key with only one value should fail if the value doesn't match */
  ret = ostree_kernel_args_delete (karg, "single_key=non_existent_value", &error);
  g_assert (!ret);
  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED);
  g_clear_error (&error);

  /* Delete a key with only one value should succeed by only specifying key */
  ret = ostree_kernel_args_delete (karg, "single_key", &error);
  g_assert_no_error (error);
  g_assert (ret);
  /* verify the value array is properly updated */
  GPtrArray *kargs_array = _ostree_kernel_arg_get_key_array (karg);
  g_assert (!ot_ptr_array_find_with_equal_func (kargs_array, "single_key",
                                                kernel_args_entry_value_equal, NULL));
  g_assert (!check_string_existance (karg, "single_key"));

  /* Delete specific key/value pair */
  ret = ostree_kernel_args_delete (karg, "test=secondval", &error);
  g_assert_no_error (error);
  g_assert (ret);
  g_assert (!check_string_existance (karg, "test=secondval"));

  /* Delete key/value pair with empty string value */
  ret = ostree_kernel_args_delete (karg, "test=", &error);
  g_assert_no_error (error);
  g_assert (ret);
  g_assert (!check_string_existance (karg, "test="));

  ret = ostree_kernel_args_delete (karg, "test=firstval", &error);
  g_assert_no_error (error);
  g_assert (ret);
  g_assert (!check_string_existance (karg, "test=firstval"));

  /* Check that we can delete duplicate keys */
  ostree_kernel_args_append (karg, "test=foo");
  ostree_kernel_args_append (karg, "test=foo");
  check_string_existance (karg, "test=foo");
  ret = ostree_kernel_args_delete (karg, "test=foo", &error);
  g_assert_no_error (error);
  g_assert (ret);
  g_assert (check_string_existance (karg, "test=foo"));
  ret = ostree_kernel_args_delete (karg, "test=foo", &error);
  g_assert_no_error (error);
  g_assert (ret);
  g_assert (!check_string_existance (karg, "test=foo"));

  /* Make sure we also gracefully do this for key-only args */
  ostree_kernel_args_append (karg, "nosmt");
  ostree_kernel_args_append (karg, "nosmt");
  check_string_existance (karg, "nosmt");
  ret = ostree_kernel_args_delete (karg, "nosmt", &error);
  g_assert_no_error (error);
  g_assert (ret);
  g_assert (check_string_existance (karg, "nosmt"));
  ret = ostree_kernel_args_delete (karg, "nosmt", &error);
  g_assert_no_error (error);
  g_assert (ret);
  g_assert (!check_string_existance (karg, "nosmt"));

  /* Make sure we also support quotes and spaces */
  ostree_kernel_args_append (karg, "foo=\"1 2\" bar=0");
  check_string_existance (karg, "foo=\"1 2\"");
  check_string_existance (karg, "bar=0");
  ret = ostree_kernel_args_delete (karg, "foo=\"1 2\" bar=0", &error);
  g_assert_no_error (error);
  g_assert (ret);
  g_assert (!check_string_existance (karg, "foo=\"1 2\""));
  g_assert (!check_string_existance (karg, "bar=0"));
}

static void
test_kargs_replace (void)
{
  g_autoptr (GError) error = NULL;
  gboolean ret;
  __attribute__ ((cleanup (ostree_kernel_args_cleanup))) OstreeKernelArgs *karg
      = ostree_kernel_args_new ();

  ostree_kernel_args_append (karg, "single_key");
  ostree_kernel_args_append (karg, "test=firstval");
  ostree_kernel_args_append (karg, "test=secondval");

  /* Replace when the input key is non-existant should fail */
  ret = ostree_kernel_args_new_replace (karg, "nonexistantkey", &error);
  g_assert (!ret);
  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED);
  g_clear_error (&error);

  /* Replace non-existant value with input key=nonexistantvalue=newvalue should fail */
  ret = ostree_kernel_args_new_replace (karg, "single_key=nonexistantval=newval", &error);
  g_assert (!ret);
  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED);
  g_clear_error (&error);

  /* Replace with input key=value will fail for a key with multiple values */
  ret = ostree_kernel_args_new_replace (karg, "test=newval", &error);
  g_assert (!ret);
  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED);
  g_clear_error (&error);

  /* Replace with input key=value for a key with single value should succeed
   * Also note, we also allow ''(empty string) valid to be a value
   */
  ret = ostree_kernel_args_new_replace (karg, "single_key=newvalue", &error);
  g_assert_no_error (error);
  g_assert (ret);
  g_assert (!check_string_existance (karg, "single_key"));
  g_assert (check_string_existance (karg, "single_key=newvalue"));

  /* Replace with input key=value=newvalue if key and value both
   * exist, the action should succeed
   */
  ret = ostree_kernel_args_new_replace (karg, "test=firstval=newval", &error);
  g_assert_no_error (error);
  g_assert (ret);
  g_assert (!check_string_existance (karg, "test=firstval"));
  g_assert (check_string_existance (karg, "test=newval"));

  /* Replace with input contains quotes and spaces, it should support */
  ostree_kernel_args_append (karg, "foo=1 bar=\"1 2\"");
  ret = ostree_kernel_args_new_replace (karg, "foo=\"1 2\" bar", &error);
  g_assert_no_error (error);
  g_assert (ret);
  g_assert (!check_string_existance (karg, "foo=1"));
  g_assert (!check_string_existance (karg, "bar=\"1 2\""));
  g_assert (check_string_existance (karg, "foo=\"1 2\""));
  g_assert (check_string_existance (karg, "bar"));
}

/* In this function, we want to verify that ostree_kernel_args_append
 * and ostree_kernel_args_to_string is correct. After that
 * we will use these two functions(append and tostring) in other tests: delete and replace
 */
static void
test_kargs_append (void)
{
  __attribute__ ((cleanup (ostree_kernel_args_cleanup))) OstreeKernelArgs *append_arg
      = ostree_kernel_args_new ();
  /* Some valid cases (key=value) pair */
  ostree_kernel_args_append (append_arg, "test=valid");
  ostree_kernel_args_append (append_arg, "test=secondvalid");
  ostree_kernel_args_append (append_arg, "test=");
  ostree_kernel_args_append (append_arg, "test");
  ostree_kernel_args_append (append_arg, "second_test");
  ostree_kernel_args_append (append_arg, "second_test=0 second_test=\"1 2\"");

  /* We loops through the kargs inside table to verify
   * the functionality of append because at this stage
   * we have yet to find the conversion kargs to string fully "functional"
   */
  GHashTable *kargs_table = _ostree_kernel_arg_get_kargs_table (append_arg);
  GLNX_HASH_TABLE_FOREACH_KV (kargs_table, const char *, key, GPtrArray *, value_array)
    {
      if (g_str_equal (key, "test"))
        {
          g_assert (ot_ptr_array_find_with_equal_func (value_array, "valid",
                                                       kernel_args_entry_value_equal, NULL));
          g_assert (ot_ptr_array_find_with_equal_func (value_array, "secondvalid",
                                                       kernel_args_entry_value_equal, NULL));
          g_assert (ot_ptr_array_find_with_equal_func (value_array, "",
                                                       kernel_args_entry_value_equal, NULL));
          g_assert (ot_ptr_array_find_with_equal_func (value_array, NULL,
                                                       kernel_args_entry_value_equal, NULL));
        }
      else
        {
          g_assert_cmpstr (key, ==, "second_test");
          g_assert (ot_ptr_array_find_with_equal_func (value_array, NULL,
                                                       kernel_args_entry_value_equal, NULL));
          g_assert (ot_ptr_array_find_with_equal_func (value_array, "0",
                                                       kernel_args_entry_value_equal, NULL));
          g_assert (ot_ptr_array_find_with_equal_func (value_array, "\"1 2\"",
                                                       kernel_args_entry_value_equal, NULL));
        }
    }

  /* verify the value array is properly updated */
  GPtrArray *kargs_array = _ostree_kernel_arg_get_key_array (append_arg);
  g_assert (
      ot_ptr_array_find_with_equal_func (kargs_array, "test", kernel_args_entry_key_equal, NULL));
  g_assert (ot_ptr_array_find_with_equal_func (kargs_array, "second_test",
                                               kernel_args_entry_key_equal, NULL));

  /* Up till this point, we verified that the above was all correct, we then
   * check ostree_kernel_args_to_string has the right result
   */
  g_auto (GStrv) kargs_list = ostree_kernel_args_to_strv (append_arg);
  g_assert (g_strv_contains ((const char *const *)kargs_list, "test=valid"));
  g_assert (g_strv_contains ((const char *const *)kargs_list, "test=secondvalid"));
  g_assert (g_strv_contains ((const char *const *)kargs_list, "test="));
  g_assert (g_strv_contains ((const char *const *)kargs_list, "test"));
  g_assert (g_strv_contains ((const char *const *)kargs_list, "second_test"));
  g_assert (g_strv_contains ((const char *const *)kargs_list, "second_test=0"));
  g_assert (g_strv_contains ((const char *const *)kargs_list, "second_test=\"1 2\""));
  g_assert_cmpint (7, ==, g_strv_length (kargs_list));
}

/* Regression test: parsing a /proc/cmdline-style string containing quoted
 * kernel args with spaces must not crash.  Commit abc7d5b9 added a
 * quote-aware splitter (split_kernel_args) but ostree_kernel_args_append_proc_cmdline
 * still used a naive g_strsplit(" ") that broke quoted tokens apart, causing a
 * g_assert_false(quoted) abort.
 *
 * Bootloaders may reformat quotes -- e.g. GRUB turns testparam="value with spaces"
 * into "testparam=value with spaces" in /proc/cmdline.  Both forms must be handled.
 */
static void
test_kargs_quoted_cmdline (void)
{
  /* Test 1: Standard quoted value (as stored by rpm-ostree) */
  {
    __attribute__ ((cleanup (ostree_kernel_args_cleanup))) OstreeKernelArgs *karg
        = ostree_kernel_args_new ();
    ostree_kernel_args_parse_append (karg,
                                     "root=UUID=abc quiet testparam=\"value with spaces\" rw");
    g_assert (check_string_existance (karg, "root=UUID=abc"));
    g_assert (check_string_existance (karg, "quiet"));
    g_assert (check_string_existance (karg, "testparam=\"value with spaces\""));
    g_assert (check_string_existance (karg, "rw"));
  }

  /* Test 2: GRUB-reformatted quoting (quotes wrapping entire token)
   * /proc/cmdline may show: "testparam=value with spaces" */
  {
    __attribute__ ((cleanup (ostree_kernel_args_cleanup))) OstreeKernelArgs *karg
        = ostree_kernel_args_new ();
    ostree_kernel_args_parse_append (karg,
                                     "root=UUID=abc quiet \"testparam=value with spaces\" rw");
    g_assert (check_string_existance (karg, "root=UUID=abc"));
    g_assert (check_string_existance (karg, "quiet"));
    g_assert (check_string_existance (karg, "\"testparam=value with spaces\""));
    g_assert (check_string_existance (karg, "rw"));
  }

  /* Test 3: Multiple quoted args */
  {
    __attribute__ ((cleanup (ostree_kernel_args_cleanup))) OstreeKernelArgs *karg
        = ostree_kernel_args_new ();
    ostree_kernel_args_parse_append (karg, "foo=\"1 2\" bar=\"3 4\"");
    g_assert (check_string_existance (karg, "foo=\"1 2\""));
    g_assert (check_string_existance (karg, "bar=\"3 4\""));
    g_auto (GStrv) kargs_list = ostree_kernel_args_to_strv (karg);
    g_assert_cmpint (2, ==, g_strv_length (kargs_list));
  }
}

int
main (int argc, char *argv[])
{
  g_test_init (&argc, &argv, NULL);

  g_test_add_func ("/kargs/kargs_append", test_kargs_append);
  g_test_add_func ("/kargs/kargs_delete", test_kargs_delete);
  g_test_add_func ("/kargs/kargs_replace", test_kargs_replace);
  g_test_add_func ("/kargs/kargs_quoted_cmdline", test_kargs_quoted_cmdline);
  return g_test_run ();
}
