* 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>
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)
{
fixture->stream = fmemopen (fixture->buffer, sizeof fixture->buffer, "w");
setbuf (fixture->stream, NULL);
rewind (fixture->stream);
* cause a crash if accessed.
*/
fixture->pagesize = sysconf(_SC_PAGE_SIZE);
char *region = mmap (NULL, 3 * fixture->pagesize, PROT_NONE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
g_assert (region != MAP_FAILED);
int r = mprotect (region + fixture->pagesize, fixture->pagesize, PROT_READ | PROT_WRITE);
g_assert (r == 0);
* 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;
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)
{
fixture->buffer[ftell (fixture->stream)] = '\0';
*fixture->expected_end = '\0';
g_assert_cmpint (strlen (fixture->buffer), ==, ftell (fixture->stream));
g_assert (strchr (fixture->expected_buffer, '\0') == fixture->expected_end);
g_assert_cmpstr (fixture->buffer, ==, fixture->expected_buffer);
}
static void
test_print_string (TestFixture *fixture,
gconstpointer user_data)
{
* 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++)
{
snprintf (tmp, sizeof tmp, "\\u%04x", c);
test_fixture_expect (fixture, tmp);
buffer[offset++] = c;
}
test_fixture_expect (fixture, " !");
buffer[offset++] = 32;
buffer[offset++] = 33;
test_fixture_expect (fixture, "\\\"");
buffer[offset++] = 34;
for (int c = 35; c < 92; c++)
{
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++)
{
snprintf (tmp, sizeof tmp, "%c", c);
test_fixture_expect (fixture, tmp);
buffer[offset++] = c;
}
test_fixture_expect (fixture, "\\u007f");
buffer[offset++] = 127;
for (int c = 128; c < 256; c++)
{
test_fixture_expect (fixture, "?");
buffer[offset++] = c;
}
buffer[offset++] = '\0';
g_assert_cmpint (offset, ==, sizeof buffer);
test_fixture_expect (fixture, "\"");
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);
}
* 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)
break;
test_fixture_expect (fixture, "\"");
* 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);
}
* 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';
cockpit_json_print_string_property (fixture->stream, "key", region, -1);
test_fixture_compare_expected (fixture);
rewind (fixture->stream);
cockpit_json_print_string_property (fixture->stream, "key", region, length);
test_fixture_compare_expected (fixture);
rewind (fixture->stream);
* 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");
guint64 extra_big = 9007199254740993ull;
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");
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");
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 ();
}