/*
 * Copyright (C) 2020 Red Hat, Inc.
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#include "cockpitjsonprint.h"

#include "testlib/cockpittest.h"

#include <glib.h>

#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

/* --- testing of printing --- */

typedef struct
{
  char buffer[1024];
  FILE *stream;

  char expected_buffer[1024];
  gchar *expected_end;

  size_t pagesize;
  char *accessible;
  char *inaccessible;
} TestFixture;

static void
test_fixture_setup (TestFixture   *fixture,
                    gconstpointer  user_data)
{
  /* set up the stream */
  fixture->stream = fmemopen (fixture->buffer, sizeof fixture->buffer, "w");
  setbuf (fixture->stream, NULL);
  rewind (fixture->stream);

  /* create a range of accessible bytes surrounded by memory that will
   * cause a crash if accessed.
   */
  fixture->pagesize = sysconf(_SC_PAGE_SIZE);

  /* allocate 3 pages of memory that will crash when accessed (PROT_NONE) */
  char *region = mmap (NULL, 3 * fixture->pagesize, PROT_NONE,
                       MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
  g_assert (region != MAP_FAILED);

  /* punch a read/writable hole in the middle of the 3 pages */
  int r = mprotect (region + fixture->pagesize, fixture->pagesize, PROT_READ | PROT_WRITE);
  g_assert (r == 0);

  /* record the first and last(+1) accessible bytes.  accessing the
   * bytes immediately outside of this range is guaranteed to crash.
   * this allows us to ensure proper memory behaviour of the code we're
   * testing.
   */
  fixture->accessible = region + fixture->pagesize;
  fixture->inaccessible = fixture->accessible + fixture->pagesize;

  /* setup the expected buffer, empty to start */
  fixture->expected_end = fixture->expected_buffer;
}

static void
test_fixture_teardown (TestFixture   *fixture,
                       gconstpointer  user_data)
{
  char *region = fixture->accessible - fixture->pagesize;
  munmap (region, 3 * fixture->pagesize);
  fixture->accessible = NULL;
  fixture->inaccessible = NULL;

  fixture->expected_end = NULL;

  fclose (fixture->stream);
  fixture->stream = NULL;
}

static void
test_fixture_expect (TestFixture *fixture,
                     const gchar *expected)
{
  fixture->expected_end = stpcpy (fixture->expected_end, expected);
}

static void
test_fixture_compare_expected (TestFixture *fixture)
{
  /* Ensure that both strings are nul terminated */
  fixture->buffer[ftell (fixture->stream)] = '\0';
  *fixture->expected_end = '\0';

  /* Ensure that neither string has embedded nuls */
  g_assert_cmpint (strlen (fixture->buffer), ==, ftell (fixture->stream));
  g_assert (strchr (fixture->expected_buffer, '\0') == fixture->expected_end);

  /* Compare! */
  g_assert_cmpstr (fixture->buffer, ==, fixture->expected_buffer);
}

static void
test_print_string (TestFixture   *fixture,
                   gconstpointer  user_data)
{
  /* create a string with every possible byte in it and check that
   * everything is correctly escaped
   */
  char buffer[256];
  int offset = 0;
  char tmp[10];

  test_fixture_expect (fixture, ", \"key\": \"");

  for (int c = 1; c < 32; c++) /* control characters (before space) */
    {
      /* ascii control characters printed as unicode escapes */
      snprintf (tmp, sizeof tmp, "\\u%04x", c);
      test_fixture_expect (fixture, tmp);

      buffer[offset++] = c;
    }

  test_fixture_expect (fixture, " !");
  buffer[offset++] = 32; /* space */
  buffer[offset++] = 33; /* ! */

  test_fixture_expect (fixture, "\\\"");
  buffer[offset++] = 34; /* " */

  for (int c = 35; c < 92; c++) /* # through [ */
    {
      snprintf (tmp, sizeof tmp, "%c", c);
      test_fixture_expect (fixture, tmp);
      buffer[offset++] = c;
    }

  test_fixture_expect (fixture, "\\\\");
  buffer[offset++] = 92; /* \ */

  for (int c = 93; c < 127; c++) /* ] through ~ */
    {
      snprintf (tmp, sizeof tmp, "%c", c);
      test_fixture_expect (fixture, tmp);
      buffer[offset++] = c;
    }

  test_fixture_expect (fixture, "\\u007f");
  buffer[offset++] = 127; /* DEL */

  for (int c = 128; c < 256; c++) /* non-ascii */
    {
      test_fixture_expect (fixture, "?");
      buffer[offset++] = c;
    }

  buffer[offset++] = '\0';
  g_assert_cmpint (offset, ==, sizeof buffer);
  test_fixture_expect (fixture, "\"");

  /* print it with -1, correct length, and "too big" length */
  cockpit_json_print_string_property (fixture->stream, "key", buffer, -1);
  test_fixture_compare_expected (fixture);
  rewind (fixture->stream);

  cockpit_json_print_string_property (fixture->stream, "key", buffer, 255);
  test_fixture_compare_expected (fixture);
  rewind (fixture->stream);

  cockpit_json_print_string_property (fixture->stream, "key", buffer, 256);
  test_fixture_compare_expected (fixture);
  rewind (fixture->stream);
}

/* The following test tries to catch bad behaviour from the scanner that
 * finds the groups of unescaped characters for fast printing.  It
 * ensures that:
 *
 *   1) the groups are always scanned correctly and correct escaped
 *      output is produced
 *
 *   2) a nul is always honoured, regardless of a larger given
 *      max_length parameter
 *
 *   3) max_length is always honoured, regardless of if a nul is present
 *      or not.
 *
 * In particular, we use our fixture to ensure that we never touch
 * memory past either the final nul character, or past the specified
 * max_length.
 *
 * We test strings composed of a given number 'groups'.  Each group
 * consists of a certain number of repetitions ('reps') of a given
 * character.
 *
 * We use a sequence counter to determine the character and the number
 * of reps for each group.  At each step we take the modulus of a
 * division to make a decision about the given variable, using the whole
 * result as a residual for future decisions.  This effectively
 * implements a counter with an arbitrary radix at each position.  Once
 * we see a non-zero resitual, we know that we've surely exhausted all
 * possible combinations.
 *
 * These constants could easily be a bit higher, but the running time
 * explodes pretty quickly, and this test benefits from being run under
 * valgrind.
 */
#define MIN_GROUPS  1
#define MAX_GROUPS  3
#define MAX_REPS    5

static guint
divmod (guint *residual,
        guint  divisor)
{
  guint result = *residual % divisor;

  *residual /= divisor;

  return result;
}

static void
test_print_string_memory_safety (TestFixture   *fixture,
                                 gconstpointer  user_data)
{
  gchar characters[] = { '\n', ' ', 'a', '\\', '\"', 0xcc };
  const gchar *escaped[] = { "\\u000a", " ", "a", "\\\\", "\\\"", "?" };
  gchar buffer[MAX_GROUPS * MAX_REPS];

  for (gint n_groups = MIN_GROUPS; n_groups <= MAX_GROUPS; n_groups++)
    {
      for (guint seq = 0;; seq++)
        {
          guint residual = seq;
          gint length = 0;
          int reps = 0;

          fixture->expected_end = stpcpy (fixture->expected_buffer, ", \"key\": \"");

          for (gint group = 0; group < n_groups; group++)
            {
              gint c = divmod (&residual, sizeof characters);
              reps = divmod (&residual, MAX_REPS) + 1;

              memset (buffer + length, characters[c], reps);
              length += reps;

              for (gint i = 0; i < reps; i++)
                test_fixture_expect (fixture, escaped[c]);
            }

          g_assert_cmpint (length, <=, sizeof buffer);

          if (residual)
            /* non-zero residual → we've already tried all cases */
            break;

          test_fixture_expect (fixture, "\"");

          /* Test various cases of the string not being nul terminated.
           * We avoid starting from 0 each time in order to avoid
           * effectively testing fewer groups.  `reps` is leftover from
           * the last iteration of the loop above.  Starting at:
           *
           *    length - reps + 1
           *
           * makes sure that we see at least one character from this
           * final group.
           *
           * We position the subset of the string at `region` (at a
           * negative offset to the inaccessible area in the fixture) to
           * ensure that we don't read more than the requested `i`
           * characters.
           *
           * This test doesn't ensure that the correct output is
           * produced.  It's difficult to cut the expected string to the
           * correct length, given the different lengths of escaped
           * characters.
           */
          for (gint i = length - reps + 1; i <= length; i++)
            {
              gchar *region = fixture->inaccessible - i;

              memcpy (region, buffer, i);
              cockpit_json_print_string_property (fixture->stream, "key", region, i);
              rewind (fixture->stream);
            }

          /* These ones test a complete nul-terminated string.  As such,
           * we configure the region to be exactly large enough to hold
           * the nul-terminated string.  Then we try giving different
           * lengths.
           */
          gchar *region = fixture->inaccessible - (length + 1);
          memcpy (region, buffer, length);
          region[length] = '\0';

          /* First with -1 */
          cockpit_json_print_string_property (fixture->stream, "key", region, -1);
          test_fixture_compare_expected (fixture);
          rewind (fixture->stream);

          /* Then the exact length */
          cockpit_json_print_string_property (fixture->stream, "key", region, length);
          test_fixture_compare_expected (fixture);
          rewind (fixture->stream);

          /* Then lengths bigger than the string (keeping in mind that
           * this is a *max* length parameter).
           */
          for (gint i = length + 1; i <= length + 3; i++)
            {
              cockpit_json_print_string_property (fixture->stream, "key", region, i);
              test_fixture_compare_expected (fixture);
              rewind (fixture->stream);
            }
        }
    }
}

static void
test_print_numeric (TestFixture   *fixture,
                    gconstpointer  user_data)
{
  cockpit_json_print_integer_property (fixture->stream, "zero", 0);
  test_fixture_expect (fixture, ", \"zero\": 0");

  cockpit_json_print_integer_property (fixture->stream, "one", 1);
  test_fixture_expect (fixture, ", \"one\": 1");

  cockpit_json_print_integer_property (fixture->stream, "million", 1000000);
  test_fixture_expect (fixture, ", \"million\": 1000000");

  /* check that numbers that can't be encoded in double still work */
  guint64 extra_big = 9007199254740993ull; /* 2^53 + 1 */
  g_assert_cmpint (extra_big, !=, (guint64) (double) extra_big);
  cockpit_json_print_integer_property (fixture->stream, "extrabig", extra_big);
  test_fixture_expect (fixture, ", \"extrabig\": 9007199254740993");

  /* check these special values to make sure they're being handled as uint64 */
  cockpit_json_print_integer_property (fixture->stream, "intmax", INT64_MAX);
  test_fixture_expect (fixture, ", \"intmax\": 9223372036854775807");
  cockpit_json_print_integer_property (fixture->stream, "intmaxplusone", INT64_MAX + 1ull);
  test_fixture_expect (fixture, ", \"intmaxplusone\": 9223372036854775808");
  cockpit_json_print_integer_property (fixture->stream, "uintmax", UINT64_MAX);
  test_fixture_expect (fixture, ", \"uintmax\": 18446744073709551615");
  cockpit_json_print_integer_property (fixture->stream, "minus1", -1);
  test_fixture_expect (fixture, ", \"minus1\": 18446744073709551615");

  /* make sure it all worked out */
  test_fixture_compare_expected (fixture);
}

static void
test_print_boolean (TestFixture   *fixture,
                    gconstpointer  user_data)
{
  test_fixture_expect (fixture, ", \"true\": true, \"false\": false, \"alsotrue\": true");

  cockpit_json_print_bool_property (fixture->stream, "true", true);
  cockpit_json_print_bool_property (fixture->stream, "false", false);
  cockpit_json_print_bool_property (fixture->stream, "alsotrue", 123456);

  test_fixture_compare_expected (fixture);
}

int
main (int    argc,
      char **argv)
{
  cockpit_test_init (&argc, &argv);

  g_test_add ("/jsonprint/string", TestFixture, NULL,
              test_fixture_setup, test_print_string, test_fixture_teardown);
  g_test_add ("/jsonprint/string/memory-safety", TestFixture, NULL,
              test_fixture_setup,  test_print_string_memory_safety, test_fixture_teardown);
  g_test_add ("/jsonprint/numeric", TestFixture, NULL,
              test_fixture_setup, test_print_numeric, test_fixture_teardown);
  g_test_add ("/jsonprint/boolean", TestFixture, NULL,
              test_fixture_setup, test_print_boolean, test_fixture_teardown);

  return g_test_run ();
}
