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

#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"

#include "nm-json-aux.h"

#include <dlfcn.h>

/*****************************************************************************/

/* If RTLD_DEEPBIND isn't available just ignore it. This can cause problems
 * with jansson, json-glib, and cjson symbols clashing (and as such crashing the
 * program). But that needs to be fixed by the json libraries, and it is by adding
 * symbol versioning in recent versions. */
#ifndef RTLD_DEEPBIND
    #define RTLD_DEEPBIND 0
#endif

/*****************************************************************************/

static void
_gstr_append_string_len(GString *gstr, const char *str, gsize len)
{
    g_string_append_c(gstr, '\"');

    while (len > 0) {
        gsize       n;
        const char *end;
        gboolean    valid;

        nm_assert(len > 0);

        valid = g_utf8_validate(str, len, &end);

        nm_assert(end && end >= str && end <= &str[len]);

        if (end > str) {
            const char *s;

            for (s = str; s < end; s++) {
                nm_assert(s[0] != '\0');

                if (s[0] < 0x20) {
                    const char *text;

                    switch (s[0]) {
                    case '\\':
                        text = "\\\\";
                        break;
                    case '\"':
                        text = "\\\"";
                        break;
                    case '\b':
                        text = "\\b";
                        break;
                    case '\f':
                        text = "\\f";
                        break;
                    case '\n':
                        text = "\\n";
                        break;
                    case '\r':
                        text = "\\r";
                        break;
                    case '\t':
                        text = "\\t";
                        break;
                    default:
                        g_string_append_printf(gstr, "\\u%04X", (guint) s[0]);
                        continue;
                    }
                    g_string_append(gstr, text);
                    continue;
                }

                if (NM_IN_SET(s[0], '\\', '\"'))
                    g_string_append_c(gstr, '\\');
                g_string_append_c(gstr, s[0]);
            }
        } else
            nm_assert(!valid);

        if (valid) {
            nm_assert(end == &str[len]);
            break;
        }

        nm_assert(end < &str[len]);

        if (end[0] == '\0') {
            /* there is a NUL byte in the string. Technically this is valid UTF-8, so we
             * encode it there. However, this will likely result in a truncated string when
             * parsing. */
            g_string_append(gstr, "\\u0000");
        } else {
            /* the character is not valid UTF-8. There is nothing we can do about it, because
             * JSON can only contain UTF-8 and even the escape sequences can only escape Unicode
             * codepoints (but not binary).
             *
             * The argument is not a string (in any known encoding), hence we cannot represent
             * it as a JSON string (which are unicode strings).
             *
             * Print an underscore instead of the invalid char :) */
            g_string_append_c(gstr, '_');
        }

        n = str - end;
        nm_assert(n < len);
        n++;
        str += n;
        len -= n;
    }

    g_string_append_c(gstr, '\"');
}

void
nm_json_gstr_append_string_len(GString *gstr, const char *str, gsize n)
{
    g_return_if_fail(gstr);

    _gstr_append_string_len(gstr, str, n);
}

void
nm_json_gstr_append_string(GString *gstr, const char *str)
{
    g_return_if_fail(gstr);

    if (!str)
        g_string_append(gstr, "null");
    else
        _gstr_append_string_len(gstr, str, strlen(str));
}

void
nm_json_gstr_append_obj_name(GString *gstr, const char *key, char start_container)
{
    g_return_if_fail(gstr);
    g_return_if_fail(key);

    nm_json_gstr_append_string(gstr, key);

    if (start_container != '\0') {
        nm_assert(NM_IN_SET(start_container, '[', '{'));
        g_string_append_printf(gstr, ": %c ", start_container);
    } else
        g_string_append(gstr, ": ");
}

/*****************************************************************************/

typedef struct {
    NMJsonVt vt;
    void *   dl_handle;
} NMJsonVtInternal;

static NMJsonVtInternal *
_nm_json_vt_internal_load(void)
{
    NMJsonVtInternal *v;
    const char *      soname;
    void *            handle;

    v = g_new0(NMJsonVtInternal, 1);

#if WITH_JANSSON && defined(JANSSON_SONAME)
    G_STATIC_ASSERT_EXPR(NM_STRLEN(JANSSON_SONAME) > 0);
    nm_assert(strlen(JANSSON_SONAME) > 0);
    soname = JANSSON_SONAME;
#elif !WITH_JANSSON && !defined(JANSSON_SONAME)
    soname = NULL;
#else
    #error "WITH_JANSON and JANSSON_SONAME are defined inconsistently."
#endif

    if (!soname)
        return v;

    handle = dlopen(soname,
                    RTLD_LAZY | RTLD_LOCAL | RTLD_NODELETE
#if !defined(ASAN_BUILD)
                        | RTLD_DEEPBIND
#endif
                        | 0);
    if (!handle)
        return v;

#define TRY_BIND_SYMBOL(symbol)              \
    G_STMT_START                             \
    {                                        \
        void *_sym = dlsym(handle, #symbol); \
                                             \
        if (!_sym)                           \
            goto fail_symbol;                \
        v->vt.nm_##symbol = _sym;            \
    }                                        \
    G_STMT_END

    TRY_BIND_SYMBOL(json_array);
    TRY_BIND_SYMBOL(json_array_append_new);
    TRY_BIND_SYMBOL(json_array_get);
    TRY_BIND_SYMBOL(json_array_size);
    TRY_BIND_SYMBOL(json_delete);
    TRY_BIND_SYMBOL(json_dumps);
    TRY_BIND_SYMBOL(json_false);
    TRY_BIND_SYMBOL(json_integer);
    TRY_BIND_SYMBOL(json_integer_value);
    TRY_BIND_SYMBOL(json_loads);
    TRY_BIND_SYMBOL(json_object);
    TRY_BIND_SYMBOL(json_object_del);
    TRY_BIND_SYMBOL(json_object_get);
    TRY_BIND_SYMBOL(json_object_iter);
    TRY_BIND_SYMBOL(json_object_iter_key);
    TRY_BIND_SYMBOL(json_object_iter_next);
    TRY_BIND_SYMBOL(json_object_iter_value);
    TRY_BIND_SYMBOL(json_object_key_to_iter);
    TRY_BIND_SYMBOL(json_object_set_new);
    TRY_BIND_SYMBOL(json_object_size);
    TRY_BIND_SYMBOL(json_string);
    TRY_BIND_SYMBOL(json_string_value);
    TRY_BIND_SYMBOL(json_true);

    v->vt.loaded = TRUE;
    v->dl_handle = handle;
    return v;

fail_symbol:
    dlclose(&handle);
    *v = (NMJsonVtInternal){};
    return v;
}

const NMJsonVt *_nm_json_vt_ptr = NULL;

const NMJsonVt *
_nm_json_vt_init(void)
{
    NMJsonVtInternal *v;

again:
    v = g_atomic_pointer_get((gpointer *) &_nm_json_vt_ptr);
    if (G_UNLIKELY(!v)) {
        v = _nm_json_vt_internal_load();
        if (!g_atomic_pointer_compare_and_exchange((gpointer *) &_nm_json_vt_ptr, NULL, v)) {
            if (v->dl_handle)
                dlclose(v->dl_handle);
            g_free(v);
            goto again;
        }

        /* we transfer ownership. */
    }

    nm_assert(v && v == g_atomic_pointer_get((gpointer *) &_nm_json_vt_ptr));
    return &v->vt;
}

const NMJsonVt *
nmtst_json_vt_reset(gboolean loaded)
{
    NMJsonVtInternal *v_old;
    NMJsonVtInternal *v;

    v_old = g_atomic_pointer_get((gpointer *) &_nm_json_vt_ptr);

    if (!loaded) {
        /* load a fake instance for testing. */
        v = g_new0(NMJsonVtInternal, 1);
    } else
        v = _nm_json_vt_internal_load();

    if (!g_atomic_pointer_compare_and_exchange((gpointer *) &_nm_json_vt_ptr, v_old, v))
        g_assert_not_reached();

    if (v_old) {
        if (v_old->dl_handle)
            dlclose(v_old->dl_handle);
        g_free((gpointer *) v_old);
    }

    return v->vt.loaded ? &v->vt : NULL;
}
