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

#define NM_WANT_NM_ARRAY_FIND_BSEARCH_INLINE

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

#include "nm-shared-utils.h"

#include <pwd.h>
#include <arpa/inet.h>
#include <poll.h>
#include <fcntl.h>
#include <sys/syscall.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <pthread.h>

#include "c-list/src/c-list.h"
#include "nm-errno.h"
#include "nm-time-utils.h"
#include "nm-str-buf.h"
#include "nm-time-utils.h"

G_STATIC_ASSERT(sizeof(NMEtherAddr) == 6);
G_STATIC_ASSERT(_nm_alignof(NMEtherAddr) == 1);

G_STATIC_ASSERT(sizeof(NMUtilsNamedEntry) == sizeof(const char *));
G_STATIC_ASSERT(G_STRUCT_OFFSET(NMUtilsNamedValue, value_ptr) == sizeof(const char *));

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

const char _nm_hexchar_table_lower[] = "0123456789abcdef";
const char _nm_hexchar_table_upper[] = "0123456789ABCDEF";

const void *const _NM_PTRARRAY_EMPTY[1] = {NULL};

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

G_STATIC_ASSERT(ETH_ALEN == sizeof(struct ether_addr));
G_STATIC_ASSERT(ETH_ALEN == 6);
G_STATIC_ASSERT(ETH_ALEN == sizeof(NMEtherAddr));

G_STATIC_ASSERT(_nm_alignof(struct ether_addr) <= _nm_alignof(NMEtherAddr));

NMEtherAddr *
nm_ether_addr_from_string(NMEtherAddr *addr, const char *str)
{
    nm_assert(addr);

    if (!str || !_nm_utils_hwaddr_aton_exact(str, addr, ETH_ALEN)) {
        *addr = NM_ETHER_ADDR_INIT(0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
        return NULL;
    }

    return addr;
}

guint
nm_ether_addr_hash(const NMEtherAddr *a)
{
    NMHashState h;

    nm_hash_init(&h, 1947951703u);
    nm_hash_update(&h, a, sizeof(NMEtherAddr));

    return nm_hash_complete(&h);
}

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

/**
 * nm_utils_inet6_is_token:
 * @in6addr: the AF_INET6 address structure
 *
 * Checks if only the bottom 64bits of the address are set.
 *
 * Returns: %TRUE or %FALSE
 */
gboolean
_nm_utils_inet6_is_token(const struct in6_addr *in6addr)
{
    if (in6addr->s6_addr[0] || in6addr->s6_addr[1] || in6addr->s6_addr[2] || in6addr->s6_addr[3]
        || in6addr->s6_addr[4] || in6addr->s6_addr[5] || in6addr->s6_addr[6] || in6addr->s6_addr[7])
        return FALSE;

    if (in6addr->s6_addr[8] || in6addr->s6_addr[9] || in6addr->s6_addr[10] || in6addr->s6_addr[11]
        || in6addr->s6_addr[12] || in6addr->s6_addr[13] || in6addr->s6_addr[14]
        || in6addr->s6_addr[15])
        return TRUE;

    return FALSE;
}

/**
 * nm_utils_ipv6_interface_identifier_get_from_token:
 * @iid: output %NMUtilsIPv6IfaceId interface identifier set from the token
 * @token: token encoded as string
 *
 * Converts the %in6_addr encoded token (as used in ip6 settings) to
 * the interface identifier.
 *
 * Returns: %TRUE if the @token is a valid token, %FALSE otherwise
 */
gboolean
nm_utils_ipv6_interface_identifier_get_from_token(NMUtilsIPv6IfaceId *iid, const char *token)
{
    struct in6_addr i6_token;

    g_return_val_if_fail(token, FALSE);

    if (!inet_pton(AF_INET6, token, &i6_token))
        return FALSE;

    if (!_nm_utils_inet6_is_token(&i6_token))
        return FALSE;

    nm_utils_ipv6_interface_identifier_get_from_addr(iid, &i6_token);
    return TRUE;
}

/**
 * nm_utils_inet6_interface_identifier_to_token:
 * @iid: %NMUtilsIPv6IfaceId interface identifier
 * @buf: the destination buffer of at least %NM_INET_ADDRSTRLEN
 *   bytes.
 *
 * Converts the interface identifier to a string token.
 *
 * Returns: the input buffer filled with the id as string.
 */
const char *
nm_utils_inet6_interface_identifier_to_token(const NMUtilsIPv6IfaceId *iid,
                                             char                      buf[static INET6_ADDRSTRLEN])
{
    struct in6_addr i6_token = {.s6_addr = {
                                    0,
                                }};

    nm_assert(buf);
    nm_utils_ipv6_addr_set_interface_identifier(&i6_token, iid);
    return nm_inet6_ntop(&i6_token, buf);
}

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

pid_t
nm_utils_gettid(void)
{
    return (pid_t) syscall(SYS_gettid);
}

/* Used for asserting that this function is called on the main-thread.
 * The main-thread is determined by remembering the thread-id
 * of when the function was called the first time.
 *
 * When forking, the thread-id is again reset upon first call.
 *
 * Note that this is only used for asserting, to check that we don't
 * call the function on the wrong thread. As it's difficult to correctly
 * cache the tid/pid, we might get this wrong during fork. That is not
 * a problem, because we err on the side of pretending all is good. */
gboolean
_nm_assert_on_main_thread(void)
{
    static GMutex lock;
    static int    seen_tid;
    pid_t         tid;
    int           t;
    gboolean      success = FALSE;

    tid = nm_utils_gettid();
    nm_assert(tid != 0);
    nm_assert(({
        const int tt = tid;

        tt == tid;
    }));

    t = g_atomic_int_get(&seen_tid);
    if (G_LIKELY(t == tid)) {
        /* we don't care about false positives (when the process forked, and the thread-id
         * is accidentally re-used) . It's for assertions only. */
        return TRUE;
    }

    g_mutex_lock(&lock);

    t = g_atomic_int_get(&seen_tid);
    if (G_UNLIKELY(t == tid))
        success = TRUE;
    else {
        static pid_t seen_pid;
        pid_t        pid;

        pid = getpid();
        nm_assert(pid != 0);

        if (t == 0 || seen_pid != pid) {
            /* either this is the first time we call the function, or the process
             * forked. In both cases, update the thread-id. */
            g_atomic_int_set(&seen_tid, tid);
            seen_pid = pid;
            success  = TRUE;
        }
    }

    g_mutex_unlock(&lock);

    return success;
}

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

void
nm_strbuf_append_c(char **buf, gsize *len, char c)
{
    switch (*len) {
    case 0:
        return;
    case 1:
        (*buf)[0] = '\0';
        *len      = 0;
        (*buf)++;
        return;
    default:
        (*buf)[0] = c;
        (*buf)[1] = '\0';
        (*len)--;
        (*buf)++;
        return;
    }
}

void
nm_strbuf_append_bin(char **buf, gsize *len, gconstpointer str, gsize str_len)
{
    switch (*len) {
    case 0:
        return;
    case 1:
        if (str_len == 0) {
            (*buf)[0] = '\0';
            return;
        }
        (*buf)[0] = '\0';
        *len      = 0;
        (*buf)++;
        return;
    default:
        if (str_len == 0) {
            (*buf)[0] = '\0';
            return;
        }
        if (str_len >= *len) {
            memcpy(*buf, str, *len - 1);
            (*buf)[*len - 1] = '\0';
            *buf             = &(*buf)[*len];
            *len             = 0;
        } else {
            memcpy(*buf, str, str_len);
            *buf      = &(*buf)[str_len];
            (*buf)[0] = '\0';
            *len -= str_len;
        }
        return;
    }
}

void
nm_strbuf_append_str(char **buf, gsize *len, const char *str)
{
    gsize src_len;

    switch (*len) {
    case 0:
        return;
    case 1:
        if (!str || !*str) {
            (*buf)[0] = '\0';
            return;
        }
        (*buf)[0] = '\0';
        *len      = 0;
        (*buf)++;
        return;
    default:
        if (!str || !*str) {
            (*buf)[0] = '\0';
            return;
        }
        src_len = g_strlcpy(*buf, str, *len);
        if (src_len >= *len) {
            *buf = &(*buf)[*len];
            *len = 0;
        } else {
            *buf = &(*buf)[src_len];
            *len -= src_len;
        }
        return;
    }
}

void
nm_strbuf_append(char **buf, gsize *len, const char *format, ...)
{
    char   *p = *buf;
    va_list args;
    int     retval;

    if (*len == 0)
        return;

    va_start(args, format);
    retval = g_vsnprintf(p, *len, format, args);
    va_end(args);

    if ((gsize) retval >= *len) {
        *buf = &p[*len];
        *len = 0;
    } else {
        *buf = &p[retval];
        *len -= retval;
    }
}

/**
 * nm_strbuf_seek_end:
 * @buf: the input/output buffer
 * @len: the input/output length of the buffer.
 *
 * Commonly, one uses nm_strbuf_append*(), to incrementally
 * append strings to the buffer. However, sometimes we need to use
 * existing API to write to the buffer.
 * After doing so, we want to adjust the buffer counter.
 * Essentially,
 *
 *   g_snprintf (buf, len, ...);
 *   nm_strbuf_seek_end (&buf, &len);
 *
 * is almost the same as
 *
 *   nm_strbuf_append (&buf, &len, ...);
 *
 * The only difference is the behavior when the string got truncated:
 * nm_strbuf_append() will recognize that and set the remaining
 * length to zero.
 *
 * In general, the behavior is:
 *
 *  - if *len is zero, do nothing
 *  - if the buffer contains a NUL byte within the first *len characters,
 *    the buffer is pointed to the NUL byte and len is adjusted. In this
 *    case, the remaining *len is always >= 1.
 *    In particular, that is also the case if the NUL byte is at the very last
 *    position ((*buf)[*len -1]). That happens, when the previous operation
 *    either fit the string exactly into the buffer or the string was truncated
 *    by g_snprintf(). The difference cannot be determined.
 *  - if the buffer contains no NUL bytes within the first *len characters,
 *    write NUL at the last position, set *len to zero, and point *buf past
 *    the NUL byte. This would happen with
 *
 *       strncpy (buf, long_str, len);
 *       nm_strbuf_seek_end (&buf, &len).
 *
 *    where strncpy() does truncate the string and not NUL terminate it.
 *    nm_strbuf_seek_end() would then NUL terminate it.
 */
void
nm_strbuf_seek_end(char **buf, gsize *len)
{
    gsize l;
    char *end;

    nm_assert(len);
    nm_assert(buf && *buf);

    if (*len <= 1) {
        if (*len == 1 && (*buf)[0])
            goto truncate;
        return;
    }

    end = memchr(*buf, 0, *len);
    if (end) {
        l = end - *buf;
        nm_assert(l < *len);

        *buf = end;
        *len -= l;
        return;
    }

truncate:
    /* hm, no NUL character within len bytes.
     * Just NUL terminate the array and consume them
     * all. */
    *buf += *len;
    (*buf)[-1] = '\0';
    *len       = 0;
    return;
}

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

GBytes *
nm_g_bytes_get_empty(void)
{
    static GBytes *bytes = NULL;
    GBytes        *b;

again:
    b = g_atomic_pointer_get(&bytes);
    if (G_UNLIKELY(!b)) {
        b = g_bytes_new_static("", 0);
        if (!g_atomic_pointer_compare_and_exchange(&bytes, NULL, b)) {
            g_bytes_unref(b);
            goto again;
        }
    }
    return b;
}

GBytes *
nm_g_bytes_new_from_str(const char *str)
{
    gsize l;

    if (!str)
        return NULL;

    /* the returned array is guaranteed to have a trailing '\0'
     * character *after* the length. */

    l = strlen(str);
    return g_bytes_new_take(nm_memdup(str, l + 1u), l);
}

GBytes *
nm_g_bytes_new_from_variant_ay(GVariant *var)
{
    if (!var)
        return NULL;
    if (!g_variant_is_of_type(var, G_VARIANT_TYPE_BYTESTRING))
        g_return_val_if_reached(NULL);
    return g_variant_get_data_as_bytes(var);
}

/**
 * nm_g_bytes_equal_mem:
 * @bytes: (nullable): a #GBytes array to compare. Note that
 *   %NULL is treated like an #GBytes array of length zero.
 * @mem_data: the data pointer with @mem_len bytes
 * @mem_len: the length of the data pointer
 *
 * Returns: %TRUE if @bytes contains the same data as @mem_data. As a
 *   special case, a %NULL @bytes is treated like an empty array.
 */
gboolean
nm_g_bytes_equal_mem(GBytes *bytes, gconstpointer mem_data, gsize mem_len)
{
    gconstpointer p;
    gsize         l;

    if (!bytes) {
        /* as a special case, let %NULL GBytes compare identical
         * to an empty array. */
        return (mem_len == 0);
    }

    p = g_bytes_get_data(bytes, &l);
    return l == mem_len
           && (mem_len == 0 /* allow @mem_data to be %NULL */
               || memcmp(p, mem_data, mem_len) == 0);
}

GVariant *
nm_g_bytes_to_variant_ay(const GBytes *bytes)
{
    const guint8 *p = NULL;
    gsize         l = 0;

    if (!bytes) {
        /* for convenience, accept NULL to return an empty variant */
    } else
        p = g_bytes_get_data((GBytes *) bytes, &l);

    return nm_g_variant_new_ay(p, l);
}

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

#define _variant_singleton_get(create_variant)                                       \
    ({                                                                               \
        static GVariant *_singleton = NULL;                                          \
        GVariant        *_v;                                                         \
                                                                                     \
        while (TRUE) {                                                               \
            _v = g_atomic_pointer_get(&_singleton);                                  \
            if (G_UNLIKELY(!_v)) {                                                   \
                _v = (create_variant);                                               \
                nm_assert(_v);                                                       \
                nm_assert(g_variant_is_floating(_v));                                \
                g_variant_ref_sink(_v);                                              \
                if (!g_atomic_pointer_compare_and_exchange(&_singleton, NULL, _v)) { \
                    g_variant_unref(_v);                                             \
                    continue;                                                        \
                }                                                                    \
            }                                                                        \
            break;                                                                   \
        }                                                                            \
        _v;                                                                          \
    })

GVariant *
nm_g_variant_singleton_u_0(void)
{
    return _variant_singleton_get(g_variant_new_uint32(0));
}

GVariant *
nm_g_variant_singleton_i_0(void)
{
    return _variant_singleton_get(g_variant_new_int32(0));
}

GVariant *
nm_g_variant_singleton_b(gboolean value)
{
    return value ? _variant_singleton_get(g_variant_new_boolean(TRUE))
                 : _variant_singleton_get(g_variant_new_boolean(FALSE));
}

GVariant *
nm_g_variant_singleton_s_empty(void)
{
    return _variant_singleton_get(g_variant_new_string(""));
}

static GVariant *
_variant_singleton_get_array_init(GVariant **p_singleton, const char *variant_type)
{
    GVariant *v;

    v = g_variant_new_array(G_VARIANT_TYPE(variant_type), NULL, 0);
    g_variant_ref_sink(v);

    if (G_LIKELY(g_atomic_pointer_compare_and_exchange(p_singleton, NULL, v)))
        return v;

    g_variant_unref(v);

    return g_atomic_pointer_get(p_singleton);
}

#define _variant_singleton_get_array(variant_type)                                   \
    ({                                                                               \
        static GVariant *_singleton = NULL;                                          \
        GVariant        *_v;                                                         \
                                                                                     \
        _v = g_atomic_pointer_get(&_singleton);                                      \
        if (G_UNLIKELY(!_v)) {                                                       \
            _v = _variant_singleton_get_array_init(&_singleton, "" variant_type ""); \
            nm_assert(_v);                                                           \
        }                                                                            \
        nm_assert(g_variant_is_of_type(_v, G_VARIANT_TYPE("a" variant_type "")));    \
        _v;                                                                          \
    })

GVariant *
nm_g_variant_singleton_au(void)
{
    return _variant_singleton_get_array("u");
}

GVariant *
nm_g_variant_singleton_aay(void)
{
    return _variant_singleton_get_array("ay");
}

GVariant *
nm_g_variant_singleton_as(void)
{
    return _variant_singleton_get_array("s");
}

GVariant *
nm_g_variant_singleton_aLsvI(void)
{
    return _variant_singleton_get_array("{sv}");
}

GVariant *
nm_g_variant_singleton_aLsaLsvII(void)
{
    return _variant_singleton_get_array("{sa{sv}}");
}

GVariant *
nm_g_variant_singleton_aaLsvI(void)
{
    return _variant_singleton_get_array("a{sv}");
}

GVariant *
nm_g_variant_singleton_ao(void)
{
    return _variant_singleton_get_array("o");
}

GVariant *
nm_g_variant_maybe_singleton_i(gint32 value)
{
    /* Warning: this function always returns a non-floating reference
     * that must be consumed (and later unrefed) by the caller.
     *
     * The instance is either a singleton instance or a newly created
     * instance.
     *
     * The idea of this is that common values (zero) can use the immutable
     * singleton/flyweight instance and avoid allocating a new instance in
     * the (presumable) common case.
     */
    switch (value) {
    case 0:
        return g_variant_ref(nm_g_variant_singleton_i_0());
    default:
        return g_variant_take_ref(g_variant_new_int32(value));
    }
}

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

static int
_variant_type_cmp(const GVariantType *type1, const GVariantType *type2)
{
    const char *string1;
    const char *string2;
    gsize       size;

    NM_CMP_SELF(type1, type2);

    size = g_variant_type_get_string_length(type1);

    NM_CMP_DIRECT(size, g_variant_type_get_string_length(type2));

    string1 = g_variant_type_peek_string(type1);
    string2 = g_variant_type_peek_string(type2);

    NM_CMP_DIRECT_MEMCMP(string1, string2, size);
    return 0;
}

int
nm_g_variant_type_cmp(const GVariantType *type1, const GVariantType *type2)
{
    int r;

    r = _variant_type_cmp(type1, type2);
    nm_assert((!!g_variant_type_equal(type1, type2)) == (r == 0));
    return r;
}

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

typedef enum {
    VARIANT_CMP_TYPE_VARIANT,
    VARIANT_CMP_TYPE_STRDICT,
    VARIANT_CMP_TYPE_VARDICT,
} VariantCmpType;

static int
_variant_cmp_array(GVariant *value1, GVariant *value2, VariantCmpType type)
{
    gsize len;
    gsize i;

    len = g_variant_n_children(value1);

    NM_CMP_DIRECT(len, g_variant_n_children(value2));

    for (i = 0; i < len; i++) {
        gs_unref_variant GVariant *child1 = g_variant_get_child_value(value1, i);
        gs_unref_variant GVariant *child2 = g_variant_get_child_value(value2, i);
        const char                *key1;
        const char                *key2;
        const char                *val1_str;
        const char                *val2_str;

        nm_assert(child1);
        nm_assert(child2);

        switch (type) {
        case VARIANT_CMP_TYPE_VARIANT:
            NM_CMP_RETURN(nm_g_variant_cmp(child1, child2));
            break;
        case VARIANT_CMP_TYPE_STRDICT:
            g_variant_get(child1, "{&s&s}", &key1, &val1_str);
            g_variant_get(child2, "{&s&s}", &key2, &val2_str);
            NM_CMP_DIRECT_STRCMP(key1, key2);
            NM_CMP_DIRECT_STRCMP(val1_str, val2_str);
            break;
        case VARIANT_CMP_TYPE_VARDICT:
        {
            gs_unref_variant GVariant *val1_var = NULL;
            gs_unref_variant GVariant *val2_var = NULL;

            g_variant_get(child1, "{&sv}", &key1, &val1_var);
            g_variant_get(child2, "{&sv}", &key2, &val2_var);
            NM_CMP_DIRECT_STRCMP(key1, key2);
            NM_CMP_RETURN(nm_g_variant_cmp(val1_var, val2_var));
            break;
        }
        }
    }

    return 0;
}

static int
_variant_cmp_generic(GVariant *value1, GVariant *value2)
{
    gs_free char *str1 = NULL;
    gs_free char *str2 = NULL;

    /* This is like g_variant_equal(), which also resorts to pretty-printing
     * the variants for comparison.
     *
     * Note that the variant types are already checked and equal. We thus don't
     * need to include the type annotation. */
    str1 = g_variant_print(value1, FALSE);
    str2 = g_variant_print(value2, FALSE);

    NM_CMP_DIRECT_STRCMP(str1, str2);
    return 0;
}

static int
_variant_cmp(GVariant *value1, GVariant *value2)
{
    const GVariantType *type;

    NM_CMP_SELF(value1, value2);

    type = g_variant_get_type(value1);

    NM_CMP_RETURN(nm_g_variant_type_cmp(type, g_variant_get_type(value2)));

    if (g_variant_type_is_basic(type))
        NM_CMP_RETURN(g_variant_compare(value1, value2));
    else if (g_variant_type_is_subtype_of(type, G_VARIANT_TYPE("a{ss}")))
        NM_CMP_RETURN(_variant_cmp_array(value1, value2, VARIANT_CMP_TYPE_STRDICT));
    else if (g_variant_type_is_subtype_of(type, G_VARIANT_TYPE("a{sv}")))
        NM_CMP_RETURN(_variant_cmp_array(value1, value2, VARIANT_CMP_TYPE_VARDICT));
    else if (g_variant_type_is_array(type) || g_variant_type_is_tuple(type))
        NM_CMP_RETURN(_variant_cmp_array(value1, value2, VARIANT_CMP_TYPE_VARIANT));
    else
        NM_CMP_RETURN(_variant_cmp_generic(value1, value2));

    return 0;
}

int
nm_g_variant_cmp(GVariant *value1, GVariant *value2)
{
    int r;

    r = _variant_cmp(value1, value2);

    nm_assert((!!nm_g_variant_equal(value1, value2)) == (r == 0));

    return r;
}

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

GHashTable *
nm_strdict_clone(GHashTable *src)
{
    GHashTable    *dst;
    GHashTableIter iter;
    const char    *key;
    const char    *val;

    if (!src)
        return NULL;

    dst = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free);
    g_hash_table_iter_init(&iter, src);
    while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &val))
        g_hash_table_insert(dst, g_strdup(key), g_strdup(val));
    return dst;
}

/* Convert a hash table with "char *" keys and values to an "a{ss}" GVariant.
 * The keys will be sorted asciibetically.
 * Returns a floating reference.
 */
GVariant *
nm_strdict_to_variant_ass(GHashTable *strdict)
{
    gs_free NMUtilsNamedValue *values_free = NULL;
    NMUtilsNamedValue          values_prepared[20];
    const NMUtilsNamedValue   *values;
    GVariantBuilder            builder;
    guint                      i;
    guint                      n;

    values = nm_utils_named_values_from_strdict(strdict, &n, values_prepared, &values_free);

    g_variant_builder_init(&builder, G_VARIANT_TYPE("a{ss}"));
    for (i = 0; i < n; i++) {
        g_variant_builder_add(&builder, "{ss}", values[i].name, values[i].value_str);
    }
    return g_variant_builder_end(&builder);
}

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

GVariant *
nm_strdict_to_variant_asv(GHashTable *strdict)
{
    gs_free NMUtilsNamedValue *values_free = NULL;
    NMUtilsNamedValue          values_prepared[20];
    const NMUtilsNamedValue   *values;
    GVariantBuilder            builder;
    guint                      i;
    guint                      n;

    values = nm_utils_named_values_from_strdict(strdict, &n, values_prepared, &values_free);

    g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
    for (i = 0; i < n; i++) {
        g_variant_builder_add(&builder,
                              "{sv}",
                              values[i].name,
                              g_variant_new_string(values[i].value_str));
    }
    return g_variant_builder_end(&builder);
}

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

/**
 * nm_strquote:
 * @buf: the output buffer of where to write the quoted @str argument.
 * @buf_len: the size of @buf.
 * @str: (nullable): the string to quote.
 *
 * Writes @str to @buf with quoting. The resulting buffer
 * is always NUL terminated, unless @buf_len is zero.
 * If @str is %NULL, it writes "(null)".
 *
 * If @str needs to be truncated, the closing quote is '^' instead
 * of '"'.
 *
 * This is similar to nm_strquote_a(), which however uses alloca()
 * to allocate a new buffer. Also, here @buf_len is the size of @buf,
 * while nm_strquote_a() has the number of characters to print. The latter
 * doesn't include the quoting.
 *
 * Returns: the input buffer with the quoted string.
 */
const char *
nm_strquote(char *buf, gsize buf_len, const char *str)
{
    const char *const buf0 = buf;

    if (!str) {
        nm_strbuf_append_str(&buf, &buf_len, "(null)");
        goto out;
    }

    if (G_UNLIKELY(buf_len <= 2)) {
        switch (buf_len) {
        case 2:
            *(buf++) = '^';
            /* fall-through */
        case 1:
            *(buf++) = '\0';
            break;
        }
        goto out;
    }

    *(buf++) = '"';
    buf_len--;

    nm_strbuf_append_str(&buf, &buf_len, str);

    /* if the string was too long we indicate truncation with a
     * '^' instead of a closing quote. */
    if (G_UNLIKELY(buf_len <= 1)) {
        switch (buf_len) {
        case 1:
            buf[-1] = '^';
            break;
        case 0:
            buf[-2] = '^';
            break;
        default:
            nm_assert_not_reached();
            break;
        }
    } else {
        nm_assert(buf_len >= 2);
        *(buf++) = '"';
        *(buf++) = '\0';
    }

out:
    return buf0;
}

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

_nm_thread_local char _nm_utils_to_string_buffer[] = {0};

void
nm_utils_to_string_buffer_init(char **buf, gsize *len)
{
    if (!*buf) {
        *buf = _nm_utils_to_string_buffer;
        *len = NM_UTILS_TO_STRING_BUFFER_SIZE;

        /* We no longer want to support callers to omit the buffer
         * and fallback to the global buffer. Callers should be fixed
         * to always provide a valid buffer. */
        g_return_if_reached();
    }
}

gboolean
nm_utils_to_string_buffer_init_null(gconstpointer obj, char **buf, gsize *len)
{
    nm_utils_to_string_buffer_init(buf, len);
    if (!obj) {
        g_strlcpy(*buf, "(null)", *len);
        return FALSE;
    }
    return TRUE;
}

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

const char *
nm_utils_flags2str(const NMUtilsFlags2StrDesc *descs,
                   gsize                       n_descs,
                   unsigned                    flags,
                   char                       *buf,
                   gsize                       len)
{
    gsize i;
    char *p;

#if NM_MORE_ASSERTS > 10
    nm_assert(descs);
    nm_assert(n_descs > 0);
    for (i = 0; i < n_descs; i++) {
        gsize j;

        nm_assert(descs[i].name && descs[i].name[0]);
        for (j = 0; j < i; j++)
            nm_assert(descs[j].flag != descs[i].flag);
    }
#endif

    nm_utils_to_string_buffer_init(&buf, &len);

    if (!len)
        return buf;

    buf[0] = '\0';
    p      = buf;
    if (!flags) {
        for (i = 0; i < n_descs; i++) {
            if (!descs[i].flag) {
                nm_strbuf_append_str(&p, &len, descs[i].name);
                break;
            }
        }
        return buf;
    }

    for (i = 0; flags && i < n_descs; i++) {
        if (descs[i].flag && NM_FLAGS_ALL(flags, descs[i].flag)) {
            flags &= ~descs[i].flag;

            if (buf[0] != '\0')
                nm_strbuf_append_c(&p, &len, ',');
            nm_strbuf_append_str(&p, &len, descs[i].name);
        }
    }
    if (flags) {
        if (buf[0] != '\0')
            nm_strbuf_append_c(&p, &len, ',');
        nm_strbuf_append(&p, &len, "0x%x", flags);
    }
    return buf;
};

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

gboolean
nm_utils_parse_next_line(const char **inout_ptr,
                         gsize       *inout_len,
                         const char **out_line,
                         gsize       *out_line_len)
{
    gboolean    eol_is_carriage_return;
    const char *line_start;
    gsize       line_len;

    nm_assert(inout_ptr);
    nm_assert(inout_len);
    nm_assert(*inout_len == 0 || *inout_ptr);
    nm_assert(out_line);
    nm_assert(out_line_len);

    if (G_UNLIKELY(*inout_len == 0))
        return FALSE;

    line_start = *inout_ptr;

    eol_is_carriage_return = FALSE;
    for (line_len = 0;; line_len++) {
        if (line_len >= *inout_len) {
            /* if we consumed the entire line, we place the pointer at
             * one character after the end. */
            *inout_ptr = &line_start[line_len];
            *inout_len = 0;
            goto done;
        }
        switch (line_start[line_len]) {
        case '\r':
            eol_is_carriage_return = TRUE;
            /* fall-through*/
        case '\0':
        case '\n':
            *inout_ptr = &line_start[line_len + 1];
            *inout_len = *inout_len - line_len - 1u;
            if (eol_is_carriage_return && *inout_len > 0 && (*inout_ptr)[0] == '\n') {
                /* also consume "\r\n" as one. */
                (*inout_len)--;
                (*inout_ptr)++;
            }
            goto done;
        }
    }

done:
    *out_line     = line_start;
    *out_line_len = line_len;
    return TRUE;
}

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

/**
 * nm_g_ascii_strtoll()
 * @nptr: the string to parse
 * @endptr: the pointer on the first invalid chars
 * @base: the base.
 *
 * This wraps g_ascii_strtoll() and should in almost all cases behave identical
 * to it.
 *
 * However, it seems there are situations where g_ascii_strtoll() might set
 * errno to some unexpected value EAGAIN. Possibly this is related to creating
 * the C locale during
 *
 *   #ifdef USE_XLOCALE
 *   return strtoll_l (nptr, endptr, base, get_C_locale ());
 *
 * This wrapper tries to workaround that condition.
 */
gint64
nm_g_ascii_strtoll(const char *nptr, char **endptr, guint base)
{
    int       try_count = 2;
    gint64    v;
    const int errsv_orig = errno;
    int       errsv;

    nm_assert(nptr);
    nm_assert(base == 0u || (base >= 2u && base <= 36u));

again:
    errno = 0;
    v     = g_ascii_strtoll(nptr, endptr, base);
    errsv = errno;

    if (errsv == 0) {
        if (errsv_orig != 0)
            errno = errsv_orig;
        return v;
    }

    if (errsv == ERANGE && NM_IN_SET(v, G_MININT64, G_MAXINT64))
        return v;

    if (errsv == EINVAL && v == 0 && nptr && nptr[0] == '\0')
        return v;

    if (try_count-- > 0)
        goto again;

#if NM_MORE_ASSERTS
    g_critical("g_ascii_strtoll() for \"%s\" failed with errno=%d (%s) and v=%" G_GINT64_FORMAT,
               nptr,
               errsv,
               nm_strerror_native(errsv),
               v);
#endif

    return v;
}

/* See nm_g_ascii_strtoll() */
guint64
nm_g_ascii_strtoull(const char *nptr, char **endptr, guint base)
{
    int       try_count = 2;
    guint64   v;
    const int errsv_orig = errno;
    int       errsv;

    nm_assert(nptr);
    nm_assert(base == 0u || (base >= 2u && base <= 36u));

again:
    errno = 0;
    v     = g_ascii_strtoull(nptr, endptr, base);
    errsv = errno;

    if (errsv == 0) {
        if (errsv_orig != 0)
            errno = errsv_orig;
        return v;
    }

    if (errsv == ERANGE && NM_IN_SET(v, G_MAXUINT64))
        return v;

    if (errsv == EINVAL && v == 0 && nptr && nptr[0] == '\0')
        return v;

    if (try_count-- > 0)
        goto again;

#if NM_MORE_ASSERTS
    g_critical("g_ascii_strtoull() for \"%s\" failed with errno=%d (%s) and v=%" G_GUINT64_FORMAT,
               nptr,
               errsv,
               nm_strerror_native(errsv),
               v);
#endif

    return v;
}

/* see nm_g_ascii_strtoll(). */
double
nm_g_ascii_strtod(const char *nptr, char **endptr)
{
    int    try_count = 2;
    double v;
    int    errsv;

    nm_assert(nptr);

again:
    v     = g_ascii_strtod(nptr, endptr);
    errsv = errno;

    if (errsv == 0)
        return v;

    if (errsv == ERANGE)
        return v;

    if (try_count-- > 0)
        goto again;

#if NM_MORE_ASSERTS
    g_critical("g_ascii_strtod() for \"%s\" failed with errno=%d (%s) and v=%f",
               nptr,
               errsv,
               nm_strerror_native(errsv),
               v);
#endif

    /* Not really much else to do. Return the parsed value and leave errno set
     * to the unexpected value. */
    return v;
}

/* _nm_utils_ascii_str_to_int64:
 *
 * A wrapper for g_ascii_strtoll, that checks whether the whole string
 * can be successfully converted to a number and is within a given
 * range. On any error, @fallback will be returned and %errno will be set
 * to a non-zero value. On success, %errno will be set to zero, check %errno
 * for errors. Any trailing or leading (ascii) white space is ignored and the
 * functions is locale independent.
 *
 * The function is guaranteed to return a value between @min and @max
 * (inclusive) or @fallback. Also, the parsing is rather strict, it does
 * not allow for any unrecognized characters, except leading and trailing
 * white space.
 **/
gint64
_nm_utils_ascii_str_to_int64(const char *str, guint base, gint64 min, gint64 max, gint64 fallback)
{
    gint64      v;
    const char *s = NULL;

    str = nm_str_skip_leading_spaces(str);
    if (!str || !str[0]) {
        errno = EINVAL;
        return fallback;
    }

    errno = 0;
    v     = nm_g_ascii_strtoll(str, (char **) &s, base);

    if (errno != 0)
        return fallback;

    if (s[0] != '\0') {
        s = nm_str_skip_leading_spaces(s);
        if (s[0] != '\0') {
            errno = EINVAL;
            return fallback;
        }
    }
    if (v > max || v < min) {
        errno = ERANGE;
        return fallback;
    }

    return v;
}

guint64
_nm_utils_ascii_str_to_uint64(const char *str,
                              guint       base,
                              guint64     min,
                              guint64     max,
                              guint64     fallback)
{
    guint64     v;
    const char *s = NULL;

    if (str) {
        while (g_ascii_isspace(str[0]))
            str++;
    }
    if (!str || !str[0]) {
        errno = EINVAL;
        return fallback;
    }

    errno = 0;
    v     = nm_g_ascii_strtoull(str, (char **) &s, base);

    if (errno != 0)
        return fallback;
    if (s[0] != '\0') {
        while (g_ascii_isspace(s[0]))
            s++;
        if (s[0] != '\0') {
            errno = EINVAL;
            return fallback;
        }
    }
    if (v > max || v < min) {
        errno = ERANGE;
        return fallback;
    }

    if (v != 0 && str[0] == '-') {
        /* As documented, g_ascii_strtoull() accepts negative values, and returns their
         * absolute value. We don't. */
        errno = ERANGE;
        return fallback;
    }

    return v;
}

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

gint64
_nm_utils_ascii_str_to_int64_bin(const char *str,
                                 gssize      len,
                                 guint       base,
                                 gint64      min,
                                 gint64      max,
                                 gint64      fallback)
{
    gs_free char *str_clone = NULL;

    /* This is like _nm_utils_ascii_str_to_int64(), but the user may provide
     * an optional string length, in which case str is not assumed to be NUL
     * terminated. In that case, any NUL characters inside the first len characters
     * lead to a failure, except one last NUL character is allowed. */

    if (len >= 0) {
        gsize l = len;

        nm_assert(l == 0 || str);

        if (l > 0 && str[l - 1u] == '\0') {
            /* we accept one '\0' at the end of the string. */
            l--;
        }

        if (l > 0 && memchr(str, '\0', l)) {
            /* but we don't accept other NUL characters in the middle. */
            errno = EINVAL;
            return fallback;
        }

        str = nm_strndup_a(300, str, len, &str_clone);
    }

    return _nm_utils_ascii_str_to_int64(str, base, min, max, fallback);
}

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

int
nm_strcmp_with_data(gconstpointer a, gconstpointer b, gpointer user_data)
{
    const char *s1 = a;
    const char *s2 = b;

    return strcmp(s1, s2);
}

/* like nm_strcmp_p(), suitable for g_ptr_array_sort_with_data().
 * g_ptr_array_sort() just casts nm_strcmp_p() to a function of different
 * signature. I guess, in glib there are knowledgeable people that ensure
 * that this additional argument doesn't cause problems due to different ABI
 * for every architecture that glib supports.
 * For NetworkManager, we'd rather avoid such stunts.
 **/
int
nm_strcmp_p_with_data(gconstpointer a, gconstpointer b, gpointer user_data)
{
    const char *s1 = *((const char **) a);
    const char *s2 = *((const char **) b);

    return strcmp(s1, s2);
}

int
nm_strcmp0_p_with_data(gconstpointer a, gconstpointer b, gpointer user_data)
{
    const char *s1 = *((const char **) a);
    const char *s2 = *((const char **) b);

    return nm_strcmp0(s1, s2);
}

int
nm_strcmp_ascii_case_with_data(gconstpointer a, gconstpointer b, gpointer user_data)
{
    const char *s1 = a;
    const char *s2 = b;

    return g_ascii_strcasecmp(s1, s2);
}

int
nm_cmp_uint32_p_with_data(gconstpointer p_a, gconstpointer p_b, gpointer user_data)
{
    const guint32 a = *((const guint32 *) p_a);
    const guint32 b = *((const guint32 *) p_b);

    if (a < b)
        return -1;
    if (a > b)
        return 1;
    return 0;
}

int
nm_cmp_int2ptr_p_with_data(gconstpointer p_a, gconstpointer p_b, gpointer user_data)
{
    /* p_a and p_b are two pointers to a pointer, where the pointer is
     * interpreted as a integer using GPOINTER_TO_INT().
     *
     * That is the case of a hash-table that uses GINT_TO_POINTER() to
     * convert integers as pointers, and the resulting keys-as-array
     * array. */
    const int a = GPOINTER_TO_INT(*((gconstpointer *) p_a));
    const int b = GPOINTER_TO_INT(*((gconstpointer *) p_b));

    if (a < b)
        return -1;
    if (a > b)
        return 1;
    return 0;
}

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

const char *
nm_utils_dbus_path_get_last_component(const char *dbus_path)
{
    if (dbus_path) {
        dbus_path = strrchr(dbus_path, '/');
        if (dbus_path)
            return dbus_path + 1;
    }
    return NULL;
}

static gint64
_dbus_path_component_as_num(const char *p)
{
    gint64 n;

    /* no odd stuff. No leading zeros, only a non-negative, decimal integer.
     *
     * Otherwise, there would be multiple ways to encode the same number "10"
     * and "010". That is just confusing. A number has no leading zeros,
     * if it has, it's not a number (as far as we are concerned here). */
    if (p[0] == '0') {
        if (p[1] != '\0')
            return -1;
        else
            return 0;
    }
    if (!(p[0] >= '1' && p[0] <= '9'))
        return -1;
    if (!NM_STRCHAR_ALL(&p[1], ch, (ch >= '0' && ch <= '9')))
        return -1;
    n = _nm_utils_ascii_str_to_int64(p, 10, 0, G_MAXINT64, -1);
    nm_assert(n == -1 || nm_streq0(p, nm_sprintf_bufa(100, "%" G_GINT64_FORMAT, n)));
    return n;
}

int
nm_utils_dbus_path_cmp(const char *dbus_path_a, const char *dbus_path_b)
{
    const char *l_a, *l_b;
    gsize       plen;
    gint64      n_a, n_b;

    /* compare function for two D-Bus paths. It behaves like
     * strcmp(), except, if both paths have the same prefix,
     * and both end in a (positive) number, then the paths
     * will be sorted by number. */

    NM_CMP_SELF(dbus_path_a, dbus_path_b);

    /* if one or both paths have no slash (and no last component)
     * compare the full paths directly. */
    if (!(l_a = nm_utils_dbus_path_get_last_component(dbus_path_a))
        || !(l_b = nm_utils_dbus_path_get_last_component(dbus_path_b)))
        goto comp_full;

    /* check if both paths have the same prefix (up to the last-component). */
    plen = l_a - dbus_path_a;
    if (plen != (l_b - dbus_path_b))
        goto comp_full;
    NM_CMP_RETURN(strncmp(dbus_path_a, dbus_path_b, plen));

    n_a = _dbus_path_component_as_num(l_a);
    n_b = _dbus_path_component_as_num(l_b);
    if (n_a == -1 && n_b == -1)
        goto comp_l;

    /* both components must be convertible to a number. If they are not,
     * (and only one of them is), then we must always strictly sort numeric parts
     * after non-numeric components. If we wouldn't, we wouldn't have
     * a total order.
     *
     * An example of a not total ordering would be:
     *   "8"   < "010"  (numeric)
     *   "0x"  < "8"    (lexical)
     *   "0x"  > "010"  (lexical)
     * We avoid this, by forcing that a non-numeric entry "0x" always sorts
     * before numeric entries.
     *
     * Additionally, _dbus_path_component_as_num() would also reject "010" as
     * not a valid number.
     */
    if (n_a == -1)
        return -1;
    if (n_b == -1)
        return 1;

    NM_CMP_DIRECT(n_a, n_b);
    nm_assert(nm_streq(dbus_path_a, dbus_path_b));
    return 0;

comp_full:
    NM_CMP_DIRECT_STRCMP0(dbus_path_a, dbus_path_b);
    return 0;
comp_l:
    NM_CMP_DIRECT_STRCMP0(l_a, l_b);
    nm_assert(nm_streq(dbus_path_a, dbus_path_b));
    return 0;
}

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

typedef struct {
    union {
        guint8  table[256];
        guint64 _dummy_for_alignment;
    };
} CharLookupTable;

static void
_char_lookup_table_set_one(CharLookupTable *lookup, char ch)
{
    lookup->table[(guint8) ch] = 1;
}

static void
_char_lookup_table_set_all(CharLookupTable *lookup, const char *candidates)
{
    while (candidates[0] != '\0')
        _char_lookup_table_set_one(lookup, (candidates++)[0]);
}

static void
_char_lookup_table_init(CharLookupTable *lookup, const char *candidates)
{
    *lookup = (CharLookupTable) {
        .table = {0},
    };
    if (candidates)
        _char_lookup_table_set_all(lookup, candidates);
}

static gboolean
_char_lookup_has(const CharLookupTable *lookup, char ch)
{
    /* with some optimization levels, the compiler thinks this code
     * might access uninitialized @lookup. It is not -- when you look at the
     * callers of this function. */
    NM_PRAGMA_WARNING_DISABLE("-Wmaybe-uninitialized")
    nm_assert(lookup->table[(guint8) '\0'] == 0);
    return lookup->table[(guint8) ch] != 0;
    NM_PRAGMA_WARNING_REENABLE
}

static gboolean
_char_lookup_has_all(const CharLookupTable *lookup, const char *candidates)
{
    if (candidates) {
        while (candidates[0] != '\0') {
            if (!_char_lookup_has(lookup, (candidates++)[0]))
                return FALSE;
        }
    }
    return TRUE;
}

/**
 * nm_strsplit_set_full:
 * @str: the string to split.
 * @delimiters: the set of delimiters.
 * @flags: additional flags for controlling the operation.
 *
 * This is a replacement for g_strsplit_set() which avoids copying
 * each word once (the entire strv array), but instead copies it once
 * and all words point into that internal copy.
 *
 * Note that for @str %NULL and "", this always returns %NULL too. That differs
 * from g_strsplit_set(), which would return an empty strv array for "".
 * This never returns an empty array.
 *
 * Returns: %NULL if @str is %NULL or "".
 *   If @str only contains delimiters and %NM_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY
 *   is not set, it also returns %NULL.
 *   Otherwise, a %NULL terminated strv array containing the split words.
 *   (delimiter characters are removed).
 *   The strings to which the result strv array points to are allocated
 *   after the returned result itself. Don't free the strings themself,
 *   but free everything with g_free().
 *   It is however safe and allowed to modify the individual strings in-place,
 *   like "g_strstrip((char *) iter[0])".
 */
const char **
nm_strsplit_set_full(const char *str, const char *delimiters, NMUtilsStrsplitSetFlags flags)
{
    const char    **ptr;
    gsize           num_tokens;
    gsize           i_token;
    gsize           str_len_p1;
    const char     *c_str;
    char           *s;
    CharLookupTable ch_lookup;
    const gboolean  f_escaped = NM_FLAGS_HAS(flags, NM_STRSPLIT_SET_FLAGS_ESCAPED);
    const gboolean  f_allow_escaping =
        f_escaped || NM_FLAGS_HAS(flags, NM_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING);
    const gboolean f_preserve_empty = NM_FLAGS_HAS(flags, NM_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY);
    const gboolean f_strstrip       = NM_FLAGS_HAS(flags, NM_STRSPLIT_SET_FLAGS_STRSTRIP);

    if (!str)
        return NULL;

    if (!delimiters) {
        nm_assert_not_reached();
        delimiters = " \t\n";
    }
    _char_lookup_table_init(&ch_lookup, delimiters);

    nm_assert(!f_allow_escaping || !_char_lookup_has(&ch_lookup, '\\'));

    if (!f_preserve_empty) {
        while (_char_lookup_has(&ch_lookup, str[0]))
            str++;
    }

    if (!str[0]) {
        /* We return %NULL here, also with NM_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY.
         * That makes nm_strsplit_set_full() with NM_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY
         * different from g_strsplit_set(), which would in this case return an empty array.
         * If you need to handle %NULL, and "" specially, then check the input string first. */
        return NULL;
    }

#define _char_is_escaped(str_start, str_cur)              \
    ({                                                    \
        const char *const _str_start = (str_start);       \
        const char *const _str_cur   = (str_cur);         \
        const char       *_str_i     = (_str_cur);        \
                                                          \
        while (_str_i > _str_start && _str_i[-1] == '\\') \
            _str_i--;                                     \
        (((_str_cur - _str_i) % 2) != 0);                 \
    })

    num_tokens = 1;
    c_str      = str;
    while (TRUE) {
        while (G_LIKELY(!_char_lookup_has(&ch_lookup, c_str[0]))) {
            if (c_str[0] == '\0')
                goto done1;
            c_str++;
        }

        /* we assume escapings are not frequent. After we found
         * this delimiter, check whether it was escaped by counting
         * the backslashed before. */
        if (f_allow_escaping && _char_is_escaped(str, c_str)) {
            /* the delimiter is escaped. This was not an accepted delimiter. */
            c_str++;
            continue;
        }

        c_str++;

        /* if we drop empty tokens, then we now skip over all consecutive delimiters. */
        if (!f_preserve_empty) {
            while (_char_lookup_has(&ch_lookup, c_str[0]))
                c_str++;
            if (c_str[0] == '\0')
                break;
        }

        num_tokens++;
    }

done1:

    nm_assert(c_str[0] == '\0');

    str_len_p1 = (c_str - str) + 1;

    nm_assert(str[str_len_p1 - 1] == '\0');

    ptr = g_malloc((sizeof(const char *) * (num_tokens + 1)) + str_len_p1);
    s   = (char *) &ptr[num_tokens + 1];
    memcpy(s, str, str_len_p1);

    i_token = 0;

    while (TRUE) {
        nm_assert(i_token < num_tokens);
        ptr[i_token++] = s;

        if (s[0] == '\0') {
            nm_assert(f_preserve_empty);
            goto done2;
        }
        nm_assert(f_preserve_empty || !_char_lookup_has(&ch_lookup, s[0]));

        while (!_char_lookup_has(&ch_lookup, s[0])) {
            if (G_UNLIKELY(s[0] == '\\' && f_allow_escaping)) {
                s++;
                if (s[0] == '\0')
                    goto done2;
                s++;
            } else if (s[0] == '\0')
                goto done2;
            else
                s++;
        }

        nm_assert(_char_lookup_has(&ch_lookup, s[0]));
        s[0] = '\0';
        s++;

        if (!f_preserve_empty) {
            while (_char_lookup_has(&ch_lookup, s[0]))
                s++;
            if (s[0] == '\0')
                goto done2;
        }
    }

done2:
    nm_assert(i_token == num_tokens);
    ptr[i_token] = NULL;

    if (f_strstrip) {
        gsize i;

        i_token = 0;
        for (i = 0; ptr[i]; i++) {
            s = (char *) nm_str_skip_leading_spaces(ptr[i]);
            if (s[0] != '\0') {
                char *s_last;

                s_last = &s[strlen(s) - 1];
                while (s_last > s && g_ascii_isspace(s_last[0])
                       && (!f_allow_escaping || !_char_is_escaped(s, s_last)))
                    (s_last--)[0] = '\0';
            }

            if (!f_preserve_empty && s[0] == '\0')
                continue;

            ptr[i_token++] = s;
        }

        if (i_token == 0) {
            g_free(ptr);
            return NULL;
        }
        ptr[i_token] = NULL;
    }

    if (f_escaped) {
        gsize i, j;

        /* We no longer need ch_lookup for its original purpose. Modify it, so it
         * can detect the delimiters, '\\', and (optionally) whitespaces. */
        _char_lookup_table_set_one(&ch_lookup, '\\');
        if (f_strstrip)
            _char_lookup_table_set_all(&ch_lookup, NM_ASCII_SPACES);

        for (i_token = 0; ptr[i_token]; i_token++) {
            s = (char *) ptr[i_token];
            j = 0;
            for (i = 0; s[i] != '\0';) {
                if (s[i] == '\\' && _char_lookup_has(&ch_lookup, s[i + 1]))
                    i++;
                s[j++] = s[i++];
            }
            s[j] = '\0';
        }
    }

    nm_assert(ptr && ptr[0]);
    return ptr;
}

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

const char *
nm_utils_escaped_tokens_escape_full(const char                     *str,
                                    const char                     *delimiters,
                                    const char                     *delimiters_as_needed,
                                    NMUtilsEscapedTokensEscapeFlags flags,
                                    char                          **out_to_free)
{
    CharLookupTable ch_lookup;
    CharLookupTable ch_lookup_as_needed;
    gboolean        has_ch_lookup_as_needed = FALSE;
    char           *ret;
    gsize           str_len;
    gsize           alloc_len;
    gsize           n_escapes;
    gsize           i, j;
    gboolean        escape_leading_space;
    gboolean        escape_trailing_space;
    gboolean        escape_backslash_as_needed;

    nm_assert(
        !delimiters_as_needed
        || (delimiters_as_needed[0]
            && NM_FLAGS_HAS(flags,
                            NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_AS_NEEDED)));

    if (!str || str[0] == '\0') {
        *out_to_free = NULL;
        return str;
    }

    str_len = strlen(str);

    _char_lookup_table_init(&ch_lookup, delimiters);
    if (!delimiters || NM_FLAGS_HAS(flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_SPACES)) {
        flags &= ~(NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE
                   | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE);
        _char_lookup_table_set_all(&ch_lookup, NM_ASCII_SPACES);
    }

    if (NM_FLAGS_HAS(flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_ALWAYS)) {
        _char_lookup_table_set_one(&ch_lookup, '\\');
        escape_backslash_as_needed = FALSE;
    } else if (_char_lookup_has(&ch_lookup, '\\'))
        escape_backslash_as_needed = FALSE;
    else {
        escape_backslash_as_needed =
            NM_FLAGS_HAS(flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_AS_NEEDED);
        if (escape_backslash_as_needed) {
            if (NM_FLAGS_ANY(flags,
                             NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE
                                 | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE)
                && !_char_lookup_has_all(&ch_lookup, NM_ASCII_SPACES)) {
                /* ESCAPE_LEADING_SPACE and ESCAPE_TRAILING_SPACE implies that we escape backslash
                 * before whitespaces. */
                if (!has_ch_lookup_as_needed) {
                    has_ch_lookup_as_needed = TRUE;
                    _char_lookup_table_init(&ch_lookup_as_needed, NULL);
                }
                _char_lookup_table_set_all(&ch_lookup_as_needed, NM_ASCII_SPACES);
            }
            if (delimiters_as_needed && !_char_lookup_has_all(&ch_lookup, delimiters_as_needed)) {
                if (!has_ch_lookup_as_needed) {
                    has_ch_lookup_as_needed = TRUE;
                    _char_lookup_table_init(&ch_lookup_as_needed, NULL);
                }
                _char_lookup_table_set_all(&ch_lookup_as_needed, delimiters_as_needed);
            }
        }
    }

    escape_leading_space =
        NM_FLAGS_HAS(flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE)
        && g_ascii_isspace(str[0]) && !_char_lookup_has(&ch_lookup, str[0]);
    if (str_len == 1)
        escape_trailing_space = FALSE;
    else {
        escape_trailing_space =
            NM_FLAGS_HAS(flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE)
            && g_ascii_isspace(str[str_len - 1]) && !_char_lookup_has(&ch_lookup, str[str_len - 1]);
    }

    n_escapes = 0;
    for (i = 0; str[i] != '\0'; i++) {
        if (_char_lookup_has(&ch_lookup, str[i]))
            n_escapes++;
        else if (str[i] == '\\' && escape_backslash_as_needed
                 && (_char_lookup_has(&ch_lookup, str[i + 1]) || NM_IN_SET(str[i + 1], '\0', '\\')
                     || (has_ch_lookup_as_needed
                         && _char_lookup_has(&ch_lookup_as_needed, str[i + 1]))))
            n_escapes++;
    }
    if (escape_leading_space)
        n_escapes++;
    if (escape_trailing_space)
        n_escapes++;

    if (n_escapes == 0u) {
        *out_to_free = NULL;
        return str;
    }

    alloc_len = str_len + n_escapes + 1u;
    ret       = g_new(char, alloc_len);

    j = 0;
    i = 0;

    if (escape_leading_space) {
        ret[j++] = '\\';
        ret[j++] = str[i++];
    }
    for (; str[i] != '\0'; i++) {
        if (_char_lookup_has(&ch_lookup, str[i]))
            ret[j++] = '\\';
        else if (str[i] == '\\' && escape_backslash_as_needed
                 && (_char_lookup_has(&ch_lookup, str[i + 1]) || NM_IN_SET(str[i + 1], '\0', '\\')
                     || (has_ch_lookup_as_needed
                         && _char_lookup_has(&ch_lookup_as_needed, str[i + 1]))))
            ret[j++] = '\\';
        ret[j++] = str[i];
    }
    if (escape_trailing_space) {
        nm_assert(!_char_lookup_has(&ch_lookup, ret[j - 1]) && g_ascii_isspace(ret[j - 1]));
        ret[j]     = ret[j - 1];
        ret[j - 1] = '\\';
        j++;
    }

    nm_assert(j == alloc_len - 1);
    ret[j] = '\0';
    nm_assert(strlen(ret) == j);

    *out_to_free = ret;
    return ret;
}

/**
 * nm_utils_escaped_tokens_options_split:
 * @str: the src string. This string will be modified in-place.
 *   The output values will point into @str.
 * @out_key: (out) (nullable): the returned output key. This will always be set
 *   to @str itself. @str will be modified to contain only the unescaped,
 *   truncated key name.
 * @out_val: (out) (nullable): returns the parsed (and unescaped) value or
 *   %NULL, if @str contains no '=' delimiter.
 *
 * Honors backslash escaping to parse @str as "key=value" pairs. Optionally, if no '='
 * is present, @out_val will be returned as %NULL. Backslash can be used to escape
 * '=', ',', '\\', and ascii whitespace. Other backslash sequences are taken verbatim.
 *
 * For keys, '=' obviously must be escaped. For values, that is optional because an
 * unescaped '=' is just taken verbatim. For example, in a key, the sequence "\\="
 * must be escaped as "\\\\\\=". For the value, that works too, but "\\\\=" is also
 * accepted.
 *
 * Unescaped Space around the key and value are also removed. Space in general must
 * not be escaped, unless they are at the beginning or the end of key/value.
 */
void
nm_utils_escaped_tokens_options_split(char *str, const char **out_key, const char **out_val)
{
    const char *val = NULL;
    gsize       i;
    gsize       j;
    gsize       last_space_idx;
    gboolean    last_space_has;

    nm_assert(str);

    i = 0;
    while (g_ascii_isspace(str[i]))
        i++;

    j              = 0;
    last_space_idx = 0;
    last_space_has = FALSE;
    while (str[i] != '\0') {
        if (g_ascii_isspace(str[i])) {
            if (!last_space_has) {
                last_space_has = TRUE;
                last_space_idx = j;
            }
        } else {
            if (str[i] == '\\') {
                if (NM_IN_SET(str[i + 1u], '\\', ',', '=') || g_ascii_isspace(str[i + 1u]))
                    i++;
            } else if (str[i] == '=') {
                /* Encounter an unescaped '=' character. When we still parse the key, this
                 * is the separator we were waiting for. If we are parsing the value,
                 * we take the character verbatim. */
                if (!val) {
                    if (last_space_has) {
                        str[last_space_idx] = '\0';
                        j                   = last_space_idx + 1;
                        last_space_has      = FALSE;
                    } else
                        str[j++] = '\0';
                    val = &str[j];
                    i++;
                    while (g_ascii_isspace(str[i]))
                        i++;
                    continue;
                }
            }
            last_space_has = FALSE;
        }
        str[j++] = str[i++];
    }

    if (last_space_has)
        str[last_space_idx] = '\0';
    else
        str[j] = '\0';

    *out_key = str;
    *out_val = val;
}

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

/**
 * nm_utils_strsplit_quoted:
 * @str: the string to split (e.g. from /proc/cmdline).
 *
 * This basically does that systemd's extract_first_word() does
 * with the flags "EXTRACT_UNQUOTE | EXTRACT_RELAX". This is what
 * systemd uses to parse /proc/cmdline, and we do too.
 *
 * Splits the string. We have nm_strsplit_set() which
 * supports a variety of flags. However, extending that already
 * complex code to also support quotation and escaping is hard.
 * Instead, add a naive implementation.
 *
 * Returns: (transfer full): the split string.
 */
char **
nm_utils_strsplit_quoted(const char *str)
{
    char          **arr       = NULL;
    gsize           arr_len   = 0;
    gsize           arr_alloc = 0;
    gs_free char   *str_out   = NULL;
    CharLookupTable ch_lookup;

    nm_assert(str);

    _char_lookup_table_init(&ch_lookup, NM_ASCII_WHITESPACES);

    for (;;) {
        char  quote;
        gsize j;

        while (_char_lookup_has(&ch_lookup, str[0]))
            str++;

        if (str[0] == '\0')
            break;

        if (!str_out)
            str_out = g_new(char, strlen(str) + 1);

        quote = '\0';
        j     = 0;
        for (;;) {
            if (str[0] == '\\') {
                str++;
                if (str[0] == '\0')
                    break;
                str_out[j++] = str[0];
                str++;
                continue;
            }
            if (quote) {
                if (str[0] == '\0')
                    break;
                if (str[0] == quote) {
                    quote = '\0';
                    str++;
                    continue;
                }
                str_out[j++] = str[0];
                str++;
                continue;
            }
            if (str[0] == '\0')
                break;
            if (NM_IN_SET(str[0], '\'', '"')) {
                quote = str[0];
                str++;
                continue;
            }
            if (_char_lookup_has(&ch_lookup, str[0])) {
                str++;
                break;
            }
            str_out[j++] = str[0];
            str++;
        }

        if (arr_len >= arr_alloc) {
            if (arr_alloc == 0)
                arr_alloc = 4;
            else
                arr_alloc *= 2;
            arr = g_realloc(arr, sizeof(char *) * arr_alloc);
        }

        arr[arr_len++] = g_strndup(str_out, j);
    }

    if (!arr)
        return nm_strv_empty_new();

    /* We want to return an optimally sized strv array, with no excess
     * memory allocated. Hence, clone once more. */

    if (arr_len + 1u != arr_alloc) {
        gs_free char **arr_old = arr;

        arr = g_new(char *, arr_len + 1u);
        memcpy(arr, arr_old, sizeof(char *) * arr_len);
    }

    arr[arr_len] = NULL;
    return arr;
}

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

/**
 * _nm_strv_find_first:
 * @list: the strv list to search
 * @len: the length of the list, or a negative value if @list is %NULL terminated.
 * @needle: the value to search for. The search is done using strcmp().
 *
 * Searches @list for @needle and returns the index of the first match (based
 * on strcmp()).
 *
 * Returns: index of first occurrence or -1 if @needle is not found in @list.
 */
gssize
_nm_strv_find_first(const char *const *list, gssize len, const char *needle)
{
    gssize i;

    if (len > 0) {
        g_return_val_if_fail(list, -1);

        if (!needle) {
            /* if we search a list with known length, %NULL is a valid @needle. */
            for (i = 0; i < len; i++) {
                if (!list[i])
                    return i;
            }
        } else {
            for (i = 0; i < len; i++) {
                if (list[i] && nm_streq(needle, list[i]))
                    return i;
            }
        }
    } else if (len < 0) {
        g_return_val_if_fail(needle, -1);

        if (list) {
            for (i = 0; list[i]; i++) {
                if (nm_streq(needle, list[i]))
                    return i;
            }
        }
    }
    return -1;
}

gboolean
nm_strv_has_duplicate(const char *const *strv, gssize len, gboolean is_sorted)
{
    gsize l;
    gsize i;
    gsize j;

    l = len < 0 ? NM_PTRARRAY_LEN(strv) : (gsize) len;

    if (is_sorted) {
#if NM_MORE_ASSERTS > 10
        for (i = 1; i < l; i++)
            nm_assert(nm_strcmp0(strv[i - 1], strv[i]) <= 0);
#endif
        for (i = 1; i < l; i++) {
            if (nm_streq0(strv[i - 1], strv[i]))
                return TRUE;
        }
    } else {
        for (i = 1; i < l; i++) {
            for (j = 0; j < i; j++) {
                if (nm_streq0(strv[j], strv[i]))
                    return TRUE;
            }
        }
    }

    return FALSE;
}

gboolean
nm_strv_is_same_unordered(const char *const *strv1,
                          gssize             len1,
                          const char *const *strv2,
                          gssize             len2)
{
    gs_free const char **ss1_free = NULL;
    gs_free const char **ss2_free = NULL;
    gsize                l2;
    gsize                l;
    gsize                i;

    if (len1 < 0)
        l = NM_PTRARRAY_LEN(strv1);
    else
        l = (gsize) len1;

    if (len2 < 0)
        l2 = NM_PTRARRAY_LEN(strv2);
    else
        l2 = (gsize) len2;

    if (l != l2)
        return FALSE;

    if (l == 0) {
        /* An empty array. We treat (NULL, -1), (NULL, 0) and ([...], 0)
         * all the same. */
        return TRUE;
    }

    if (l > 1) {
        strv1 = nm_memdup_maybe_a(300, strv1, sizeof(char *) * l, &ss1_free);
        strv2 = nm_memdup_maybe_a(300, strv2, sizeof(char *) * l2, &ss2_free);
        _nm_strv_sort((const char **) strv1, l);
        _nm_strv_sort((const char **) strv2, l);
    }

    for (i = 0; i < l; i++) {
        if (!nm_streq0(strv1[i], strv2[i]))
            return FALSE;
    }

    return TRUE;
}

const char **
nm_strv_cleanup_const(const char **strv, gboolean no_empty, gboolean no_duplicates)
{
    gsize i;
    gsize j;

    if (!strv || !*strv)
        return strv;

    if (!no_empty && !no_duplicates)
        return strv;

    j = 0;
    for (i = 0; strv[i]; i++) {
        if ((no_empty && !*strv[i]) || (no_duplicates && nm_strv_contains(strv, j, strv[i])))
            continue;
        strv[j++] = strv[i];
    }
    strv[j] = NULL;
    return strv;
}

char **
nm_strv_cleanup(char **strv, gboolean strip_whitespace, gboolean no_empty, gboolean no_duplicates)
{
    gsize i;
    gsize j;

    if (!strv || !*strv)
        return strv;

    if (strip_whitespace) {
        /* we only modify the strings pointed to by @strv if @strip_whitespace is
         * requested. Otherwise, the strings themselves are untouched. */
        for (i = 0; strv[i]; i++)
            g_strstrip(strv[i]);
    }
    if (!no_empty && !no_duplicates)
        return strv;
    j = 0;
    for (i = 0; strv[i]; i++) {
        if ((no_empty && !*strv[i]) || (no_duplicates && nm_strv_contains(strv, j, strv[i])))
            g_free(strv[i]);
        else
            strv[j++] = strv[i];
    }
    strv[j] = NULL;
    return strv;
}

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

GPtrArray *
nm_g_ptr_array_new_clone(GPtrArray     *array,
                         GCopyFunc      func,
                         gpointer       user_data,
                         GDestroyNotify element_free_func)
{
    GPtrArray *new_array;
    guint      i;

    g_return_val_if_fail(array, NULL);
    nm_assert((!!func) == (!!element_free_func));

    new_array = g_ptr_array_new_full(array->len, element_free_func);
    for (i = 0; i < array->len; i++) {
        g_ptr_array_add(new_array, func ? func(array->pdata[i], user_data) : array->pdata[i]);
    }
    return new_array;
}

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

int
_nm_utils_ascii_str_to_bool(const char *str, int default_value)
{
    gs_free char *str_free = NULL;

    if (!str)
        return default_value;

    str = nm_strstrip_avoid_copy_a(300, str, &str_free);
    if (str[0] == '\0')
        return default_value;

    if (!g_ascii_strcasecmp(str, "true") || !g_ascii_strcasecmp(str, "yes")
        || !g_ascii_strcasecmp(str, "on") || !g_ascii_strcasecmp(str, "1"))
        return TRUE;

    if (!g_ascii_strcasecmp(str, "false") || !g_ascii_strcasecmp(str, "no")
        || !g_ascii_strcasecmp(str, "off") || !g_ascii_strcasecmp(str, "0"))
        return FALSE;

    return default_value;
}

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

NM_CACHED_QUARK_FCN("nm-manager-error-quark", nm_manager_error_quark);

NM_CACHED_QUARK_FCN("nm-utils-error-quark", nm_utils_error_quark);

void
nm_utils_error_set_cancelled(GError **error, gboolean is_disposing, const char *instance_name)
{
    if (is_disposing) {
        g_set_error(error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_CANCELLED_DISPOSING,
                    "Disposing %s instance",
                    instance_name && *instance_name ? instance_name : "source");
    } else {
        g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_CANCELLED, "Request cancelled");
    }
}

gboolean
nm_utils_error_is_cancelled_or_disposing(GError *error)
{
    if (error) {
        if (error->domain == G_IO_ERROR)
            return NM_IN_SET(error->code, G_IO_ERROR_CANCELLED);
        if (error->domain == NM_UTILS_ERROR)
            return NM_IN_SET(error->code, NM_UTILS_ERROR_CANCELLED_DISPOSING);
    }
    return FALSE;
}

gboolean
nm_utils_error_is_notfound(GError *error)
{
    if (error) {
        if (error->domain == G_IO_ERROR)
            return NM_IN_SET(error->code, G_IO_ERROR_NOT_FOUND);
        if (error->domain == G_FILE_ERROR)
            return NM_IN_SET(error->code, G_FILE_ERROR_NOENT);
    }
    return FALSE;
}

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

void
nm_gobject_notify_together_by_pspec_v(gpointer                 obj,
                                      const GParamSpec *const *param_specs,
                                      gsize                    param_specs_len)
{
    GObject *const    gobj        = obj;
    gboolean          frozen      = FALSE;
    const GParamSpec *pspec_first = NULL;

    nm_assert(G_IS_OBJECT(gobj));
    nm_assert(param_specs_len > 0);

    while (param_specs_len-- > 0) {
        const GParamSpec *pspec = (param_specs++)[0];

        if (!pspec)
            continue;

        if (!frozen) {
            if (!pspec_first) {
                pspec_first = pspec;
                continue;
            }
            frozen = TRUE;
            g_object_freeze_notify(gobj);
            g_object_notify_by_pspec(gobj, (GParamSpec *) pspec_first);
        }
        g_object_notify_by_pspec(gobj, (GParamSpec *) pspec);
    }

    if (frozen)
        g_object_thaw_notify(gobj);
    else if (pspec_first)
        g_object_notify_by_pspec(gobj, (GParamSpec *) pspec_first);
}

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

/**
 * nm_g_object_set_property:
 * @object: the target object
 * @property_name: the property name
 * @value: the #GValue to set
 * @error: optional error argument
 *
 * A reimplementation of g_object_set_property(), but instead
 * returning an error instead of logging a warning. All g_object_set*()
 * versions in glib require you to not pass invalid types or they will
 * log a g_warning() -- without reporting an error. We don't want that,
 * so we need to hack error checking around it.
 *
 * Returns: whether the value was successfully set.
 */
gboolean
nm_g_object_set_property(GObject      *object,
                         const char   *property_name,
                         const GValue *value,
                         GError      **error)
{
    GParamSpec                 *pspec;
    nm_auto_unset_gvalue GValue tmp_value = G_VALUE_INIT;
    GObjectClass               *klass;

    g_return_val_if_fail(G_IS_OBJECT(object), FALSE);
    g_return_val_if_fail(property_name != NULL, FALSE);
    g_return_val_if_fail(G_IS_VALUE(value), FALSE);
    g_return_val_if_fail(!error || !*error, FALSE);

    /* g_object_class_find_property() does g_param_spec_get_redirect_target(),
     * where we differ from a plain g_object_set_property(). */
    pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(object), property_name);

    if (!pspec) {
        g_set_error(error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_UNKNOWN,
                    _("object class '%s' has no property named '%s'"),
                    G_OBJECT_TYPE_NAME(object),
                    property_name);
        return FALSE;
    }
    if (!(pspec->flags & G_PARAM_WRITABLE)) {
        g_set_error(error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_UNKNOWN,
                    _("property '%s' of object class '%s' is not writable"),
                    pspec->name,
                    G_OBJECT_TYPE_NAME(object));
        return FALSE;
    }
    if ((pspec->flags & G_PARAM_CONSTRUCT_ONLY)) {
        g_set_error(error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_UNKNOWN,
                    _("construct property \"%s\" for object '%s' can't be set after construction"),
                    pspec->name,
                    G_OBJECT_TYPE_NAME(object));
        return FALSE;
    }

    klass = g_type_class_peek(pspec->owner_type);
    if (klass == NULL) {
        g_set_error(error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_UNKNOWN,
                    _("'%s::%s' is not a valid property name; '%s' is not a GObject subtype"),
                    g_type_name(pspec->owner_type),
                    pspec->name,
                    g_type_name(pspec->owner_type));
        return FALSE;
    }

    /* provide a copy to work from, convert (if necessary) and validate */
    g_value_init(&tmp_value, pspec->value_type);
    if (!g_value_transform(value, &tmp_value)) {
        g_set_error(error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_UNKNOWN,
                    _("unable to set property '%s' of type '%s' from value of type '%s'"),
                    pspec->name,
                    g_type_name(pspec->value_type),
                    G_VALUE_TYPE_NAME(value));
        return FALSE;
    }
    if (g_param_value_validate(pspec, &tmp_value) && !(pspec->flags & G_PARAM_LAX_VALIDATION)) {
        gs_free char *contents = g_strdup_value_contents(value);

        g_set_error(error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_UNKNOWN,
                    _("value \"%s\" of type '%s' is invalid or out of range for property '%s' of "
                      "type '%s'"),
                    contents,
                    G_VALUE_TYPE_NAME(value),
                    pspec->name,
                    g_type_name(pspec->value_type));
        return FALSE;
    }

    g_object_set_property(object, property_name, &tmp_value);
    return TRUE;
}

#define _set_property(object, property_name, gtype, gtype_set, value, error)          \
    G_STMT_START                                                                      \
    {                                                                                 \
        nm_auto_unset_gvalue GValue gvalue = {0};                                     \
                                                                                      \
        g_value_init(&gvalue, gtype);                                                 \
        gtype_set(&gvalue, (value));                                                  \
        return nm_g_object_set_property((object), (property_name), &gvalue, (error)); \
    }                                                                                 \
    G_STMT_END

gboolean
nm_g_object_set_property_string(GObject    *object,
                                const char *property_name,
                                const char *value,
                                GError    **error)
{
    _set_property(object, property_name, G_TYPE_STRING, g_value_set_string, value, error);
}

gboolean
nm_g_object_set_property_string_static(GObject    *object,
                                       const char *property_name,
                                       const char *value,
                                       GError    **error)
{
    _set_property(object, property_name, G_TYPE_STRING, g_value_set_static_string, value, error);
}

gboolean
nm_g_object_set_property_string_take(GObject    *object,
                                     const char *property_name,
                                     char       *value,
                                     GError    **error)
{
    _set_property(object, property_name, G_TYPE_STRING, g_value_take_string, value, error);
}

gboolean
nm_g_object_set_property_boolean(GObject    *object,
                                 const char *property_name,
                                 gboolean    value,
                                 GError    **error)
{
    _set_property(object, property_name, G_TYPE_BOOLEAN, g_value_set_boolean, !!value, error);
}

gboolean
nm_g_object_set_property_char(GObject    *object,
                              const char *property_name,
                              gint8       value,
                              GError    **error)
{
    /* glib says about G_TYPE_CHAR:
     *
     * The type designated by G_TYPE_CHAR is unconditionally an 8-bit signed integer.
     *
     * This is always a (signed!) char. */
    _set_property(object, property_name, G_TYPE_CHAR, g_value_set_schar, value, error);
}

gboolean
nm_g_object_set_property_uchar(GObject    *object,
                               const char *property_name,
                               guint8      value,
                               GError    **error)
{
    _set_property(object, property_name, G_TYPE_UCHAR, g_value_set_uchar, value, error);
}

gboolean
nm_g_object_set_property_int(GObject *object, const char *property_name, int value, GError **error)
{
    _set_property(object, property_name, G_TYPE_INT, g_value_set_int, value, error);
}

gboolean
nm_g_object_set_property_int64(GObject    *object,
                               const char *property_name,
                               gint64      value,
                               GError    **error)
{
    _set_property(object, property_name, G_TYPE_INT64, g_value_set_int64, value, error);
}

gboolean
nm_g_object_set_property_uint(GObject    *object,
                              const char *property_name,
                              guint       value,
                              GError    **error)
{
    _set_property(object, property_name, G_TYPE_UINT, g_value_set_uint, value, error);
}

gboolean
nm_g_object_set_property_uint64(GObject    *object,
                                const char *property_name,
                                guint64     value,
                                GError    **error)
{
    _set_property(object, property_name, G_TYPE_UINT64, g_value_set_uint64, value, error);
}

gboolean
nm_g_object_set_property_flags(GObject    *object,
                               const char *property_name,
                               GType       gtype,
                               guint       value,
                               GError    **error)
{
    nm_assert(({
        nm_auto_unref_gtypeclass GTypeClass *gtypeclass = g_type_class_ref(gtype);
        G_IS_FLAGS_CLASS(gtypeclass);
    }));
    _set_property(object, property_name, gtype, g_value_set_flags, value, error);
}

gboolean
nm_g_object_set_property_enum(GObject    *object,
                              const char *property_name,
                              GType       gtype,
                              int         value,
                              GError    **error)
{
    nm_assert(({
        nm_auto_unref_gtypeclass GTypeClass *gtypeclass = g_type_class_ref(gtype);
        G_IS_ENUM_CLASS(gtypeclass);
    }));
    _set_property(object, property_name, gtype, g_value_set_enum, value, error);
}

GParamSpec *
nm_g_object_class_find_property_from_gtype(GType gtype, const char *property_name)
{
    nm_auto_unref_gtypeclass GObjectClass *gclass = NULL;

    gclass = g_type_class_ref(gtype);
    return g_object_class_find_property(gclass, property_name);
}

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

/**
 * nm_g_type_find_implementing_class_for_property:
 * @gtype: the GObject type which has a property @pname
 * @pname: the name of the property to look up
 *
 * This is only a helper function for printf debugging. It's not
 * used in actual code. Hence, the function just asserts that
 * @pname and @gtype arguments are suitable. It cannot fail.
 *
 * Returns: the most ancestor type of @gtype, that
 *   implements the property @pname. It means, it
 *   searches the type hierarchy to find the type
 *   that added @pname.
 */
GType
nm_g_type_find_implementing_class_for_property(GType gtype, const char *pname)
{
    nm_auto_unref_gtypeclass GObjectClass *klass = NULL;
    GParamSpec                            *pspec;

    g_return_val_if_fail(pname, G_TYPE_INVALID);

    klass = g_type_class_ref(gtype);
    g_return_val_if_fail(G_IS_OBJECT_CLASS(klass), G_TYPE_INVALID);

    pspec = g_object_class_find_property(klass, pname);
    g_return_val_if_fail(pspec, G_TYPE_INVALID);

    gtype = G_TYPE_FROM_CLASS(klass);

    while (TRUE) {
        nm_auto_unref_gtypeclass GObjectClass *k = NULL;

        k = g_type_class_ref(g_type_parent(gtype));

        g_return_val_if_fail(G_IS_OBJECT_CLASS(k), G_TYPE_INVALID);

        if (g_object_class_find_property(k, pname) != pspec)
            return gtype;

        gtype = G_TYPE_FROM_CLASS(k);
    }
}

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

static void
_str_buf_append_c_escape_octal(NMStrBuf *strbuf, char ch)
{
    nm_str_buf_append_c(strbuf,
                        '\\',
                        '0' + ((char) ((((guchar) ch) >> 6) & 07)),
                        '0' + ((char) ((((guchar) ch) >> 3) & 07)),
                        '0' + ((char) ((((guchar) ch)) & 07)));
}

/**
 * nm_utils_buf_utf8safe_unescape:
 * @str: (nullable): the string to unescape. The string itself is a NUL terminated
 *   ASCII string, that can have C-style backslash escape sequences (which
 *   are to be unescaped). Non-ASCII characters (e.g. UTF-8) are taken verbatim, so
 *   it doesn't care that this string is UTF-8. However, usually this is a UTF-8 encoded
 *   string.
 * @flags: flags for unescaping. The following flags are supported.
 *   %NM_UTILS_STR_UTF8_SAFE_UNESCAPE_STRIP_SPACES performs a g_strstrip() on the input string,
 *   but preserving escaped spaces. For example, "a\\t " gives "a\t" (that is, the escaped space does
 *   not get stripped). Likewise, the invalid escape sequence "a\\  " results in "a " (stripping
 *   the unescaped space, but preserving the escaped one).
 * @out_len: (out): the length of the parsed string.
 * @to_free: (out): if @str requires unescaping, the function will clone the string. In
 *   that case, the allocated buffer will be returned here.
 *
 * See C-style escapes at https://en.wikipedia.org/wiki/Escape_sequences_in_C#Table_of_escape_sequences.
 * Note that hex escapes ("\\xhh") and unicode escapes ("\\uhhhh", "\\Uhhhhhhhh") are not supported.
 *
 * Also, this function is very similar to g_strcompress() but without issuing g_warning()
 * assertions and proper handling of "\\000" escape sequences.
 *
 * Invalid escape sequences (or non-UTF-8 input) are gracefully accepted. For example "\\ "
 * is an invalid escape sequence, in this case the backslash is removed and " " gets returned.
 *
 * The function never leaks secrets in memory.
 *
 * Returns: the unescaped buffer of length @out_len. If @str is %NULL, this returns %NULL
 *   and sets @out_len to 0. Otherwise, a non-%NULL binary buffer is returned with
 *   @out_len bytes. Note that the binary buffer is guaranteed to be NUL terminated
 *   (@result[@out_len] is NUL).
 *   Note that the result is binary, and may have embedded NUL characters and non-UTF-8.
 *   If the function can avoid cloning the input string, it will return a pointer inside
 *   the input @str. For example, if there is no backslash, no cloning is necessary. In that
 *   case, @to_free will be %NULL. Otherwise, @to_free is set to a newly allocated buffer
 *   containing the unescaped string and returned.
 */
gconstpointer
nm_utils_buf_utf8safe_unescape(const char             *str,
                               NMUtilsStrUtf8SafeFlags flags,
                               gsize                  *out_len,
                               gpointer               *to_free)
{
    gboolean    strip_spaces = NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_UNESCAPE_STRIP_SPACES);
    NMStrBuf    strbuf;
    const char *s;
    gsize       len;

    g_return_val_if_fail(to_free, NULL);
    g_return_val_if_fail(out_len, NULL);

    if (!str) {
        *out_len = 0;
        *to_free = NULL;
        return NULL;
    }

    if (strip_spaces)
        str = nm_str_skip_leading_spaces(str);

    len = strlen(str);

    s = memchr(str, '\\', len);
    if (!s) {
        if (strip_spaces && len > 0 && g_ascii_isspace(str[len - 1])) {
            len--;
            while (len > 0 && g_ascii_isspace(str[len - 1]))
                len--;
            *out_len = len;
            return (*to_free = g_strndup(str, len));
        }
        *out_len = len;
        *to_free = NULL;
        return str;
    }

    strbuf = NM_STR_BUF_INIT(len + 1u, FALSE);

    nm_str_buf_append_len(&strbuf, str, s - str);
    str = s;

    for (;;) {
        char  ch;
        guint v;

        nm_assert(str[0] == '\\');

        ch = (++str)[0];

        if (ch == '\0') {
            /* error. Trailing '\\' */
            break;
        }

        if (ch >= '0' && ch <= '9') {
            v  = ch - '0';
            ch = (++str)[0];
            if (ch >= '0' && ch <= '7') {
                v  = v * 8 + (ch - '0');
                ch = (++str)[0];
                if (ch >= '0' && ch <= '7') {
                    /* technically, escape sequences larger than \3FF are out of range
                     * and invalid. We don't check for that, and do the same as
                     * g_strcompress(): silently clip the value with & 0xFF. */
                    v = v * 8 + (ch - '0');
                    ++str;
                }
            }
            ch = v;
        } else {
            switch (ch) {
            case 'b':
                ch = '\b';
                break;
            case 'f':
                ch = '\f';
                break;
            case 'n':
                ch = '\n';
                break;
            case 'r':
                ch = '\r';
                break;
            case 't':
                ch = '\t';
                break;
            case 'v':
                ch = '\v';
                break;
            default:
                /* Here we handle "\\\\", but all other unexpected escape sequences are really a bug.
                 * Take them literally, after removing the escape character */
                break;
            }
            str++;
        }

        nm_str_buf_append_c(&strbuf, ch);

        s = strchr(str, '\\');
        if (!s) {
            gsize l = strlen(str);

            if (strip_spaces) {
                while (l > 0 && g_ascii_isspace(str[l - 1]))
                    l--;
            }
            nm_str_buf_append_len(&strbuf, str, l);
            break;
        }

        nm_str_buf_append_len(&strbuf, str, s - str);
        str = s;
    }

    /* assert that no reallocation was necessary. For one, unescaping should
     * never result in a longer string than the input. Also, when unescaping
     * secrets, we want to ensure that we don't leak secrets in memory. */
    nm_assert(strbuf.allocated == len + 1u);

    return (*to_free = nm_str_buf_finalize(&strbuf, out_len));
}

/**
 * nm_utils_buf_utf8safe_escape:
 * @buf: byte array, possibly in utf-8 encoding, may have NUL characters.
 * @buflen: the length of @buf in bytes, or -1 if @buf is a NUL terminated
 *   string. Note that if @buflen is zero, then the function returns NULL.
 *   If @buflen is negative, then the function returns NULL if @buf is NULL
 *   and @buf if @buf is "".
 * @flags: #NMUtilsStrUtf8SafeFlags flags
 * @to_free: (out): return the pointer location of the string
 *   if a copying was necessary.
 *
 * Based on the assumption, that @buf contains UTF-8 encoded bytes,
 * this will return valid UTF-8 sequence, and invalid sequences
 * will be escaped with backslash (C escaping, like g_strescape()).
 * This is sanitize non UTF-8 characters. The result is valid
 * UTF-8.
 *
 * The operation can be reverted with nm_utils_buf_utf8safe_unescape().
 * Note that if, and only if @buf contains no NUL bytes, the operation
 * can also be reverted with g_strcompress().
 *
 * Depending on @flags, valid UTF-8 characters are not escaped at all
 * (except the escape character '\\'). This is the difference to g_strescape(),
 * which escapes all non-ASCII characters. This allows one to pass on
 * valid UTF-8 characters as-is and can be directly shown to the user
 * as UTF-8 -- with exception of the backslash escape character,
 * invalid UTF-8 sequences, and other (depending on @flags).
 *
 * Returns: the escaped input buffer, as valid UTF-8. If no escaping
 *   is necessary and @buflen is negative, it returns the input @buf
 *   that can be interpreted as NUL terminated UTF-8 string.
 *   Otherwise, an allocated string @to_free is returned which must be freed
 *   by the caller with g_free().
 *   The escaping can be reverted by nm_utils_buf_utf8safe_unescape()
 *   (or, if in the absence of NUL characters, with g_strcompress()).
 *   There are cases where this function returns %NULL:
 *   - if @buflen is 0.
 *   - if @buflen is negative and @buf is NULL.
 **/
const char *
nm_utils_buf_utf8safe_escape(gconstpointer           buf,
                             gssize                  buflen,
                             NMUtilsStrUtf8SafeFlags flags,
                             char                  **to_free)
{
    const char *const str = buf;
    const char       *p   = NULL;
    const char       *s;
    gboolean          nul_terminated = FALSE;
    NMStrBuf          strbuf;

    g_return_val_if_fail(to_free, NULL);

    *to_free = NULL;

    if (buflen == 0)
        return NULL;

    if (buflen < 0) {
        if (!str)
            return NULL;
        buflen = strlen(str);
        if (buflen == 0)
            return str;
        nul_terminated = TRUE;
    }

    if (g_utf8_validate(str, buflen, &p) && nul_terminated) {
        /* note that g_utf8_validate() does not allow NUL character inside @str. Good.
         * We can treat @str like a NUL terminated string. */
        if (!NM_STRCHAR_ANY(
                str,
                ch,
                (ch == '\\'
                 || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL)
                     && nm_ascii_is_ctrl_or_del(ch))
                 || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII)
                     && nm_ascii_is_non_ascii(ch))
                 || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_DOUBLE_QUOTE)
                     && ch == '"'))))
            return str;
    }

    strbuf = NM_STR_BUF_INIT(buflen + 5, NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_SECRET));

    s = str;
    do {
        buflen -= p - s;
        nm_assert(buflen >= 0);

        for (; s < p; s++) {
            char ch = s[0];

            nm_assert(ch);
            if (ch == '\\')
                nm_str_buf_append_c(&strbuf, '\\', '\\');
            else if ((NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL)
                      && nm_ascii_is_ctrl_or_del(ch))
                     || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII)
                         && nm_ascii_is_non_ascii(ch))
                     || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_DOUBLE_QUOTE)
                         && ch == '"'))
                _str_buf_append_c_escape_octal(&strbuf, ch);
            else
                nm_str_buf_append_c(&strbuf, ch);
        }

        if (buflen <= 0)
            break;

        _str_buf_append_c_escape_octal(&strbuf, p[0]);

        buflen--;
        if (buflen == 0)
            break;

        s = &p[1];
        (void) g_utf8_validate(s, buflen, &p);
    } while (TRUE);

    return (*to_free = nm_str_buf_finalize(&strbuf, NULL));
}

const char *
nm_utils_buf_utf8safe_escape_bytes(GBytes *bytes, NMUtilsStrUtf8SafeFlags flags, char **to_free)
{
    gconstpointer p;
    gsize         l;

    if (bytes)
        p = g_bytes_get_data(bytes, &l);
    else {
        p = NULL;
        l = 0;
    }

    return nm_utils_buf_utf8safe_escape(p, l, flags, to_free);
}

char *
nm_utils_buf_utf8safe_escape_cp(gconstpointer buf, gssize buflen, NMUtilsStrUtf8SafeFlags flags)
{
    const char *s_const;
    char       *s;

    s_const = nm_utils_buf_utf8safe_escape(buf, buflen, flags, &s);
    nm_assert(!s || s == s_const);
    return s ?: g_strdup(s_const);
}

/**
 * nm_utils_buf_utf8safe_escape_strv:
 * @strv: an array of strings of length @strv_len
 * @strv_len: the length of @strv, or -1 for a NULL terminated strv array.
 * @flags: #NMUtilsStrUtf8SafeFlags flags
 * @to_free: (out): return the pointer location of the newly created
 *   strv if copying was necessary.
 *
 * Ensures all strings in a strv are valid UTF-8, copying them unless they
 * need to be escaped, and escaping them using nm_utils_buf_utf8safe_escape().
 *
 * Returns: a strv with all its strings escaped, as valid UTF-8. All the strings
 *   contained within are escaped using nm_utils_buf_utf8safe_escape().
 *   If no escaping was necessary it returns the input @strv.
 *   Otherwise, an allocated strv @to_free is returned which must be freed
 *   by the caller with g_strfreev().
 **/
const char *const *
nm_utils_buf_utf8safe_escape_strv(const char *const      *strv,
                                  gssize                  strv_len,
                                  NMUtilsStrUtf8SafeFlags flags,
                                  char                 ***out_to_free)
{
    char **new_strv = NULL;
    guint  len;

    g_return_val_if_fail(strv, NULL);
    g_return_val_if_fail(out_to_free, NULL);

    *out_to_free = NULL;
    len          = strv_len < 0 ? g_strv_length((char **) strv) : strv_len;

    for (guint i = 0; i < len; ++i) {
        char *to_free_str = NULL;

        nm_utils_buf_utf8safe_escape(strv[i], -1, flags, &to_free_str);

        if (to_free_str) {
            if (!new_strv) {
                new_strv = nm_strv_dup(strv, len, TRUE);
            }

            g_free(new_strv[i]);
            new_strv[i] = to_free_str;
        }
    }

    if (new_strv) {
        return (const char *const *) (*out_to_free = new_strv);
    }

    return strv;
}

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

const char *
nm_utils_str_utf8safe_unescape(const char *str, NMUtilsStrUtf8SafeFlags flags, char **to_free)
{
    const char *res;
    gsize       len;

    g_return_val_if_fail(to_free, NULL);

    res = nm_utils_buf_utf8safe_unescape(str, flags, &len, (gpointer *) to_free);

    nm_assert((!res && len == 0) || (strlen(res) <= len));

    return res;
}

/**
 * nm_utils_str_utf8safe_escape:
 * @str: NUL terminated input string, possibly in utf-8 encoding
 * @flags: #NMUtilsStrUtf8SafeFlags flags
 * @to_free: (out): return the pointer location of the string
 *   if a copying was necessary.
 *
 * Returns the possible non-UTF-8 NUL terminated string @str
 * and uses backslash escaping (C escaping, like g_strescape())
 * to sanitize non UTF-8 characters. The result is valid
 * UTF-8.
 *
 * The operation can be reverted with g_strcompress() or
 * nm_utils_str_utf8safe_unescape().
 *
 * Depending on @flags, valid UTF-8 characters are not escaped at all
 * (except the escape character '\\'). This is the difference to g_strescape(),
 * which escapes all non-ASCII characters. This allows one to pass on
 * valid UTF-8 characters as-is and can be directly shown to the user
 * as UTF-8 -- with exception of the backslash escape character,
 * invalid UTF-8 sequences, and other (depending on @flags).
 *
 * Returns: the escaped input string, as valid UTF-8. If no escaping
 *   is necessary, it returns the input @str. Otherwise, an allocated
 *   string @to_free is returned which must be freed by the caller
 *   with g_free. The escaping can be reverted by g_strcompress().
 **/
const char *
nm_utils_str_utf8safe_escape(const char *str, NMUtilsStrUtf8SafeFlags flags, char **to_free)
{
    return nm_utils_buf_utf8safe_escape(str, -1, flags, to_free);
}

/**
 * nm_utils_str_utf8safe_escape_cp:
 * @str: NUL terminated input string, possibly in utf-8 encoding
 * @flags: #NMUtilsStrUtf8SafeFlags flags
 *
 * Like nm_utils_str_utf8safe_escape(), except the returned value
 * is always a copy of the input and must be freed by the caller.
 *
 * Returns: the escaped input string in UTF-8 encoding. The returned
 *   value should be freed with g_free().
 *   The escaping can be reverted by g_strcompress().
 **/
char *
nm_utils_str_utf8safe_escape_cp(const char *str, NMUtilsStrUtf8SafeFlags flags)
{
    char *s;

    nm_utils_str_utf8safe_escape(str, flags, &s);
    return s ?: g_strdup(str);
}

char *
nm_utils_str_utf8safe_unescape_cp(const char *str, NMUtilsStrUtf8SafeFlags flags)
{
    char *s;

    str = nm_utils_str_utf8safe_unescape(str, flags, &s);
    return s ?: g_strdup(str);
}

char *
nm_utils_str_utf8safe_escape_take(char *str, NMUtilsStrUtf8SafeFlags flags)
{
    char *str_to_free;

    nm_utils_str_utf8safe_escape(str, flags, &str_to_free);
    if (str_to_free) {
        g_free(str);
        return str_to_free;
    }
    return str;
}

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

/* taken from systemd's fd_wait_for_event(). Note that the timeout
 * is here in nano-seconds, not micro-seconds. */
int
nm_utils_fd_wait_for_event(int fd, int event, gint64 timeout_nsec)
{
    struct pollfd pollfd = {
        .fd     = fd,
        .events = event,
    };
    struct timespec ts, *pts;
    int             r;

    nm_assert(fd >= 0);

    if (timeout_nsec < 0)
        pts = NULL;
    else {
        ts.tv_sec  = (time_t) (timeout_nsec / NM_UTILS_NSEC_PER_SEC);
        ts.tv_nsec = (long int) (timeout_nsec % NM_UTILS_NSEC_PER_SEC);
        pts        = &ts;
    }

    r = ppoll(&pollfd, 1, pts, NULL);
    if (r < 0)
        return -NM_ERRNO_NATIVE(errno);
    if (r == 0)
        return 0;

    nm_assert(r == 1);
    nm_assert(pollfd.revents > 0);

    if (pollfd.revents & POLLNVAL)
        return nm_assert_unreachable_val(-EBADF);

    return pollfd.revents;
}

/* taken from systemd's loop_read() */
ssize_t
nm_utils_fd_read_loop(int fd, void *buf, size_t nbytes, bool do_poll)
{
    uint8_t *p = buf;
    ssize_t  n = 0;

    g_return_val_if_fail(fd >= 0, -EINVAL);
    g_return_val_if_fail(buf, -EINVAL);

    /* If called with nbytes == 0, let's call read() at least
     * once, to validate the operation */

    if (nbytes > (size_t) SSIZE_MAX)
        return -EINVAL;

    do {
        ssize_t k;

        k = read(fd, p, nbytes);
        if (k < 0) {
            int errsv = errno;

            if (errsv == EINTR)
                continue;

            if (errsv == EAGAIN && do_poll) {
                /* We knowingly ignore any return value here,
                 * and expect that any error/EOF is reported
                 * via read() */

                (void) nm_utils_fd_wait_for_event(fd, POLLIN, -1);
                continue;
            }

            return n > 0 ? n : -NM_ERRNO_NATIVE(errsv);
        }

        if (k == 0)
            return n;

        g_assert((size_t) k <= nbytes);

        p += k;
        nbytes -= k;
        n += k;
    } while (nbytes > 0);

    return n;
}

/* taken from systemd's loop_read_exact() */
int
nm_utils_fd_read_loop_exact(int fd, void *buf, size_t nbytes, bool do_poll)
{
    ssize_t n;

    n = nm_utils_fd_read_loop(fd, buf, nbytes, do_poll);
    if (n < 0)
        return (int) n;
    if ((size_t) n != nbytes)
        return -EIO;

    return 0;
}

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

void
nm_utils_named_value_clear_with_g_free(NMUtilsNamedValue *val)
{
    if (val) {
        nm_clear_g_free(&val->name_mutable);
        nm_clear_g_free(&val->value_ptr);
    }
}

G_STATIC_ASSERT(G_STRUCT_OFFSET(NMUtilsNamedValue, name) == 0);

gssize
nm_utils_named_value_list_find(const NMUtilsNamedValue *arr,
                               gsize                    len,
                               const char              *name,
                               gboolean                 sorted)
{
    gsize i;

    nm_assert(name);

#if NM_MORE_ASSERTS > 5
    {
        for (i = 0; i < len; i++) {
            const NMUtilsNamedValue *v = &arr[i];

            nm_assert(v->name);
            if (sorted && i > 0)
                nm_assert(strcmp(arr[i - 1].name, v->name) < 0);
        }
    }

    nm_assert(!sorted || nm_utils_named_value_list_is_sorted(arr, len, FALSE, NULL, NULL));
#endif

    if (sorted) {
        return nm_array_find_bsearch(arr,
                                     len,
                                     sizeof(NMUtilsNamedValue),
                                     &name,
                                     nm_strcmp_p_with_data,
                                     NULL);
    }
    for (i = 0; i < len; i++) {
        if (nm_streq(arr[i].name, name))
            return i;
    }
    return ~((gssize) len);
}

gboolean
nm_utils_named_value_list_is_sorted(const NMUtilsNamedValue *arr,
                                    gsize                    len,
                                    gboolean                 accept_duplicates,
                                    GCompareDataFunc         compare_func,
                                    gpointer                 user_data)
{
    gsize i;
    int   c_limit;

    if (len == 0)
        return TRUE;

    g_return_val_if_fail(arr, FALSE);

    if (!compare_func)
        compare_func = nm_strcmp_p_with_data;

    c_limit = accept_duplicates ? 0 : -1;

    for (i = 1; i < len; i++) {
        int c;

        c = compare_func(&arr[i - 1], &arr[i], user_data);
        if (c > c_limit)
            return FALSE;
    }
    return TRUE;
}

void
nm_utils_named_value_list_sort(NMUtilsNamedValue *arr,
                               gsize              len,
                               GCompareDataFunc   compare_func,
                               gpointer           user_data)
{
    if (len == 0)
        return;

    g_return_if_fail(arr);

    if (len == 1)
        return;

    g_qsort_with_data(arr,
                      len,
                      sizeof(NMUtilsNamedValue),
                      compare_func ?: nm_strcmp_p_with_data,
                      user_data);
}

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

gpointer *
nm_utils_hash_keys_to_array(GHashTable      *hash,
                            GCompareDataFunc compare_func,
                            gpointer         user_data,
                            guint           *out_len)
{
    guint     len;
    gpointer *keys;

    /* by convention, we never return an empty array. In that
     * case, always %NULL. */
    if (!hash || g_hash_table_size(hash) == 0) {
        NM_SET_OUT(out_len, 0);
        return NULL;
    }

    keys = g_hash_table_get_keys_as_array(hash, &len);
    if (len > 1 && compare_func) {
        g_qsort_with_data(keys, len, sizeof(gpointer), compare_func, user_data);
    }
    NM_SET_OUT(out_len, len);
    return keys;
}

gpointer *
nm_utils_hash_values_to_array(GHashTable      *hash,
                              GCompareDataFunc compare_func,
                              gpointer         user_data,
                              guint           *out_len)
{
    GHashTableIter iter;
    gpointer       value;
    gpointer      *arr;
    guint          i, len;

    if (!hash || (len = g_hash_table_size(hash)) == 0u) {
        NM_SET_OUT(out_len, 0);
        return NULL;
    }

    arr = g_new(gpointer, ((gsize) len) + 1);
    i   = 0;
    g_hash_table_iter_init(&iter, hash);
    while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &value))
        arr[i++] = value;

    nm_assert(i == len);
    arr[len] = NULL;

    if (len > 1 && compare_func) {
        g_qsort_with_data(arr, len, sizeof(gpointer), compare_func, user_data);
    }

    NM_SET_OUT(out_len, len);
    return arr;
}

NMUtilsNamedValue *
nm_utils_hash_to_array_full(GHashTable         *hash,
                            guint              *out_len,
                            GCompareDataFunc    compare_func,
                            gpointer            user_data,
                            NMUtilsNamedValue  *provided_buffer,
                            guint               provided_buffer_len,
                            NMUtilsNamedValue **out_allocated_buffer)
{
    GHashTableIter     iter;
    NMUtilsNamedValue *values;
    guint              len;
    guint              i;

    nm_assert(provided_buffer_len == 0 || provided_buffer);
    nm_assert(!out_allocated_buffer || !*out_allocated_buffer);

    if (!hash || ((len = g_hash_table_size(hash)) == 0)) {
        NM_SET_OUT(out_len, 0);
        return NULL;
    }

    if (provided_buffer_len >= len + 1) {
        /* the buffer provided by the caller is large enough. Use it. */
        values = provided_buffer;
    } else {
        /* allocate a new buffer. */
        values = g_new(NMUtilsNamedValue, len + 1);
        NM_SET_OUT(out_allocated_buffer, values);
    }

    i = 0;
    g_hash_table_iter_init(&iter, hash);
    while (g_hash_table_iter_next(&iter, &values[i].name_ptr, &values[i].value_ptr))
        i++;
    nm_assert(i == len);
    values[i].name_ptr  = NULL;
    values[i].value_ptr = NULL;

    if (compare_func && len > 1)
        g_qsort_with_data(values, len, sizeof(NMUtilsNamedValue), compare_func, user_data);

    NM_SET_OUT(out_len, len);
    return values;
}

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

/**
 * nm_utils_hashtable_equal:
 * @a: one #GHashTable
 * @b: other #GHashTable
 * @treat_null_as_empty: if %TRUE, when either @a or @b is %NULL, it is
 *   treated like an empty hash. It means, a %NULL hash will compare equal
 *   to an empty hash.
 * @equal_func: the equality function, for comparing the values.
 *   If %NULL, the values are not compared. In that case, the function
 *   only checks, if both dictionaries have the same keys -- according
 *   to @b's key equality function.
 *   Note that the values of @a will be passed as first argument
 *   to @equal_func.
 *
 * Compares two hash tables, whether they have equal content.
 * This only makes sense, if @a and @b have the same key types and
 * the same key compare-function.
 *
 * Returns: %TRUE, if both dictionaries have the same content.
 */
gboolean
nm_utils_hashtable_equal(const GHashTable *a,
                         const GHashTable *b,
                         gboolean          treat_null_as_empty,
                         GEqualFunc        equal_func)
{
    guint          n;
    GHashTableIter iter;
    gconstpointer  key, v_a, v_b;

    if (a == b)
        return TRUE;
    if (!treat_null_as_empty) {
        if (!a || !b)
            return FALSE;
    }

    n = a ? g_hash_table_size((GHashTable *) a) : 0;
    if (n != (b ? g_hash_table_size((GHashTable *) b) : 0))
        return FALSE;

    if (n > 0) {
        g_hash_table_iter_init(&iter, (GHashTable *) a);
        while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &v_a)) {
            if (!g_hash_table_lookup_extended((GHashTable *) b, key, NULL, (gpointer *) &v_b))
                return FALSE;
            if (equal_func && !equal_func(v_a, v_b))
                return FALSE;
        }
    }

    return TRUE;
}

static gboolean
_utils_hashtable_equal(GHashTable      *hash_a,
                       GHashTable      *hash_b,
                       GCompareDataFunc cmp_values,
                       gpointer         user_data)
{
    GHashTableIter h;
    gpointer       a_key;
    gpointer       a_val;
    gpointer       b_val;

    nm_assert(hash_a);
    nm_assert(hash_b);
    nm_assert(hash_a != hash_b);
    nm_assert(g_hash_table_size(hash_a) == g_hash_table_size(hash_b));

    /* We rely on both hashes to have the same hash/equal function. Otherwise, we would have to iterate
     * both hashes and check whether all keys/values are present in the respective other hash (which
     * would be O(n^2), since we couldn't use the plain lookup function. That is not a useful thing
     * for this function. */

    g_hash_table_iter_init(&h, hash_a);
    while (g_hash_table_iter_next(&h, &a_key, &a_val)) {
        if (!g_hash_table_lookup_extended(hash_b, a_key, NULL, &b_val))
            return FALSE;

        if (!cmp_values) {
            /* we accept %NULL compare function to indicate that we don't care about the key. */
            continue;
        }

        if (cmp_values(a_val, b_val, user_data) != 0)
            return FALSE;
    }

    return TRUE;
}

/**
 * nm_utils_hashtable_cmp_equal:
 * @a: (nullable): the hash table or %NULL
 * @b: (nullable): the other hash table or %NULL
 * @cmp_values: (nullable): if %NULL, only the keys
 *   will be compared. Otherwise, this function is used to
 *   check whether all keys are equal.
 * @user_data: the argument for @cmp_values.
 *
 * It is required that both @a and @b have the same hash and equals
 * function.
 *
 * Returns: %TRUE, if both keys have the same keys and (if
 *   @cmp_values is given) all values are the same.
 */
gboolean
nm_utils_hashtable_cmp_equal(const GHashTable *a,
                             const GHashTable *b,
                             GCompareDataFunc  cmp_values,
                             gpointer          user_data)
{
    GHashTable *hash_a = (GHashTable *) a;
    GHashTable *hash_b = (GHashTable *) b;
    gboolean    same;
    guint       size;

    if (hash_a == hash_b)
        return TRUE;

    if (!hash_a || !hash_b)
        return FALSE;

    size = g_hash_table_size(hash_a);
    if (size != g_hash_table_size(hash_b))
        return FALSE;

    if (size == 0)
        return TRUE;

    same = _utils_hashtable_equal(hash_a, hash_b, cmp_values, user_data);

#if NM_MORE_ASSERTS > 5
    nm_assert(same == _utils_hashtable_equal(hash_b, hash_a, cmp_values, user_data));
#endif

    return same;
}

typedef struct {
    gpointer key;
    gpointer val;
} HashTableCmpData;

typedef struct {
    GCompareDataFunc cmp_keys;
    gpointer         user_data;
} HashTableUserData;

static int
_hashtable_cmp_func(gconstpointer a, gconstpointer b, gpointer user_data)
{
    const HashTableUserData *d   = user_data;
    const HashTableCmpData  *d_a = *((const HashTableCmpData *const *) a);
    const HashTableCmpData  *d_b = *((const HashTableCmpData *const *) b);

    NM_CMP_RETURN(d->cmp_keys(d_a, d_b, d->user_data));
    return 0;
}

/**
 * nm_utils_hashtable_cmp:
 * @a: (nullable): the hash to compare. May be %NULL.
 * @b: (nullable): the other hash to compare. May be %NULL.
 * @do_fast_precheck: if %TRUE, assume that the hashes are equal
 *   and that it is worth calling nm_utils_hashtable_cmp_equal() first.
 *   That requires, that both hashes have the same equals function
 *   which is compatible with the @cmp_keys function.
 * @cmp_keys: the compare function for keys. Usually, the hash/equal function
 *   of both hashes corresponds to this function. If you set @do_fast_precheck
 *   to false, then this is not a requirement.
 * @cmp_values: (nullable): if %NULL, only the keys are compared.
 *   Otherwise, the values must are also compared with this function.
 *
 * Both hashes must have keys/values of the same domain, so that
 * they can be effectively compared with @cmp_keys and @cmp_values.
 *
 * %NULL hashes compare equal to %NULL, but not to empty hashes.
 *
 * Returns: 0 if both hashes are equal, or -1 or 1 if one of the hashes
 *   sorts before/after.
 */
int
nm_utils_hashtable_cmp(const GHashTable *a,
                       const GHashTable *b,
                       gboolean          do_fast_precheck,
                       GCompareDataFunc  cmp_keys,
                       GCompareDataFunc  cmp_values,
                       gpointer          user_data)
{
    GHashTable               *hash_a         = (GHashTable *) a;
    GHashTable               *hash_b         = (GHashTable *) b;
    gs_free HashTableCmpData *cmp_array_free = NULL;
    HashTableCmpData         *cmp_array_a;
    HashTableCmpData         *cmp_array_b;
    GHashTableIter            h;
    gpointer                  i_key;
    gpointer                  i_val;
    gsize                     size2;
    guint                     size;
    guint                     i;

    nm_assert(cmp_keys);

    NM_CMP_SELF(hash_a, hash_b);

    size = g_hash_table_size(hash_a);

    NM_CMP_DIRECT(size, g_hash_table_size(hash_b));

    if (size == 0)
        return 0;

    if (do_fast_precheck) {
        gboolean same;

        /* we expect that the hashes are equal and the caller ensures us that they
         * use the same hash/equal functions. Do a fast path check first...
         *
         * It's unclear whether this is worth it. The full comparison is O(n*ln(n)),
         * while the fast check (using the hash lookup) is O(n). But then, the pre-check
         * makes additional requirements on the hash's hash/equal functions -- the
         * full comparison does not make such requirements. */
        same = _utils_hashtable_equal(hash_a, hash_b, cmp_values, user_data);
#if NM_MORE_ASSERTS > 5
        nm_assert(same == _utils_hashtable_equal(hash_b, hash_a, cmp_values, user_data));
#endif
        if (same)
            return 0;
    }

    size2 = ((gsize) size) * 2u;
    if (size2 > 600u / sizeof(HashTableCmpData)) {
        cmp_array_free = g_new(HashTableCmpData, size2);
        cmp_array_a    = cmp_array_free;
    } else
        cmp_array_a = g_newa(HashTableCmpData, size2);
    cmp_array_b = &cmp_array_a[size];

    i = 0;
    g_hash_table_iter_init(&h, hash_a);
    while (g_hash_table_iter_next(&h, &i_key, &i_val)) {
        nm_assert(i < size);
        cmp_array_a[i++] = (HashTableCmpData) {
            .key = i_key,
            .val = i_val,
        };
    }
    nm_assert(i == size);

    i = 0;
    g_hash_table_iter_init(&h, hash_b);
    while (g_hash_table_iter_next(&h, &i_key, &i_val)) {
        nm_assert(i < size);
        cmp_array_b[i++] = (HashTableCmpData) {
            .key = i_key,
            .val = i_val,
        };
    }
    nm_assert(i == size);

    g_qsort_with_data(cmp_array_a,
                      size,
                      sizeof(HashTableCmpData),
                      _hashtable_cmp_func,
                      &((HashTableUserData) {
                          .cmp_keys  = cmp_keys,
                          .user_data = user_data,
                      }));

    g_qsort_with_data(cmp_array_b,
                      size,
                      sizeof(HashTableCmpData),
                      _hashtable_cmp_func,
                      &((HashTableUserData) {
                          .cmp_keys  = cmp_keys,
                          .user_data = user_data,
                      }));

    for (i = 0; i < size; i++) {
        NM_CMP_RETURN(cmp_keys(cmp_array_a[i].key, cmp_array_b[i].key, user_data));
    }

    if (cmp_values) {
        for (i = 0; i < size; i++) {
            NM_CMP_RETURN(cmp_values(cmp_array_a[i].val, cmp_array_b[i].val, user_data));
        }
    }

    /* the fast-precheck should have already told that the arrays are equal. */
    nm_assert(!do_fast_precheck);

    return 0;
}

char **
nm_strv_make_deep_copied(const char **strv)
{
    gsize i;

    /* it takes a strv list, and copies each
     * strings. Note that this updates @strv *in-place*
     * and returns it. */

    if (!strv)
        return NULL;
    for (i = 0; strv[i]; i++)
        strv[i] = g_strdup(strv[i]);

    return (char **) strv;
}

char **
nm_strv_make_deep_copied_n(const char **strv, gsize len)
{
    gsize i;

    /* it takes a strv array with len elements, and copies each
     * strings. Note that this updates @strv *in-place*
     * and returns it. */

    if (!strv)
        return NULL;
    for (i = 0; i < len; i++)
        strv[i] = g_strdup(strv[i]);

    return (char **) strv;
}

/**
 * @strv: the strv array to copy. It may be %NULL if @len
 *   is negative or zero (in which case %NULL will be returned).
 * @len: the length of strings in @str. If negative, strv is assumed
 *   to be a NULL terminated array.
 * @deep_copied: if %TRUE, clones the individual strings. In that case,
 *   the returned array must be freed with g_strfreev(). Otherwise, the
 *   strings themself are not copied. You must take care of who owns the
 *   strings yourself.
 * @preserved_empty: affects how to handle if the strv array is empty (length 0).
 *   If TRUE, results in a non-NULL, empty, allocated strv array. If FALSE,
 *   returns NULL instead of an empty strv array.
 *
 * Like g_strdupv(), with two differences:
 *
 * - accepts a @len parameter for non-null terminated strv array.
 *
 * - this never returns an empty strv array, but always %NULL if
 *   there are no strings.
 *
 * Note that if @len is non-negative, then it still must not
 * contain any %NULL pointers within the first @len elements.
 * Otherwise, you would leak elements if you try to free the
 * array with g_strfreev(). Allowing that would be error prone.
 *
 * Returns: (transfer full): a clone of the strv array. Always
 *   %NULL terminated. Depending on @deep_copied, the strings are
 *   cloned or not.
 */
char **
_nm_strv_dup_full(const char *const *strv,
                  gssize             len,
                  gboolean           deep_copied,
                  gboolean           preserve_empty)
{
    gsize  i, l;
    char **v;

    if (len < 0)
        l = NM_PTRARRAY_LEN(strv);
    else
        l = len;

    if (l == 0 && !preserve_empty) {
        /* An empty strv array is not returned (as requested by
         * !preserved_empty). Instead, return NULL. */
        return NULL;
    }

    nm_assert(l < G_MAXSIZE);

    v = g_new(char *, l + 1u);
    for (i = 0; i < l; i++) {
        if (G_UNLIKELY(!strv[i])) {
            /* NULL strings are not allowed. Clear the remainder of the array
             * and return it (with assertion failure). */
            l++;
            for (; i < l; i++)
                v[i] = NULL;
            g_return_val_if_reached(v);
        }

        if (deep_copied)
            v[i] = g_strdup(strv[i]);
        else
            v[i] = (char *) strv[i];
    }
    v[l] = NULL;
    return v;
}

const char **
_nm_strv_dup_packed(const char *const *strv, gssize len)

{
    gs_free gsize *str_len_free = NULL;
    gsize         *str_len;
    const char   **result;
    gsize          mem_len;
    gsize          pre_len;
    gsize          len2;
    char          *sbuf;
    gsize          i;

    nm_assert(len >= -1);

    if (G_LIKELY(len < 0)) {
        if (!strv || !strv[0]) {
            /* This function never returns an empty strv array. If you need that, handle it
             * yourself. */
            return NULL;
        }
        len2 = NM_PTRARRAY_LEN(strv);
    } else {
        if (len == 0)
            return NULL;
        len2 = len;
    }

    if (len2 > 300u / sizeof(gsize)) {
        str_len_free = g_new(gsize, len2);
        str_len      = str_len_free;
    } else
        str_len = g_newa(gsize, len2);

    mem_len = 0;
    for (i = 0; i < len2; i++) {
        gsize l;

        if (G_LIKELY(strv[i]))
            l = strlen(strv[i]) + 1u;
        else
            l = 0;
        str_len[i] = l;
        mem_len += l;
    }

    pre_len = sizeof(const char *) * (len2 + 1u);

    result = g_malloc(pre_len + mem_len);
    sbuf   = &(((char *) result)[pre_len]);
    for (i = 0; i < len2; i++) {
        gsize l;

        if (G_UNLIKELY(!strv[i])) {
            /* Technically there is no problem with accepting NULL strings. But that
             * does not really result in a strv array, and likely this only happens due
             * to a bug. We want to catch such bugs by asserting.
             *
             * We clear the remainder of the buffer and fail with an assertion. */
            len2++;
            for (; i < len2; i++)
                result[i] = NULL;
            g_return_val_if_reached(result);
        }

        result[i] = sbuf;

        l = str_len[i];
        memcpy(sbuf, strv[i], l);
        sbuf += l;
    }
    result[i] = NULL;
    nm_assert(i == len2);
    nm_assert(sbuf == (&((const char *) result)[pre_len]) + mem_len);

    return result;
}

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

gssize
nm_utils_ptrarray_find_first(gconstpointer *list, gssize len, gconstpointer needle)
{
    gssize i;

    if (len == 0)
        return -1;

    if (len > 0) {
        g_return_val_if_fail(list, -1);
        for (i = 0; i < len; i++) {
            if (list[i] == needle)
                return i;
        }
    } else {
        g_return_val_if_fail(needle, -1);
        for (i = 0; list && list[i]; i++) {
            if (list[i] == needle)
                return i;
        }
    }
    return -1;
}

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

gboolean
nm_utils_ptrarray_is_sorted(gconstpointer   *list,
                            gsize            len,
                            gboolean         require_strict,
                            GCompareDataFunc cmpfcn,
                            gpointer         user_data)
{
    gsize i;

    for (i = 1; i < len; i++) {
        int c;

        c = cmpfcn(list[i - 1], list[i], user_data);
        if (G_LIKELY(c < 0))
            continue;

        if (c > 0 || require_strict)
            return FALSE;
    }
    return TRUE;
}

gssize
nm_ptrarray_find_bsearch(gconstpointer   *list,
                         gsize            len,
                         gconstpointer    needle,
                         GCompareDataFunc cmpfcn,
                         gpointer         user_data)
{
    gssize imax;
    gssize imid;
    gssize imin;
    int    cmp;

    nm_assert(list || len == 0);
    nm_assert(cmpfcn);

    imin = 0;
    if (len > 0) {
        imax = len - 1;

        while (imin <= imax) {
            imid = imin + (imax - imin) / 2;

            cmp = cmpfcn(list[imid], needle, user_data);
            if (cmp == 0)
                return imid;

            if (cmp < 0)
                imin = imid + 1;
            else
                imax = imid - 1;
        }
    }

    /* return the inverse of @imin. This is a negative number, but
     * also is ~imin the position where the value should be inserted. */
    imin = ~imin;
    return imin;
}

gssize
nm_ptrarray_find_bsearch_range(gconstpointer   *list,
                               gsize            len,
                               gconstpointer    needle,
                               GCompareDataFunc cmpfcn,
                               gpointer         user_data,
                               gssize          *out_idx_first,
                               gssize          *out_idx_last)
{
    gssize imax;
    gssize imid;
    gssize imin;
    gssize i2max;
    gssize i2mid;
    gssize i2min;
    int    cmp;

    nm_assert(list || len == 0);
    nm_assert(cmpfcn);

    imin = 0;
    if (len > 0) {
        imax = len - 1;

        while (imin <= imax) {
            imid = imin + (imax - imin) / 2;

            cmp = cmpfcn(list[imid], needle, user_data);
            if (cmp == 0) {
                /* we found a matching entry at index imid.
                 *
                 * Does the caller request the first/last index as well (in case that
                 * there are multiple entries which compare equal). */

                if (out_idx_first) {
                    i2min = imin;
                    i2max = imid + 1;
                    while (i2min <= i2max) {
                        i2mid = i2min + (i2max - i2min) / 2;

                        cmp = cmpfcn(list[i2mid], needle, user_data);
                        if (cmp == 0)
                            i2max = i2mid - 1;
                        else {
                            nm_assert(cmp < 0);
                            i2min = i2mid + 1;
                        }
                    }
                    *out_idx_first = i2min;
                }
                if (out_idx_last) {
                    i2min = imid + 1;
                    i2max = imax;
                    while (i2min <= i2max) {
                        i2mid = i2min + (i2max - i2min) / 2;

                        cmp = cmpfcn(list[i2mid], needle, user_data);
                        if (cmp == 0)
                            i2min = i2mid + 1;
                        else {
                            nm_assert(cmp > 0);
                            i2max = i2mid - 1;
                        }
                    }
                    *out_idx_last = i2min - 1;
                }
                return imid;
            }

            if (cmp < 0)
                imin = imid + 1;
            else
                imax = imid - 1;
        }
    }

    /* return the inverse of @imin. This is a negative number, but
     * also is ~imin the position where the value should be inserted. */
    imin = ~imin;
    NM_SET_OUT(out_idx_first, imin);
    NM_SET_OUT(out_idx_last, imin);
    return imin;
}

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

/**
 * nm_array_find_bsearch:
 * @list: the list to search. It must be sorted according to @cmpfcn ordering.
 * @len: the number of elements in @list
 * @elem_size: the size in bytes of each element in the list
 * @needle: the value that is searched
 * @cmpfcn: the compare function. The elements @list are passed as first
 *   argument to @cmpfcn, while @needle is passed as second. Usually, the
 *   needle is the same data type as inside the list, however, that is
 *   not necessary, as long as @cmpfcn takes care to cast the two arguments
 *   accordingly.
 * @user_data: optional argument passed to @cmpfcn
 *
 * Performs binary search for @needle in @list. On success, returns the
 * (non-negative) index where the compare function found the searched element.
 * On success, it returns a negative value. Note that the return negative value
 * is the bitwise inverse of the position where the element should be inserted.
 *
 * If the list contains multiple matching elements, an arbitrary index is
 * returned.
 *
 * Returns: the index to the element in the list, or the (negative, bitwise inverted)
 *   position where it should be.
 */
gssize
nm_array_find_bsearch(gconstpointer    list,
                      gsize            len,
                      gsize            elem_size,
                      gconstpointer    needle,
                      GCompareDataFunc cmpfcn,
                      gpointer         user_data)
{
    return nm_array_find_bsearch_inline(list, len, elem_size, needle, cmpfcn, user_data);
}

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

/**
 * nm_utils_get_start_time_for_pid:
 * @pid: the process identifier
 * @out_state: return the state character, like R, S, Z. See `man 5 proc`.
 * @out_ppid: parent process id
 *
 * Originally copied from polkit source (src/polkit/polkitunixprocess.c)
 * and adjusted.
 *
 * Returns: the timestamp when the process started (by parsing /proc/$PID/stat).
 * If an error occurs (e.g. the process does not exist), 0 is returned.
 *
 * The returned start time counts since boot, in the unit HZ (with HZ usually being (1/100) seconds)
 **/
guint64
nm_utils_get_start_time_for_pid(pid_t pid, char *out_state, pid_t *out_ppid)
{
    guint64              start_time;
    char                 filename[256];
    gs_free char        *contents = NULL;
    size_t               length;
    gs_free const char **tokens = NULL;
    char                *p;
    char                 state = ' ';
    gint64               ppid  = 0;

    start_time = 0;
    contents   = NULL;

    g_return_val_if_fail(pid > 0, 0);

    G_STATIC_ASSERT_EXPR(sizeof(GPid) >= sizeof(pid_t));

    nm_sprintf_buf(filename, "/proc/%" G_PID_FORMAT "/stat", (GPid) pid);

    if (!g_file_get_contents(filename, &contents, &length, NULL))
        goto fail;

    /* start time is the token at index 19 after the '(process name)' entry - since only this
     * field can contain the ')' character, search backwards for this to avoid malicious
     * processes trying to fool us
     */
    p = strrchr(contents, ')');
    if (!p)
        goto fail;
    p += 2; /* skip ') ' */
    if (p - contents >= (int) length)
        goto fail;

    state = p[0];

    tokens = nm_strsplit_set(p, " ");

    if (NM_PTRARRAY_LEN(tokens) < 20)
        goto fail;

    if (out_ppid) {
        ppid = _nm_utils_ascii_str_to_int64(tokens[1], 10, 1, G_MAXINT, 0);
        if (ppid == 0)
            goto fail;
    }

    start_time = _nm_utils_ascii_str_to_int64(tokens[19], 10, 1, G_MAXINT64, 0);
    if (start_time == 0)
        goto fail;

    NM_SET_OUT(out_state, state);
    NM_SET_OUT(out_ppid, ppid);
    return start_time;

fail:
    NM_SET_OUT(out_state, ' ');
    NM_SET_OUT(out_ppid, 0);
    return 0;
}

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

/**
 * _nm_strv_sort:
 * @strv: pointer containing strings that will be sorted
 *   in-place, %NULL is allowed, unless @len indicates
 *   that there are more elements.
 * @len: the number of elements in strv. If negative,
 *   strv must be a NULL terminated array and the length
 *   will be calculated first. If @len is a positive
 *   number, @strv is allowed to contain %NULL strings
 *   too.
 *
 * Ascending sort of the array @strv inplace, using plain strcmp() string
 * comparison.
 */
void
_nm_strv_sort(const char **strv, gssize len)
{
    GCompareDataFunc cmp;
    gsize            l;

    if (len < 0) {
        l   = NM_PTRARRAY_LEN(strv);
        cmp = nm_strcmp_p_with_data;
    } else {
        l   = len;
        cmp = nm_strcmp0_p_with_data;
    }

    if (l <= 1)
        return;

    nm_assert(l <= (gsize) G_MAXINT);

    g_qsort_with_data(strv, l, sizeof(const char *), cmp, NULL);
}

/**
 * _nm_strv_cmp_n:
 * @strv1: a string array
 * @len1: the length of @strv1, or -1 for NULL terminated array.
 * @strv2: a string array
 * @len2: the length of @strv2, or -1 for NULL terminated array.
 *
 * Note that
 *   - len == -1 && strv == NULL
 * is treated like a %NULL argument and compares differently from
 * other arrays.
 *
 * Note that an empty array can be represented as
 *   - len == -1 &&  strv && !strv[0]
 *   - len ==  0 && !strv
 *   - len ==  0 &&  strv
 * These 3 forms all compare equal.
 * It also means, if length is 0, then it is permissible for strv to be %NULL.
 *
 * The strv arrays may contain %NULL strings (if len is positive).
 *
 * Returns: 0 if the arrays are equal (using strcmp).
 **/
int
_nm_strv_cmp_n(const char *const *strv1, gssize len1, const char *const *strv2, gssize len2)
{
    gsize n, n2;

    if (len1 < 0) {
        if (!strv1)
            return (len2 < 0 && !strv2) ? 0 : -1;
        n = NM_PTRARRAY_LEN(strv1);
    } else
        n = len1;

    if (len2 < 0) {
        if (!strv2)
            return 1;
        n2 = NM_PTRARRAY_LEN(strv2);
    } else
        n2 = len2;

    NM_CMP_DIRECT(n, n2);
    for (; n > 0; n--, strv1++, strv2++)
        NM_CMP_DIRECT_STRCMP0(*strv1, *strv2);
    return 0;
}

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

/**
 * nm_utils_g_slist_find_str:
 * @list: the #GSList with NUL terminated strings to search
 * @needle: the needle string to look for.
 *
 * Search the list for @needle and return the first found match
 * (or %NULL if not found). Uses strcmp() for finding the first matching
 * element.
 *
 * Returns: the #GSList element with @needle as string value or
 *   %NULL if not found.
 */
GSList *
nm_utils_g_slist_find_str(const GSList *list, const char *needle)
{
    nm_assert(needle);

    for (; list; list = list->next) {
        nm_assert(list->data);
        if (nm_streq(list->data, needle))
            return (GSList *) list;
    }
    return NULL;
}

/**
 * nm_utils_g_slist_strlist_cmp:
 * @a: the left #GSList of strings
 * @b: the right #GSList of strings to compare.
 *
 * Compares two string lists. The data elements are compared with
 * strcmp(), allowing %NULL elements.
 *
 * Returns: 0, 1, or -1, depending on how the lists compare.
 */
int
nm_utils_g_slist_strlist_cmp(const GSList *a, const GSList *b)
{
    while (TRUE) {
        if (!a)
            return !b ? 0 : -1;
        if (!b)
            return 1;
        NM_CMP_DIRECT_STRCMP0(a->data, b->data);
        a = a->next;
        b = b->next;
    }
}

char *
nm_utils_g_slist_strlist_join(const GSList *a, const char *separator)
{
    GString *str = NULL;

    if (!a)
        return NULL;

    for (; a; a = a->next) {
        if (!str)
            str = g_string_new(NULL);
        else
            g_string_append(str, separator);
        g_string_append(str, a->data);
    }
    return g_string_free(str, FALSE);
}

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

NMUtilsUserData *
_nm_utils_user_data_pack(int nargs, gconstpointer *args)
{
    int       i;
    gpointer *data;

    nm_assert(nargs > 0);
    nm_assert(args);

    data = g_slice_alloc(((gsize) nargs) * sizeof(gconstpointer));
    for (i = 0; i < nargs; i++)
        data[i] = (gpointer) args[i];
    return (NMUtilsUserData *) data;
}

void
_nm_utils_user_data_unpack(NMUtilsUserData *user_data, int nargs, ...)
{
    gpointer *data = (gpointer *) user_data;
    va_list   ap;
    int       i;

    nm_assert(data);
    nm_assert(nargs > 0);

    va_start(ap, nargs);
    for (i = 0; i < nargs; i++) {
        gpointer *dst;

        dst = va_arg(ap, gpointer *);
        nm_assert(dst);

        *dst = data[i];
    }
    va_end(ap);

    g_slice_free1(((gsize) nargs) * sizeof(gconstpointer), data);
}

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

typedef struct {
    gpointer                    callback_user_data;
    GCancellable               *cancellable;
    GSource                    *source;
    NMUtilsInvokeOnIdleCallback callback;
    gulong                      cancelled_id;
} InvokeOnIdleData;

static void
_nm_utils_invoke_on_idle_complete(InvokeOnIdleData *data)
{
    nm_clear_g_signal_handler(data->cancellable, &data->cancelled_id);

    data->callback(data->callback_user_data, data->cancellable);

    nm_g_object_unref(data->cancellable);
    g_source_destroy(data->source);
    nm_g_slice_free(data);
}

static gboolean
_nm_utils_invoke_on_idle_cb_idle(gpointer user_data)
{
    _nm_utils_invoke_on_idle_complete(user_data);
    return G_SOURCE_REMOVE;
}

static void
_nm_utils_invoke_on_idle_cb_cancelled(GCancellable *cancellable, InvokeOnIdleData *data)
{
    if (data->cancelled_id == 0) {
        /* this can only happen during _nm_utils_invoke_on_idle_start(). Don't do anything,
         * we still schedule an idle action. */
        return;
    }

    /* On cancellation, we invoke the callback synchronously.
     *
     * Note that this is not thread-safe, meaning: you can only cancel the cancellable
     * while not iterating the GMainContext (that has the idle/timeout source attached).
     * Making this thread safe would be complicated, and it's simply not used by our
     * callers. */
    _nm_utils_invoke_on_idle_complete(data);
}

static void
_nm_utils_invoke_on_idle_start(gboolean                    use_timeout,
                               guint                       timeout_msec,
                               GCancellable               *cancellable,
                               NMUtilsInvokeOnIdleCallback callback,
                               gpointer                    callback_user_data)
{
    InvokeOnIdleData *data;
    GSource          *source;

    g_return_if_fail(callback);

    data  = g_slice_new(InvokeOnIdleData);
    *data = (InvokeOnIdleData) {
        .callback           = callback,
        .callback_user_data = callback_user_data,
        .cancellable        = nm_g_object_ref(cancellable),
        .cancelled_id       = 0,
    };

    if (cancellable) {
        gulong cancelled_id;

        cancelled_id = g_cancellable_connect(cancellable,
                                             G_CALLBACK(_nm_utils_invoke_on_idle_cb_cancelled),
                                             data,
                                             NULL);
        if (cancelled_id == 0) {
            /* the cancellable is already cancelled. We still schedule an idle action. */
            use_timeout = FALSE;
        } else
            data->cancelled_id = cancelled_id;
    }

    if (use_timeout) {
        /* We use G_PRIORITY_DEFAULT_IDLE both for the with/without timeout
         * case. The reason is not strong, but it seems right that the caller
         * requests a lower priority than G_PRIORITY_DEFAULT. That is unlike
         * what g_timeout_add() would do. */
        source = nm_g_timeout_source_new(timeout_msec,
                                         G_PRIORITY_DEFAULT_IDLE,
                                         _nm_utils_invoke_on_idle_cb_idle,
                                         data,
                                         NULL);
    } else {
        source = nm_g_idle_source_new(G_PRIORITY_DEFAULT_IDLE,
                                      _nm_utils_invoke_on_idle_cb_idle,
                                      data,
                                      NULL);
    }

    /* use the current thread default context. */
    g_source_attach(source, g_main_context_get_thread_default());

    data->source = source;
}

void
nm_utils_invoke_on_idle(GCancellable               *cancellable,
                        NMUtilsInvokeOnIdleCallback callback,
                        gpointer                    callback_user_data)
{
    _nm_utils_invoke_on_idle_start(FALSE, 0, cancellable, callback, callback_user_data);
}

void
nm_utils_invoke_on_timeout(guint                       timeout_msec,
                           GCancellable               *cancellable,
                           NMUtilsInvokeOnIdleCallback callback,
                           gpointer                    callback_user_data)
{
    _nm_utils_invoke_on_idle_start(TRUE, timeout_msec, cancellable, callback, callback_user_data);
}

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

int
nm_utils_getpagesize(void)
{
    static volatile int val = 0;
    long                l;
    int                 v;

    v = g_atomic_int_get(&val);

    if (G_UNLIKELY(v == 0)) {
        l = sysconf(_SC_PAGESIZE);

        g_return_val_if_fail(l > 0 && l < G_MAXINT, 4 * 1024);

        v = (int) l;
        if (!g_atomic_int_compare_and_exchange(&val, 0, v)) {
            v = g_atomic_int_get(&val);
            g_return_val_if_fail(v > 0, 4 * 1024);
        }
    }

    nm_assert(v > 0);
#if NM_MORE_ASSERTS > 5
    nm_assert(v == getpagesize());
    nm_assert(v == sysconf(_SC_PAGESIZE));
#endif

    return v;
}

gboolean
nm_utils_memeqzero(gconstpointer data, gsize length)
{
    const unsigned char *p = data;
    int                  len;

    /* Taken from https://github.com/rustyrussell/ccan/blob/9d2d2c49f053018724bcc6e37029da10b7c3d60d/ccan/mem/mem.c#L92,
     * CC-0 licensed. */

    /* Check first 16 bytes manually */
    for (len = 0; len < 16; len++) {
        if (!length)
            return TRUE;
        if (*p)
            return FALSE;
        p++;
        length--;
    }

    /* Now we know that's zero, memcmp with self. */
    return memcmp(data, p, length) == 0;
}

/**
 * nm_utils_bin2hexstr_fuller:
 * @addr: pointer of @length bytes. If @length is zero, this may
 *   also be %NULL.
 * @length: number of bytes in @addr. May also be zero, in which
 *   case this will return an empty string.
 * @delimiter: either '\0', otherwise the output string will have the
 *   given delimiter character between each two hex numbers.
 * @upper_case: if TRUE, use upper case ASCII characters for hex.
 * @with_leading_zero: if TRUE, then the hex values from 0 to 0xf
 *   are written as "00" to "0f", respectively. Otherwise, the leading
 *   zero is dropped. With @with_leading_zero set to FALSE, the resulting
 *   string may be shorter than expected. @delimiter must be set
 *   if @with_leading_zero is FALSE.
 * @out: if %NULL, the function will allocate a new buffer of
 *   either (@length*2+1) or MAX(1, (@length*3)) bytes, depending on whether
 *   a @delimiter is specified. In that case, the allocated buffer will
 *   be returned and must be freed by the caller.
 *   If not %NULL, the buffer must already be preallocated and contain
 *   at least (@length*2+1) or MAX(1, (@length*3)) bytes, depending on the delimiter.
 *   If @length is zero, then of course at least one byte will be allocated
 *   or @out (if given) must contain at least room for the trailing NUL byte.
 *
 * Returns: the binary value converted to a hex string. If @out is given,
 *   this always returns @out. If @out is %NULL, a newly allocated string
 *   is returned. This never returns %NULL, for buffers of length zero
 *   an empty string is returned.
 */
char *
nm_utils_bin2hexstr_fuller(gconstpointer addr,
                           gsize         length,
                           char          delimiter,
                           gboolean      upper_case,
                           gboolean      with_leading_zero,
                           char         *out)
{
    const guint8 *in     = addr;
    const char   *LOOKUP = upper_case ? "0123456789ABCDEF" : "0123456789abcdef";
    char         *out0;

    nm_assert(with_leading_zero || delimiter != '\0');

    /* @out must contain at least (MAX(1, @length*3)) bytes if @delimiter is set,
     * otherwise, @length*2+1. */

    if (!out)
        out = g_new(char, length == 0 ? 1u : (delimiter == '\0' ? length * 2u + 1u : length * 3u));

    out0 = out;

    if (length > 0) {
        nm_assert(in);
        for (;;) {
            const guint8 v = *in++;
            guint8       v_hi;

            v_hi = (v >> 4);
            if (v_hi != 0 || with_leading_zero) {
                nm_assert(v_hi < 16);
                *out++ = LOOKUP[v_hi];
            }
            *out++ = LOOKUP[v & 0x0F];
            length--;
            if (length == 0)
                break;
            if (delimiter != '\0')
                *out++ = delimiter;
        }
    }

    *out = '\0';
    return out0;
}

char *
_nm_utils_bin2hexstr(gconstpointer src, gsize len, int final_len)
{
    char *result;
    gsize buflen = (len * 2) + 1;

    nm_assert(src);
    nm_assert(len > 0 && (buflen - 1) / 2 == len);
    nm_assert(final_len < 0 || (gsize) final_len < buflen);

    result = g_malloc(buflen);

    nm_utils_bin2hexstr_full(src, len, '\0', FALSE, result);

    /* Cut converted key off at the correct length for this cipher type */
    if (final_len >= 0 && (gsize) final_len < buflen)
        result[final_len] = '\0';

    return result;
}

guint8 *
nm_utils_hexstr2bin_full(const char *hexstr,
                         gboolean    allow_0x_prefix,
                         gboolean    delimiter_required,
                         gboolean    hexdigit_pairs_required,
                         const char *delimiter_candidates,
                         gsize       required_len,
                         guint8     *buffer,
                         gsize       buffer_len,
                         gsize      *out_len)
{
    const char *in            = hexstr;
    guint8     *out           = buffer;
    gboolean    delimiter_has = TRUE;
    guint8      delimiter     = '\0';
    gsize       len;

    nm_assert(hexstr);
    nm_assert(buffer);
    nm_assert(required_len > 0 || out_len);

    if (allow_0x_prefix && in[0] == '0' && in[1] == 'x')
        in += 2;

    while (TRUE) {
        const guint8 d1 = in[0];
        guint8       d2;
        int          i1, i2;

        i1 = nm_utils_hexchar_to_int(d1);
        if (i1 < 0)
            goto fail;

        /* If there's no leading zero (ie "aa:b:cc") then fake it */
        d2 = in[1];
        if (d2 && (i2 = nm_utils_hexchar_to_int(d2)) >= 0) {
            *out++ = (i1 << 4) + i2;
            d2     = in[2];
            if (!d2)
                break;
            in += 2;
        } else {
            /* Fake leading zero */
            *out++ = i1;
            if (!d2) {
                if (!delimiter_has || hexdigit_pairs_required) {
                    /* when using no delimiter, there must be pairs of hex chars */
                    goto fail;
                }
                break;
            } else if (hexdigit_pairs_required)
                goto fail;
            in += 1;
        }

        if (--buffer_len == 0)
            goto fail;

        if (delimiter_has) {
            if (d2 != delimiter) {
                if (delimiter)
                    goto fail;
                if (delimiter_candidates) {
                    while (delimiter_candidates[0]) {
                        if (delimiter_candidates++[0] == d2)
                            delimiter = d2;
                    }
                }
                if (!delimiter) {
                    if (delimiter_required)
                        goto fail;
                    delimiter_has = FALSE;
                    continue;
                }
            }
            in++;
        }
    }

    len = out - buffer;
    if (required_len == 0 || len == required_len) {
        NM_SET_OUT(out_len, len);
        return buffer;
    }

fail:
    NM_SET_OUT(out_len, 0);
    return NULL;
}

guint8 *
nm_utils_hexstr2bin_alloc(const char *hexstr,
                          gboolean    allow_0x_prefix,
                          gboolean    delimiter_required,
                          const char *delimiter_candidates,
                          gsize       required_len,
                          gsize      *out_len)
{
    guint8 *buffer;
    gsize   buffer_len, len;

    if (G_UNLIKELY(!hexstr)) {
        NM_SET_OUT(out_len, 0);
        g_return_val_if_fail(hexstr, NULL);
    }

    nm_assert(required_len > 0 || out_len);

    if (allow_0x_prefix && hexstr[0] == '0' && hexstr[1] == 'x')
        hexstr += 2;

    if (!hexstr[0])
        goto fail;

    if (required_len > 0)
        buffer_len = required_len;
    else
        buffer_len = strlen(hexstr) / 2 + 3;

    buffer = g_malloc(buffer_len);

    if (nm_utils_hexstr2bin_full(hexstr,
                                 FALSE,
                                 delimiter_required,
                                 FALSE,
                                 delimiter_candidates,
                                 required_len,
                                 buffer,
                                 buffer_len,
                                 &len)) {
        NM_SET_OUT(out_len, len);
        return buffer;
    }

    g_free(buffer);

fail:
    NM_SET_OUT(out_len, 0);
    return NULL;
}

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

GVariant *
nm_utils_gvariant_vardict_filter(GVariant *src,
                                 gboolean (*filter_fcn)(const char *key,
                                                        GVariant   *val,
                                                        char      **out_key,
                                                        GVariant  **out_val,
                                                        gpointer    user_data),
                                 gpointer user_data)
{
    GVariantIter    iter;
    GVariantBuilder builder;
    const char     *key;
    GVariant       *val;

    g_return_val_if_fail(src && g_variant_is_of_type(src, G_VARIANT_TYPE_VARDICT), NULL);
    g_return_val_if_fail(filter_fcn, NULL);

    g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);

    g_variant_iter_init(&iter, src);
    while (g_variant_iter_next(&iter, "{&sv}", &key, &val)) {
        _nm_unused gs_unref_variant GVariant *val_free = val;
        gs_free char                         *key2     = NULL;
        gs_unref_variant GVariant            *val2     = NULL;

        if (filter_fcn(key, val, &key2, &val2, user_data)) {
            g_variant_builder_add(&builder, "{sv}", key2 ?: key, val2 ?: val);
        }
    }

    return g_variant_builder_end(&builder);
}

static gboolean
_gvariant_vardict_filter_drop_one(const char *key,
                                  GVariant   *val,
                                  char      **out_key,
                                  GVariant  **out_val,
                                  gpointer    user_data)
{
    return !nm_streq(key, user_data);
}

GVariant *
nm_utils_gvariant_vardict_filter_drop_one(GVariant *src, const char *key)
{
    return nm_utils_gvariant_vardict_filter(src, _gvariant_vardict_filter_drop_one, (gpointer) key);
}

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

static gboolean
debug_key_matches(const char *key, const char *token, guint length)
{
    /* may not call GLib functions: see note in g_parse_debug_string() */
    for (; length; length--, key++, token++) {
        char k = (*key == '_') ? '-' : g_ascii_tolower(*key);
        char t = (*token == '_') ? '-' : g_ascii_tolower(*token);

        if (k != t)
            return FALSE;
    }

    return *key == '\0';
}

/**
 * nm_utils_parse_debug_string:
 * @string: the string to parse
 * @keys: the debug keys
 * @nkeys: number of entries in @keys
 *
 * Similar to g_parse_debug_string(), but does not special
 * case "help" or "all".
 *
 * Returns: the flags
 */
guint
nm_utils_parse_debug_string(const char *string, const GDebugKey *keys, guint nkeys)
{
    guint       i;
    guint       result = 0;
    const char *q;

    if (string == NULL)
        return 0;

    while (*string) {
        q = strpbrk(string, ":;, \t");
        if (!q)
            q = string + strlen(string);

        for (i = 0; i < nkeys; i++) {
            if (debug_key_matches(keys[i].key, string, q - string))
                result |= keys[i].value;
        }

        string = q;
        if (*string)
            string++;
    }

    return result;
}

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

GSource *_nm_g_source_sentinel[] = {
    NULL,
};

GSource *
_nm_g_source_sentinel_get_init(GSource **p_source)
{
    static const GSourceFuncs source_funcs = {
        NULL,
    };
    GSource *source;

    source = g_source_new((GSourceFuncs *) &source_funcs, sizeof(GSource));
    g_source_set_priority(source, G_PRIORITY_DEFAULT_IDLE);
    g_source_set_name(source, "nm_g_source_sentinel");

    if (!g_atomic_pointer_compare_and_exchange(p_source, NULL, source)) {
        g_source_unref(source);
        source = g_atomic_pointer_get(p_source);
        nm_assert(source);
    }

    return source;
}

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

GSource *
nm_g_idle_source_new(int            priority,
                     GSourceFunc    func,
                     gpointer       user_data,
                     GDestroyNotify destroy_notify)
{
    GSource *source;

    source = g_idle_source_new();
    if (priority != G_PRIORITY_DEFAULT)
        g_source_set_priority(source, priority);
    g_source_set_callback(source, func, user_data, destroy_notify);
    return source;
}

GSource *
nm_g_timeout_source_new(guint          timeout_msec,
                        int            priority,
                        GSourceFunc    func,
                        gpointer       user_data,
                        GDestroyNotify destroy_notify)
{
    GSource *source;

    source = g_timeout_source_new(timeout_msec);
    if (priority != G_PRIORITY_DEFAULT)
        g_source_set_priority(source, priority);
    g_source_set_callback(source, func, user_data, destroy_notify);
    return source;
}

GSource *
nm_g_timeout_source_new_seconds(guint          timeout_sec,
                                int            priority,
                                GSourceFunc    func,
                                gpointer       user_data,
                                GDestroyNotify destroy_notify)
{
    GSource *source;

    source = g_timeout_source_new_seconds(timeout_sec);
    if (priority != G_PRIORITY_DEFAULT)
        g_source_set_priority(source, priority);
    g_source_set_callback(source, func, user_data, destroy_notify);
    return source;
}

GSource *
nm_g_unix_signal_source_new(int            signum,
                            int            priority,
                            GSourceFunc    handler,
                            gpointer       user_data,
                            GDestroyNotify notify)
{
    GSource *source;

    source = g_unix_signal_source_new(signum);

    if (priority != G_PRIORITY_DEFAULT)
        g_source_set_priority(source, priority);
    g_source_set_callback(source, handler, user_data, notify);
    return source;
}

GSource *
nm_g_unix_fd_source_new(int               fd,
                        GIOCondition      io_condition,
                        int               priority,
                        GUnixFDSourceFunc source_func,
                        gpointer          user_data,
                        GDestroyNotify    destroy_notify)
{
    GSource *source;

    source = g_unix_fd_source_new(fd, io_condition);

    if (priority != G_PRIORITY_DEFAULT)
        g_source_set_priority(source, priority);
    g_source_set_callback(source, G_SOURCE_FUNC(source_func), user_data, destroy_notify);
    return source;
}

GSource *
nm_g_child_watch_source_new(GPid            pid,
                            int             priority,
                            GChildWatchFunc handler,
                            gpointer        user_data,
                            GDestroyNotify  notify)
{
    GSource *source;

    source = g_child_watch_source_new(pid);

    if (priority != G_PRIORITY_DEFAULT)
        g_source_set_priority(source, priority);
    g_source_set_callback(source, G_SOURCE_FUNC(handler), user_data, notify);
    return source;
}

gboolean
nm_g_timeout_reschedule(GSource   **src,
                        gint64     *p_expiry_msec,
                        gint64      expiry_msec,
                        GSourceFunc func,
                        gpointer    user_data)
{
    gint64 now_msec;
    gint64 timeout_msec;

    /* (Re-)Schedules a timeout at "expiry_msec" (in
     * nm_utils_get_monotonic_timestamp_msec() scale).
     *
     * If a source is already scheduled in "*src" and "*p_expiry_msec" is
     * identical to "expiry_msec", then we assume the timer is already ticking,
     * and nothing is rescheduled.
     *
     * Otherwise, "*src" gets cancelled (if any), a new timer is scheduled
     * (assigned to "*src") and the new expiry is written to "*p_expiry_msec".
     */

    nm_assert(src);
    nm_assert(p_expiry_msec);

    if (*src) {
        if (*p_expiry_msec == expiry_msec) {
            /* already scheduled with same expiry. */
            return FALSE;
        }
        nm_clear_g_source_inst(src);
    }

    now_msec = nm_utils_get_monotonic_timestamp_msec();

    if (expiry_msec <= now_msec)
        timeout_msec = 0;
    else
        timeout_msec = NM_MIN(expiry_msec - now_msec, (gint64) G_MAXUINT);

    *p_expiry_msec = expiry_msec;
    *src           = nm_g_timeout_add_source(timeout_msec, func, user_data);
    return TRUE;
}

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

#define _CTX_LOG(fmt, ...)                                                                       \
    G_STMT_START                                                                                 \
    {                                                                                            \
        if (FALSE) {                                                                             \
            gint64 _ts = g_get_monotonic_time() / 100;                                           \
                                                                                                 \
            g_printerr(">>>> [%" G_GINT64_FORMAT ".%05" G_GINT64_FORMAT "] [src:%p]: " fmt "\n", \
                       _ts / 10000,                                                              \
                       _ts % 10000,                                                              \
                       (ctx_src),                                                                \
                       ##__VA_ARGS__);                                                           \
        }                                                                                        \
    }                                                                                            \
    G_STMT_END

typedef struct {
    int   fd;
    guint events;
    guint registered_events;
    union {
        int  one;
        int *many;
    } idx;
    gpointer tag;
    bool     stale : 1;
    bool     has_many_idx : 1;
} PollData;

typedef struct {
    GSource       source;
    GMainContext *context;
    GHashTable   *fds;
    GPollFD      *fds_arr;
    guint         fds_len;
    int           max_priority;
    bool          acquired : 1;
} CtxIntegSource;

static void
_poll_data_free(gpointer user_data)
{
    PollData *poll_data = user_data;

    if (poll_data->has_many_idx)
        g_free(poll_data->idx.many);
    nm_g_slice_free(poll_data);
}

static void
_ctx_integ_source_reacquire(CtxIntegSource *ctx_src)
{
    if (G_LIKELY(ctx_src->acquired && g_main_context_is_owner(ctx_src->context)))
        return;

    /* the parent context now iterates on a different thread.
     * We need to release and reacquire the inner context. */

    if (ctx_src->acquired)
        g_main_context_release(ctx_src->context);

    if (G_UNLIKELY(!g_main_context_acquire(ctx_src->context))) {
        /* Nobody is supposed to reacquire the context while we use it. This is a bug
         * of the user. */
        ctx_src->acquired = FALSE;
        g_return_if_reached();
    }
    ctx_src->acquired = TRUE;
}

static gboolean
_ctx_integ_source_prepare(GSource *source, int *out_timeout)
{
    CtxIntegSource  *ctx_src = ((CtxIntegSource *) source);
    int              max_priority;
    int              timeout = -1;
    gboolean         any_ready;
    GHashTableIter   h_iter;
    PollData        *poll_data;
    gboolean         fds_changed;
    GPollFD          new_fds_stack[300u / sizeof(GPollFD)];
    gs_free GPollFD *new_fds_heap = NULL;
    GPollFD         *new_fds;
    guint            new_fds_len;
    guint            new_fds_alloc;
    guint            i;

    _CTX_LOG("prepare...");

    _ctx_integ_source_reacquire(ctx_src);

    any_ready = g_main_context_prepare(ctx_src->context, &max_priority);

    new_fds_alloc = NM_MAX(G_N_ELEMENTS(new_fds_stack), ctx_src->fds_len);

    if (new_fds_alloc > G_N_ELEMENTS(new_fds_stack)) {
        new_fds_heap = g_new(GPollFD, new_fds_alloc);
        new_fds      = new_fds_heap;
    } else
        new_fds = new_fds_stack;

    for (;;) {
        int l;

        nm_assert(new_fds_alloc <= (guint) G_MAXINT);

        l = g_main_context_query(ctx_src->context,
                                 max_priority,
                                 &timeout,
                                 new_fds,
                                 (int) new_fds_alloc);
        nm_assert(l >= 0);

        new_fds_len = (guint) l;

        if (G_LIKELY(new_fds_len <= new_fds_alloc))
            break;

        new_fds_alloc = new_fds_len;
        g_free(new_fds_heap);
        new_fds_heap = g_new(GPollFD, new_fds_alloc);
        new_fds      = new_fds_heap;
    }

    fds_changed = FALSE;
    if (new_fds_len != ctx_src->fds_len)
        fds_changed = TRUE;
    else {
        for (i = 0; i < new_fds_len; i++) {
            if (new_fds[i].fd != ctx_src->fds_arr[i].fd
                || new_fds[i].events != ctx_src->fds_arr[i].events) {
                fds_changed = TRUE;
                break;
            }
        }
    }

    if (G_UNLIKELY(fds_changed)) {
        g_free(ctx_src->fds_arr);
        ctx_src->fds_len = new_fds_len;
        if (G_LIKELY(new_fds == new_fds_stack) || new_fds_alloc != new_fds_len)
            ctx_src->fds_arr = nm_memdup(new_fds, sizeof(*new_fds) * new_fds_len);
        else
            ctx_src->fds_arr = g_steal_pointer(&new_fds_heap);

        g_hash_table_iter_init(&h_iter, ctx_src->fds);
        while (g_hash_table_iter_next(&h_iter, (gpointer *) &poll_data, NULL))
            poll_data->stale = TRUE;

        for (i = 0; i < ctx_src->fds_len; i++) {
            const GPollFD *fd = &ctx_src->fds_arr[i];

            poll_data = g_hash_table_lookup(ctx_src->fds, &fd->fd);

            if (G_UNLIKELY(!poll_data)) {
                poll_data  = g_slice_new(PollData);
                *poll_data = (PollData) {
                    .fd                = fd->fd,
                    .idx.one           = i,
                    .has_many_idx      = FALSE,
                    .events            = fd->events,
                    .registered_events = 0,
                    .tag               = NULL,
                    .stale             = FALSE,
                };
                g_hash_table_add(ctx_src->fds, poll_data);
                nm_assert(poll_data == g_hash_table_lookup(ctx_src->fds, &fd->fd));
                continue;
            }

            if (G_LIKELY(poll_data->stale)) {
                if (poll_data->has_many_idx) {
                    g_free(poll_data->idx.many);
                    poll_data->has_many_idx = FALSE;
                }
                poll_data->events  = fd->events;
                poll_data->idx.one = i;
                poll_data->stale   = FALSE;
                continue;
            }

            /* How odd. We have duplicate FDs. In fact, currently g_main_context_query() always
             * coalesces the FDs and this cannot happen. However, that is not documented behavior,
             * so we should not rely on that. So we need to keep a list of indexes... */
            poll_data->events |= fd->events;
            if (!poll_data->has_many_idx) {
                int idx0;

                idx0                    = poll_data->idx.one;
                poll_data->has_many_idx = TRUE;
                poll_data->idx.many     = g_new(int, 4);
                poll_data->idx.many[0]  = 2; /* number allocated */
                poll_data->idx.many[1]  = 2; /* number used */
                poll_data->idx.many[2]  = idx0;
                poll_data->idx.many[3]  = i;
            } else {
                if (poll_data->idx.many[0] == poll_data->idx.many[1]) {
                    poll_data->idx.many[0] *= 2;
                    poll_data->idx.many =
                        g_realloc(poll_data->idx.many, sizeof(int) * (2 + poll_data->idx.many[0]));
                }
                poll_data->idx.many[2 + poll_data->idx.many[1]] = i;
                poll_data->idx.many[1]++;
            }
        }

        g_hash_table_iter_init(&h_iter, ctx_src->fds);
        while (g_hash_table_iter_next(&h_iter, (gpointer *) &poll_data, NULL)) {
            if (poll_data->stale) {
                nm_assert(poll_data->tag);
                nm_assert(poll_data->events == poll_data->registered_events);
                _CTX_LOG("prepare: remove poll fd=%d, events=0x%x",
                         poll_data->fd,
                         poll_data->events);
                g_source_remove_unix_fd(&ctx_src->source, poll_data->tag);
                g_hash_table_iter_remove(&h_iter);
                continue;
            }
            if (!poll_data->tag) {
                _CTX_LOG("prepare: add poll fd=%d, events=0x%x", poll_data->fd, poll_data->events);
                poll_data->registered_events = poll_data->events;
                poll_data->tag               = g_source_add_unix_fd(&ctx_src->source,
                                                      poll_data->fd,
                                                      poll_data->registered_events);
                continue;
            }
            if (poll_data->registered_events != poll_data->events) {
                _CTX_LOG("prepare: update poll fd=%d, events=0x%x",
                         poll_data->fd,
                         poll_data->events);
                poll_data->registered_events = poll_data->events;
                g_source_modify_unix_fd(&ctx_src->source,
                                        poll_data->tag,
                                        poll_data->registered_events);
            }
        }
    }

    NM_SET_OUT(out_timeout, timeout);
    ctx_src->max_priority = max_priority;

    _CTX_LOG("prepare: done, any-ready=%d, timeout=%d, max-priority=%d",
             any_ready,
             timeout,
             max_priority);

    /* we always need to poll, because we have some file descriptors. */
    return FALSE;
}

static gboolean
_ctx_integ_source_check(GSource *source)
{
    CtxIntegSource *ctx_src = ((CtxIntegSource *) source);
    GHashTableIter  h_iter;
    gboolean        some_ready;
    PollData       *poll_data;

    nm_assert(ctx_src->context);

    _CTX_LOG("check");

    _ctx_integ_source_reacquire(ctx_src);

    g_hash_table_iter_init(&h_iter, ctx_src->fds);
    while (g_hash_table_iter_next(&h_iter, (gpointer *) &poll_data, NULL)) {
        guint revents;

        revents = g_source_query_unix_fd(&ctx_src->source, poll_data->tag);
        if (G_UNLIKELY(poll_data->has_many_idx)) {
            int  num   = poll_data->idx.many[1];
            int *p_idx = &poll_data->idx.many[2];

            for (; num > 0; num--, p_idx++)
                ctx_src->fds_arr[*p_idx].revents = revents;
        } else
            ctx_src->fds_arr[poll_data->idx.one].revents = revents;
    }

    nm_assert(ctx_src->fds_len <= (guint) G_MAXINT);

    some_ready = g_main_context_check(ctx_src->context,
                                      ctx_src->max_priority,
                                      ctx_src->fds_arr,
                                      (int) ctx_src->fds_len);

    _CTX_LOG("check (some-ready=%d)...", some_ready);

    return some_ready;
}

static gboolean
_ctx_integ_source_dispatch(GSource *source, GSourceFunc callback, gpointer user_data)
{
    CtxIntegSource *ctx_src = ((CtxIntegSource *) source);

    nm_assert(ctx_src->context);

    _ctx_integ_source_reacquire(ctx_src);

    _CTX_LOG("dispatch");

    g_main_context_dispatch(ctx_src->context);

    return G_SOURCE_CONTINUE;
}

static void
_ctx_integ_source_finalize(GSource *source)
{
    CtxIntegSource *ctx_src = ((CtxIntegSource *) source);
    GHashTableIter  h_iter;
    PollData       *poll_data;

    g_return_if_fail(ctx_src->context);

    _CTX_LOG("finalize...");

    g_hash_table_iter_init(&h_iter, ctx_src->fds);
    while (g_hash_table_iter_next(&h_iter, (gpointer *) &poll_data, NULL)) {
        nm_assert(poll_data->tag);
        _CTX_LOG("prepare: remove poll fd=%d, events=0x%x", poll_data->fd, poll_data->events);
        g_source_remove_unix_fd(&ctx_src->source, poll_data->tag);
        g_hash_table_iter_remove(&h_iter);
    }

    nm_clear_pointer(&ctx_src->fds, g_hash_table_unref);
    nm_clear_g_free(&ctx_src->fds_arr);
    ctx_src->fds_len = 0;

    if (ctx_src->acquired) {
        ctx_src->acquired = FALSE;
        g_main_context_release(ctx_src->context);
    }

    nm_clear_pointer(&ctx_src->context, g_main_context_unref);
}

static GSourceFuncs ctx_integ_source_funcs = {
    .prepare  = _ctx_integ_source_prepare,
    .check    = _ctx_integ_source_check,
    .dispatch = _ctx_integ_source_dispatch,
    .finalize = _ctx_integ_source_finalize,
};

/**
 * nm_utils_g_main_context_create_integrate_source:
 * @inner_context: the inner context that will be integrated to an
 *   outer #GMainContext.
 *
 * By integrating the inner context with an outer context, when iterating the outer
 * context sources on the inner context will be dispatched. Note that while the
 * created source exists, the @inner_context will be acquired. The user gets restricted
 * what to do with the inner context. In particular while the inner context is integrated,
 * the user should not acquire the inner context again or explicitly iterate it. What
 * the user of course still can (and wants to) do is attaching new sources to the inner
 * context.
 *
 * Note that GSource has a priority. While each context dispatches events based on
 * their source's priorities, the outer context dispatches to the inner context
 * only with one priority (the priority of the created source). That is, the sources
 * from the two contexts are kept separate and are not sorted by their priorities.
 *
 * Returns: a newly created GSource that should be attached to the
 *   outer context.
 */
GSource *
nm_utils_g_main_context_create_integrate_source(GMainContext *inner_context)
{
    CtxIntegSource *ctx_src;

    g_return_val_if_fail(inner_context, NULL);

    if (!g_main_context_acquire(inner_context)) {
        /* We require to acquire the context while it's integrated. We need to keep it acquired
         * for the entire duration.
         *
         * This is also necessary because g_source_attach() only wakes up the context, if
         * the context is currently acquired. */
        g_return_val_if_reached(NULL);
    }

    ctx_src = (CtxIntegSource *) g_source_new(&ctx_integ_source_funcs, sizeof(CtxIntegSource));

    g_source_set_name(&ctx_src->source, "ContextIntegrateSource");

    ctx_src->context  = g_main_context_ref(inner_context);
    ctx_src->fds      = g_hash_table_new_full(nm_pint_hash, nm_pint_equal, _poll_data_free, NULL);
    ctx_src->fds_len  = 0;
    ctx_src->fds_arr  = NULL;
    ctx_src->acquired = TRUE;
    ctx_src->max_priority = G_MAXINT;

    _CTX_LOG("create new integ-source for %p", inner_context);

    return &ctx_src->source;
}

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

void
nm_utils_ifname_cpy(char *dst, const char *name)
{
    int i;

    g_return_if_fail(dst);
    g_return_if_fail(name && name[0]);

    nm_assert(nm_utils_ifname_valid_kernel(name, NULL));

    /* ensures NUL padding of the entire IFNAMSIZ buffer. */

    for (i = 0; i < (int) IFNAMSIZ && name[i] != '\0'; i++)
        dst[i] = name[i];

    nm_assert(name[i] == '\0');

    for (; i < (int) IFNAMSIZ; i++)
        dst[i] = '\0';
}

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

gboolean
nm_utils_ifname_valid_kernel(const char *name, GError **error)
{
    int i;

    /* This function follows kernel's interface validation
     * function dev_valid_name() in net/core/dev.c.
     */

    if (!name) {
        g_set_error_literal(error,
                            NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            _("interface name is missing"));
        return FALSE;
    }

    if (name[0] == '\0') {
        g_set_error_literal(error,
                            NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            _("interface name is too short"));
        return FALSE;
    }

    if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) {
        g_set_error_literal(error,
                            NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            _("interface name is reserved"));
        return FALSE;
    }

    for (i = 0; i < IFNAMSIZ; i++) {
        char ch = name[i];

        if (ch == '\0')
            return TRUE;
        if (NM_IN_SET(ch, '/', ':') || nm_ascii_is_space_kernel(ch)) {
            g_set_error_literal(error,
                                NM_UTILS_ERROR,
                                NM_UTILS_ERROR_UNKNOWN,
                                _("interface name contains an invalid character"));
            return FALSE;
        }
    }

    g_set_error_literal(error,
                        NM_UTILS_ERROR,
                        NM_UTILS_ERROR_UNKNOWN,
                        _("interface name is longer than 15 characters"));
    return FALSE;
}

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

static gboolean
_nm_utils_ifname_valid_kernel(const char *name, GError **error)
{
    if (!nm_utils_ifname_valid_kernel(name, error))
        return FALSE;

    if (strchr(name, '%')) {
        /* Kernel's dev_valid_name() accepts (almost) any binary up to 15 chars.
         * However, '%' is treated special as a format specifier. Try
         *
         *   ip link add 'dummy%dx' type dummy
         *
         * Don't allow that for "connection.interface-name", which either
         * matches an existing netdev name (thus, it cannot have a '%') or
         * is used to configure a name (in which case we don't want kernel
         * to replace the format specifier). */
        g_set_error_literal(error,
                            NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            _("'%%' is not allowed in interface names"));
        return FALSE;
    }

    if (NM_IN_STRSET(name, "all", "default", "bonding_masters")) {
        /* Certain names are not allowed. The "all" and "default" names are reserved
         * due to their directories in "/proc/sys/net/ipv4/conf/" and "/proc/sys/net/ipv6/conf/".
         *
         * Also, there is "/sys/class/net/bonding_masters" file.
         */
        nm_utils_error_set(error,
                           NM_UTILS_ERROR_UNKNOWN,
                           _("'%s' is not allowed as interface name"),
                           name);
        return FALSE;
    }

    return TRUE;
}

static gboolean
_nm_utils_ifname_valid_ovs(const char *name, GError **error)
{
    const char *ch;

    /* OVS actually accepts a wider range of chars (all printable UTF-8 chars),
     * NetworkManager restricts this to ASCII char as it's a safer option for
     * now since OVS is not well documented on this matter.
     **/
    for (ch = name; *ch; ++ch) {
        if (*ch == '\\' || *ch == '/' || !g_ascii_isgraph(*ch)) {
            g_set_error_literal(error,
                                NM_UTILS_ERROR,
                                NM_UTILS_ERROR_UNKNOWN,
                                _("interface name must be alphanumerical with "
                                  "no forward or backward slashes"));
            return FALSE;
        }
    };
    return TRUE;
}

gboolean
nm_utils_ifname_valid(const char *name, NMUtilsIfaceType type, GError **error)
{
    g_return_val_if_fail(!error || !(*error), FALSE);

    if (!name || !(name[0])) {
        g_set_error_literal(error,
                            NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            _("interface name must not be empty"));
        return FALSE;
    }

    if (!g_utf8_validate(name, -1, NULL)) {
        g_set_error_literal(error,
                            NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            _("interface name must be UTF-8 encoded"));
        return FALSE;
    }

    switch (type) {
    case NMU_IFACE_KERNEL:
        return _nm_utils_ifname_valid_kernel(name, error);
    case NMU_IFACE_OVS:
        return _nm_utils_ifname_valid_ovs(name, error);
    case NMU_IFACE_OVS_AND_KERNEL:
        return _nm_utils_ifname_valid_kernel(name, error)
               && _nm_utils_ifname_valid_ovs(name, error);
    case NMU_IFACE_ANY:
    {
        gs_free_error GError *local = NULL;

        if (_nm_utils_ifname_valid_kernel(name, error ? &local : NULL))
            return TRUE;
        if (_nm_utils_ifname_valid_ovs(name, NULL))
            return TRUE;
        if (error)
            g_propagate_error(error, g_steal_pointer(&local));
        return FALSE;
    }
    }

    g_return_val_if_reached(FALSE);
}

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

void
_nm_str_buf_ensure_size(NMStrBuf *strbuf, gsize new_size, gboolean reserve_exact)
{
    _nm_str_buf_assert(strbuf);

    /* Currently, this only supports strictly growing the buffer. */
    nm_assert(new_size > strbuf->_priv_allocated);

    if (!reserve_exact) {
        new_size = nm_utils_get_next_realloc_size(!strbuf->_priv_do_bzero_mem, new_size);
    }

    if (strbuf->_priv_malloced) {
        strbuf->_priv_str = nm_secret_mem_realloc(strbuf->_priv_str,
                                                  strbuf->_priv_do_bzero_mem,
                                                  strbuf->_priv_allocated,
                                                  new_size);
    } else {
        char *old = strbuf->_priv_str;

        strbuf->_priv_str = g_malloc(new_size);
        if (strbuf->_priv_len > 0) {
            memcpy(strbuf->_priv_str, old, strbuf->_priv_len);
            if (strbuf->_priv_do_bzero_mem)
                nm_explicit_bzero(old, strbuf->_priv_len);
        }
        strbuf->_priv_malloced = TRUE;
    }
    strbuf->_priv_allocated = new_size;
}

void
nm_str_buf_append_printfv(NMStrBuf *strbuf, const char *format, va_list args)
{
    va_list args_copy;
    gsize   available;
    int     l;

    _nm_str_buf_assert(strbuf);

    available = strbuf->_priv_allocated - strbuf->_priv_len;

    nm_assert(available < G_MAXULONG);

    va_copy(args_copy, args);
    l = g_vsnprintf(strbuf->_priv_allocated > 0 ? &strbuf->_priv_str[strbuf->_priv_len] : NULL,
                    available,
                    format,
                    args_copy);
    va_end(args_copy);

    nm_assert(l >= 0);
    nm_assert(l < G_MAXINT);

    if ((gsize) l >= available) {
        gsize l2;

        if (l == 0)
            return;

        l2 = ((gsize) l) + 1u;

        nm_str_buf_maybe_expand(strbuf, l2, FALSE);

        va_copy(args_copy, args);
        l = g_vsnprintf(&strbuf->_priv_str[strbuf->_priv_len], l2, format, args_copy);
        va_end(args_copy);

        nm_assert(l >= 0);
        nm_assert((gsize) l == l2 - 1u);
    }

    strbuf->_priv_len += (gsize) l;
}

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

/**
 * nm_indirect_g_free:
 * @arg: a pointer to a pointer that is to be freed.
 *
 * This does the same as nm_clear_g_free(arg) (g_clear_pointer (arg, g_free)).
 * This is for example useful when you have a GArray with pointers and a
 * clear function to free them. g_array_set_clear_func()'s destroy notify
 * function gets a pointer to the array location, so we have to follow
 * the first pointer.
 */
void
nm_indirect_g_free(gpointer arg)
{
    gpointer *p = arg;

    nm_clear_g_free(p);
}

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

static char *
attribute_escape(const char *src, char c1, char c2)
{
    char *ret, *dest;

    dest = ret = g_malloc(strlen(src) * 2 + 1);

    while (*src) {
        if (*src == c1 || *src == c2 || *src == '\\')
            *dest++ = '\\';
        *dest++ = *src++;
    }
    *dest++ = '\0';

    return ret;
}

void
_nm_utils_format_variant_attributes_full(GString                             *str,
                                         const NMUtilsNamedValue             *values,
                                         guint                                num_values,
                                         const NMVariantAttributeSpec *const *spec,
                                         char                                 attr_separator,
                                         char                                 key_value_separator)
{
    const NMVariantAttributeSpec *const *s;
    const char                          *name, *value;
    GVariant                            *variant;
    char                                *escaped;
    char                                 buf[64];
    char                                 sep = 0;
    guint                                i;

    for (i = 0; i < num_values; i++) {
        name    = values[i].name;
        variant = values[i].value_ptr;
        value   = NULL;
        s       = NULL;

        if (spec) {
            for (s = spec; *s; s++) {
                if (nm_streq0((*s)->name, name))
                    break;
            }

            if (!*s)
                continue;
        }

        if (g_variant_is_of_type(variant, G_VARIANT_TYPE_UINT32))
            value = nm_sprintf_buf(buf, "%u", g_variant_get_uint32(variant));
        else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_INT32))
            value = nm_sprintf_buf(buf, "%d", (int) g_variant_get_int32(variant));
        else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_UINT64))
            value = nm_sprintf_buf(buf, "%" G_GUINT64_FORMAT, g_variant_get_uint64(variant));
        else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_BYTE))
            value = nm_sprintf_buf(buf, "%hhu", g_variant_get_byte(variant));
        else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_BOOLEAN))
            value = g_variant_get_boolean(variant) ? "true" : "false";
        else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_STRING))
            value = g_variant_get_string(variant, NULL);
        else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_BYTESTRING)) {
            /* FIXME: there is no guarantee that the byte array
             * is valid UTF-8.*/
            value = g_variant_get_bytestring(variant);
        } else
            continue;

        if (sep)
            g_string_append_c(str, sep);

        escaped = attribute_escape(name, attr_separator, key_value_separator);
        g_string_append(str, escaped);
        g_free(escaped);

        if (!s || !*s || !(*s)->no_value) {
            g_string_append_c(str, key_value_separator);

            escaped = attribute_escape(value, attr_separator, key_value_separator);
            g_string_append(str, escaped);
            g_free(escaped);
        }

        sep = attr_separator;
    }
}

char *
_nm_utils_format_variant_attributes(GHashTable                          *attributes,
                                    const NMVariantAttributeSpec *const *spec,
                                    char                                 attr_separator,
                                    char                                 key_value_separator)
{
    gs_free NMUtilsNamedValue *values_free = NULL;
    NMUtilsNamedValue          values_prepared[20];
    const NMUtilsNamedValue   *values;
    GString                   *str = NULL;
    guint                      len;

    g_return_val_if_fail(attr_separator, NULL);
    g_return_val_if_fail(key_value_separator, NULL);

    if (!attributes)
        return NULL;

    values = nm_utils_named_values_from_strdict(attributes, &len, values_prepared, &values_free);
    if (len == 0)
        return NULL;

    str = g_string_new("");
    _nm_utils_format_variant_attributes_full(str,
                                             values,
                                             len,
                                             spec,
                                             attr_separator,
                                             key_value_separator);
    return g_string_free(str, FALSE);
}

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

gboolean
nm_utils_is_localhost(const char *name)
{
    static const char *const NAMES[] = {
        "localhost",
        "localhost4",
        "localhost6",
        "localhost.localdomain",
        "localhost4.localdomain4",
        "localhost6.localdomain6",
    };
    gsize name_len;
    int   i;

    if (!name)
        return FALSE;

    /* This tries to identify local host and domain names
     * described in RFC6761 plus the redhatism of localdomain.
     *
     * Similar to systemd's is_localhost(). */

    name_len = strlen(name);

    if (name_len == 0)
        return FALSE;

    if (name[name_len - 1] == '.') {
        /* one trailing dot is fine. Hide it. */
        name_len--;
    }

    for (i = 0; i < (int) G_N_ELEMENTS(NAMES); i++) {
        const char *n = NAMES[i];
        gsize       l = strlen(n);
        gsize       s;

        if (name_len < l)
            continue;

        s = name_len - l;

        if (g_ascii_strncasecmp(&name[s], n, l) != 0)
            continue;

        /* we accept the name if it is equal to one of the well-known names,
         * or if it is some prefix, a '.' and the well-known name. */
        if (s == 0)
            return TRUE;
        if (name[s - 1] == '.')
            return TRUE;
    }

    return FALSE;
}

static gboolean
_nm_utils_check_hostname(const char *name, bool allow_localhost)
{
    if (nm_str_is_empty(name))
        return FALSE;

    if (nm_streq(name, "(none)")) {
        /* This is not a special hostname. Probably an artefact by somebody wrongly
         * printing NULL. */
        return FALSE;
    }

    if (!allow_localhost && nm_utils_is_localhost(name))
        return FALSE;

    /* FIXME: properly validate the hostname, like systemd's hostname_is_valid() */

    return TRUE;
}

gboolean
nm_utils_is_specific_hostname(const char *name)
{
    return _nm_utils_check_hostname(name, FALSE);
}

gboolean
nm_utils_is_not_empty_hostname(const char *name)
{
    return _nm_utils_check_hostname(name, TRUE);
}

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

typedef struct {
    struct passwd pw;
    _nm_alignas(max_align_t) char buf[];
} GetPwuidData;

/**
 * nm_getpwuid:
 * uid: the user Id to look up
 *
 * Calls getpwuid_r() to lookup the passwd entry. See the manual.
 * Allocates and returns a suitable buffer.
 *
 * The returned buffer is likely much large than required. You don't want to
 * keep this buffer around for longer than necessary.
 *
 * Returns: (transfer full): the passwd entry, if found or NULL on error
 *   or if the entry was not found.
 */
struct passwd *
nm_getpwuid(uid_t uid)
{
    gs_free GetPwuidData *data = NULL;
    gsize                 bufsize;
    const gsize           OFFSET = G_STRUCT_OFFSET(GetPwuidData, buf);
    long int              size_max;

    size_max = sysconf(_SC_GETPW_R_SIZE_MAX);
    if (size_max > 0 && ((unsigned long int) size_max < G_MAXSIZE - OFFSET))
        bufsize = size_max;
    else
        bufsize = 4096;

    for (;;) {
        struct passwd *pw = NULL;
        int            r;

        if (bufsize >= G_MAXSIZE - OFFSET)
            return NULL;

        nm_clear_g_free(&data);
        data = g_malloc(OFFSET + bufsize);

        r = getpwuid_r(uid, &data->pw, data->buf, bufsize, &pw);
        if (r == 0) {
            if (!pw)
                return NULL;
            nm_assert(pw == (gpointer) data);
            return (gpointer) g_steal_pointer(&data);
        }

        if (r != ERANGE)
            return NULL;

        if (bufsize > G_MAXSIZE / 2u)
            return NULL;
        bufsize *= 2u;
    }
}

const char *
nm_passwd_name(const struct passwd *pw)
{
    /* Normalize "pw->pw_name" and return it. */
    return pw ? nm_str_not_empty(pw->pw_name) : NULL;
}

char *
nm_utils_uid_to_name(uid_t uid)
{
    gs_free struct passwd *pw = NULL;

    pw = nm_getpwuid(uid);
    return g_strdup(nm_passwd_name(pw));
}

/* taken from systemd's nss_user_record_by_name() */
gboolean
nm_utils_name_to_uid(const char *name, uid_t *out_uid)
{
    gs_free char *buf_heap = NULL;
    char          buf_stack[4096];
    gsize         bufsize;
    char         *buf;

    if (!name)
        return nm_assert_unreachable_val(FALSE);

    bufsize = sizeof(buf_stack);
    buf     = buf_stack;

    for (;;) {
        struct passwd *result;
        struct passwd  pwd;
        int            r;

        r = getpwnam_r(name, &pwd, buf, bufsize, &result);
        if (r == 0) {
            if (!result)
                return FALSE;
            NM_SET_OUT(out_uid, pwd.pw_uid);
            return TRUE;
        }

        if (r != ERANGE)
            return FALSE;

        if (bufsize > G_MAXSIZE / 2u)
            return FALSE;

        bufsize *= 2u;
        g_free(buf_heap);
        buf_heap = g_malloc(bufsize);
        buf      = buf_heap;
    }
}

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

static double
_exp10(guint16 ex)
{
    double v;

    if (ex == 0)
        return 1.0;

    v = _exp10(ex / 2);
    v = v * v;
    if (ex % 2)
        v *= 10;
    return v;
}

/*
 * nm_utils_exp10:
 * @ex: the exponent
 *
 * Returns: 10^ex, or pow(10, ex), or exp10(ex).
 */
double
nm_utils_exp10(gint16 ex)
{
    if (ex >= 0)
        return _exp10(ex);
    return 1.0 / _exp10(-((gint32) ex));
}

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

gboolean
_nm_utils_is_empty_ssid_arr(const guint8 *ssid, gsize len)
{
    if (len == 0)
        return TRUE;

    /* Single white space is for Linksys APs */
    if (len == 1 && ssid[0] == ' ')
        return TRUE;

    /* Otherwise, if the entire ssid is 0, we assume it is hidden */
    while (len--) {
        if (ssid[len] != '\0')
            return FALSE;
    }
    return TRUE;
}

gboolean
_nm_utils_is_empty_ssid_gbytes(GBytes *ssid)
{
    const guint8 *p;
    gsize         l;

    g_return_val_if_fail(ssid, FALSE);

    p = g_bytes_get_data(ssid, &l);
    return _nm_utils_is_empty_ssid_arr(p, l);
}

char *
_nm_utils_ssid_to_string_arr(const guint8 *ssid, gsize len)
{
    gs_free char *s_copy = NULL;
    const char   *s_cnst;

    if (len == 0)
        return g_strdup("(empty)");

    s_cnst =
        nm_utils_buf_utf8safe_escape(ssid, len, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL, &s_copy);
    nm_assert(s_cnst);

    if (_nm_utils_is_empty_ssid_arr(ssid, len))
        return g_strdup_printf("\"%s\" (hidden)", s_cnst);

    return g_strdup_printf("\"%s\"", s_cnst);
}

char *
_nm_utils_ssid_to_string_gbytes(GBytes *ssid)
{
    gconstpointer p;
    gsize         l;

    if (!ssid)
        return g_strdup("(none)");

    p = g_bytes_get_data(ssid, &l);
    return _nm_utils_ssid_to_string_arr(p, l);
}

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

#define IPV6_PROPERTY_DIR "/proc/sys/net/ipv6/conf/"
#define IPV4_PROPERTY_DIR "/proc/sys/net/ipv4/conf/"

G_STATIC_ASSERT(sizeof(IPV4_PROPERTY_DIR) == sizeof(IPV6_PROPERTY_DIR));
G_STATIC_ASSERT(NM_STRLEN(IPV6_PROPERTY_DIR) + IFNAMSIZ + 60
                == NM_UTILS_SYSCTL_IP_CONF_PATH_BUFSIZE);

/**
 * nm_utils_sysctl_ip_conf_path:
 * @addr_family: either AF_INET or AF_INET6.
 * @buf: the output buffer where to write the path. It
 *   must be at least NM_UTILS_SYSCTL_IP_CONF_PATH_BUFSIZE bytes
 *   long.
 * @ifname: an interface name
 * @property: a property name
 *
 * Returns: the path to IPv6 property @property on @ifname. Note that
 * this returns the input argument @buf.
 */
const char *
nm_utils_sysctl_ip_conf_path(int addr_family, char *buf, const char *ifname, const char *property)
{
    int len;

    nm_assert(buf);
    nm_assert_addr_family(addr_family);

    g_assert(nm_utils_ifname_valid_kernel(ifname, NULL));
    property = NM_ASSERT_VALID_PATH_COMPONENT(property);

    len = g_snprintf(buf,
                     NM_UTILS_SYSCTL_IP_CONF_PATH_BUFSIZE,
                     "%s%s/%s",
                     addr_family == AF_INET6 ? IPV6_PROPERTY_DIR : IPV4_PROPERTY_DIR,
                     ifname,
                     property);
    g_assert(len < NM_UTILS_SYSCTL_IP_CONF_PATH_BUFSIZE - 1);
    return buf;
}

gboolean
nm_utils_sysctl_ip_conf_is_path(int         addr_family,
                                const char *path,
                                const char *ifname,
                                const char *property)
{
    g_return_val_if_fail(path, FALSE);
    NM_ASSERT_VALID_PATH_COMPONENT(property);
    g_assert(!ifname || nm_utils_ifname_valid_kernel(ifname, NULL));

    if (addr_family == AF_INET) {
        if (!g_str_has_prefix(path, IPV4_PROPERTY_DIR))
            return FALSE;
        path += NM_STRLEN(IPV4_PROPERTY_DIR);
    } else if (addr_family == AF_INET6) {
        if (!g_str_has_prefix(path, IPV6_PROPERTY_DIR))
            return FALSE;
        path += NM_STRLEN(IPV6_PROPERTY_DIR);
    } else
        g_return_val_if_reached(FALSE);

    if (ifname) {
        if (!g_str_has_prefix(path, ifname))
            return FALSE;
        path += strlen(ifname);
        if (path[0] != '/')
            return FALSE;
        path++;
    } else {
        const char *slash;
        char        buf[IFNAMSIZ];
        gsize       l;

        slash = strchr(path, '/');
        if (!slash)
            return FALSE;
        l = slash - path;
        if (l >= IFNAMSIZ)
            return FALSE;
        memcpy(buf, path, l);
        buf[l] = '\0';
        if (!nm_utils_ifname_valid_kernel(buf, NULL))
            return FALSE;
        path = slash + 1;
    }

    if (!nm_streq(path, property))
        return FALSE;

    return TRUE;
}

gboolean
nm_utils_is_valid_path_component(const char *name)
{
    const char *n;

    if (name == NULL || name[0] == '\0')
        return FALSE;

    if (name[0] == '.') {
        if (name[1] == '\0')
            return FALSE;
        if (name[1] == '.' && name[2] == '\0')
            return FALSE;
    }
    n = name;
    do {
        if (*n == '/')
            return FALSE;
    } while (*(++n) != '\0');

    return TRUE;
}

const char *
NM_ASSERT_VALID_PATH_COMPONENT(const char *name)
{
    if (G_LIKELY(nm_utils_is_valid_path_component(name)))
        return name;

    g_error("FATAL: Failed asserting path component: %s%s%s",
            NM_PRINT_FMT_QUOTED(name, "\"", name, "\"", "(null)"));
    g_assert_not_reached();
}

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

void
nm_crypto_md5_hash(const guint8 *salt,
                   gsize         salt_len,
                   const guint8 *password,
                   gsize         password_len,
                   guint8       *buffer,
                   gsize         buflen)
{
    nm_auto_free_checksum GChecksum                  *ctx = NULL;
    nm_auto_clear_static_secret_ptr const NMSecretPtr digest =
        NM_SECRET_PTR_STATIC(NM_UTILS_CHECKSUM_LENGTH_MD5);
    gsize bufidx = 0;
    int   i;

    g_return_if_fail(password_len == 0 || password);
    g_return_if_fail(buffer);
    g_return_if_fail(buflen > 0);
    g_return_if_fail(salt_len == 0 || salt);

    ctx = g_checksum_new(G_CHECKSUM_MD5);

    for (;;) {
        if (password_len > 0)
            g_checksum_update(ctx, (const guchar *) password, password_len);
        if (salt_len > 0)
            g_checksum_update(ctx, (const guchar *) salt, salt_len);

        nm_utils_checksum_get_digest_len(ctx, digest.bin, NM_UTILS_CHECKSUM_LENGTH_MD5);

        for (i = 0; i < NM_UTILS_CHECKSUM_LENGTH_MD5; i++) {
            if (bufidx >= buflen)
                return;
            buffer[bufidx++] = digest.bin[i];
        }

        g_checksum_reset(ctx);
        g_checksum_update(ctx, digest.ptr, NM_UTILS_CHECKSUM_LENGTH_MD5);
    }
}

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

const char *
nm_utils_get_process_exit_status_desc_buf(int status, char *buf, gsize buf_len)
{
    const char *buf0 = buf;

    nm_assert(buf_len == 0 || buf);

    /* This should give a partial sentence, it it can be combined with
     * prinft("command XYZ %s.\n", desc) */

    if (WIFEXITED(status))
        nm_strbuf_append(&buf, &buf_len, "exited with status %d", WEXITSTATUS(status));
    else if (WIFSIGNALED(status))
        nm_strbuf_append(&buf, &buf_len, "killed by signal %d", WTERMSIG(status));
    else if (WIFSTOPPED(status))
        nm_strbuf_append(&buf, &buf_len, "stopped by signal %d", WSTOPSIG(status));
    else if (WIFCONTINUED(status))
        nm_strbuf_append(&buf, &buf_len, "resumed by SIGCONT");
    else
        nm_strbuf_append(&buf, &buf_len, "exited with unknown status 0x%x", status);

    return buf0;
}

char *
nm_utils_get_process_exit_status_desc(int status)
{
    char buf[NM_UTILS_GET_PROCESS_EXIT_STATUS_BUF_LEN];

    nm_utils_get_process_exit_status_desc_buf(status, buf, sizeof(buf));

    return g_strdup(buf);
}

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

gboolean
nm_utils_validate_hostname(const char *hostname)
{
    const char *p;
    gboolean    dot = TRUE;

    if (!hostname || !hostname[0])
        return FALSE;

    for (p = hostname; *p; p++) {
        if (*p == '.') {
            if (dot)
                return FALSE;
            dot = TRUE;
        } else {
            if (!g_ascii_isalnum(*p) && (*p != '-') && (*p != '_'))
                return FALSE;
            dot = FALSE;
        }
    }

    if (dot)
        return FALSE;

    return (p - hostname <= NM_HOST_NAME_MAX);
}

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

typedef struct {
    CList          lst;
    gpointer       tls_data;
    GDestroyNotify destroy_notify;
} TlsRegData;

static pthread_key_t _tls_reg_key;

static void
_tls_reg_destroy(gpointer data)
{
    CList      *lst_head = data;
    TlsRegData *entry;

    if (!lst_head)
        return;

    /* For no strong reason are we destroying the elements in reverse
     * order than they were added. It seems a bit more sensible (but shouldn't
     * matter nor should you rely on that). */
    while ((entry = c_list_last_entry(lst_head, TlsRegData, lst))) {
        c_list_unlink_stale(&entry->lst);
        entry->destroy_notify(entry->tls_data);
        nm_g_slice_free(entry);
    }

    nm_g_slice_free(lst_head);
}

static void
_tls_reg_make_key(void)
{
    if (pthread_key_create(&_tls_reg_key, _tls_reg_destroy) != 0)
        g_return_if_reached();
}

/**
 * nm_utils_thread_local_register_destroy:
 * @tls_data: the thread local storage data that should be destroyed when the thread
 *   exits. This pointer will be "owned" by the current thread. There is no way
 *   to un-register the destruction.
 * @destroy_notify: the free function that will be called when the thread exits.
 *
 * If _nm_tread_local storage is heap allocated it requires freeing the pointer
 * when the thread exits. Use this function to register the pointer to be
 * released.
 *
 * This function does not change errno.
 */
void
nm_utils_thread_local_register_destroy(gpointer tls_data, GDestroyNotify destroy_notify)
{
    NM_AUTO_PROTECT_ERRNO(errsv);
    static pthread_once_t key_once = PTHREAD_ONCE_INIT;
    CList                *lst_head;
    TlsRegData           *entry;

    nm_assert(destroy_notify);

    if (pthread_once(&key_once, _tls_reg_make_key) != 0)
        g_return_if_reached();

    if ((lst_head = pthread_getspecific(_tls_reg_key)) == NULL) {
        lst_head = g_slice_new(CList);
        c_list_init(lst_head);
        if (pthread_setspecific(_tls_reg_key, lst_head) != 0)
            g_return_if_reached();
    }

    entry                 = g_slice_new(TlsRegData);
    entry->tls_data       = tls_data;
    entry->destroy_notify = destroy_notify;
    c_list_link_tail(lst_head, &entry->lst);
}

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

static gboolean
_iterate_for_msec_timeout(gpointer user_data)
{
    GSource **p_source = user_data;

    nm_clear_g_source_inst(p_source);
    return G_SOURCE_CONTINUE;
}

void
nm_g_main_context_iterate_for_msec(GMainContext *context, guint timeout_msec)
{
    GSource *source;

    /* In production is this function not very useful. It is however useful to
     * have in the toolbox for printf debugging. */

    source = g_timeout_source_new(timeout_msec);
    g_source_set_callback(source, _iterate_for_msec_timeout, &source, NULL);

    if (!context)
        context = g_main_context_default();

    g_source_attach(source, context);
    while (source)
        g_main_context_iteration(context, TRUE);
}

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

gboolean
nm_g_main_context_can_acquire(GMainContext *context)
{
    /* Fast path. Usually we don't pass contexts between threads
     * and operate while iterating the context. Hence, usually we
     * already acquired the context. Check that first. */
    if (g_main_context_is_owner(context))
        return TRUE;

    /* Either the context is not owned, or owned by somebody else. Only
     * one way to find out. */
    if (!g_main_context_acquire(context))
        return FALSE;

    g_main_context_release(context);
    return TRUE;
}

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

int
nm_unbase64char(char c)
{
    /* copied from systemd's unbase64char():
     * https://github.com/systemd/systemd/blob/688efe7703328c5a0251fafac55757b8864a9f9a/src/basic/hexdecoct.c#L539 */

    switch (c) {
    case 'A' ... 'Z':
        return c - 'A';
    case 'a' ... 'z':
        return (c - 'a') + ('Z' - 'A' + 1);
    case '0' ... '9':
        return (c - '0') + (('Z' - 'A' + 1) + ('z' - 'a' + 1));
    case '+':
        return ('Z' - 'A' + 1) + ('z' - 'a' + 1) + ('9' - '0' + 1);
    case '/':
        return ('Z' - 'A' + 1) + ('z' - 'a' + 1) + ('9' - '0' + 1) + 1;
    case '=':
        /* The padding is a different kind of base64 character. Return
         * a special error code for it. */
        return -ERANGE;
    default:
        return -EINVAL;
    }
}

static int
unbase64_next(const char **p, size_t *l)
{
    int ret;

    nm_assert(p);
    nm_assert(l);

    /* copied from systemd's unbase64_next():
     * https://github.com/systemd/systemd/blob/688efe7703328c5a0251fafac55757b8864a9f9a/src/basic/hexdecoct.c#L709 */

    /* Find the next non-whitespace character, and decode it. If we find padding, we return it as INT_MAX. We
     * greedily skip all preceding and all following whitespace. */

    for (;;) {
        if (*l == 0)
            return -EPIPE;

        if (!nm_ascii_is_whitespace(**p))
            break;

        /* Skip leading whitespace */
        (*p)++;
        (*l)--;
    }

    ret = nm_unbase64char(**p);
    if (ret < 0) {
        nm_assert(NM_IN_SET(ret, -EINVAL, -ERANGE));
        if (ret != -ERANGE)
            return ret;
    }

    for (;;) {
        (*p)++;
        (*l)--;

        if (*l == 0)
            break;
        if (!nm_ascii_is_whitespace(**p))
            break;

        /* Skip following whitespace */
    }

    nm_assert(ret == -ERANGE || ret >= 0);
    return ret;
}

/**
 * nm_unbase64mem_full:
 * @p: a valid base64 string. Whitespace is ignored, but invalid encodings
 *   will cause the function to fail.
 * @l: the length of @p. @p is not treated as NUL terminated string but
 *   merely as a buffer of ascii characters.
 * @secure: whether the temporary memory will be cleared to avoid leaving
 *   secrets in memory (see also nm_explicit_bzero()).
 * @mem: (transfer full): the decoded buffer on success.
 * @len: the length of @mem on success.
 *
 * glib provides g_base64_decode(), but that does not report any errors
 * from invalid encodings. Our own implementation (based on systemd code)
 * rejects invalid inputs.
 *
 * Returns: a non-negative code on success. Invalid encoding let the
 *   function fail.
 */
int
nm_unbase64mem_full(const char *p, gsize l, gboolean secure, guint8 **ret, gsize *ret_size)
{
    gs_free uint8_t *buf = NULL;
    const char      *x;
    guint8          *z;
    gsize            len;
    int              r;

    /* copied from systemd's unbase64mem_full():
     * https://github.com/systemd/systemd/blob/688efe7703328c5a0251fafac55757b8864a9f9a/src/basic/hexdecoct.c#L751 */

    nm_assert(p || l == 0);

    if (l == G_MAXSIZE)
        l = strlen(p);

    /* A group of four input bytes needs three output bytes, in case of padding we need to add two or three extra
     * bytes. Note that this calculation is an upper boundary, as we ignore whitespace while decoding */
    len = (l / 4) * 3 + (l % 4 != 0 ? (l % 4) - 1 : 0);

    buf = g_malloc(len + 1);

    for (x = p, z = buf;;) {
        int a; /* a == 00XXXXXX */
        int b; /* b == 00YYYYYY */
        int c; /* c == 00ZZZZZZ */
        int d; /* d == 00WWWWWW */

        a = unbase64_next(&x, &l);
        if (a < 0) {
            if (a == -EPIPE) /* End of string */
                break;
            if (a == -ERANGE) { /* Padding is not allowed at the beginning of a 4ch block */
                r = -EINVAL;
                goto on_failure;
            }
            r = a;
            goto on_failure;
        }

        b = unbase64_next(&x, &l);
        if (b < 0) {
            if (b == -ERANGE) {
                /* Padding is not allowed at the second character of a 4ch block either */
                r = -EINVAL;
                goto on_failure;
            }
            r = b;
            goto on_failure;
        }

        c = unbase64_next(&x, &l);
        if (c < 0) {
            if (c != -ERANGE) {
                r = c;
                goto on_failure;
            }
        }

        d = unbase64_next(&x, &l);
        if (d < 0) {
            if (d != -ERANGE) {
                r = d;
                goto on_failure;
            }
        }

        if (c == -ERANGE) { /* Padding at the third character */

            if (d != -ERANGE) { /* If the third character is padding, the fourth must be too */
                r = -EINVAL;
                goto on_failure;
            }

            /* b == 00YY0000 */
            if (b & 15) {
                r = -EINVAL;
                goto on_failure;
            }

            if (l > 0) { /* Trailing rubbish? */
                r = -ENAMETOOLONG;
                goto on_failure;
            }

            *(z++) = (uint8_t) a << 2 | (uint8_t) (b >> 4); /* XXXXXXYY */
            break;
        }

        if (d == -ERANGE) {
            /* c == 00ZZZZ00 */
            if (c & 3) {
                r = -EINVAL;
                goto on_failure;
            }

            if (l > 0) { /* Trailing rubbish? */
                r = -ENAMETOOLONG;
                goto on_failure;
            }

            *(z++) = (uint8_t) a << 2 | (uint8_t) b >> 4; /* XXXXXXYY */
            *(z++) = (uint8_t) b << 4 | (uint8_t) c >> 2; /* YYYYZZZZ */
            break;
        }

        *(z++) = (uint8_t) a << 2 | (uint8_t) b >> 4; /* XXXXXXYY */
        *(z++) = (uint8_t) b << 4 | (uint8_t) c >> 2; /* YYYYZZZZ */
        *(z++) = (uint8_t) c << 6 | (uint8_t) d;      /* ZZWWWWWW */
    }

    *z = '\0';

    NM_SET_OUT(ret_size, (gsize) (z - buf));
    NM_SET_OUT(ret, g_steal_pointer(&buf));
    return 0;

on_failure:
    if (secure)
        nm_explicit_bzero(buf, len);
    return r;
}

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

static const char *
skip_slash_or_dot(const char *p)
{
    for (; !nm_str_is_empty(p);) {
        if (p[0] == '/') {
            p += 1;
            continue;
        }
        if (p[0] == '.' && p[1] == '/') {
            p += 2;
            continue;
        }
        break;
    }
    return p;
}

int
nm_path_find_first_component(const char **p, gboolean accept_dot_dot, const char **ret)
{
    const char *q, *first, *end_first, *next;
    size_t      len;

    /* Copied from systemd's path_compare()
     * https://github.com/systemd/systemd/blob/bc85f8b51d962597360e982811e674c126850f56/src/basic/path-util.c#L809 */

    nm_assert(p);

    /* When a path is input, then returns the pointer to the first component and its length, and
     * move the input pointer to the next component or nul. This skips both over any '/'
     * immediately *before* and *after* the first component before returning.
     *
     * Examples
     *   Input:  p: "//.//aaa///bbbbb/cc"
     *   Output: p: "bbbbb///cc"
     *           ret: "aaa///bbbbb/cc"
     *           return value: 3 (== strlen("aaa"))
     *
     *   Input:  p: "aaa//"
     *   Output: p: (pointer to NUL)
     *           ret: "aaa//"
     *           return value: 3 (== strlen("aaa"))
     *
     *   Input:  p: "/", ".", ""
     *   Output: p: (pointer to NUL)
     *           ret: NULL
     *           return value: 0
     *
     *   Input:  p: NULL
     *   Output: p: NULL
     *           ret: NULL
     *           return value: 0
     *
     *   Input:  p: "(too long component)"
     *   Output: return value: -EINVAL
     *
     *   (when accept_dot_dot is false)
     *   Input:  p: "//..//aaa///bbbbb/cc"
     *   Output: return value: -EINVAL
     */

    q = *p;

    first = skip_slash_or_dot(q);
    if (nm_str_is_empty(first)) {
        *p = first;
        if (ret)
            *ret = NULL;
        return 0;
    }
    if (nm_streq(first, ".")) {
        *p = first + 1;
        if (ret)
            *ret = NULL;
        return 0;
    }

    end_first = strchrnul(first, '/');
    len       = end_first - first;

    if (len > NAME_MAX)
        return -EINVAL;
    if (!accept_dot_dot && len == 2 && first[0] == '.' && first[1] == '.')
        return -EINVAL;

    next = skip_slash_or_dot(end_first);

    *p = next + (nm_streq(next, ".") ? 1 : 0);
    if (ret)
        *ret = first;
    return len;
}

int
nm_path_compare(const char *a, const char *b)
{
    /* Copied from systemd's path_compare()
     * https://github.com/systemd/systemd/blob/bc85f8b51d962597360e982811e674c126850f56/src/basic/path-util.c#L415 */

    /* Order NULL before non-NULL */
    NM_CMP_SELF(a, b);

    /* A relative path and an absolute path must not compare as equal.
     * Which one is sorted before the other does not really matter.
     * Here a relative path is ordered before an absolute path. */
    NM_CMP_DIRECT(nm_path_is_absolute(a), nm_path_is_absolute(b));

    for (;;) {
        const char *aa, *bb;
        int         j, k;

        j = nm_path_find_first_component(&a, TRUE, &aa);
        k = nm_path_find_first_component(&b, TRUE, &bb);

        if (j < 0 || k < 0) {
            /* When one of paths is invalid, order invalid path after valid one. */
            NM_CMP_DIRECT(j < 0, k < 0);

            /* fallback to use strcmp() if both paths are invalid. */
            NM_CMP_DIRECT_STRCMP(a, b);
            return 0;
        }

        /* Order prefixes first: "/foo" before "/foo/bar" */
        if (j == 0) {
            if (k == 0)
                return 0;
            return -1;
        }
        if (k == 0)
            return 1;

        /* Alphabetical sort: "/foo/aaa" before "/foo/b" */
        NM_CMP_DIRECT_MEMCMP(aa, bb, NM_MIN(j, k));

        /* Sort "/foo/a" before "/foo/aaa" */
        NM_CMP_DIRECT(j, k);
    }
}

char *
nm_path_startswith_full(const char *path, const char *prefix, gboolean accept_dot_dot)
{
    /* Copied from systemd's path_startswith_full()
     * https://github.com/systemd/systemd/blob/bc85f8b51d962597360e982811e674c126850f56/src/basic/path-util.c#L375 */

    nm_assert(path);
    nm_assert(prefix);

    /* Returns a pointer to the start of the first component after the parts matched by
     * the prefix, iff
     * - both paths are absolute or both paths are relative,
     * and
     * - each component in prefix in turn matches a component in path at the same position.
     * An empty string will be returned when the prefix and path are equivalent.
     *
     * Returns NULL otherwise.
     */

    if ((path[0] == '/') != (prefix[0] == '/'))
        return NULL;

    for (;;) {
        const char *p, *q;
        int         r, k;

        r = nm_path_find_first_component(&path, accept_dot_dot, &p);
        if (r < 0)
            return NULL;

        k = nm_path_find_first_component(&prefix, accept_dot_dot, &q);
        if (k < 0)
            return NULL;

        if (k == 0)
            return (char *) (p ?: path);

        if (r != k)
            return NULL;

        if (strncmp(p, q, r) != 0)
            return NULL;
    }
}

char *
nm_path_simplify(char *path)
{
    bool  add_slash = false;
    char *f         = path;
    int   r;

    /* Copied from systemd's path_simplify()
     * https://github.com/systemd/systemd/blob/bc85f8b51d962597360e982811e674c126850f56/src/basic/path-util.c#L325 */

    nm_assert(path);

    /* Removes redundant inner and trailing slashes. Also removes unnecessary dots.
     * Modifies the passed string in-place.
     *
     * ///foo//./bar/.   becomes /foo/bar
     * .//./foo//./bar/. becomes foo/bar
     */

    if (path[0] == '\0')
        return path;

    if (nm_path_is_absolute(path))
        f++;

    for (const char *p = f;;) {
        const char *e;

        r = nm_path_find_first_component(&p, TRUE, &e);
        if (r == 0)
            break;

        if (add_slash)
            *f++ = '/';

        if (r < 0) {
            /* if path is invalid, then refuse to simplify remaining part. */
            memmove(f, p, strlen(p) + 1);
            return path;
        }

        memmove(f, e, r);
        f += r;

        add_slash = TRUE;
    }

    /* Special rule, if we stripped everything, we need a "." for the current directory. */
    if (f == path)
        *f++ = '.';

    *f = '\0';
    return path;
}

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

static gboolean
valid_ldh_char(char c)
{
    /* "LDH" → "Letters, digits, hyphens", as per RFC 5890, Section 2.3.1 */

    return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-';
}

/**
 * nm_hostname_is_valid:
 * @s: the hostname to check.
 * @trailing_dot: Accept trailing dot on multi-label names.
 *
 * Returns: %TRUE if valid.
 */
gboolean
nm_hostname_is_valid(const char *s, gboolean trailing_dot)
{
    unsigned    n_dots = 0;
    const char *p;
    gboolean    dot;
    gboolean    hyphen;

    /* Copied from systemd's hostname_is_valid()
     * https://github.com/systemd/systemd/blob/bc85f8b51d962597360e982811e674c126850f56/src/basic/hostname-util.c#L85 */

    /* Check if s looks like a valid hostname or FQDN. This does not do full DNS validation, but only
     * checks if the name is composed of allowed characters and the length is not above the maximum
     * allowed by Linux (c.f. dns_name_is_valid()). A trailing dot is allowed if
     * VALID_HOSTNAME_TRAILING_DOT flag is set and at least two components are present in the name. Note
     * that due to the restricted charset and length this call is substantially more conservative than
     * dns_name_is_valid(). Doesn't accept empty hostnames, hostnames with leading dots, and hostnames
     * with multiple dots in a sequence. Doesn't allow hyphens at the beginning or end of label. */

    if (nm_str_is_empty(s))
        return FALSE;

    for (p = s, dot = hyphen = TRUE; *p; p++)
        if (*p == '.') {
            if (dot || hyphen)
                return FALSE;

            dot    = TRUE;
            hyphen = FALSE;
            n_dots++;

        } else if (*p == '-') {
            if (dot)
                return FALSE;

            dot    = FALSE;
            hyphen = TRUE;

        } else {
            if (!valid_ldh_char(*p))
                return FALSE;

            dot    = FALSE;
            hyphen = FALSE;
        }

    if (dot && (n_dots < 2 || !trailing_dot))
        return FALSE;
    if (hyphen)
        return FALSE;

    /* Note that HOST_NAME_MAX is 64 on Linux, but DNS allows domain names up to
     * 255 characters */
    if (p - s > NM_HOST_NAME_MAX)
        return FALSE;

    return TRUE;
}

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

typedef struct {
    GTask                    *task;
    GSource                  *source_timeout;
    GSource                  *source_next_poll;
    GMainContext             *context;
    GCancellable             *internal_cancellable;
    NMUtilsPollProbeStartFcn  probe_start_fcn;
    NMUtilsPollProbeFinishFcn probe_finish_fcn;
    gpointer                  probe_user_data;
    gulong                    cancellable_id;
    gint64                    last_poll_start_ms;
    int                       sleep_timeout_ms;
    int                       ratelimit_timeout_ms;
    bool                      completed : 1;
} PollTaskData;

static void
_poll_task_data_free(gpointer data)
{
    PollTaskData *poll_task_data = data;

    nm_assert(G_IS_TASK(poll_task_data->task));
    nm_assert(!poll_task_data->source_next_poll);
    nm_assert(!poll_task_data->source_timeout);
    nm_assert(poll_task_data->cancellable_id == 0);

    g_main_context_unref(poll_task_data->context);

    nm_g_slice_free(poll_task_data);
}

static void
_poll_return(PollTaskData *poll_task_data, GError *error_take)
{
    nm_clear_g_source_inst(&poll_task_data->source_next_poll);
    nm_clear_g_source_inst(&poll_task_data->source_timeout);
    nm_clear_g_cancellable_disconnect(g_task_get_cancellable(poll_task_data->task),
                                      &poll_task_data->cancellable_id);

    nm_clear_g_cancellable(&poll_task_data->internal_cancellable);

    if (error_take)
        g_task_return_error(poll_task_data->task, g_steal_pointer(&error_take));
    else
        g_task_return_boolean(poll_task_data->task, TRUE);

    g_object_unref(poll_task_data->task);
}

static gboolean _poll_start_cb(gpointer user_data);

static void
_poll_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    PollTaskData                     *poll_task_data = user_data;
    _nm_unused gs_unref_object GTask *task =
        poll_task_data->task; /* balance ref from _poll_start_cb() */
    gs_free_error GError *error = NULL;
    gint64                now_ms;
    gint64                wait_ms;
    gboolean              is_finished;

    is_finished =
        poll_task_data->probe_finish_fcn(source, result, poll_task_data->probe_user_data, &error);

    if (nm_utils_error_is_cancelled(error)) {
        /* we already handle this differently. Nothing to do. */
        return;
    }

    if (error || is_finished) {
        _poll_return(poll_task_data, g_steal_pointer(&error));
        return;
    }

    now_ms = nm_utils_get_monotonic_timestamp_msec();
    if (poll_task_data->ratelimit_timeout_ms > 0)
        wait_ms =
            (poll_task_data->last_poll_start_ms + poll_task_data->ratelimit_timeout_ms) - now_ms;
    else
        wait_ms = 0;
    if (poll_task_data->sleep_timeout_ms > 0)
        wait_ms = NM_MAX(wait_ms, poll_task_data->sleep_timeout_ms);

    poll_task_data->source_next_poll =
        nm_g_source_attach(nm_g_timeout_source_new(NM_MAX(1, wait_ms),
                                                   G_PRIORITY_DEFAULT,
                                                   _poll_start_cb,
                                                   poll_task_data,
                                                   NULL),
                           poll_task_data->context);
}

static gboolean
_poll_start_cb(gpointer user_data)
{
    PollTaskData *poll_task_data = user_data;

    nm_clear_g_source_inst(&poll_task_data->source_next_poll);

    poll_task_data->last_poll_start_ms = nm_utils_get_monotonic_timestamp_msec();

    g_object_ref(poll_task_data->task); /* balanced by _poll_done_cb() */

    poll_task_data->probe_start_fcn(poll_task_data->internal_cancellable,
                                    poll_task_data->probe_user_data,
                                    _poll_done_cb,
                                    poll_task_data);

    return G_SOURCE_CONTINUE;
}

static gboolean
_poll_timeout_cb(gpointer user_data)
{
    PollTaskData *poll_task_data = user_data;

    _poll_return(poll_task_data, nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "timeout expired"));
    return G_SOURCE_CONTINUE;
}

static void
_poll_cancelled_cb(GObject *object, gpointer user_data)
{
    PollTaskData *poll_task_data = user_data;
    GError       *error          = NULL;

    nm_clear_g_signal_handler(g_task_get_cancellable(poll_task_data->task),
                              &poll_task_data->cancellable_id);
    nm_utils_error_set_cancelled(&error, FALSE, NULL);
    _poll_return(poll_task_data, error);
}

/**
 * nm_utils_poll:
 * @poll_timeout_ms: if >= 0, then this is the overall timeout for how long we poll.
 *   When this timeout expires, the request completes with failure (and error set).
 * @ratelimit_timeout_ms: if > 0, we ratelimit the starts from one prope_start_fcn
 *   call to the next. We will wait at least this time between two consecutive polls.
 * @sleep_timeout_ms: if > 0, then we wait after a probe finished this timeout
 *   before the next. Together with @ratelimit_timeout_ms this determines how
 *   frequently we probe. We will wait at least this time between the end of the
 *   previous poll and the next one.
 * @probe_register_object_fcn: (allow-none): called by nm_utils_poll()
 *   synchronously, with the new, internal GTask instance. The purpose of this
 *   callback is a bit obscure, you may want to pass NULL here. It's used by some
 *   caller to register a weak pointer on the internal GTask instance to track
 *   the lifetime of the operation.
 * @probe_start_fcn: used to start a (asynchronous) probe. A probe must be
 *   completed by calling the provided callback. While a probe is in progress, we
 *   will not start another. The function is called the first time on an idle
 *   handler, afterwards it gets called again on each timeout for polling.
 * @probe_finish_fcn: will be called from the callback of @probe_start_fcn. If the
 *   function returns %TRUE (polling done) or an error, polling stops. Otherwise,
 *   another poll will be started.
 * @probe_user_data: user_data for the probe functions.
 * @cancellable: cancellable for polling.
 * @callback: when polling completes.
 * @user_data: for @callback.
 *
 * This uses the current g_main_context_get_thread_default() for scheduling
 * actions.
 */
void
nm_utils_poll(int                               poll_timeout_ms,
              int                               ratelimit_timeout_ms,
              int                               sleep_timeout_ms,
              NMUtilsPollProbeRegisterObjectFcn probe_register_object_fcn,
              NMUtilsPollProbeStartFcn          probe_start_fcn,
              NMUtilsPollProbeFinishFcn         probe_finish_fcn,
              gpointer                          probe_user_data,
              GCancellable                     *cancellable,
              GAsyncReadyCallback               callback,
              gpointer                          user_data)
{
    PollTaskData *poll_task_data;

    poll_task_data  = g_slice_new(PollTaskData);
    *poll_task_data = (PollTaskData) {
        .task             = nm_g_task_new(NULL, cancellable, nm_utils_poll, callback, user_data),
        .probe_start_fcn  = probe_start_fcn,
        .probe_finish_fcn = probe_finish_fcn,
        .probe_user_data  = probe_user_data,
        .completed        = FALSE,
        .context          = g_main_context_ref_thread_default(),
        .sleep_timeout_ms = sleep_timeout_ms,
        .ratelimit_timeout_ms = ratelimit_timeout_ms,
        .internal_cancellable = g_cancellable_new(),
    };

    g_task_set_task_data(poll_task_data->task, poll_task_data, _poll_task_data_free);

    if (probe_register_object_fcn)
        probe_register_object_fcn((GObject *) poll_task_data->task, probe_user_data);

    if (poll_timeout_ms >= 0) {
        poll_task_data->source_timeout =
            nm_g_source_attach(nm_g_timeout_source_new(poll_timeout_ms,
                                                       G_PRIORITY_DEFAULT,
                                                       _poll_timeout_cb,
                                                       poll_task_data,
                                                       NULL),
                               poll_task_data->context);
    }

    poll_task_data->source_next_poll = nm_g_source_attach(
        nm_g_idle_source_new(G_PRIORITY_DEFAULT_IDLE, _poll_start_cb, poll_task_data, NULL),
        poll_task_data->context);

    if (cancellable) {
        gulong signal_id;

        signal_id = g_cancellable_connect(cancellable,
                                          G_CALLBACK(_poll_cancelled_cb),
                                          poll_task_data,
                                          NULL);
        if (signal_id == 0) {
            /* the request is already cancelled. Return. */
            return;
        }
        poll_task_data->cancellable_id = signal_id;
    }
}

/**
 * nm_utils_poll_finish:
 * @result: the GAsyncResult from the GAsyncReadyCallback callback.
 * @probe_user_data: the user data provided to nm_utils_poll().
 * @error: the failure code.
 *
 * Returns: %TRUE if the polling completed with success. In that case,
 *   the error won't be set.
 *   If the request was cancelled, this is indicated by @error and
 *   %FALSE will be returned.
 *   If the probe returned a failure, this returns %FALSE and the error
 *   provided by @probe_finish_fcn.
 *   If the request times out, this returns %FALSE with error set.
 *   Error is always set if (and only if) the function returns %FALSE.
 */
gboolean
nm_utils_poll_finish(GAsyncResult *result, gpointer *probe_user_data, GError **error)
{
    GTask        *task;
    PollTaskData *poll_task_data;

    g_return_val_if_fail(nm_g_task_is_valid(result, NULL, nm_utils_poll), FALSE);
    g_return_val_if_fail(!error || !*error, FALSE);

    task = G_TASK(result);

    if (probe_user_data) {
        poll_task_data = g_task_get_task_data(task);
        NM_SET_OUT(probe_user_data, poll_task_data->probe_user_data);
    }

    return g_task_propagate_boolean(task, error);
}

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

void
nm_utils_env_var_encode_name(const char *key, GString *str_buffer)
{
    gsize i;

    nm_assert(key);
    nm_assert(str_buffer);

    for (i = 0; key[i]; i++) {
        char ch = key[i];

        /* we encode the key in only upper case letters, digits, and underscore.
         * As we expect lower-case letters to be more common, we encode lower-case
         * letters as upper case, and upper-case letters with a leading underscore. */

        if (ch >= '0' && ch <= '9') {
            g_string_append_c(str_buffer, ch);
            continue;
        }
        if (ch >= 'a' && ch <= 'z') {
            g_string_append_c(str_buffer, ch - 'a' + 'A');
            continue;
        }
        if (ch == '.') {
            g_string_append(str_buffer, "__");
            continue;
        }
        if (ch >= 'A' && ch <= 'Z') {
            g_string_append_c(str_buffer, '_');
            g_string_append_c(str_buffer, ch);
            continue;
        }
        g_string_append_printf(str_buffer, "_%03o", (unsigned) ch);
    }
}

gboolean
nm_utils_env_var_decode_name(const char *name, GString *str_buffer)
{
    gsize i;

    nm_assert(name);
    nm_assert(str_buffer);

    if (!name[0])
        return FALSE;

    for (i = 0; name[i];) {
        char ch = name[i];

        if (ch >= '0' && ch <= '9') {
            g_string_append_c(str_buffer, ch);
            i++;
            continue;
        }
        if (ch >= 'A' && ch <= 'Z') {
            g_string_append_c(str_buffer, ch - 'A' + 'a');
            i++;
            continue;
        }

        if (ch == '_') {
            ch = name[i + 1];
            if (ch == '_') {
                g_string_append_c(str_buffer, '.');
                i += 2;
                continue;
            }
            if (ch >= 'A' && ch <= 'Z') {
                g_string_append_c(str_buffer, ch);
                i += 2;
                continue;
            }
            if (ch >= '0' && ch <= '7') {
                char     ch2, ch3;
                unsigned v;

                ch2 = name[i + 2];
                if (!(ch2 >= '0' && ch2 <= '7'))
                    return FALSE;

                ch3 = name[i + 3];
                if (!(ch3 >= '0' && ch3 <= '7'))
                    return FALSE;

#define OCTAL_VALUE(ch) ((unsigned) ((ch) - '0'))
                v = (OCTAL_VALUE(ch) << 6) + (OCTAL_VALUE(ch2) << 3) + OCTAL_VALUE(ch3);
                if (v > 0xFF || v == 0)
                    return FALSE;
                ch = (char) v;
                if ((ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || (ch == '.')
                    || (ch >= 'a' && ch <= 'z')) {
                    /* such characters are not expected to be encoded via
                     * octal representation. The encoding is invalid. */
                    return FALSE;
                }
                g_string_append_c(str_buffer, ch);
                i += 4;
                continue;
            }
            return FALSE;
        }

        return FALSE;
    }

    return TRUE;
}
