/*
 * Copyright (C) 2016-2017 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "string-utils.h"
#include "string-utils.c"

#include <glib.h>

static void test_sc_streq(void) {
    g_assert_false(sc_streq(NULL, NULL));
    g_assert_false(sc_streq(NULL, "text"));
    g_assert_false(sc_streq("text", NULL));
    g_assert_false(sc_streq("foo", "bar"));
    g_assert_false(sc_streq("foo", "barbar"));
    g_assert_false(sc_streq("foofoo", "bar"));
    g_assert_true(sc_streq("text", "text"));
    g_assert_true(sc_streq("", ""));
}

static void test_sc_endswith(void) {
    // NULL doesn't end with anything, nothing ends with NULL
    g_assert_false(sc_endswith("", NULL));
    g_assert_false(sc_endswith(NULL, ""));
    g_assert_false(sc_endswith(NULL, NULL));
    // Empty string ends with an empty string
    g_assert_true(sc_endswith("", ""));
    // Ends-with (matches)
    g_assert_true(sc_endswith("foobar", "bar"));
    g_assert_true(sc_endswith("foobar", "ar"));
    g_assert_true(sc_endswith("foobar", "r"));
    g_assert_true(sc_endswith("foobar", ""));
    g_assert_true(sc_endswith("bar", "bar"));
    // Ends-with (non-matches)
    g_assert_false(sc_endswith("foobar", "quux"));
    g_assert_false(sc_endswith("", "bar"));
    g_assert_false(sc_endswith("b", "bar"));
    g_assert_false(sc_endswith("ba", "bar"));
}

static void test_sc_startswith(void) {
    // NULL doesn't start with anything, nothing starts with NULL
    g_assert_false(sc_startswith("", NULL));
    g_assert_false(sc_startswith(NULL, ""));
    g_assert_false(sc_startswith(NULL, NULL));
    // Empty string starts with an empty string
    g_assert_true(sc_startswith("", ""));
    // Starts-with (matches)
    g_assert_true(sc_startswith("foobar", "foo"));
    g_assert_true(sc_startswith("foobar", "fo"));
    g_assert_true(sc_startswith("foobar", "f"));
    g_assert_true(sc_startswith("foobar", ""));
    g_assert_true(sc_startswith("bar", "bar"));
    // Starts-with (non-matches)
    g_assert_false(sc_startswith("foobar", "quux"));
    g_assert_false(sc_startswith("", "bar"));
    g_assert_false(sc_startswith("b", "bar"));
    g_assert_false(sc_startswith("ba", "bar"));
}

static void test_sc_must_snprintf(void) {
    char buf[5] = {0};
    sc_must_snprintf(buf, sizeof buf, "1234");
    g_assert_cmpstr(buf, ==, "1234");
}

static void test_sc_must_snprintf__fail(void) {
    if (g_test_subprocess()) {
        char buf[5];
        sc_must_snprintf(buf, sizeof buf, "12345");
        g_test_message("expected sc_must_snprintf not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot format string: 1234\n");
}

// Check that appending to a buffer works OK.
static void test_sc_string_append(void) {
    union {
        char bigbuf[6];
        struct {
            signed char canary1;
            char buf[4];
            signed char canary2;
        };
    } data = {
        .buf = {'f', '\0', 0xFF, 0xFF},
        .canary1 = ~0,
        .canary2 = ~0,
    };

    // Sanity check, ensure that the layout of structures is as spelled above.
    // (first canary1, then buf and finally canary2.
    g_assert_cmpint(((char *)&data.buf[0]) - ((char *)&data.canary1), ==, 1);
    g_assert_cmpint(((char *)&data.buf[4]) - ((char *)&data.canary2), ==, 0);

    sc_string_append(data.buf, sizeof data.buf, "oo");

    // Check that we didn't corrupt either canary.
    g_assert_cmpint(data.canary1, ==, ~0);
    g_assert_cmpint(data.canary2, ==, ~0);

    // Check that we got the result that was expected.
    g_assert_cmpstr(data.buf, ==, "foo");
}

// Check that appending an empty string to a full buffer is valid.
static void test_sc_string_append__empty_to_full(void) {
    union {
        char bigbuf[6];
        struct {
            signed char canary1;
            char buf[4];
            signed char canary2;
        };
    } data = {
        .buf = {'f', 'o', 'o', '\0'},
        .canary1 = ~0,
        .canary2 = ~0,
    };

    // Sanity check, ensure that the layout of structures is as spelled above.
    // (first canary1, then buf and finally canary2.
    g_assert_cmpint(((char *)&data.buf[0]) - ((char *)&data.canary1), ==, 1);
    g_assert_cmpint(((char *)&data.buf[4]) - ((char *)&data.canary2), ==, 0);

    sc_string_append(data.buf, sizeof data.buf, "");

    // Check that we didn't corrupt either canary.
    g_assert_cmpint(data.canary1, ==, ~0);
    g_assert_cmpint(data.canary2, ==, ~0);

    // Check that we got the result that was expected.
    g_assert_cmpstr(data.buf, ==, "foo");
}

// Check that the overflow detection works.
static void test_sc_string_append__overflow(void) {
    if (g_test_subprocess()) {
        char buf[4] = {0};
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Warray-bounds"
        // Try to append a string that's one character too long.
        sc_string_append(buf, sizeof buf, "1234");
#pragma GCC diagnostic pop
        g_test_message("expected sc_string_append not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot append string: str is too long or unterminated\n");
}

// Check that the uninitialized buffer detection works.
static void test_sc_string_append__uninitialized_buf(void) {
    if (g_test_subprocess()) {
        char buf[4] = {0xFF, 0xFF, 0xFF, 0xFF};

        // Try to append a string to a buffer which is not a valic C-string.
        sc_string_append(buf, sizeof buf, "");

        g_test_message("expected sc_string_append not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot append string: dst is unterminated\n");
}

// Check that `buf' cannot be NULL.
static void test_sc_string_append__NULL_buf(void) {
    if (g_test_subprocess()) {
        char buf[4];

        sc_string_append(NULL, sizeof buf, "foo");

        g_test_message("expected sc_string_append not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot append string: buffer is NULL\n");
}

// Check that `src' cannot be NULL.
static void test_sc_string_append__NULL_str(void) {
    if (g_test_subprocess()) {
        char buf[4];

        sc_string_append(buf, sizeof buf, NULL);

        g_test_message("expected sc_string_append not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot append string: string is NULL\n");
}

static void test_sc_string_init__normal(void) {
    char buf[1] = {0xFF};

    sc_string_init(buf, sizeof buf);
    g_assert_cmpint(buf[0], ==, 0);
}

static void test_sc_string_init__empty_buf(void) {
    if (g_test_subprocess()) {
        char buf[1] = {0xFF};

        sc_string_init(buf, 0);

        g_test_message("expected sc_string_init not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot initialize string, buffer is too small\n");
}

static void test_sc_string_init__NULL_buf(void) {
    if (g_test_subprocess()) {
        sc_string_init(NULL, 1);

        g_test_message("expected sc_string_init not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot initialize string, buffer is NULL\n");
}

static void test_sc_string_append_char__uninitialized_buf(void) {
    if (g_test_subprocess()) {
        char buf[2] = {0xFF, 0xFF};
        sc_string_append_char(buf, sizeof buf, 'a');

        g_test_message("expected sc_string_append_char not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot append character: dst is unterminated\n");
}

static void test_sc_string_append_char__NULL_buf(void) {
    if (g_test_subprocess()) {
        sc_string_append_char(NULL, 2, 'a');

        g_test_message("expected sc_string_append_char not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot append character: buffer is NULL\n");
}

static void test_sc_string_append_char__overflow(void) {
    if (g_test_subprocess()) {
        char buf[1] = {0};
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Warray-bounds"
        sc_string_append_char(buf, sizeof buf, 'a');
#pragma GCC diagnostic pop
        g_test_message("expected sc_string_append_char not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot append character: not enough space\n");
}

static void test_sc_string_append_char__invalid_zero(void) {
    if (g_test_subprocess()) {
        char buf[2] = {0};
        sc_string_append_char(buf, sizeof buf, '\0');

        g_test_message("expected sc_string_append_char not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot append character: cannot append string terminator\n");
}

static void test_sc_string_append_char__normal(void) {
    char buf[16];
    size_t len;
    sc_string_init(buf, sizeof buf);

    len = sc_string_append_char(buf, sizeof buf, 'h');
    g_assert_cmpstr(buf, ==, "h");
    g_assert_cmpint(len, ==, 1);
    len = sc_string_append_char(buf, sizeof buf, 'e');
    g_assert_cmpstr(buf, ==, "he");
    g_assert_cmpint(len, ==, 2);
    len = sc_string_append_char(buf, sizeof buf, 'l');
    g_assert_cmpstr(buf, ==, "hel");
    g_assert_cmpint(len, ==, 3);
    len = sc_string_append_char(buf, sizeof buf, 'l');
    g_assert_cmpstr(buf, ==, "hell");
    g_assert_cmpint(len, ==, 4);
    len = sc_string_append_char(buf, sizeof buf, 'o');
    g_assert_cmpstr(buf, ==, "hello");
    g_assert_cmpint(len, ==, 5);
}

static void test_sc_string_append_char_pair__uninitialized_buf(void) {
    if (g_test_subprocess()) {
        char buf[3] = {0xFF, 0xFF, 0xFF};
        sc_string_append_char_pair(buf, sizeof buf, 'a', 'b');

        g_test_message("expected sc_string_append_char_pair not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot append character pair: dst is unterminated\n");
}

static void test_sc_string_append_char_pair__NULL_buf(void) {
    if (g_test_subprocess()) {
        sc_string_append_char_pair(NULL, 3, 'a', 'b');

        g_test_message("expected sc_string_append_char_pair not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot append character pair: buffer is NULL\n");
}

static void test_sc_string_append_char_pair__overflow(void) {
    if (g_test_subprocess()) {
        char buf[2] = {0};
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Warray-bounds"
        sc_string_append_char_pair(buf, sizeof buf, 'a', 'b');
#pragma GCC diagnostic pop
        g_test_message("expected sc_string_append_char_pair not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot append character pair: not enough space\n");
}

static void test_sc_string_append_char_pair__invalid_zero_c1(void) {
    if (g_test_subprocess()) {
        char buf[3] = {0};
        sc_string_append_char_pair(buf, sizeof buf, '\0', 'a');

        g_test_message("expected sc_string_append_char_pair not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot append character pair: cannot append string terminator\n");
}

static void test_sc_string_append_char_pair__invalid_zero_c2(void) {
    if (g_test_subprocess()) {
        char buf[3] = {0};
        sc_string_append_char_pair(buf, sizeof buf, 'a', '\0');

        g_test_message("expected sc_string_append_char_pair not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot append character pair: cannot append string terminator\n");
}

static void test_sc_string_append_char_pair__normal(void) {
    char buf[16];
    size_t len;
    sc_string_init(buf, sizeof buf);

    len = sc_string_append_char_pair(buf, sizeof buf, 'h', 'e');
    g_assert_cmpstr(buf, ==, "he");
    g_assert_cmpint(len, ==, 2);
    len = sc_string_append_char_pair(buf, sizeof buf, 'l', 'l');
    g_assert_cmpstr(buf, ==, "hell");
    g_assert_cmpint(len, ==, 4);
    len = sc_string_append_char_pair(buf, sizeof buf, 'o', '!');
    g_assert_cmpstr(buf, ==, "hello!");
    g_assert_cmpint(len, ==, 6);
}

static void test_sc_string_quote_NULL_str(void) {
    if (g_test_subprocess()) {
        char buf[16] = {0};
        sc_string_quote(buf, sizeof buf, NULL);

        g_test_message("expected sc_string_quote not to return");
        g_test_fail();
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("cannot quote string: string is NULL\n");
}

static void test_quoting_of(bool tested[], int c, const char *expected) {
    char buf[16];

    g_assert_cmpint(c, >=, 0);
    g_assert_cmpint(c, <=, 255);

    // Create an input string with one character.
    char input[2] = {(unsigned char)c, 0};
    sc_string_quote(buf, sizeof buf, input);

    // Ensure it was quoted as we expected.
    g_assert_cmpstr(buf, ==, expected);

    tested[c] = true;
}

static void test_sc_string_quote(void) {
#define DQ "\""
    char buf[16];
    bool is_tested[256] = {false};

    // Exhaustive test for quoting of every 8bit input.  This is very verbose
    // but the goal is to have a very obvious and correct test that ensures no
    // edge case is lost.
    //
    // block 1: 0x00 - 0x0f
    test_quoting_of(is_tested, 0x00, DQ "" DQ);
    test_quoting_of(is_tested, 0x01, DQ "\\x01" DQ);
    test_quoting_of(is_tested, 0x02, DQ "\\x02" DQ);
    test_quoting_of(is_tested, 0x03, DQ "\\x03" DQ);
    test_quoting_of(is_tested, 0x04, DQ "\\x04" DQ);
    test_quoting_of(is_tested, 0x05, DQ "\\x05" DQ);
    test_quoting_of(is_tested, 0x06, DQ "\\x06" DQ);
    test_quoting_of(is_tested, 0x07, DQ "\\x07" DQ);
    test_quoting_of(is_tested, 0x08, DQ "\\x08" DQ);
    test_quoting_of(is_tested, 0x09, DQ "\\t" DQ);
    test_quoting_of(is_tested, 0x0a, DQ "\\n" DQ);
    test_quoting_of(is_tested, 0x0b, DQ "\\v" DQ);
    test_quoting_of(is_tested, 0x0c, DQ "\\x0c" DQ);
    test_quoting_of(is_tested, 0x0d, DQ "\\r" DQ);
    test_quoting_of(is_tested, 0x0e, DQ "\\x0e" DQ);
    test_quoting_of(is_tested, 0x0f, DQ "\\x0f" DQ);
    // block 2: 0x10 - 0x1f
    test_quoting_of(is_tested, 0x10, DQ "\\x10" DQ);
    test_quoting_of(is_tested, 0x11, DQ "\\x11" DQ);
    test_quoting_of(is_tested, 0x12, DQ "\\x12" DQ);
    test_quoting_of(is_tested, 0x13, DQ "\\x13" DQ);
    test_quoting_of(is_tested, 0x14, DQ "\\x14" DQ);
    test_quoting_of(is_tested, 0x15, DQ "\\x15" DQ);
    test_quoting_of(is_tested, 0x16, DQ "\\x16" DQ);
    test_quoting_of(is_tested, 0x17, DQ "\\x17" DQ);
    test_quoting_of(is_tested, 0x18, DQ "\\x18" DQ);
    test_quoting_of(is_tested, 0x19, DQ "\\x19" DQ);
    test_quoting_of(is_tested, 0x1a, DQ "\\x1a" DQ);
    test_quoting_of(is_tested, 0x1b, DQ "\\x1b" DQ);
    test_quoting_of(is_tested, 0x1c, DQ "\\x1c" DQ);
    test_quoting_of(is_tested, 0x1d, DQ "\\x1d" DQ);
    test_quoting_of(is_tested, 0x1e, DQ "\\x1e" DQ);
    test_quoting_of(is_tested, 0x1f, DQ "\\x1f" DQ);
    // block 3: 0x20 - 0x2f
    test_quoting_of(is_tested, 0x20, DQ " " DQ);
    test_quoting_of(is_tested, 0x21, DQ "!" DQ);
    test_quoting_of(is_tested, 0x22, DQ "\\\"" DQ);
    test_quoting_of(is_tested, 0x23, DQ "#" DQ);
    test_quoting_of(is_tested, 0x24, DQ "$" DQ);
    test_quoting_of(is_tested, 0x25, DQ "%" DQ);
    test_quoting_of(is_tested, 0x26, DQ "&" DQ);
    test_quoting_of(is_tested, 0x27, DQ "'" DQ);
    test_quoting_of(is_tested, 0x28, DQ "(" DQ);
    test_quoting_of(is_tested, 0x29, DQ ")" DQ);
    test_quoting_of(is_tested, 0x2a, DQ "*" DQ);
    test_quoting_of(is_tested, 0x2b, DQ "+" DQ);
    test_quoting_of(is_tested, 0x2c, DQ "," DQ);
    test_quoting_of(is_tested, 0x2d, DQ "-" DQ);
    test_quoting_of(is_tested, 0x2e, DQ "." DQ);
    test_quoting_of(is_tested, 0x2f, DQ "/" DQ);
    // block 4: 0x30 - 0x3f
    test_quoting_of(is_tested, 0x30, DQ "0" DQ);
    test_quoting_of(is_tested, 0x31, DQ "1" DQ);
    test_quoting_of(is_tested, 0x32, DQ "2" DQ);
    test_quoting_of(is_tested, 0x33, DQ "3" DQ);
    test_quoting_of(is_tested, 0x34, DQ "4" DQ);
    test_quoting_of(is_tested, 0x35, DQ "5" DQ);
    test_quoting_of(is_tested, 0x36, DQ "6" DQ);
    test_quoting_of(is_tested, 0x37, DQ "7" DQ);
    test_quoting_of(is_tested, 0x38, DQ "8" DQ);
    test_quoting_of(is_tested, 0x39, DQ "9" DQ);
    test_quoting_of(is_tested, 0x3a, DQ ":" DQ);
    test_quoting_of(is_tested, 0x3b, DQ ";" DQ);
    test_quoting_of(is_tested, 0x3c, DQ "<" DQ);
    test_quoting_of(is_tested, 0x3d, DQ "=" DQ);
    test_quoting_of(is_tested, 0x3e, DQ ">" DQ);
    test_quoting_of(is_tested, 0x3f, DQ "?" DQ);
    // block 5: 0x40 - 0x4f
    test_quoting_of(is_tested, 0x40, DQ "@" DQ);
    test_quoting_of(is_tested, 0x41, DQ "A" DQ);
    test_quoting_of(is_tested, 0x42, DQ "B" DQ);
    test_quoting_of(is_tested, 0x43, DQ "C" DQ);
    test_quoting_of(is_tested, 0x44, DQ "D" DQ);
    test_quoting_of(is_tested, 0x45, DQ "E" DQ);
    test_quoting_of(is_tested, 0x46, DQ "F" DQ);
    test_quoting_of(is_tested, 0x47, DQ "G" DQ);
    test_quoting_of(is_tested, 0x48, DQ "H" DQ);
    test_quoting_of(is_tested, 0x49, DQ "I" DQ);
    test_quoting_of(is_tested, 0x4a, DQ "J" DQ);
    test_quoting_of(is_tested, 0x4b, DQ "K" DQ);
    test_quoting_of(is_tested, 0x4c, DQ "L" DQ);
    test_quoting_of(is_tested, 0x4d, DQ "M" DQ);
    test_quoting_of(is_tested, 0x4e, DQ "N" DQ);
    test_quoting_of(is_tested, 0x4f, DQ "O" DQ);
    // block 6: 0x50 - 0x5f
    test_quoting_of(is_tested, 0x50, DQ "P" DQ);
    test_quoting_of(is_tested, 0x51, DQ "Q" DQ);
    test_quoting_of(is_tested, 0x52, DQ "R" DQ);
    test_quoting_of(is_tested, 0x53, DQ "S" DQ);
    test_quoting_of(is_tested, 0x54, DQ "T" DQ);
    test_quoting_of(is_tested, 0x55, DQ "U" DQ);
    test_quoting_of(is_tested, 0x56, DQ "V" DQ);
    test_quoting_of(is_tested, 0x57, DQ "W" DQ);
    test_quoting_of(is_tested, 0x58, DQ "X" DQ);
    test_quoting_of(is_tested, 0x59, DQ "Y" DQ);
    test_quoting_of(is_tested, 0x5a, DQ "Z" DQ);
    test_quoting_of(is_tested, 0x5b, DQ "[" DQ);
    test_quoting_of(is_tested, 0x5c, DQ "\\\\" DQ);
    test_quoting_of(is_tested, 0x5d, DQ "]" DQ);
    test_quoting_of(is_tested, 0x5e, DQ "^" DQ);
    test_quoting_of(is_tested, 0x5f, DQ "_" DQ);
    // block 7: 0x60 - 0x6f
    test_quoting_of(is_tested, 0x60, DQ "`" DQ);
    test_quoting_of(is_tested, 0x61, DQ "a" DQ);
    test_quoting_of(is_tested, 0x62, DQ "b" DQ);
    test_quoting_of(is_tested, 0x63, DQ "c" DQ);
    test_quoting_of(is_tested, 0x64, DQ "d" DQ);
    test_quoting_of(is_tested, 0x65, DQ "e" DQ);
    test_quoting_of(is_tested, 0x66, DQ "f" DQ);
    test_quoting_of(is_tested, 0x67, DQ "g" DQ);
    test_quoting_of(is_tested, 0x68, DQ "h" DQ);
    test_quoting_of(is_tested, 0x69, DQ "i" DQ);
    test_quoting_of(is_tested, 0x6a, DQ "j" DQ);
    test_quoting_of(is_tested, 0x6b, DQ "k" DQ);
    test_quoting_of(is_tested, 0x6c, DQ "l" DQ);
    test_quoting_of(is_tested, 0x6d, DQ "m" DQ);
    test_quoting_of(is_tested, 0x6e, DQ "n" DQ);
    test_quoting_of(is_tested, 0x6f, DQ "o" DQ);
    // block 8: 0x70 - 0x7f
    test_quoting_of(is_tested, 0x70, DQ "p" DQ);
    test_quoting_of(is_tested, 0x71, DQ "q" DQ);
    test_quoting_of(is_tested, 0x72, DQ "r" DQ);
    test_quoting_of(is_tested, 0x73, DQ "s" DQ);
    test_quoting_of(is_tested, 0x74, DQ "t" DQ);
    test_quoting_of(is_tested, 0x75, DQ "u" DQ);
    test_quoting_of(is_tested, 0x76, DQ "v" DQ);
    test_quoting_of(is_tested, 0x77, DQ "w" DQ);
    test_quoting_of(is_tested, 0x78, DQ "x" DQ);
    test_quoting_of(is_tested, 0x79, DQ "y" DQ);
    test_quoting_of(is_tested, 0x7a, DQ "z" DQ);
    test_quoting_of(is_tested, 0x7b, DQ "{" DQ);
    test_quoting_of(is_tested, 0x7c, DQ "|" DQ);
    test_quoting_of(is_tested, 0x7d, DQ "}" DQ);
    test_quoting_of(is_tested, 0x7e, DQ "~" DQ);
    test_quoting_of(is_tested, 0x7f, DQ "\\x7f" DQ);
    // block 9 (8-bit): 0x80 - 0x8f
    test_quoting_of(is_tested, 0x80, DQ "\\x80" DQ);
    test_quoting_of(is_tested, 0x81, DQ "\\x81" DQ);
    test_quoting_of(is_tested, 0x82, DQ "\\x82" DQ);
    test_quoting_of(is_tested, 0x83, DQ "\\x83" DQ);
    test_quoting_of(is_tested, 0x84, DQ "\\x84" DQ);
    test_quoting_of(is_tested, 0x85, DQ "\\x85" DQ);
    test_quoting_of(is_tested, 0x86, DQ "\\x86" DQ);
    test_quoting_of(is_tested, 0x87, DQ "\\x87" DQ);
    test_quoting_of(is_tested, 0x88, DQ "\\x88" DQ);
    test_quoting_of(is_tested, 0x89, DQ "\\x89" DQ);
    test_quoting_of(is_tested, 0x8a, DQ "\\x8a" DQ);
    test_quoting_of(is_tested, 0x8b, DQ "\\x8b" DQ);
    test_quoting_of(is_tested, 0x8c, DQ "\\x8c" DQ);
    test_quoting_of(is_tested, 0x8d, DQ "\\x8d" DQ);
    test_quoting_of(is_tested, 0x8e, DQ "\\x8e" DQ);
    test_quoting_of(is_tested, 0x8f, DQ "\\x8f" DQ);
    // block 10 (8-bit): 0x90 - 0x9f
    test_quoting_of(is_tested, 0x90, DQ "\\x90" DQ);
    test_quoting_of(is_tested, 0x91, DQ "\\x91" DQ);
    test_quoting_of(is_tested, 0x92, DQ "\\x92" DQ);
    test_quoting_of(is_tested, 0x93, DQ "\\x93" DQ);
    test_quoting_of(is_tested, 0x94, DQ "\\x94" DQ);
    test_quoting_of(is_tested, 0x95, DQ "\\x95" DQ);
    test_quoting_of(is_tested, 0x96, DQ "\\x96" DQ);
    test_quoting_of(is_tested, 0x97, DQ "\\x97" DQ);
    test_quoting_of(is_tested, 0x98, DQ "\\x98" DQ);
    test_quoting_of(is_tested, 0x99, DQ "\\x99" DQ);
    test_quoting_of(is_tested, 0x9a, DQ "\\x9a" DQ);
    test_quoting_of(is_tested, 0x9b, DQ "\\x9b" DQ);
    test_quoting_of(is_tested, 0x9c, DQ "\\x9c" DQ);
    test_quoting_of(is_tested, 0x9d, DQ "\\x9d" DQ);
    test_quoting_of(is_tested, 0x9e, DQ "\\x9e" DQ);
    test_quoting_of(is_tested, 0x9f, DQ "\\x9f" DQ);
    // block 11 (8-bit): 0xa0 - 0xaf
    test_quoting_of(is_tested, 0xa0, DQ "\\xa0" DQ);
    test_quoting_of(is_tested, 0xa1, DQ "\\xa1" DQ);
    test_quoting_of(is_tested, 0xa2, DQ "\\xa2" DQ);
    test_quoting_of(is_tested, 0xa3, DQ "\\xa3" DQ);
    test_quoting_of(is_tested, 0xa4, DQ "\\xa4" DQ);
    test_quoting_of(is_tested, 0xa5, DQ "\\xa5" DQ);
    test_quoting_of(is_tested, 0xa6, DQ "\\xa6" DQ);
    test_quoting_of(is_tested, 0xa7, DQ "\\xa7" DQ);
    test_quoting_of(is_tested, 0xa8, DQ "\\xa8" DQ);
    test_quoting_of(is_tested, 0xa9, DQ "\\xa9" DQ);
    test_quoting_of(is_tested, 0xaa, DQ "\\xaa" DQ);
    test_quoting_of(is_tested, 0xab, DQ "\\xab" DQ);
    test_quoting_of(is_tested, 0xac, DQ "\\xac" DQ);
    test_quoting_of(is_tested, 0xad, DQ "\\xad" DQ);
    test_quoting_of(is_tested, 0xae, DQ "\\xae" DQ);
    test_quoting_of(is_tested, 0xaf, DQ "\\xaf" DQ);
    // block 12 (8-bit): 0xb0 - 0xbf
    test_quoting_of(is_tested, 0xb0, DQ "\\xb0" DQ);
    test_quoting_of(is_tested, 0xb1, DQ "\\xb1" DQ);
    test_quoting_of(is_tested, 0xb2, DQ "\\xb2" DQ);
    test_quoting_of(is_tested, 0xb3, DQ "\\xb3" DQ);
    test_quoting_of(is_tested, 0xb4, DQ "\\xb4" DQ);
    test_quoting_of(is_tested, 0xb5, DQ "\\xb5" DQ);
    test_quoting_of(is_tested, 0xb6, DQ "\\xb6" DQ);
    test_quoting_of(is_tested, 0xb7, DQ "\\xb7" DQ);
    test_quoting_of(is_tested, 0xb8, DQ "\\xb8" DQ);
    test_quoting_of(is_tested, 0xb9, DQ "\\xb9" DQ);
    test_quoting_of(is_tested, 0xba, DQ "\\xba" DQ);
    test_quoting_of(is_tested, 0xbb, DQ "\\xbb" DQ);
    test_quoting_of(is_tested, 0xbc, DQ "\\xbc" DQ);
    test_quoting_of(is_tested, 0xbd, DQ "\\xbd" DQ);
    test_quoting_of(is_tested, 0xbe, DQ "\\xbe" DQ);
    test_quoting_of(is_tested, 0xbf, DQ "\\xbf" DQ);
    // block 13 (8-bit): 0xc0 - 0xcf
    test_quoting_of(is_tested, 0xc0, DQ "\\xc0" DQ);
    test_quoting_of(is_tested, 0xc1, DQ "\\xc1" DQ);
    test_quoting_of(is_tested, 0xc2, DQ "\\xc2" DQ);
    test_quoting_of(is_tested, 0xc3, DQ "\\xc3" DQ);
    test_quoting_of(is_tested, 0xc4, DQ "\\xc4" DQ);
    test_quoting_of(is_tested, 0xc5, DQ "\\xc5" DQ);
    test_quoting_of(is_tested, 0xc6, DQ "\\xc6" DQ);
    test_quoting_of(is_tested, 0xc7, DQ "\\xc7" DQ);
    test_quoting_of(is_tested, 0xc8, DQ "\\xc8" DQ);
    test_quoting_of(is_tested, 0xc9, DQ "\\xc9" DQ);
    test_quoting_of(is_tested, 0xca, DQ "\\xca" DQ);
    test_quoting_of(is_tested, 0xcb, DQ "\\xcb" DQ);
    test_quoting_of(is_tested, 0xcc, DQ "\\xcc" DQ);
    test_quoting_of(is_tested, 0xcd, DQ "\\xcd" DQ);
    test_quoting_of(is_tested, 0xce, DQ "\\xce" DQ);
    test_quoting_of(is_tested, 0xcf, DQ "\\xcf" DQ);
    // block 14 (8-bit): 0xd0 - 0xdf
    test_quoting_of(is_tested, 0xd0, DQ "\\xd0" DQ);
    test_quoting_of(is_tested, 0xd1, DQ "\\xd1" DQ);
    test_quoting_of(is_tested, 0xd2, DQ "\\xd2" DQ);
    test_quoting_of(is_tested, 0xd3, DQ "\\xd3" DQ);
    test_quoting_of(is_tested, 0xd4, DQ "\\xd4" DQ);
    test_quoting_of(is_tested, 0xd5, DQ "\\xd5" DQ);
    test_quoting_of(is_tested, 0xd6, DQ "\\xd6" DQ);
    test_quoting_of(is_tested, 0xd7, DQ "\\xd7" DQ);
    test_quoting_of(is_tested, 0xd8, DQ "\\xd8" DQ);
    test_quoting_of(is_tested, 0xd9, DQ "\\xd9" DQ);
    test_quoting_of(is_tested, 0xda, DQ "\\xda" DQ);
    test_quoting_of(is_tested, 0xdb, DQ "\\xdb" DQ);
    test_quoting_of(is_tested, 0xdc, DQ "\\xdc" DQ);
    test_quoting_of(is_tested, 0xdd, DQ "\\xdd" DQ);
    test_quoting_of(is_tested, 0xde, DQ "\\xde" DQ);
    test_quoting_of(is_tested, 0xdf, DQ "\\xdf" DQ);
    // block 15 (8-bit): 0xe0 - 0xef
    test_quoting_of(is_tested, 0xe0, DQ "\\xe0" DQ);
    test_quoting_of(is_tested, 0xe1, DQ "\\xe1" DQ);
    test_quoting_of(is_tested, 0xe2, DQ "\\xe2" DQ);
    test_quoting_of(is_tested, 0xe3, DQ "\\xe3" DQ);
    test_quoting_of(is_tested, 0xe4, DQ "\\xe4" DQ);
    test_quoting_of(is_tested, 0xe5, DQ "\\xe5" DQ);
    test_quoting_of(is_tested, 0xe6, DQ "\\xe6" DQ);
    test_quoting_of(is_tested, 0xe7, DQ "\\xe7" DQ);
    test_quoting_of(is_tested, 0xe8, DQ "\\xe8" DQ);
    test_quoting_of(is_tested, 0xe9, DQ "\\xe9" DQ);
    test_quoting_of(is_tested, 0xea, DQ "\\xea" DQ);
    test_quoting_of(is_tested, 0xeb, DQ "\\xeb" DQ);
    test_quoting_of(is_tested, 0xec, DQ "\\xec" DQ);
    test_quoting_of(is_tested, 0xed, DQ "\\xed" DQ);
    test_quoting_of(is_tested, 0xee, DQ "\\xee" DQ);
    test_quoting_of(is_tested, 0xef, DQ "\\xef" DQ);
    // block 16 (8-bit): 0xf0 - 0xff
    test_quoting_of(is_tested, 0xf0, DQ "\\xf0" DQ);
    test_quoting_of(is_tested, 0xf1, DQ "\\xf1" DQ);
    test_quoting_of(is_tested, 0xf2, DQ "\\xf2" DQ);
    test_quoting_of(is_tested, 0xf3, DQ "\\xf3" DQ);
    test_quoting_of(is_tested, 0xf4, DQ "\\xf4" DQ);
    test_quoting_of(is_tested, 0xf5, DQ "\\xf5" DQ);
    test_quoting_of(is_tested, 0xf6, DQ "\\xf6" DQ);
    test_quoting_of(is_tested, 0xf7, DQ "\\xf7" DQ);
    test_quoting_of(is_tested, 0xf8, DQ "\\xf8" DQ);
    test_quoting_of(is_tested, 0xf9, DQ "\\xf9" DQ);
    test_quoting_of(is_tested, 0xfa, DQ "\\xfa" DQ);
    test_quoting_of(is_tested, 0xfb, DQ "\\xfb" DQ);
    test_quoting_of(is_tested, 0xfc, DQ "\\xfc" DQ);
    test_quoting_of(is_tested, 0xfd, DQ "\\xfd" DQ);
    test_quoting_of(is_tested, 0xfe, DQ "\\xfe" DQ);
    test_quoting_of(is_tested, 0xff, DQ "\\xff" DQ);

    // Ensure the search was exhaustive.
    for (int i = 0; i <= 0xff; ++i) {
        g_assert_true(is_tested[i]);
    }

    // Few extra tests (repeated) for specific things.

    // Smoke test
    sc_string_quote(buf, sizeof buf, "hello 123");
    g_assert_cmpstr(buf, ==, DQ "hello 123" DQ);

    // Whitespace
    sc_string_quote(buf, sizeof buf, "\n");
    g_assert_cmpstr(buf, ==, DQ "\\n" DQ);
    sc_string_quote(buf, sizeof buf, "\r");
    g_assert_cmpstr(buf, ==, DQ "\\r" DQ);
    sc_string_quote(buf, sizeof buf, "\t");
    g_assert_cmpstr(buf, ==, DQ "\\t" DQ);
    sc_string_quote(buf, sizeof buf, "\v");
    g_assert_cmpstr(buf, ==, DQ "\\v" DQ);

    // Escape character itself
    sc_string_quote(buf, sizeof buf, "\\");
    g_assert_cmpstr(buf, ==, DQ "\\\\" DQ);

    // Double quote character
    sc_string_quote(buf, sizeof buf, "\"");
    g_assert_cmpstr(buf, ==, DQ "\\\"" DQ);

#undef DQ
}

static void test_sc_strdup(void) {
    char *s = sc_strdup("snap install everything");
    g_assert_nonnull(s);
    g_assert_cmpstr(s, ==, "snap install everything");
    free(s);
}

static void test_sc_string_split_trailing_nil(void) {
    if (g_test_subprocess()) {
        char dest[3] = {0};
        // pretend there is no place for trailing \0
        sc_string_split("_", '_', NULL, 0, dest, 0);
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
}

static void test_sc_string_split_short_instance_dest(void) {
    if (g_test_subprocess()) {
        char dest[10] = {0};
        sc_string_split("foo_barbarbarbar", '_', NULL, 0, dest, sizeof dest);
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
}

static void test_sc_string_split_null_string(void) {
    if (g_test_subprocess()) {
        char dest[10] = {0};
        sc_string_split(NULL, '_', NULL, 0, dest, sizeof dest);
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("internal error: cannot split string when it is unset\n");
}

static void test_sc_string_split_null_prefix_and_suffix(void) {
    if (g_test_subprocess()) {
        sc_string_split("some_string", '_', NULL, 0, NULL, 0);
        return;
    }
    g_test_trap_subprocess(NULL, 0, 0);
    g_test_trap_assert_failed();
    g_test_trap_assert_stderr("internal error: cannot split string when both prefix and suffix are unset\n");
}

static void test_sc_string_split_basic(void) {
    char prefix[41] = {0xff};
    char suffix[20] = {0xff};

    sc_string_split("foo_bar", '_', prefix, sizeof prefix, suffix, sizeof suffix);
    g_assert_cmpstr(prefix, ==, "foo");
    g_assert_cmpstr(suffix, ==, "bar");

    memset(prefix, 0xff, sizeof prefix);
    memset(suffix, 0xff, sizeof suffix);
    sc_string_split("foo-bar_bar", '_', prefix, sizeof prefix, suffix, sizeof suffix);
    g_assert_cmpstr(prefix, ==, "foo-bar");
    g_assert_cmpstr(suffix, ==, "bar");

    memset(prefix, 0xff, sizeof prefix);
    memset(suffix, 0xff, sizeof suffix);
    sc_string_split("foo-bar_bar", '-', prefix, sizeof prefix, suffix, sizeof suffix);
    g_assert_cmpstr(prefix, ==, "foo");
    g_assert_cmpstr(suffix, ==, "bar_bar");

    memset(prefix, 0xff, sizeof prefix);
    memset(suffix, 0xff, sizeof suffix);
    sc_string_split("foo-bar", '_', prefix, sizeof prefix, suffix, sizeof suffix);
    g_assert_cmpstr(prefix, ==, "foo-bar");
    g_assert_cmpstr(suffix, ==, "");

    memset(prefix, 0xff, sizeof prefix);
    memset(suffix, 0xff, sizeof suffix);
    sc_string_split("_baz", '_', prefix, sizeof prefix, suffix, sizeof suffix);
    g_assert_cmpstr(prefix, ==, "");
    g_assert_cmpstr(suffix, ==, "baz");

    memset(prefix, 0xff, sizeof prefix);
    memset(suffix, 0xff, sizeof suffix);
    sc_string_split("foo", '_', prefix, sizeof prefix, suffix, sizeof suffix);
    g_assert_cmpstr(prefix, ==, "foo");
    g_assert_cmpstr(suffix, ==, "");

    memset(prefix, 0xff, sizeof prefix);
    sc_string_split("foo_bar", '_', prefix, sizeof prefix, NULL, 0);
    g_assert_cmpstr(prefix, ==, "foo");

    memset(suffix, 0xff, sizeof suffix);
    sc_string_split("foo_bar", '_', NULL, 0, suffix, sizeof suffix);
    g_assert_cmpstr(suffix, ==, "bar");

    memset(prefix, 0xff, sizeof prefix);
    memset(suffix, 0xff, sizeof suffix);
    sc_string_split("hello_world_surprise", '_', prefix, sizeof prefix, suffix, sizeof suffix);
    g_assert_cmpstr(prefix, ==, "hello");
    g_assert_cmpstr(suffix, ==, "world_surprise");

    memset(prefix, 0xff, sizeof prefix);
    memset(suffix, 0xff, sizeof suffix);
    sc_string_split("", '_', prefix, sizeof prefix, suffix, sizeof suffix);
    g_assert_cmpstr(prefix, ==, "");
    g_assert_cmpstr(suffix, ==, "");

    memset(prefix, 0xff, sizeof prefix);
    memset(suffix, 0xff, sizeof suffix);
    sc_string_split("_", '_', prefix, sizeof prefix, suffix, sizeof suffix);
    g_assert_cmpstr(prefix, ==, "");
    g_assert_cmpstr(suffix, ==, "");

    memset(prefix, 0xff, sizeof prefix);
    memset(suffix, 0xff, sizeof suffix);
    sc_string_split("foo_", '_', prefix, sizeof prefix, suffix, sizeof suffix);
    g_assert_cmpstr(prefix, ==, "foo");
    g_assert_cmpstr(suffix, ==, "");
}

static void test_sc_str_chomp(void) {
    char noRet[] = "foo";
    g_assert(sc_str_chomp(noRet) == noRet);
    g_assert_cmpstr(sc_str_chomp(noRet), ==, "foo");

    char withRet[] = "foo\n";
    g_assert(sc_str_chomp(withRet) == withRet);
    g_assert_cmpstr(withRet, ==, "foo");

    char twoLines[] = "foo\nbar\n";
    g_assert(sc_str_chomp(twoLines) == twoLines);
    g_assert_cmpstr(sc_str_chomp(twoLines), ==, "foo\nbar");

    char someRets[] = "foo\n\n\n\n";
    g_assert(sc_str_chomp(someRets) == someRets);
    g_assert_cmpstr(someRets, ==, "foo");

    char withJustRet[] = "\n";
    g_assert(sc_str_chomp(withJustRet) == withJustRet);
    g_assert_cmpstr(withJustRet, ==, "");

    char empty[] = "";
    g_assert(sc_str_chomp(empty) == empty);
    g_assert_cmpstr(sc_str_chomp(empty), ==, "");
}

static void __attribute__((constructor)) init(void) {
    g_test_add_func("/string-utils/sc_streq", test_sc_streq);
    g_test_add_func("/string-utils/sc_endswith", test_sc_endswith);
    g_test_add_func("/string-utils/sc_startswith", test_sc_startswith);
    g_test_add_func("/string-utils/sc_must_snprintf", test_sc_must_snprintf);
    g_test_add_func("/string-utils/sc_must_snprintf/fail", test_sc_must_snprintf__fail);
    g_test_add_func("/string-utils/sc_string_append/normal", test_sc_string_append);
    g_test_add_func("/string-utils/sc_string_append/empty_to_full", test_sc_string_append__empty_to_full);
    g_test_add_func("/string-utils/sc_string_append/overflow", test_sc_string_append__overflow);
    g_test_add_func("/string-utils/sc_string_append/uninitialized_buf", test_sc_string_append__uninitialized_buf);
    g_test_add_func("/string-utils/sc_string_append/NULL_buf", test_sc_string_append__NULL_buf);
    g_test_add_func("/string-utils/sc_string_append/NULL_str", test_sc_string_append__NULL_str);
    g_test_add_func("/string-utils/sc_string_init/normal", test_sc_string_init__normal);
    g_test_add_func("/string-utils/sc_string_init/empty_buf", test_sc_string_init__empty_buf);
    g_test_add_func("/string-utils/sc_string_init/NULL_buf", test_sc_string_init__NULL_buf);
    g_test_add_func("/string-utils/sc_string_append_char__uninitialized_buf",
                    test_sc_string_append_char__uninitialized_buf);
    g_test_add_func("/string-utils/sc_string_append_char__NULL_buf", test_sc_string_append_char__NULL_buf);
    g_test_add_func("/string-utils/sc_string_append_char__overflow", test_sc_string_append_char__overflow);
    g_test_add_func("/string-utils/sc_string_append_char__invalid_zero", test_sc_string_append_char__invalid_zero);
    g_test_add_func("/string-utils/sc_string_append_char__normal", test_sc_string_append_char__normal);
    g_test_add_func("/string-utils/sc_string_append_char_pair__NULL_buf", test_sc_string_append_char_pair__NULL_buf);
    g_test_add_func("/string-utils/sc_string_append_char_pair__overflow", test_sc_string_append_char_pair__overflow);
    g_test_add_func("/string-utils/sc_string_append_char_pair__invalid_zero_c1",
                    test_sc_string_append_char_pair__invalid_zero_c1);
    g_test_add_func("/string-utils/sc_string_append_char_pair__invalid_zero_c2",
                    test_sc_string_append_char_pair__invalid_zero_c2);
    g_test_add_func("/string-utils/sc_string_append_char_pair__normal", test_sc_string_append_char_pair__normal);
    g_test_add_func("/string-utils/sc_string_quote__NULL_buf", test_sc_string_quote_NULL_str);
    g_test_add_func("/string-utils/sc_string_append_char_pair__uninitialized_buf",
                    test_sc_string_append_char_pair__uninitialized_buf);
    g_test_add_func("/string-utils/sc_string_quote", test_sc_string_quote);
    g_test_add_func("/string-utils/sc_strdup", test_sc_strdup);
    g_test_add_func("/string-utils/sc_string_split/basic", test_sc_string_split_basic);
    g_test_add_func("/string-utils/sc_string_split/trailing_nil", test_sc_string_split_trailing_nil);
    g_test_add_func("/string-utils/sc_string_split/short_instance_dest", test_sc_string_split_short_instance_dest);
    g_test_add_func("/string-utils/sc_string_split/null_string", test_sc_string_split_null_string);
    g_test_add_func("/string-utils/sc_string_split/null_prefix_and_suffix",
                    test_sc_string_split_null_prefix_and_suffix);
    g_test_add_func("/string-utils/sc_str_chomp", test_sc_str_chomp);
}
