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

#include "libnm-core-impl/nm-default-libnm-core.h"

#include "nm-utils.h"

#include <stdlib.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <net/if_arp.h>
#include <libintl.h>
#include <gmodule.h>
#include <sys/stat.h>
#include <linux/pkt_sched.h>
#include <linux/if_infiniband.h>

#include "libnm-glib-aux/nm-io-utils.h"
#include "libnm-glib-aux/nm-uuid.h"
#include "libnm-glib-aux/nm-json-aux.h"
#include "libnm-glib-aux/nm-str-buf.h"
#include "libnm-glib-aux/nm-enum-utils.h"
#include "libnm-glib-aux/nm-time-utils.h"
#include "libnm-glib-aux/nm-secret-utils.h"
#include "libnm-core-aux-intern/nm-common-macros.h"
#include "nm-utils-private.h"
#include "nm-setting-private.h"
#include "nm-setting-bond.h"
#include "nm-setting-bond-port.h"
#include "nm-setting-bridge.h"
#include "nm-setting-bridge-port.h"
#include "nm-setting-infiniband.h"
#include "nm-setting-ip6-config.h"
#include "nm-setting-team.h"
#include "nm-setting-vlan.h"
#include "nm-setting-wired.h"
#include "nm-setting-wireless.h"
#include "nm-errors.h"

/**
 * SECTION:nm-utils
 * @short_description: Utility functions
 *
 * A collection of utility functions for working with SSIDs, IP addresses, Wi-Fi
 * access points and devices, among other things.
 */

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

/**
 * NMUtilsPredicateStr:
 * @str: the name to check.
 *
 * This function takes a string argument and returns either %TRUE or %FALSE.
 * It is a general purpose predicate, for example used by nm_setting_option_clear_by_name().
 *
 * Returns: %TRUE if the predicate function matches.
 *
 * Since: 1.26
 */

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

struct _NMSockAddrEndpoint {
    const char *host;
    guint16     port;
    guint       refcount;
    char        endpoint[];
};

static gboolean
NM_IS_SOCK_ADDR_ENDPOINT(const NMSockAddrEndpoint *self)
{
    return self && self->refcount > 0;
}

static const char *
_parse_endpoint(char *str, guint16 *out_port)
{
    char       *s;
    const char *s_port;
    gint16      port;

    /* Like
     * - https://git.zx2c4.com/WireGuard/tree/src/tools/config.c?id=5e99a6d43fe2351adf36c786f5ea2086a8fe7ab8#n192
     * - https://github.com/systemd/systemd/blob/911649fdd43f3a9158b847947724a772a5a45c34/src/network/netdev/wireguard.c#L614
     */

    g_strstrip(str);

    if (!str[0])
        return NULL;

    if (str[0] == '[') {
        str++;
        s = strchr(str, ']');
        if (!s)
            return NULL;
        if (s == str)
            return NULL;
        if (s[1] != ':')
            return NULL;
        if (!s[2])
            return NULL;
        *s     = '\0';
        s_port = &s[2];
    } else {
        s = strrchr(str, ':');
        if (!s)
            return NULL;
        if (s == str)
            return NULL;
        if (!s[1])
            return NULL;
        *s     = '\0';
        s_port = &s[1];
    }

    if (!NM_STRCHAR_ALL(s_port, ch, (ch >= '0' && ch <= '9')))
        return NULL;

    port = _nm_utils_ascii_str_to_int64(s_port, 10, 1, G_MAXUINT16, 0);
    if (port == 0)
        return NULL;

    *out_port = port;
    return str;
}

/**
 * nm_sock_addr_endpoint_new:
 * @endpoint: the endpoint string.
 *
 * This function cannot fail, even if the @endpoint is invalid.
 * The reason is to allow NMSockAddrEndpoint also to be used
 * for tracking invalid endpoints. Use nm_sock_addr_endpoint_get_host()
 * to determine whether the endpoint is valid.
 *
 * Returns: (transfer full): the new #NMSockAddrEndpoint endpoint.
 */
NMSockAddrEndpoint *
nm_sock_addr_endpoint_new(const char *endpoint)
{
    NMSockAddrEndpoint *ep;
    gsize               l_endpoint;
    gsize               l_host = 0;
    gsize               i;
    gs_free char       *host_clone = NULL;
    const char         *host;
    guint16             port = 0;

    g_return_val_if_fail(endpoint, NULL);

    l_endpoint = strlen(endpoint) + 1;

    host = _parse_endpoint(nm_strndup_a(200, endpoint, l_endpoint - 1, &host_clone), &port);

    if (host)
        l_host = strlen(host) + 1;

    ep           = g_malloc(sizeof(NMSockAddrEndpoint) + l_endpoint + l_host);
    ep->refcount = 1;
    memcpy(ep->endpoint, endpoint, l_endpoint);
    if (host) {
        i = l_endpoint;
        memcpy(&ep->endpoint[i], host, l_host);
        ep->host = &ep->endpoint[i];
        ep->port = port;
    } else {
        ep->host = NULL;
        ep->port = 0;
    }
    return ep;
}

/**
 * nm_sock_addr_endpoint_ref:
 * @self: (nullable): the #NMSockAddrEndpoint
 */
NMSockAddrEndpoint *
nm_sock_addr_endpoint_ref(NMSockAddrEndpoint *self)
{
    if (!self)
        return NULL;

    g_return_val_if_fail(NM_IS_SOCK_ADDR_ENDPOINT(self), NULL);

    nm_assert(self->refcount < G_MAXUINT);

    self->refcount++;
    return self;
}

/**
 * nm_sock_addr_endpoint_unref:
 * @self: (nullable): the #NMSockAddrEndpoint
 */
void
nm_sock_addr_endpoint_unref(NMSockAddrEndpoint *self)
{
    if (!self)
        return;

    g_return_if_fail(NM_IS_SOCK_ADDR_ENDPOINT(self));

    if (--self->refcount == 0)
        g_free(self);
}

/**
 * nm_sock_addr_endpoint_get_endpoint:
 * @self: the #NMSockAddrEndpoint
 *
 * Gives the endpoint string. Since #NMSockAddrEndpoint's only
 * information is the endpoint string, this can be used for comparing
 * to instances for equality and order them lexically.
 *
 * Returns: (transfer none): the endpoint.
 */
const char *
nm_sock_addr_endpoint_get_endpoint(NMSockAddrEndpoint *self)
{
    g_return_val_if_fail(NM_IS_SOCK_ADDR_ENDPOINT(self), NULL);

    return self->endpoint;
}

/**
 * nm_sock_addr_endpoint_get_host:
 * @self: the #NMSockAddrEndpoint
 *
 * Returns: (transfer none): the parsed host part of the endpoint.
 *   If the endpoint is invalid, %NULL will be returned.
 */
const char *
nm_sock_addr_endpoint_get_host(NMSockAddrEndpoint *self)
{
    g_return_val_if_fail(NM_IS_SOCK_ADDR_ENDPOINT(self), NULL);

    return self->host;
}

/**
 * nm_sock_addr_endpoint_get_port:
 * @self: the #NMSockAddrEndpoint
 *
 * Returns: the parsed port part of the endpoint (the service).
 *   If the endpoint is invalid, -1 will be returned.
 */
gint32
nm_sock_addr_endpoint_get_port(NMSockAddrEndpoint *self)
{
    g_return_val_if_fail(NM_IS_SOCK_ADDR_ENDPOINT(self), -1);

    return self->host ? (int) self->port : -1;
}

gboolean
nm_sock_addr_endpoint_get_fixed_sockaddr(NMSockAddrEndpoint *self, gpointer sockaddr)
{
    int         addr_family;
    NMIPAddr    addrbin;
    const char *s;
    guint       scope_id = 0;

    g_return_val_if_fail(NM_IS_SOCK_ADDR_ENDPOINT(self), FALSE);
    g_return_val_if_fail(sockaddr, FALSE);

    if (!self->host)
        return FALSE;

    if (nm_inet_parse_bin(AF_UNSPEC, self->host, &addr_family, &addrbin))
        goto good;

    /* See if there is an IPv6 scope-id...
     *
     * Note that it does not make sense to persist connection profiles to disk,
     * that refenrence a scope-id (because the interface's ifindex changes on
     * reboot). However, we also support runtime only changes like `nmcli device modify`
     * where nothing is persisted to disk. At least in that case, passing a scope-id
     * might be reasonable. So, parse that too. */
    s = strchr(self->host, '%');
    if (!s)
        return FALSE;

    if (s[1] == '\0' || !NM_STRCHAR_ALL(&s[1], ch, (ch >= '0' && ch <= '9')))
        return FALSE;

    scope_id = _nm_utils_ascii_str_to_int64(&s[1], 10, 0, G_MAXINT32, G_MAXUINT);
    if (scope_id == G_MAXUINT && errno)
        return FALSE;

    {
        gs_free char *tmp_str = NULL;
        const char   *host_part;

        host_part = nm_strndup_a(200, self->host, s - self->host, &tmp_str);
        if (nm_inet_parse_bin(AF_INET6, host_part, &addr_family, &addrbin))
            goto good;
    }

    return FALSE;

good:
    switch (addr_family) {
    case AF_INET:
        *((struct sockaddr_in *) sockaddr) = (struct sockaddr_in) {
            .sin_family = AF_INET,
            .sin_addr   = addrbin.addr4_struct,
            .sin_port   = htons(self->port),
        };
        return TRUE;
    case AF_INET6:
        *((struct sockaddr_in6 *) sockaddr) = (struct sockaddr_in6) {
            .sin6_family   = AF_INET6,
            .sin6_addr     = addrbin.addr6,
            .sin6_port     = htons(self->port),
            .sin6_scope_id = scope_id,
            .sin6_flowinfo = 0,
        };
        return TRUE;
    }

    return FALSE;
}

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

typedef const char *const StrvArray4Type[4];

#define LL(l, ...)                    \
    {                                 \
        .name  = l,                   \
        .value = {__VA_ARGS__, NULL}, \
    }

/* 5-letter language codes */
static _NM_UTILS_STRING_TABLE_LOOKUP_DEFINE(
    _iso_lang_entries_5_lookup,
    StrvArray4Type,
    const char *const *,
    { nm_assert(name); },
    { return NULL; },
    ,
    LL("zh_cn", "euc-cn", "gb2312", "gb18030"), /* Simplified Chinese, PRC */
    LL("zh_hk", "big5", "euc-tw", "big5-hkcs"), /* Traditional Chinese, Hong Kong */
    LL("zh_mo", "big5", "euc-tw"),              /* Traditional Chinese, Macau */
    LL("zh_sg", "euc-cn", "gb2312", "gb18030"), /* Simplified Chinese, Singapore */
    LL("zh_tw", "big5", "euc-tw"),              /* Traditional Chinese, Taiwan */
);

/* 2-letter language codes; we don't care about the other 3 in this table */
static _NM_UTILS_STRING_TABLE_LOOKUP_DEFINE(
    _iso_lang_entries_2_lookup,
    StrvArray4Type,
    const char *const *,
    { nm_assert(name); },
    { return NULL; },
    ,
    LL("ar", "iso-8859-6", "windows-1256"),           /* Arabic */
    LL("be", "koi8-r", "windows-1251", "iso-8859-5"), /* Cyrillic, Belorussian */
    LL("bg", "windows-1251", "koi8-r", "iso-8859-5"), /* Cyrillic, Bulgarian */
    LL("cs", "iso-8859-2", "windows-1250"),           /* Central European, Czech */
    LL("el", "iso-8859-7", "windows-1253"),           /* Greek */
    LL("et", "iso-8859-4", "windows-1257"),           /* Baltic, Estonian */
    LL("he", "iso-8859-8", "windows-1255"),           /* Hebrew */
    LL("hr", "iso-8859-2", "windows-1250"),           /* Central European, Croatian */
    LL("hu", "iso-8859-2", "windows-1250"),           /* Central European, Hungarian */
    LL("iw", "iso-8859-8", "windows-1255"),           /* Hebrew */
    LL("ja", "euc-jp", "shift_jis", "iso-2022-jp"),   /* Japanese */
    LL("ko", "euc-kr", "iso-2022-kr", "johab"),       /* Korean */
    LL("lt", "iso-8859-4", "windows-1257"),           /* Baltic, Lithuanian */
    LL("lv", "iso-8859-4", "windows-1257"),           /* Baltic, Latvian */
    LL("mk", "koi8-r", "windows-1251", "iso-8859-5"), /* Cyrillic, Macedonian */
    LL("pl", "iso-8859-2", "windows-1250"),           /* Central European, Polish */
    LL("ro", "iso-8859-2", "windows-1250"),           /* Central European, Romanian */
    LL("ru", "koi8-r", "windows-1251", "iso-8859-5"), /* Cyrillic, Russian */
    LL("sh", "iso-8859-2", "windows-1250"),           /* Central European, Serbo-Croatian */
    LL("sk", "iso-8859-2", "windows-1250"),           /* Central European, Slovakian */
    LL("sl", "iso-8859-2", "windows-1250"),           /* Central European, Slovenian */
    LL("sr", "koi8-r", "windows-1251", "iso-8859-5"), /* Cyrillic, Serbian */
    LL("th", "iso-8859-11", "windows-874"),           /* Thai */
    LL("tr", "iso-8859-9", "windows-1254"),           /* Turkish */
    LL("uk", "koi8-u", "koi8-r", "windows-1251"),     /* Cyrillic, Ukrainian */
);

static const char *const *
_system_encodings_for_lang(const char *lang)
{
    char               tmp_lang[3];
    const char *const *e;

    nm_assert(lang);

    if (lang[0] == '\0' || lang[1] == '\0') {
        /* need at least two characters. */
        nm_assert(!_iso_lang_entries_5_lookup(lang));
        nm_assert(!_iso_lang_entries_2_lookup(lang));
        return NULL;
    }

    if (lang[2] != '\0') {
        nm_assert(!_iso_lang_entries_2_lookup(lang));

        if (lang[3] != '\0' && lang[4] != '\0' && lang[5] == '\0') {
            /* lang is 5 characters long. Try it. */
            if ((e = _iso_lang_entries_5_lookup(lang)))
                return e;
        } else
            nm_assert(!_iso_lang_entries_5_lookup(lang));

        /* extract the first 2 characters and ignore the rest. */
        tmp_lang[0] = lang[0];
        tmp_lang[1] = lang[1];
        tmp_lang[2] = '\0';
        lang        = tmp_lang;
    }

    if ((e = _iso_lang_entries_2_lookup(lang)))
        return e;

    return NULL;
}

const char *const *
nmtst_system_encodings_for_lang(const char *lang)
{
    return _system_encodings_for_lang(lang);
}

static const char *const *
_system_encodings_get_default(void)
{
    static gsize       init_once = 0;
    static const char *default_encodings[4];

    if (g_once_init_enter(&init_once)) {
        const char *e_default = NULL;
        int         i;

        g_get_charset(&e_default);

        i = 0;
        if (e_default)
            default_encodings[i++] = e_default;
        if (!nm_streq0(e_default, "iso-8859-1"))
            default_encodings[i++] = "iso-8859-1";
        if (!nm_streq0(e_default, "windows-1251"))
            default_encodings[i++] = "windows-1251";
        default_encodings[i++] = NULL;
        nm_assert(i <= G_N_ELEMENTS(default_encodings));

        g_once_init_leave(&init_once, 1);
    }

    return default_encodings;
}

const char *const *
nmtst_system_encodings_get_default(void)
{
    return _system_encodings_get_default();
}

static const char *const *
_system_encodings_get(void)
{
    static const char *const *cached = NULL;
    const char *const        *e;

again:
    if (!(e = g_atomic_pointer_get(&cached))) {
        const char *lang;

        /* Use environment variables as encoding hint */
        lang = getenv("LC_ALL") ?: getenv("LC_CTYPE") ?: getenv("LANG");

        if (lang) {
            gs_free char *lang_down = NULL;
            char         *dot;

            lang_down = g_ascii_strdown(lang, -1);
            if ((dot = strchr(lang_down, '.')))
                *dot = '\0';
            e = _system_encodings_for_lang(lang_down);
        }

        if (!e)
            e = _system_encodings_get_default();

        /* in any case, @e is now a static buffer, that we may cache. */
        nm_assert(e);

        if (!g_atomic_pointer_compare_and_exchange(&cached, NULL, e))
            goto again;
    }

    return e;
}

const char *const *
nmtst_system_encodings_get(void)
{
    return _system_encodings_get();
}

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

static void __attribute__((constructor))
_nm_utils_init(void)
{
    static int initialized = 0;

    if (g_atomic_int_get(&initialized) != 0)
        return;

    /* we don't expect this code to run multiple times, nor on multiple threads.
     *
     * In practice, it would not be a problem if two threads concurrently try to
     * run the initialization code below, all code below itself is thread-safe,
     * Hence, a poor-man guard "initialized" above is more than sufficient,
     * although it does not guarantee that the code is not run concurrently. */

    bindtextdomain(GETTEXT_PACKAGE, NMLOCALEDIR);
    bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");

    _nm_dbus_errors_init();

    g_atomic_int_set(&initialized, 1);
}

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

gboolean _nm_utils_is_manager_process;

/* ssid helpers */

/**
 * nm_utils_ssid_to_utf8:
 * @ssid: (array length=len): pointer to a buffer containing the SSID data
 * @len: length of the SSID data in @ssid
 *
 * Wi-Fi SSIDs are byte arrays, they are _not_ strings.  Thus, an SSID may
 * contain embedded NULLs and other unprintable characters.  Often it is
 * useful to print the SSID out for debugging purposes, but that should be the
 * _only_ use of this function.  Do not use this function for any persistent
 * storage of the SSID, since the printable SSID returned from this function
 * cannot be converted back into the real SSID of the access point.
 *
 * This function does almost everything humanly possible to convert the input
 * into a printable UTF-8 string, using roughly the following procedure:
 *
 * 1) if the input data is already UTF-8 safe, no conversion is performed
 * 2) attempts to get the current system language from the LANG environment
 *    variable, and depending on the language, uses a table of alternative
 *    encodings to try.  For example, if LANG=hu_HU, the table may first try
 *    the ISO-8859-2 encoding, and if that fails, try the Windows-1250 encoding.
 *    If all fallback encodings fail, replaces non-UTF-8 characters with '?'.
 * 3) If the system language was unable to be determined, falls back to the
 *    ISO-8859-1 encoding, then to the Windows-1251 encoding.
 * 4) If step 3 fails, replaces non-UTF-8 characters with '?'.
 *
 * Again, this function should be used for debugging and display purposes
 * _only_.
 *
 * Returns: (transfer full): an allocated string containing a UTF-8
 * representation of the SSID, which must be freed by the caller using g_free().
 * Returns %NULL on errors.
 **/
char *
nm_utils_ssid_to_utf8(const guint8 *ssid, gsize len)
{
    const char *const *encodings;
    const char *const *e;
    char              *converted = NULL;

    g_return_val_if_fail(ssid != NULL, NULL);

    if (g_utf8_validate((const char *) ssid, len, NULL))
        return g_strndup((const char *) ssid, len);

    encodings = _system_encodings_get();

    for (e = encodings; *e; e++) {
        converted = g_convert((const char *) ssid, len, "UTF-8", *e, NULL, NULL, NULL);
        if (converted)
            break;
    }

    if (!converted) {
        converted = g_convert_with_fallback((const char *) ssid,
                                            len,
                                            "UTF-8",
                                            encodings[0],
                                            "?",
                                            NULL,
                                            NULL,
                                            NULL);
    }

    if (!converted) {
        /* If there is still no converted string, the SSID probably
         * contains characters not valid in the current locale. Convert
         * the string to ASCII instead.
         */

        /* Use the printable range of 0x20-0x7E */
        char *valid_chars = " !\"#$%&'()*+,-./0123456789:;<=>?@"
                            "ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`"
                            "abcdefghijklmnopqrstuvwxyz{|}~";

        converted = g_strndup((const char *) ssid, len);
        g_strcanon(converted, valid_chars, '?');
    }

    return converted;
}

char *
_nm_utils_ssid_to_utf8(GBytes *ssid)
{
    const guint8 *p;
    gsize         l;

    g_return_val_if_fail(ssid, NULL);

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

/* Shamelessly ripped from the Linux kernel ieee80211 stack */
/**
 * nm_utils_is_empty_ssid:
 * @ssid: (array length=len): pointer to a buffer containing the SSID data
 * @len: length of the SSID data in @ssid
 *
 * Different manufacturers use different mechanisms for not broadcasting the
 * AP's SSID.  This function attempts to detect blank/empty SSIDs using a
 * number of known SSID-cloaking methods.
 *
 * Returns: %TRUE if the SSID is "empty", %FALSE if it is not
 **/
gboolean
nm_utils_is_empty_ssid(const guint8 *ssid, gsize len)
{
    return _nm_utils_is_empty_ssid_arr(ssid, len);
}

/**
 * nm_utils_escape_ssid:
 * @ssid: (array length=len): pointer to a buffer containing the SSID data
 * @len: length of the SSID data in @ssid
 *
 * This function does a quick printable character conversion of the SSID, simply
 * replacing embedded NULLs and non-printable characters with the hexadecimal
 * representation of that character.  Intended for debugging only, should not
 * be used for display of SSIDs.
 *
 * Warning: this function uses a static buffer. It is not thread-safe. Don't
 *   use this function.
 *
 * Returns: pointer to the escaped SSID, which uses an internal static buffer
 * and will be overwritten by subsequent calls to this function
 *
 * Deprecated: 1.46: use nm_utils_ssid_to_utf8() or nm_utils_bin2hexstr().
 **/
const char *
nm_utils_escape_ssid(const guint8 *ssid, gsize len)
{
    static char   escaped[NM_IW_ESSID_MAX_SIZE * 2 + 1];
    const guint8 *s = ssid;
    char         *d = escaped;

    if (nm_utils_is_empty_ssid(ssid, len)) {
        memcpy(escaped, "<hidden>", sizeof("<hidden>"));
        return escaped;
    }

    len = NM_MIN(len, (guint32) NM_IW_ESSID_MAX_SIZE);
    while (len--) {
        if (*s == '\0') {
            *d++ = '\\';
            *d++ = '0';
            s++;
        } else {
            *d++ = *s++;
        }
    }
    *d = '\0';
    return escaped;
}

/**
 * nm_utils_same_ssid:
 * @ssid1: (array length=len1): the first SSID to compare
 * @len1: length of the SSID data in @ssid1
 * @ssid2: (array length=len2): the second SSID to compare
 * @len2: length of the SSID data in @ssid2
 * @ignore_trailing_null: %TRUE to ignore one trailing NULL byte
 *
 * Earlier versions of the Linux kernel added a NULL byte to the end of the
 * SSID to enable easy printing of the SSID on the console or in a terminal,
 * but this behavior was problematic (SSIDs are simply byte arrays, not strings)
 * and thus was changed.  This function compensates for that behavior at the
 * cost of some compatibility with odd SSIDs that may legitimately have trailing
 * NULLs, even though that is functionally pointless.
 *
 * Returns: %TRUE if the SSIDs are the same, %FALSE if they are not
 **/
gboolean
nm_utils_same_ssid(const guint8 *ssid1,
                   gsize         len1,
                   const guint8 *ssid2,
                   gsize         len2,
                   gboolean      ignore_trailing_null)
{
    g_return_val_if_fail(ssid1 != NULL || len1 == 0, FALSE);
    g_return_val_if_fail(ssid2 != NULL || len2 == 0, FALSE);

    if (ssid1 == ssid2 && len1 == len2)
        return TRUE;
    if (!ssid1 || !ssid2)
        return FALSE;

    if (ignore_trailing_null) {
        if (len1 && ssid1[len1 - 1] == '\0')
            len1--;
        if (len2 && ssid2[len2 - 1] == '\0')
            len2--;
    }

    if (len1 != len2)
        return FALSE;

    return memcmp(ssid1, ssid2, len1) == 0 ? TRUE : FALSE;
}

gboolean
_nm_utils_string_slist_validate(GSList *list, const char **valid_values)
{
    GSList *iter;

    for (iter = list; iter; iter = iter->next) {
        if (!g_strv_contains(valid_values, (char *) iter->data))
            return FALSE;
    }

    return TRUE;
}

/**
 * _nm_utils_hash_values_to_slist:
 * @hash: a #GHashTable
 *
 * Utility function to iterate over a hash table and return
 * its values as a #GSList.
 *
 * Returns: (element-type gpointer) (transfer container): a newly allocated #GSList
 * containing the values of the hash table. The caller must free the
 * returned list with g_slist_free(). The hash values are not owned
 * by the returned list.
 **/
GSList *
_nm_utils_hash_values_to_slist(GHashTable *hash)
{
    GSList        *list = NULL;
    GHashTableIter iter;
    void          *value;

    g_return_val_if_fail(hash, NULL);

    g_hash_table_iter_init(&iter, hash);
    while (g_hash_table_iter_next(&iter, NULL, &value))
        list = g_slist_prepend(list, value);

    return list;
}

void
_nm_utils_strdict_from_dbus(_NM_SETT_INFO_PROP_FROM_DBUS_GPROP_FCN_ARGS _nm_nil)
{
    GVariantIter iter;
    const char  *key, *value;
    GHashTable  *hash;

    hash = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free);
    g_variant_iter_init(&iter, from);
    while (g_variant_iter_next(&iter, "{&s&s}", &key, &value))
        g_hash_table_insert(hash, g_strdup(key), g_strdup(value));

    g_value_take_boxed(to, hash);
}

const NMSettInfoPropertType nm_sett_info_propert_type_strdict =
    NM_SETT_INFO_PROPERT_TYPE_GPROP_INIT(NM_G_VARIANT_TYPE("a{ss}"),
                                         .typdata_from_dbus.gprop_fcn = _nm_utils_strdict_from_dbus,
                                         .typdata_to_dbus.gprop_type =
                                             NM_SETTING_PROPERTY_TO_DBUS_FCN_GPROP_TYPE_STRDICT,
                                         .compare_fcn   = _nm_setting_property_compare_fcn_default,
                                         .from_dbus_fcn = _nm_setting_property_from_dbus_fcn_gprop,
                                         .from_dbus_is_full = TRUE);

GHashTable *
_nm_utils_copy_strdict(GHashTable *strdict)
{
    GHashTable    *copy;
    GHashTableIter iter;
    gpointer       key, value;

    copy = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free);
    if (strdict) {
        g_hash_table_iter_init(&iter, strdict);
        while (g_hash_table_iter_next(&iter, &key, &value))
            g_hash_table_insert(copy, g_strdup(key), g_strdup(value));
    }
    return copy;
}

GPtrArray *
_nm_utils_copy_array(const GPtrArray *array, NMUtilsCopyFunc copy_func, GDestroyNotify free_func)
{
    GPtrArray *copy;
    guint      i;

    if (!array)
        return g_ptr_array_new_with_free_func(free_func);

    copy = g_ptr_array_new_full(array->len, free_func);
    for (i = 0; i < array->len; i++)
        g_ptr_array_add(copy, copy_func(array->pdata[i]));
    return copy;
}

GPtrArray *
_nm_utils_copy_object_array(const GPtrArray *array)
{
    return _nm_utils_copy_array(array, g_object_ref, g_object_unref);
}

void
_nm_utils_bytes_from_dbus(GVariant *dbus_value, GValue *prop_value)
{
    GBytes *bytes;

    if (g_variant_n_children(dbus_value)) {
        gconstpointer data;
        gsize         length;

        data  = g_variant_get_fixed_array(dbus_value, &length, 1);
        bytes = g_bytes_new(data, length);
    } else
        bytes = NULL;
    g_value_take_boxed(prop_value, bytes);
}

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

GSList *
nm_strv_to_gslist(char **strv, gboolean deep_copy)
{
    GSList *list = NULL;
    gsize   i;

    if (!strv)
        return NULL;

    if (deep_copy) {
        for (i = 0; strv[i]; i++)
            list = g_slist_prepend(list, g_strdup(strv[i]));
    } else {
        for (i = 0; strv[i]; i++)
            list = g_slist_prepend(list, strv[i]);
    }
    return g_slist_reverse(list);
}

char **
_nm_utils_slist_to_strv(const GSList *slist, gboolean deep_copy)
{
    const GSList *iter;
    char        **strv;
    guint         len, i;

    if (!slist)
        return NULL;

    len = g_slist_length((GSList *) slist);

    strv = g_new(char *, len + 1);

    if (deep_copy) {
        for (i = 0, iter = slist; iter; iter = iter->next, i++) {
            nm_assert(iter->data);
            strv[i] = g_strdup(iter->data);
        }
    } else {
        for (i = 0, iter = slist; iter; iter = iter->next, i++) {
            nm_assert(iter->data);
            strv[i] = iter->data;
        }
    }
    strv[i] = NULL;

    return strv;
}

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

static gboolean
device_supports_ap_ciphers(guint32 dev_caps, guint32 ap_flags, gboolean static_wep)
{
    gboolean have_pair  = FALSE;
    gboolean have_group = FALSE;
    /* Device needs to support at least one pairwise and one group cipher */

    /* Pairwise */
    if (static_wep) {
        /* Static WEP only uses group ciphers */
        have_pair = TRUE;
    } else {
        if (dev_caps & NM_WIFI_DEVICE_CAP_CIPHER_WEP40)
            if (ap_flags & NM_802_11_AP_SEC_PAIR_WEP40)
                have_pair = TRUE;
        if (dev_caps & NM_WIFI_DEVICE_CAP_CIPHER_WEP104)
            if (ap_flags & NM_802_11_AP_SEC_PAIR_WEP104)
                have_pair = TRUE;
        if (dev_caps & NM_WIFI_DEVICE_CAP_CIPHER_TKIP)
            if (ap_flags & NM_802_11_AP_SEC_PAIR_TKIP)
                have_pair = TRUE;
        if (dev_caps & NM_WIFI_DEVICE_CAP_CIPHER_CCMP)
            if (ap_flags & NM_802_11_AP_SEC_PAIR_CCMP)
                have_pair = TRUE;
    }

    /* Group */
    if (dev_caps & NM_WIFI_DEVICE_CAP_CIPHER_WEP40)
        if (ap_flags & NM_802_11_AP_SEC_GROUP_WEP40)
            have_group = TRUE;
    if (dev_caps & NM_WIFI_DEVICE_CAP_CIPHER_WEP104)
        if (ap_flags & NM_802_11_AP_SEC_GROUP_WEP104)
            have_group = TRUE;
    if (!static_wep) {
        if (dev_caps & NM_WIFI_DEVICE_CAP_CIPHER_TKIP)
            if (ap_flags & NM_802_11_AP_SEC_GROUP_TKIP)
                have_group = TRUE;
        if (dev_caps & NM_WIFI_DEVICE_CAP_CIPHER_CCMP)
            if (ap_flags & NM_802_11_AP_SEC_GROUP_CCMP)
                have_group = TRUE;
    }

    return (have_pair && have_group);
}

/**
 * nm_utils_ap_mode_security_valid:
 * @type: the security type to check device capabilities against,
 * e.g. #NMU_SEC_STATIC_WEP
 * @wifi_caps: bitfield of the capabilities of the specific Wi-Fi device, e.g.
 * #NM_WIFI_DEVICE_CAP_CIPHER_WEP40
 *
 * Given a set of device capabilities, and a desired security type to check
 * against, determines whether the combination of device capabilities and
 * desired security type are valid for AP/Hotspot connections.
 *
 * Returns: %TRUE if the device capabilities are compatible with the desired
 * @type, %FALSE if they are not.
 **/
gboolean
nm_utils_ap_mode_security_valid(NMUtilsSecurityType type, NMDeviceWifiCapabilities wifi_caps)
{
    if (!(wifi_caps & NM_WIFI_DEVICE_CAP_AP))
        return FALSE;

    /* Return TRUE for any security that wpa_supplicant's lightweight AP
     * mode can handle: which is open, WEP, and WPA/WPA2 PSK.
     */
    switch (type) {
    case NMU_SEC_NONE:
    case NMU_SEC_STATIC_WEP:
    case NMU_SEC_WPA_PSK:
    case NMU_SEC_WPA2_PSK:
    case NMU_SEC_SAE:
    case NMU_SEC_OWE:
        return TRUE;
    case NMU_SEC_LEAP:
    case NMU_SEC_DYNAMIC_WEP:
    case NMU_SEC_WPA_ENTERPRISE:
    case NMU_SEC_WPA2_ENTERPRISE:
    case NMU_SEC_WPA3_SUITE_B_192:
        return FALSE;
    case NMU_SEC_INVALID:
        break;
    }
    return FALSE;
}

/**
 * nm_utils_security_valid:
 * @type: the security type to check AP flags and device capabilities against,
 * e.g. #NMU_SEC_STATIC_WEP
 * @wifi_caps: bitfield of the capabilities of the specific Wi-Fi device, e.g.
 * #NM_WIFI_DEVICE_CAP_CIPHER_WEP40
 * @have_ap: whether the @ap_flags, @ap_wpa, and @ap_rsn arguments are valid
 * @adhoc: whether the capabilities being tested are from an Ad-Hoc AP (IBSS)
 * @ap_flags: bitfield of AP capabilities, e.g. #NM_802_11_AP_FLAGS_PRIVACY
 * @ap_wpa: bitfield of AP capabilities derived from the AP's WPA beacon,
 * e.g. (#NM_802_11_AP_SEC_PAIR_TKIP | #NM_802_11_AP_SEC_KEY_MGMT_PSK)
 * @ap_rsn: bitfield of AP capabilities derived from the AP's RSN/WPA2 beacon,
 * e.g. (#NM_802_11_AP_SEC_PAIR_CCMP | #NM_802_11_AP_SEC_PAIR_TKIP)
 *
 * Given a set of device capabilities, and a desired security type to check
 * against, determines whether the combination of device, desired security
 * type, and AP capabilities intersect.
 *
 * NOTE: this function cannot handle checking security for AP/Hotspot mode;
 * use nm_utils_ap_mode_security_valid() instead.
 *
 * Returns: %TRUE if the device capabilities and AP capabilities intersect and are
 * compatible with the desired @type, %FALSE if they are not
 **/
gboolean
nm_utils_security_valid(NMUtilsSecurityType      type,
                        NMDeviceWifiCapabilities wifi_caps,
                        gboolean                 have_ap,
                        gboolean                 adhoc,
                        NM80211ApFlags           ap_flags,
                        NM80211ApSecurityFlags   ap_wpa,
                        NM80211ApSecurityFlags   ap_rsn)
{
    switch (type) {
    case NMU_SEC_NONE:
        if (!have_ap)
            return TRUE;
        if (ap_flags & NM_802_11_AP_FLAGS_PRIVACY)
            return FALSE;
        if (ap_wpa || ap_rsn)
            return FALSE;
        return TRUE;
    case NMU_SEC_LEAP: /* require PRIVACY bit for LEAP? */
        if (adhoc)
            return FALSE;
        /* fall-through */
    case NMU_SEC_STATIC_WEP:
        if (!have_ap) {
            if (wifi_caps & (NM_WIFI_DEVICE_CAP_CIPHER_WEP40 | NM_WIFI_DEVICE_CAP_CIPHER_WEP104))
                return TRUE;
            return FALSE;
        }
        if (!(ap_flags & NM_802_11_AP_FLAGS_PRIVACY))
            return FALSE;
        if (ap_wpa || ap_rsn) {
            if (!device_supports_ap_ciphers(wifi_caps, ap_wpa, TRUE)) {
                if (!device_supports_ap_ciphers(wifi_caps, ap_rsn, TRUE))
                    return FALSE;
            }
        }
        return TRUE;
    case NMU_SEC_DYNAMIC_WEP:
        if (adhoc)
            return FALSE;
        if (!have_ap) {
            if (wifi_caps & (NM_WIFI_DEVICE_CAP_CIPHER_WEP40 | NM_WIFI_DEVICE_CAP_CIPHER_WEP104))
                return TRUE;
            return FALSE;
        }
        if (ap_rsn || !(ap_flags & NM_802_11_AP_FLAGS_PRIVACY))
            return FALSE;
        /* Some APs broadcast minimal WPA-enabled beacons that must be handled */
        if (ap_wpa) {
            if (!(ap_wpa & NM_802_11_AP_SEC_KEY_MGMT_802_1X))
                return FALSE;
            if (!device_supports_ap_ciphers(wifi_caps, ap_wpa, FALSE))
                return FALSE;
        }
        return TRUE;
    case NMU_SEC_WPA_PSK:
        if (adhoc)
            return FALSE;
        if (!(wifi_caps & NM_WIFI_DEVICE_CAP_WPA))
            return FALSE;
        if (!have_ap)
            return TRUE;
        if (ap_wpa & NM_802_11_AP_SEC_KEY_MGMT_PSK) {
            if ((ap_wpa & NM_802_11_AP_SEC_PAIR_TKIP)
                && (wifi_caps & NM_WIFI_DEVICE_CAP_CIPHER_TKIP))
                return TRUE;
            if ((ap_wpa & NM_802_11_AP_SEC_PAIR_CCMP)
                && (wifi_caps & NM_WIFI_DEVICE_CAP_CIPHER_CCMP))
                return TRUE;
        }
        return FALSE;
    case NMU_SEC_WPA2_PSK:
        if (!(wifi_caps & NM_WIFI_DEVICE_CAP_RSN))
            return FALSE;
        if (!have_ap)
            return TRUE;
        if (adhoc) {
            if (!(wifi_caps & NM_WIFI_DEVICE_CAP_IBSS_RSN))
                return FALSE;
            if ((ap_rsn & NM_802_11_AP_SEC_PAIR_CCMP)
                && (wifi_caps & NM_WIFI_DEVICE_CAP_CIPHER_CCMP))
                return TRUE;
            return FALSE;
        }
        if (ap_rsn & NM_802_11_AP_SEC_KEY_MGMT_PSK) {
            if ((ap_rsn & NM_802_11_AP_SEC_PAIR_TKIP)
                && (wifi_caps & NM_WIFI_DEVICE_CAP_CIPHER_TKIP))
                return TRUE;
            if ((ap_rsn & NM_802_11_AP_SEC_PAIR_CCMP)
                && (wifi_caps & NM_WIFI_DEVICE_CAP_CIPHER_CCMP))
                return TRUE;
        }
        return FALSE;
    case NMU_SEC_WPA_ENTERPRISE:
        if (adhoc)
            return FALSE;
        if (!(wifi_caps & NM_WIFI_DEVICE_CAP_WPA))
            return FALSE;
        if (!have_ap)
            return TRUE;
        if (!(ap_wpa & NM_802_11_AP_SEC_KEY_MGMT_802_1X))
            return FALSE;
        /* Ensure at least one WPA cipher is supported */
        if (!device_supports_ap_ciphers(wifi_caps, ap_wpa, FALSE))
            return FALSE;
        return TRUE;
    case NMU_SEC_WPA2_ENTERPRISE:
        if (adhoc)
            return FALSE;
        if (!(wifi_caps & NM_WIFI_DEVICE_CAP_RSN))
            return FALSE;
        if (!have_ap)
            return TRUE;
        if (!(ap_rsn & NM_802_11_AP_SEC_KEY_MGMT_802_1X))
            return FALSE;
        /* Ensure at least one WPA cipher is supported */
        if (!device_supports_ap_ciphers(wifi_caps, ap_rsn, FALSE))
            return FALSE;
        return TRUE;
    case NMU_SEC_SAE:
        if (!(wifi_caps & NM_WIFI_DEVICE_CAP_RSN))
            return FALSE;
        if (adhoc)
            return FALSE;
        if (!have_ap)
            return TRUE;
        if (ap_rsn & NM_802_11_AP_SEC_KEY_MGMT_SAE) {
            if ((ap_rsn & NM_802_11_AP_SEC_PAIR_CCMP)
                && (wifi_caps & NM_WIFI_DEVICE_CAP_CIPHER_CCMP))
                return TRUE;
        }
        return FALSE;
    case NMU_SEC_OWE:
        if (adhoc)
            return FALSE;
        if (!(wifi_caps & NM_WIFI_DEVICE_CAP_RSN))
            return FALSE;
        if (!have_ap)
            return TRUE;
        if (!NM_FLAGS_ANY(ap_rsn, NM_802_11_AP_SEC_KEY_MGMT_OWE | NM_802_11_AP_SEC_KEY_MGMT_OWE_TM))
            return FALSE;
        return TRUE;
    case NMU_SEC_WPA3_SUITE_B_192:
        if (adhoc)
            return FALSE;
        if (!(wifi_caps & NM_WIFI_DEVICE_CAP_RSN))
            return FALSE;
        if (!have_ap)
            return TRUE;
        if (ap_rsn & NM_802_11_AP_SEC_KEY_MGMT_EAP_SUITE_B_192)
            return TRUE;
        return FALSE;
    case NMU_SEC_INVALID:
        break;
    }

    return FALSE;
}

/**
 * nm_utils_wep_key_valid:
 * @key: a string that might be a WEP key
 * @wep_type: the #NMWepKeyType type of the WEP key
 *
 * Checks if @key is a valid WEP key
 *
 * Returns: %TRUE if @key is a WEP key, %FALSE if not
 */
gboolean
nm_utils_wep_key_valid(const char *key, NMWepKeyType wep_type)
{
    gsize keylen;
    gsize i;

    if (!key)
        return FALSE;

    if (wep_type == NM_WEP_KEY_TYPE_UNKNOWN) {
        return nm_utils_wep_key_valid(key, NM_WEP_KEY_TYPE_KEY)
               || nm_utils_wep_key_valid(key, NM_WEP_KEY_TYPE_PASSPHRASE);
    }

    keylen = strlen(key);
    if (wep_type == NM_WEP_KEY_TYPE_KEY) {
        if (keylen == 10 || keylen == 26) {
            /* Hex key */
            for (i = 0; i < keylen; i++) {
                if (!g_ascii_isxdigit(key[i]))
                    return FALSE;
            }
        } else if (keylen == 5 || keylen == 13) {
            /* ASCII key */
            for (i = 0; i < keylen; i++) {
                if (!g_ascii_isprint(key[i]))
                    return FALSE;
            }
        } else
            return FALSE;
    } else if (wep_type == NM_WEP_KEY_TYPE_PASSPHRASE) {
        if (!keylen || keylen > 64)
            return FALSE;
    }

    return TRUE;
}

/**
 * nm_utils_wpa_psk_valid:
 * @psk: a string that might be a WPA PSK
 *
 * Checks if @psk is a valid WPA PSK
 *
 * Returns: %TRUE if @psk is a WPA PSK, %FALSE if not
 */
gboolean
nm_utils_wpa_psk_valid(const char *psk)
{
    gsize psklen;
    gsize i;

    if (!psk)
        return FALSE;

    psklen = strlen(psk);
    if (psklen < 8 || psklen > 64)
        return FALSE;

    if (psklen == 64) {
        /* Hex PSK */
        for (i = 0; i < psklen; i++) {
            if (!g_ascii_isxdigit(psk[i]))
                return FALSE;
        }
    }

    return TRUE;
}

/**
 * nm_utils_ip4_dns_to_variant:
 * @dns: (type utf8): an array of IP address strings
 *
 * Utility function to convert an array of IP address strings int a #GVariant of
 * type 'au' representing an array of IPv4 addresses.
 *
 * Returns: (transfer none): a new floating #GVariant representing @dns.
 **/
GVariant *
nm_utils_ip4_dns_to_variant(char **dns)
{
    return nm_utils_dns_to_variant(AF_INET, NM_CAST_STRV_CC(dns), -1);
}

/**
 * nm_utils_ip6_dns_to_variant:
 * @dns: (type utf8): an array of IP address strings
 *
 * Utility function to convert an array of IP address strings int a #GVariant of
 * type 'aay' representing an array of IPv6 addresses.
 *
 * If a string cannot be parsed, it will be silently ignored.
 *
 * Returns: (transfer none): a new floating #GVariant representing @dns.
 **/
GVariant *
nm_utils_ip6_dns_to_variant(char **dns)
{
    return nm_utils_dns_to_variant(AF_INET6, NM_CAST_STRV_CC(dns), -1);
}

GVariant *
nm_utils_dns_to_variant(int addr_family, const char *const *dns, gssize len)
{
    const int       IS_IPv4 = NM_IS_IPv4(addr_family);
    GVariantBuilder builder;
    gsize           l;
    gsize           i;

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

    g_variant_builder_init(&builder, IS_IPv4 ? G_VARIANT_TYPE("au") : G_VARIANT_TYPE("aay"));

    for (i = 0; i < l; i++) {
        NMIPAddr ip;

        /* We can only represent the IP address on the legacy property "ipv[46].dns".
         * Expose what we can. */
        if (!nm_dns_uri_parse_plain(addr_family, dns[i], NULL, &ip))
            continue;

        if (IS_IPv4)
            g_variant_builder_add(&builder, "u", ip.addr4);
        else
            g_variant_builder_add(&builder, "@ay", nm_g_variant_new_ay_in6addr(&ip.addr6));
    }

    return g_variant_builder_end(&builder);
}

/**
 * nm_utils_ip4_dns_from_variant:
 * @value: a #GVariant of type 'au'
 *
 * Utility function to convert a #GVariant of type 'au' representing a list of
 * IPv4 addresses into an array of IP address strings.
 *
 * Since 1.46, an empty list is returned if the variant type is not valid
 * (before it was checked as assertion)
 *
 * Returns: (transfer full) (type utf8): a %NULL-terminated array of IP address strings.
 **/
char **
nm_utils_ip4_dns_from_variant(GVariant *value)
{
    return _nm_utils_ip4_dns_from_variant(value, FALSE, NULL);
}

/**
 * _nm_utils_ip4_dns_from_variant:
 * @value: a #GVariant of type 'au'
 * @strict: whether to parse in strict mode or best-effort mode
 * @error: the error location
 *
 * Like #nm_utils_ip4_dns_from_variant, but allows one to parse in strict mode. In
 * strict mode, parsing is aborted on first error and %NULL is returned.
 *
 * Returns: (transfer full) (type utf8): a %NULL-terminated array of IP address
 *   strings. In strict mode, %NULL is returned on error.
 */
char **
_nm_utils_ip4_dns_from_variant(GVariant *value, bool strict, GError **error)
{
    const guint32 *array;
    gsize          length;
    char         **dns;
    gsize          i;

    if (!g_variant_is_of_type(value, G_VARIANT_TYPE("au"))) {
        if (strict) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("Expected value of type \"au\""));
            return NULL;
        }
        dns    = g_new(char *, 1);
        dns[0] = NULL;
        return dns;
    }

    array = g_variant_get_fixed_array(value, &length, sizeof(guint32));
    dns   = g_new(char *, length + 1u);
    for (i = 0; i < length; i++)
        dns[i] = nm_inet4_ntop_dup(array[i]);
    dns[i] = NULL;

    return dns;
}

/**
 * nm_utils_ip4_addresses_to_variant:
 * @addresses: (element-type NMIPAddress): an array of #NMIPAddress objects
 * @gateway: (nullable): the gateway IP address
 *
 * Utility function to convert a #GPtrArray of #NMIPAddress objects representing
 * IPv4 addresses into a #GVariant of type 'aau' representing an array of
 * NetworkManager IPv4 addresses (which are tuples of address, prefix, and
 * gateway). The "gateway" field of the first address will get the value of
 * @gateway (if non-%NULL). In all of the other addresses, that field will be 0.
 *
 * Returns: (transfer none): a new floating #GVariant representing @addresses.
 **/
GVariant *
nm_utils_ip4_addresses_to_variant(GPtrArray *addresses, const char *gateway)
{
    GVariantBuilder builder;
    guint           i;

    g_variant_builder_init(&builder, G_VARIANT_TYPE("aau"));

    if (addresses) {
        for (i = 0; i < addresses->len; i++) {
            NMIPAddress *addr = addresses->pdata[i];
            guint32      array[3];
            in_addr_t    gw;

            if (nm_ip_address_get_family(addr) != AF_INET)
                continue;

            gw = 0u;
            if (gateway) {
                in_addr_t a;

                if (inet_pton(AF_INET, gateway, &a) == 1)
                    gw = a;
                gateway = NULL;
            }

            nm_ip_address_get_address_binary(addr, &array[0]);
            array[1] = nm_ip_address_get_prefix(addr);
            array[2] = gw;

            g_variant_builder_add(&builder, "@au", nm_g_variant_new_au(array, 3));
        }
    }

    return g_variant_builder_end(&builder);
}

/**
 * nm_utils_ip4_addresses_from_variant:
 * @value: a #GVariant of type 'aau'
 * @out_gateway: (out) (optional) (nullable) (transfer full): on return, will
 *   contain the IP gateway
 *
 * Utility function to convert a #GVariant of type 'aau' representing a list of
 * NetworkManager IPv4 addresses (which are tuples of address, prefix, and
 * gateway) into a #GPtrArray of #NMIPAddress objects. The "gateway" field of
 * the first address (if set) will be returned in @out_gateway; the "gateway" fields
 * of the other addresses are ignored. Note that invalid addresses are discarded
 * but the valid addresses are still returned.
 *
 * Since 1.46, an empty list is returned if the variant type is not valid
 * (before it was checked as assertion)
 *
 * Returns: (transfer full) (element-type NMIPAddress): a newly allocated
 *   #GPtrArray of #NMIPAddress objects
 **/
GPtrArray *
nm_utils_ip4_addresses_from_variant(GVariant *value, char **out_gateway)
{
    return _nm_utils_ip4_addresses_from_variant(value, NULL, out_gateway, FALSE, NULL);
}

/**
 * _nm_utils_ip4_addresses_from_variant:
 * @value: a #GVariant of type 'aau'
 * @labels: (optional) (nullable): a #GVariant of the type 'as'. If not-NULL,
 *   each element must contain a string with the labels that corresponds to each
 *   IP address in @value.
 * @out_gateway: (out) (optional) (nullable) (transfer full): on return, will
 *   contain the IP gateway
 * @strict: whether to parse in strict mode or best-effort mode
 * @error: the error location
 *
 * Like #nm_utils_ip4_addresses_from_variant, but allows one to parse in strict mode. In
 * strict mode, parsing is aborted on first error and %NULL is returned. It also
 * allows one to parse the address-labels at the same time than the addresses.
 *
 * The labels need to be processed at the same time than the addresses, inside
 * this function, because if there are invalid addresses they are filtered out,
 * and the returned array of addresses contains less elements than the original
 * array. If that happens, the caller don't know what label corresponds to what
 * address, because they are matched by position in the array. If you are not
 * interested in the labels, just set @labels to NULL.
 *
 * Returns: (transfer full) (element-type NMIPAddress): a newly allocated
 *   #GPtrArray of #NMIPAddress objects. In strict mode, %NULL is returned on error.
 */
GPtrArray *
_nm_utils_ip4_addresses_from_variant(GVariant *value,
                                     GVariant *labels,
                                     char    **out_gateway,
                                     bool      strict,
                                     GError  **error)
{
    gs_unref_ptrarray GPtrArray *addresses = NULL;
    GVariantIter                 iter;
    GVariant                    *item;
    const guint32               *addr_array;
    gsize                        length;
    NMIPAddress                 *addr;
    const char                  *label;
    gsize                        n_labels = 0;
    guint                        i;

    addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
    if (out_gateway)
        *out_gateway = NULL;

    if (!g_variant_is_of_type(value, G_VARIANT_TYPE("aau"))) {
        if (strict) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("Expected value of type \"aau\""));
            return NULL;
        }
        return g_steal_pointer(&addresses);
    }

    if (labels && !g_variant_is_of_type(labels, G_VARIANT_TYPE("as"))) {
        if (strict) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("Expected \"address-labels\" of type \"as\""));
            return NULL;
        }
        /* We still can parse the addresses, without the labels */
        labels = NULL;
    }

    if (labels)
        n_labels = g_variant_n_children(labels);

    g_variant_iter_init(&iter, value);

    for (i = 0; g_variant_iter_next(&iter, "@au", &item); i++) {
        gs_unref_variant GVariant *addr_var    = item;
        gs_free_error GError      *local_error = NULL;

        addr_array = g_variant_get_fixed_array(addr_var, &length, sizeof(guint32));
        if (length < 3) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("Incomplete IPv4 address (idx=%u)"),
                            i);
                return NULL;
            }
            continue;
        }

        addr = nm_ip_address_new_binary(AF_INET, &addr_array[0], addr_array[1], &local_error);
        if (!addr) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("%s (idx=%u)"),
                            local_error->message,
                            i);
                return NULL;
            }
            continue;
        }

        /* We were accepting address-labels to be shorter than addresses, so
         * let's continue doing so and not consider it as an error */
        if (labels && i < n_labels) {
            g_variant_get_child(labels, i, "&s", &label);

            if (label && label[0]) {
                nm_ip_address_set_attribute(addr,
                                            NM_IP_ADDRESS_ATTRIBUTE_LABEL,
                                            g_variant_new_string(label));
            }
        }

        g_ptr_array_add(addresses, addr);
        if (addr_array[2] && out_gateway && !*out_gateway)
            *out_gateway = nm_inet4_ntop_dup(addr_array[2]);
    }

    return g_steal_pointer(&addresses);
}

/**
 * nm_utils_ip4_routes_to_variant:
 * @routes: (element-type NMIPRoute): an array of #NMIP4Route objects
 *
 * Utility function to convert a #GPtrArray of #NMIPRoute objects representing
 * IPv4 routes into a #GVariant of type 'aau' representing an array of
 * NetworkManager IPv4 routes (which are tuples of route, prefix, next hop, and
 * metric).
 *
 * Returns: (transfer none): a new floating #GVariant representing @routes.
 **/
GVariant *
nm_utils_ip4_routes_to_variant(GPtrArray *routes)
{
    GVariantBuilder builder;
    guint           i;

    g_variant_builder_init(&builder, G_VARIANT_TYPE("aau"));

    if (routes) {
        for (i = 0; i < routes->len; i++) {
            NMIPRoute *route = routes->pdata[i];
            guint32    array[4];

            if (nm_ip_route_get_family(route) != AF_INET)
                continue;

            nm_ip_route_get_dest_binary(route, &array[0]);
            array[1] = nm_ip_route_get_prefix(route);
            nm_ip_route_get_next_hop_binary(route, &array[2]);
            /* The old routes format uses "0" for default, not "-1" */
            array[3] = NM_MAX(0, nm_ip_route_get_metric(route));

            g_variant_builder_add(&builder, "@au", nm_g_variant_new_au(array, 4));
        }
    }

    return g_variant_builder_end(&builder);
}

/**
 * nm_utils_ip4_routes_from_variant:
 * @value: #GVariant of type 'aau'
 *
 * Utility function to convert a #GVariant of type 'aau' representing an array
 * of NetworkManager IPv4 routes (which are tuples of route, prefix, next hop,
 * and metric) into a #GPtrArray of #NMIPRoute objects. Note that invalid routes
 * are discarded but the valid routes are still returned.
 *
 * Since 1.46, an empty list is returned if the variant type is not valid
 * (before it was checked as assertion)
 *
 * Returns: (transfer full) (element-type NMIPRoute): a newly allocated
 *   #GPtrArray of #NMIPRoute objects
 **/
GPtrArray *
nm_utils_ip4_routes_from_variant(GVariant *value)
{
    return _nm_utils_ip4_routes_from_variant(value, FALSE, NULL);
}

/**
 * _nm_utils_ip4_routes_from_variant:
 * @value: #GVariant of type 'aau'
 * @strict: whether to parse in strict mode or best-effort mode
 * @error: the error location
 *
 * Like #nm_utils_ip4_routes_from_variant, but allows one to parse in strict mode. In
 * strict mode, parsing is aborted on first error and %NULL is returned.
 *
 * Returns: (transfer full) (element-type NMIPRoute): a newly allocated
 *   #GPtrArray of #NMIPRoute objects. In strict mode, NULL is returned on error.
 */
GPtrArray *
_nm_utils_ip4_routes_from_variant(GVariant *value, bool strict, GError **error)
{
    gs_unref_ptrarray GPtrArray *routes = NULL;
    GVariantIter                 iter;
    GVariant                    *item;
    const guint32               *route_array;
    gsize                        length;
    NMIPRoute                   *route;
    guint                        i;

    routes = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_route_unref);

    if (!g_variant_is_of_type(value, G_VARIANT_TYPE("aau"))) {
        if (strict) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("Expected value of type \"aau\""));
            return NULL;
        }
        return g_steal_pointer(&routes);
    }

    g_variant_iter_init(&iter, value);

    for (i = 0; g_variant_iter_next(&iter, "@au", &item); i++) {
        gs_unref_variant GVariant *route_var   = item;
        gs_free_error GError      *local_error = NULL;

        route_array = g_variant_get_fixed_array(route_var, &length, sizeof(guint32));
        if (length < 4) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("Incomplete IPv4 route (idx=%u)"),
                            i);
                return NULL;
            }
            continue;
        }

        route = nm_ip_route_new_binary(AF_INET,
                                       &route_array[0],
                                       route_array[1],
                                       &route_array[2],
                                       /* The old routes format uses "0" for default, not "-1" */
                                       route_array[3] ? (gint64) route_array[3] : -1,
                                       &local_error);
        if (!route) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("%s (idx=%u)"),
                            local_error->message,
                            i);
                return NULL;
            }
            continue;
        }

        g_ptr_array_add(routes, route);
    }

    return g_steal_pointer(&routes);
}

/**
 * nm_utils_ip4_netmask_to_prefix:
 * @netmask: an IPv4 netmask in network byte order.
 *   Usually the netmask has all leading bits up to the prefix
 *   set so that the netmask is identical to having the first
 *   prefix bits of the address set.
 *   If that is not the case and there are "holes" in the
 *   mask, the prefix is determined based on the lowest bit
 *   set.
 *
 * Returns: the CIDR prefix represented by the netmask
 **/
guint32
nm_utils_ip4_netmask_to_prefix(guint32 netmask)
{
    return nm_ip4_addr_netmask_to_prefix(netmask);
}

/**
 * nm_utils_ip4_prefix_to_netmask:
 * @prefix: a CIDR prefix, must be not larger than 32.
 *
 * Returns: the netmask represented by the prefix, in network byte order
 **/
guint32
nm_utils_ip4_prefix_to_netmask(guint32 prefix)
{
    g_return_val_if_fail(prefix <= 32, 0xffffffffu);

    return nm_ip4_addr_netmask_from_prefix(prefix);
}

/**
 * nm_utils_ip4_get_default_prefix:
 * @ip: an IPv4 address (in network byte order)
 *
 * When the Internet was originally set up, various ranges of IP addresses were
 * segmented into three network classes: A, B, and C.  This function will return
 * a prefix that is associated with the IP address specified defining where it
 * falls in the predefined classes.
 *
 * Returns: the default class prefix for the given IP
 **/
/* The function is originally from ipcalc.c of Red Hat's initscripts. */
guint32
nm_utils_ip4_get_default_prefix(guint32 ip)
{
    return nm_ip4_addr_get_default_prefix(ip);
}

/**
 * nm_utils_ip6_dns_from_variant:
 * @value: a #GVariant of type 'aay'
 *
 * Utility function to convert a #GVariant of type 'aay' representing a list of
 * IPv6 addresses into an array of IP address strings. Each "ay" entry must be
 * a IPv6 address in binary form (16 bytes long). Invalid entries are silently
 * ignored.
 *
 * Since 1.46, an empty list is returned if the variant type is not valid
 * (before it was checked as assertion)
 *
 * Returns: (transfer full) (type utf8): a %NULL-terminated array of IP address strings.
 **/
char **
nm_utils_ip6_dns_from_variant(GVariant *value)
{
    return _nm_utils_ip6_dns_from_variant(value, FALSE, NULL);
}

/**
 * _nm_utils_ip6_dns_from_variant:
 * @value: a #GVariant of type 'aay'
 * @strict: whether to parse in strict mode or best-effort mode
 * @error: the error location
 *
 * Like #nm_utils_ip6_dns_from_variant, but allows one to parse in strict mode. In
 * strict mode, parsing is aborted on first error and %NULL is returned.
 *
 * Returns: (transfer full) (type utf8): a %NULL-terminated array of IP address
 *   strings. In strict mode, %NULL is returned on error.
 **/
char **
_nm_utils_ip6_dns_from_variant(GVariant *value, bool strict, GError **error)
{
    gs_strfreev char **dns = NULL;
    GVariantIter       iter;
    GVariant          *item;
    guint              i, j;

    if (!g_variant_is_of_type(value, G_VARIANT_TYPE("aay"))) {
        if (strict) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("Expected value of type \"aay\""));
            return NULL;
        }
        dns    = g_new(char *, 1);
        dns[0] = NULL;
        return g_steal_pointer(&dns);
    }

    dns    = g_new(char *, g_variant_n_children(value) + 1);
    dns[0] = NULL;

    g_variant_iter_init(&iter, value);

    for (i = 0, j = 0; g_variant_iter_next(&iter, "@ay", &item); i++) {
        gs_unref_variant GVariant *ip_var = item;
        const struct in6_addr     *ip;
        gsize                      length;

        ip = g_variant_get_fixed_array(ip_var, &length, 1);

        if (length != sizeof(struct in6_addr)) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("Invalid IPv6 DNS address length (idx=%u)"),
                            i);
                return NULL;
            }
            continue;
        }

        dns[j]   = nm_inet6_ntop_dup(ip);
        dns[++j] = NULL;
    }

    return g_steal_pointer(&dns);
}

/**
 * nm_utils_ip6_addresses_to_variant:
 * @addresses: (element-type NMIPAddress): an array of #NMIPAddress objects
 * @gateway: (nullable): the gateway IP address
 *
 * Utility function to convert a #GPtrArray of #NMIPAddress objects representing
 * IPv6 addresses into a #GVariant of type 'a(ayuay)' representing an array of
 * NetworkManager IPv6 addresses (which are tuples of address, prefix, and
 * gateway).  The "gateway" field of the first address will get the value of
 * @gateway (if non-%NULL). In all of the other addresses, that field will be
 * all 0s.
 *
 * Returns: (transfer none): a new floating #GVariant representing @addresses.
 **/
GVariant *
nm_utils_ip6_addresses_to_variant(GPtrArray *addresses, const char *gateway)
{
    GVariantBuilder builder;
    guint           i;

    g_variant_builder_init(&builder, G_VARIANT_TYPE("a(ayuay)"));

    if (addresses) {
        for (i = 0; i < addresses->len; i++) {
            NMIPAddress           *addr = addresses->pdata[i];
            struct in6_addr        address_bin;
            struct in6_addr        gateway_bin_data;
            const struct in6_addr *gateway_bin;

            if (nm_ip_address_get_family(addr) != AF_INET6)
                continue;

            nm_ip_address_get_address_binary(addr, &address_bin);

            gateway_bin = &in6addr_any;
            if (gateway) {
                if (inet_pton(AF_INET6, gateway, &gateway_bin_data) == 1)
                    gateway_bin = &gateway_bin_data;
                gateway = NULL;
            }

            g_variant_builder_add(&builder,
                                  "(@ayu@ay)",
                                  nm_g_variant_new_ay_in6addr(&address_bin),
                                  (guint32) nm_ip_address_get_prefix(addr),
                                  nm_g_variant_new_ay_in6addr(gateway_bin));
        }
    }

    return g_variant_builder_end(&builder);
}

/**
 * nm_utils_ip6_addresses_from_variant:
 * @value: a #GVariant of type 'a(ayuay)'
 * @out_gateway: (out) (optional) (nullable) (transfer full): on return, will
 *   contain the IP gateway
 *
 * Utility function to convert a #GVariant of type 'a(ayuay)' representing a
 * list of NetworkManager IPv6 addresses (which are tuples of address, prefix,
 * and gateway) into a #GPtrArray of #NMIPAddress objects. The "gateway" field
 * of the first address (if set) will be returned in @out_gateway; the "gateway"
 * fields of the other addresses are ignored. Note that invalid addresses are
 * discarded but the valid addresses are still returned.
 *
 * Since 1.46, an empty list is returned if the variant type is not valid
 * (before it was checked as assertion)
 *
 * Returns: (transfer full) (element-type NMIPAddress): a newly allocated
 *   #GPtrArray of #NMIPAddress objects
 **/
GPtrArray *
nm_utils_ip6_addresses_from_variant(GVariant *value, char **out_gateway)
{
    return _nm_utils_ip6_addresses_from_variant(value, out_gateway, FALSE, NULL);
}

/**
 * _nm_utils_ip6_addresses_from_variant:
 * @value: a #GVariant of type 'a(ayuay)'
 * @out_gateway: (out) (optional) (nullable) (transfer full): on return, will
 *   contain the IP gateway
 * @strict: whether to parse in strict mode or best-effort mode
 * @error: the error location
 *
 * Like #nm_utils_ip6_addresses_from_variant, but allows one to parse in strict mode. In
 * strict mode, parsing is aborted on first error and %NULL is returned.
 *
 * Returns: (transfer full) (element-type NMIPAddress): a newly allocated
 *   #GPtrArray of #NMIPAddress objects. In strict mode, %NULL is returned on error.
 **/
GPtrArray *
_nm_utils_ip6_addresses_from_variant(GVariant *value,
                                     char    **out_gateway,
                                     bool      strict,
                                     GError  **error)
{
    gs_unref_ptrarray GPtrArray *addresses = NULL;
    GVariantIter                 iter;
    GVariant                    *addr_item;
    GVariant                    *gateway_item;
    guint32                      prefix;
    guint                        i;

    addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
    if (out_gateway)
        *out_gateway = NULL;

    if (!g_variant_is_of_type(value, G_VARIANT_TYPE("a(ayuay)"))) {
        if (strict) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("Expected value of type \"a(ayuay)\""));
            return NULL;
        }
        return g_steal_pointer(&addresses);
    }

    g_variant_iter_init(&iter, value);

    for (i = 0; g_variant_iter_next(&iter, "(@ayu@ay)", &addr_item, &prefix, &gateway_item); i++) {
        gs_unref_variant GVariant *addr_var    = addr_item;
        gs_unref_variant GVariant *gateway_var = gateway_item;
        gs_free_error GError      *local_error = NULL;
        NMIPAddress               *addr;
        const struct in6_addr     *addr_bytes, *gateway_bytes;
        gsize                      addr_len, gateway_len;

        if (!g_variant_is_of_type(addr_var, G_VARIANT_TYPE_BYTESTRING)
            || !g_variant_is_of_type(gateway_var, G_VARIANT_TYPE_BYTESTRING)) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("Expected value of type \"(ayuay)\" (idx=%u)"),
                            i);
                return NULL;
            }
            continue;
        }

        addr_bytes = g_variant_get_fixed_array(addr_var, &addr_len, 1);
        if (addr_len != 16) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("IPv6 address with invalid length (idx=%u)"),
                            i);
                return NULL;
            }
            continue;
        }

        addr = nm_ip_address_new_binary(AF_INET6, addr_bytes, prefix, &local_error);
        if (!addr) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("%s (idx=%u)"),
                            local_error->message,
                            i);
                return NULL;
            }
            continue;
        }

        g_ptr_array_add(addresses, addr);

        if (out_gateway && !*out_gateway) {
            gateway_bytes = g_variant_get_fixed_array(gateway_var, &gateway_len, 1);
            if (gateway_len != 16) {
                if (strict) {
                    g_set_error(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("IPv6 gateway with invalid length (idx=%u)"),
                                i);
                    return NULL;
                }
                continue;
            }

            if (!IN6_IS_ADDR_UNSPECIFIED(gateway_bytes))
                NM_SET_OUT(out_gateway, nm_inet6_ntop_dup(gateway_bytes));
        }
    }

    return g_steal_pointer(&addresses);
}

/**
 * nm_utils_ip6_routes_to_variant:
 * @routes: (element-type NMIPRoute): an array of #NMIPRoute objects
 *
 * Utility function to convert a #GPtrArray of #NMIPRoute objects representing
 * IPv6 routes into a #GVariant of type 'a(ayuayu)' representing an array of
 * NetworkManager IPv6 routes (which are tuples of route, prefix, next hop, and
 * metric).
 *
 * Returns: (transfer none): a new floating #GVariant representing @routes.
 **/
GVariant *
nm_utils_ip6_routes_to_variant(GPtrArray *routes)
{
    GVariantBuilder builder;
    guint           i;

    g_variant_builder_init(&builder, G_VARIANT_TYPE("a(ayuayu)"));

    if (routes) {
        for (i = 0; i < routes->len; i++) {
            NMIPRoute      *route = routes->pdata[i];
            struct in6_addr dest_bytes;
            struct in6_addr next_hop_bytes;
            guint32         metric;

            if (nm_ip_route_get_family(route) != AF_INET6)
                continue;

            nm_ip_route_get_dest_binary(route, &dest_bytes);
            nm_ip_route_get_next_hop_binary(route, &next_hop_bytes);

            /* The old routes format uses "0" for default, not "-1" */
            metric = NM_MAX(0, nm_ip_route_get_metric(route));

            g_variant_builder_add(&builder,
                                  "(@ayu@ayu)",
                                  nm_g_variant_new_ay_in6addr(&dest_bytes),
                                  (guint32) nm_ip_route_get_prefix(route),
                                  nm_g_variant_new_ay_in6addr(&next_hop_bytes),
                                  metric);
        }
    }

    return g_variant_builder_end(&builder);
}

/**
 * nm_utils_ip6_routes_from_variant:
 * @value: #GVariant of type 'a(ayuayu)'
 *
 * Utility function to convert a #GVariant of type 'a(ayuayu)' representing an
 * array of NetworkManager IPv6 routes (which are tuples of route, prefix, next
 * hop, and metric) into a #GPtrArray of #NMIPRoute objects. Note that invalid
 * routes are ignored but the valid ones are still returned.
 *
 * Since 1.46, an empty list is returned if the variant type is not valid
 * (before it was checked as assertion)
 *
 * Returns: (transfer full) (element-type NMIPRoute): a newly allocated
 *   #GPtrArray of #NMIPRoute objects
 **/
GPtrArray *
nm_utils_ip6_routes_from_variant(GVariant *value)
{
    return _nm_utils_ip6_routes_from_variant(value, FALSE, NULL);
}

/**
 * _nm_utils_ip6_routes_from_variant:
 * @value: #GVariant of type 'a(ayuayu)'
 * @strict: whether to parse in strict mode or best-effort mode
 * @error: the error location
 *
 * Like #nm_utils_ip6_routes_from_variant, but allows one to parse in strict mode. In
 * strict mode, parsing is aborted on first error and %NULL is returned.
 *
 * Returns: (transfer full) (element-type NMIPRoute): a newly allocated
 *   #GPtrArray of #NMIPRoute objects. In strict mode, %NULL is returned on error.
 **/
GPtrArray *
_nm_utils_ip6_routes_from_variant(GVariant *value, bool strict, GError **error)
{
    gs_unref_ptrarray GPtrArray *routes = NULL;
    GVariantIter                 iter;
    GVariant                    *dest_item;
    GVariant                    *next_hop_item;
    guint32                      prefix, metric;
    guint                        i;

    routes = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_route_unref);

    if (!g_variant_is_of_type(value, G_VARIANT_TYPE("a(ayuayu)"))) {
        if (strict) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("Expected value of type \"a(ayuayu)\""));
            return NULL;
        }
        return g_steal_pointer(&routes);
    }

    g_variant_iter_init(&iter, value);

    for (i = 0;
         g_variant_iter_next(&iter, "(@ayu@ayu)", &dest_item, &prefix, &next_hop_item, &metric);
         i++) {
        gs_unref_variant GVariant *dest_var     = dest_item;
        gs_unref_variant GVariant *next_hop_var = next_hop_item;
        gs_free_error GError      *local_error  = NULL;
        NMIPRoute                 *route;
        const struct in6_addr     *dest, *next_hop;
        gsize                      dest_len, next_hop_len;

        if (!g_variant_is_of_type(dest_var, G_VARIANT_TYPE_BYTESTRING)
            || !g_variant_is_of_type(next_hop_var, G_VARIANT_TYPE_BYTESTRING)) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("Expected value of type \"(ayuayu)\" (idx=%u)"),
                            i);
                return NULL;
            }
            continue;
        }

        dest = g_variant_get_fixed_array(dest_var, &dest_len, 1);
        if (dest_len != 16) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("IPv6 dest address with invalid length (idx=%u)"),
                            i);
                return NULL;
            }
            continue;
        }

        next_hop = g_variant_get_fixed_array(next_hop_var, &next_hop_len, 1);
        if (next_hop_len != 16) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("IPv6 next-hop address with invalid length (idx=%u)"),
                            i);
                return NULL;
            }
            continue;
        }

        route = nm_ip_route_new_binary(AF_INET6,
                                       dest,
                                       prefix,
                                       next_hop,
                                       metric ? (gint64) metric : -1,
                                       &local_error);
        if (!route) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("%s (idx=%u)"),
                            local_error->message,
                            i);
                return NULL;
            }
            continue;
        }

        g_ptr_array_add(routes, route);
    }

    return g_steal_pointer(&routes);
}

/**
 * nm_utils_ip_addresses_to_variant:
 * @addresses: (element-type NMIPAddress): an array of #NMIPAddress objects
 *
 * Utility function to convert a #GPtrArray of #NMIPAddress objects representing
 * IPv4 or IPv6 addresses into a #GVariant of type 'aa{sv}' representing an
 * array of new-style NetworkManager IP addresses. All addresses will include
 * "address" (an IP address string), and "prefix" (a uint). Some addresses may
 * include additional attributes.
 *
 * Returns: (transfer none): a new floating #GVariant representing @addresses.
 *
 * Since: 1.42
 **/
GVariant *
nm_utils_ip_addresses_to_variant(GPtrArray *addresses)
{
    GVariantBuilder builder;
    guint           i;

    g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}"));

    if (addresses) {
        for (i = 0; i < addresses->len; i++) {
            NMIPAddress         *addr = addresses->pdata[i];
            GVariantBuilder      addr_builder;
            gs_free const char **names = NULL;
            guint                j, len;

            g_variant_builder_init(&addr_builder, G_VARIANT_TYPE("a{sv}"));
            g_variant_builder_add(&addr_builder,
                                  "{sv}",
                                  "address",
                                  g_variant_new_string(nm_ip_address_get_address(addr)));
            g_variant_builder_add(&addr_builder,
                                  "{sv}",
                                  "prefix",
                                  g_variant_new_uint32(nm_ip_address_get_prefix(addr)));

            names = _nm_ip_address_get_attribute_names(addr, TRUE, &len);
            for (j = 0; j < len; j++) {
                g_variant_builder_add(&addr_builder,
                                      "{sv}",
                                      names[j],
                                      nm_ip_address_get_attribute(addr, names[j]));
            }

            g_variant_builder_add(&builder, "a{sv}", &addr_builder);
        }
    }

    return g_variant_builder_end(&builder);
}

/**
 * nm_utils_ip_addresses_from_variant:
 * @value: a #GVariant of type 'aa{sv}'
 * @family: an IP address family
 *
 * Utility function to convert a #GVariant representing a list of new-style
 * NetworkManager IPv4 or IPv6 addresses (as described in the documentation for
 * nm_utils_ip_addresses_to_variant()) into a #GPtrArray of #NMIPAddress
 * objects. Note that invalid addresses are discarded but the valid addresses
 * are still returned.
 *
 * Since 1.46, an empty list is returned if the variant type is not valid
 * (before it was checked as assertion)
 *
 * Returns: (transfer full) (element-type NMIPAddress): a newly allocated
 *   #GPtrArray of #NMIPAddress objects
 *
 * Since: 1.42
 **/
GPtrArray *
nm_utils_ip_addresses_from_variant(GVariant *value, int family)
{
    return _nm_utils_ip_addresses_from_variant(value, family, FALSE, NULL);
}

/**
 * _nm_utils_ip_addresses_from_variant:
 * @value: a #GVariant of type 'aa{sv}'
 * @family: an IP address family
 * @strict: whether to parse in strict mode or best-effort mode
 * @error: the error location
 *
 * Like #nm_utils_ip_addresses_from_variant, but allows one to parse in strict mode. In
 * strict mode, parsing is aborted on first error and %NULL is returned.
 *
 * Returns: (transfer full) (element-type NMIPAddress): a newly allocated
 *   #GPtrArray of #NMIPAddress objects. In strict mode, %NULL is returned on error.
 */
GPtrArray *
_nm_utils_ip_addresses_from_variant(GVariant *value, int family, bool strict, GError **error)
{
    gs_unref_ptrarray GPtrArray *addresses = NULL;
    GVariantIter                 iter, attrs_iter;
    GVariant                    *item;
    const char                  *ip;
    guint32                      prefix;
    NMIPAddress                 *addr;
    const char                  *attr_name;
    guint                        i;

    addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);

    if (!g_variant_is_of_type(value, G_VARIANT_TYPE("aa{sv}"))) {
        if (strict) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("Expected value of type \"aa{sv}\""));
            return NULL;
        }
        return g_steal_pointer(&addresses);
    }

    g_variant_iter_init(&iter, value);

    for (i = 0; g_variant_iter_next(&iter, "@a{sv}", &item); i++) {
        gs_unref_variant GVariant *addr_var    = item;
        gs_free_error GError      *local_error = NULL;

        if (!g_variant_lookup(addr_var, "address", "&s", &ip)
            || !g_variant_lookup(addr_var, "prefix", "u", &prefix)) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("IP address requires fields \"address\" and \"prefix\" (idx=%u)"),
                            i);
                return NULL;
            }
            continue;
        }

        addr = nm_ip_address_new(family, ip, prefix, &local_error);
        if (!addr) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("%s (idx=%u)"),
                            local_error->message,
                            i);
                return NULL;
            }
            continue;
        }

        g_variant_iter_init(&attrs_iter, addr_var);
        while (g_variant_iter_next(&attrs_iter, "{&sv}", &attr_name, &item)) {
            gs_unref_variant GVariant *attr_val = item;

            if (!NM_IN_STRSET(attr_name, "address", "prefix"))
                nm_ip_address_set_attribute(addr, attr_name, attr_val);
        }

        g_ptr_array_add(addresses, addr);
    }

    return g_steal_pointer(&addresses);
}

/**
 * nm_utils_ip_routes_to_variant:
 * @routes: (element-type NMIPRoute): an array of #NMIPRoute objects
 *
 * Utility function to convert a #GPtrArray of #NMIPRoute objects representing
 * IPv4 or IPv6 routes into a #GVariant of type 'aa{sv}' representing an array
 * of new-style NetworkManager IP routes. All routes will include "dest" (an IP
 * address string), "prefix" (an uint) and optionally "next-hop" (an IP address
 * string) and "metric" (an uint). Some routes may include additional attributes.
 * Note that invalid routes are discarded and only a warning is emitted, but the
 * valid routes are still returned.
 *
 * Returns: (transfer none): a new floating #GVariant representing @routes.
 *
 * Since: 1.42
 **/
GVariant *
nm_utils_ip_routes_to_variant(GPtrArray *routes)
{
    GVariantBuilder builder;
    guint           i;

    g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}"));

    if (routes) {
        for (i = 0; i < routes->len; i++) {
            NMIPRoute           *route = routes->pdata[i];
            GVariantBuilder      route_builder;
            gs_free const char **names = NULL;
            guint                j, len;

            g_variant_builder_init(&route_builder, G_VARIANT_TYPE("a{sv}"));
            g_variant_builder_add(&route_builder,
                                  "{sv}",
                                  "dest",
                                  g_variant_new_string(nm_ip_route_get_dest(route)));
            g_variant_builder_add(&route_builder,
                                  "{sv}",
                                  "prefix",
                                  g_variant_new_uint32(nm_ip_route_get_prefix(route)));
            if (nm_ip_route_get_next_hop(route)) {
                g_variant_builder_add(&route_builder,
                                      "{sv}",
                                      "next-hop",
                                      g_variant_new_string(nm_ip_route_get_next_hop(route)));
            }
            if (nm_ip_route_get_metric(route) != -1) {
                g_variant_builder_add(
                    &route_builder,
                    "{sv}",
                    "metric",
                    g_variant_new_uint32((guint32) nm_ip_route_get_metric(route)));
            }

            names = _nm_ip_route_get_attribute_names(route, TRUE, &len);
            for (j = 0; j < len; j++) {
                g_variant_builder_add(&route_builder,
                                      "{sv}",
                                      names[j],
                                      nm_ip_route_get_attribute(route, names[j]));
            }

            g_variant_builder_add(&builder, "a{sv}", &route_builder);
        }
    }

    return g_variant_builder_end(&builder);
}

/**
 * nm_utils_ip_routes_from_variant:
 * @value: a #GVariant of type 'aa{sv}'
 * @family: an IP address family
 *
 * Utility function to convert a #GVariant representing a list of new-style
 * NetworkManager IPv4 or IPv6 addresses (as described in the documentation for
 * nm_utils_ip_routes_to_variant()) into a #GPtrArray of #NMIPRoute objects.
 * Invalid routes are discarded but the valid routes are still returned.
 *
 * Since 1.46, an empty list is returned if the variant type is not valid
 * (before it was checked as assertion)
 *
 * Returns: (transfer full) (element-type NMIPRoute): a newly allocated
 *   #GPtrArray of #NMIPRoute objects
 *
 * Since: 1.42
 **/
GPtrArray *
nm_utils_ip_routes_from_variant(GVariant *value, int family)
{
    return _nm_utils_ip_routes_from_variant(value, family, FALSE, NULL);
}

/**
 * _nm_utils_ip_routes_from_variant:
 * @value: a #GVariant of type 'aa{sv}'
 * @family: an IP address family
 * @strict: whether to parse in strict mode or best-effort mode
 * @error: the error location
 *
 * Like #nm_utils_ip_routes_from_variant, but allows one to parse in strict mode. In
 * strict mode, parsing is aborted on first error and %NULL is returned.
 *
 * Returns: (transfer full) (element-type NMIPRoute): a newly allocated
 *   #GPtrArray of #NMIPRoute objects. In strict mode %NULL is returned on error.
 */
GPtrArray *
_nm_utils_ip_routes_from_variant(GVariant *value, int family, bool strict, GError **error)
{
    gs_unref_ptrarray GPtrArray *routes = NULL;
    GVariantIter                 iter, attrs_iter;
    GVariant                    *item;
    const char                  *dest, *next_hop;
    guint32                      prefix, metric32;
    gint64                       metric;
    const char                  *attr_name;
    NMIPRoute                   *route;
    guint                        i;

    routes = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_route_unref);

    if (!g_variant_is_of_type(value, G_VARIANT_TYPE("aa{sv}"))) {
        if (strict) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("Expected value of type \"aa{sv}\""));
            return NULL;
        }
        return g_steal_pointer(&routes);
    }

    g_variant_iter_init(&iter, value);

    for (i = 0; g_variant_iter_next(&iter, "@a{sv}", &item); i++) {
        gs_unref_variant GVariant *route_var   = item;
        gs_free_error GError      *local_error = NULL;

        if (!g_variant_lookup(route_var, "dest", "&s", &dest)
            || !g_variant_lookup(route_var, "prefix", "u", &prefix)) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("Route requires fields \"dest\" and \"prefix\" (idx=%u)"),
                            i);
                return NULL;
            }
            continue;
        }
        if (!g_variant_lookup(route_var, "next-hop", "&s", &next_hop))
            next_hop = NULL;
        if (g_variant_lookup(route_var, "metric", "u", &metric32))
            metric = metric32;
        else
            metric = -1;

        route = nm_ip_route_new(family, dest, prefix, next_hop, metric, &local_error);
        if (!route) {
            if (strict) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("%s (idx=%u)"),
                            local_error->message,
                            i);
                return NULL;
            }
            continue;
        }

        g_variant_iter_init(&attrs_iter, route_var);
        while (g_variant_iter_next(&attrs_iter, "{&sv}", &attr_name, &item)) {
            gs_unref_variant GVariant *attr_val = item;

            if (!NM_IN_STRSET(attr_name, "dest", "prefix", "next-hop", "metric"))
                nm_ip_route_set_attribute(route, attr_name, attr_val);
        }

        g_ptr_array_add(routes, route);
    }

    return g_steal_pointer(&routes);
}

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

static void
_string_append_tc_handle(GString *string, guint32 handle)
{
    g_string_append_printf(string, "%x:", TC_H_MAJ(handle) >> 16);
    if (TC_H_MIN(handle) != TC_H_UNSPEC)
        g_string_append_printf(string, "%x", TC_H_MIN(handle));
}

/**
 * _nm_utils_string_append_tc_parent:
 * @string: the string to write the parent handle to
 * @prefix: optional prefix for the numeric handle
 * @parent: the parent handle
 *
 * This is used to either write out the parent handle to the tc qdisc string
 * or to pretty-format (use symbolic name for root) the key in keyfile.
 * The presence of prefix determines which one is the case.
 *
 * Private API due to general ugliness and overall uselessness for anything
 * sensible.
 */
void
_nm_utils_string_append_tc_parent(GString *string, const char *prefix, guint32 parent)
{
    if (parent == TC_H_ROOT) {
        g_string_append(string, "root");
    } else {
        if (prefix) {
            if (parent == TC_H_INGRESS)
                return;
            g_string_append_printf(string, "%s ", prefix);
        }
        _string_append_tc_handle(string, parent);
    }

    if (prefix)
        g_string_append_c(string, ' ');
}

/**
 * _nm_utils_parse_tc_handle:
 * @str: the string representation of a qdisc handle
 * @error: location of the error
 *
 * Parses tc style handle number into a numeric representation.
 * Don't use this, use nm_utils_tc_qdisc_from_str() instead.
 */
guint32
_nm_utils_parse_tc_handle(const char *str, GError **error)
{
    gint64      maj;
    gint64      min = 0;
    const char *sep;

    nm_assert(str);

    maj = nm_g_ascii_strtoll(str, (char **) &sep, 0x10);
    if (sep == str)
        goto fail;

    sep = nm_str_skip_leading_spaces(sep);

    if (sep[0] == ':') {
        const char *str2 = &sep[1];

        min = nm_g_ascii_strtoll(str2, (char **) &sep, 0x10);
        sep = nm_str_skip_leading_spaces(sep);
        if (sep[0] != '\0')
            goto fail;
    } else if (sep[0] != '\0')
        goto fail;

    if (maj <= 0 || maj > 0xffff || min < 0 || min > 0xffff
        || !NM_STRCHAR_ALL(str, ch, (g_ascii_isxdigit(ch) || ch == ':' || g_ascii_isspace(ch)))) {
        goto fail;
    }

    return TC_H_MAKE(((guint32) maj) << 16, (guint32) min);
fail:
    nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, _("'%s' is not a valid handle."), str);
    return TC_H_UNSPEC;
}

static const NMVariantAttributeSpec *const tc_object_attribute_spec[] = {
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("root", G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("parent", G_VARIANT_TYPE_STRING, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("handle", G_VARIANT_TYPE_STRING, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("kind", G_VARIANT_TYPE_STRING, .no_value = TRUE, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("",
                                     G_VARIANT_TYPE_STRING,
                                     .no_value      = TRUE,
                                     .consumes_rest = TRUE, ),
    NULL,
};

static const NMVariantAttributeSpec *const tc_qdisc_sfq_spec[] = {
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("quantum", G_VARIANT_TYPE_UINT32, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("perturb", G_VARIANT_TYPE_INT32, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("limit", G_VARIANT_TYPE_UINT32, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("divisor", G_VARIANT_TYPE_UINT32, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("flows", G_VARIANT_TYPE_UINT32, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("depth", G_VARIANT_TYPE_UINT32, ),
    NULL,
};

static const NMVariantAttributeSpec *const tc_qdisc_tbf_spec[] = {
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("rate", G_VARIANT_TYPE_UINT64, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("burst", G_VARIANT_TYPE_UINT32, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("limit", G_VARIANT_TYPE_UINT32, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("latency", G_VARIANT_TYPE_UINT32, ),
    NULL,
};

static const NMVariantAttributeSpec *const tc_qdisc_fq_codel_spec[] = {
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("limit", G_VARIANT_TYPE_UINT32, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("flows", G_VARIANT_TYPE_UINT32, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("target", G_VARIANT_TYPE_UINT32, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("interval", G_VARIANT_TYPE_UINT32, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("quantum", G_VARIANT_TYPE_UINT32, ),

    /* 0x83126E97u is not a valid value (it means "disabled"). We should reject that
     * value. Or alternatively, reject all values >= MAX_INT(32). */
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("ce_threshold", G_VARIANT_TYPE_UINT32, ),

    /* kernel clamps the value at 2^31. Possibly such values should be rejected from configuration
     * as they cannot be configured. Leaving the attribute unspecified causes kernel to choose
     * a default (currently 32MB). */
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("memory_limit", G_VARIANT_TYPE_UINT32, ),

    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("ecn", G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE, ),
    NULL,
};

typedef struct {
    const char                          *kind;
    const NMVariantAttributeSpec *const *attrs;
} NMQdiscAttributeSpec;

static const NMQdiscAttributeSpec *const tc_qdisc_attribute_spec[] = {
    &(const NMQdiscAttributeSpec) {"fq_codel", tc_qdisc_fq_codel_spec},
    &(const NMQdiscAttributeSpec) {"sfq", tc_qdisc_sfq_spec},
    &(const NMQdiscAttributeSpec) {"tbf", tc_qdisc_tbf_spec},
    NULL,
};

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

/**
 * _nm_utils_string_append_tc_qdisc_rest:
 * @string: the string to write the formatted qdisc to
 * @qdisc: the %NMTCQdisc
 *
 * This formats the rest of the qdisc string but the parent. Useful to format
 * the keyfile value and nowhere else.
 * Use nm_utils_tc_qdisc_to_str() that also includes the parent instead.
 */
void
_nm_utils_string_append_tc_qdisc_rest(GString *string, NMTCQdisc *qdisc)
{
    guint32       handle = nm_tc_qdisc_get_handle(qdisc);
    const char   *kind   = nm_tc_qdisc_get_kind(qdisc);
    gs_free char *str    = NULL;

    if (handle != TC_H_UNSPEC && !NM_IN_STRSET(kind, "ingress", "clsact")) {
        g_string_append(string, "handle ");
        _string_append_tc_handle(string, handle);
        g_string_append_c(string, ' ');
    }

    g_string_append(string, kind);

    str = nm_utils_format_variant_attributes(_nm_tc_qdisc_get_attributes(qdisc), ' ', ' ');
    if (str) {
        g_string_append_c(string, ' ');
        g_string_append(string, str);
    }
}

/**
 * nm_utils_tc_qdisc_to_str:
 * @qdisc: the %NMTCQdisc
 * @error: location of the error
 *
 * Turns the %NMTCQdisc into a tc style string representation of the queueing
 * discipline.
 *
 * Returns: formatted string or %NULL
 *
 * Since: 1.12
 */
char *
nm_utils_tc_qdisc_to_str(NMTCQdisc *qdisc, GError **error)
{
    GString *string;

    string = g_string_sized_new(60);

    _nm_utils_string_append_tc_parent(string, "parent", nm_tc_qdisc_get_parent(qdisc));
    _nm_utils_string_append_tc_qdisc_rest(string, qdisc);

    return g_string_free(string, FALSE);
}

static gboolean
_tc_read_common_opts(const char *str,
                     guint32    *handle,
                     guint32    *parent,
                     char      **kind,
                     char      **rest,
                     GError    **error)
{
    gs_unref_hashtable GHashTable *ht = NULL;
    GVariant                      *variant;

    ht = nm_utils_parse_variant_attributes(str, ' ', ' ', FALSE, tc_object_attribute_spec, error);
    if (!ht)
        return FALSE;

    if (g_hash_table_contains(ht, "root"))
        *parent = TC_H_ROOT;

    variant = g_hash_table_lookup(ht, "parent");
    if (variant) {
        if (*parent != TC_H_UNSPEC) {
            g_set_error(error,
                        1,
                        0,
                        _("'%s' unexpected: parent already specified."),
                        g_variant_get_string(variant, NULL));
            return FALSE;
        }
        *parent = _nm_utils_parse_tc_handle(g_variant_get_string(variant, NULL), error);
        if (*parent == TC_H_UNSPEC)
            return FALSE;
    }

    variant = g_hash_table_lookup(ht, "handle");
    if (variant) {
        *handle = _nm_utils_parse_tc_handle(g_variant_get_string(variant, NULL), error);
        if (*handle == TC_H_UNSPEC)
            return FALSE;
        if (TC_H_MIN(*handle)) {
            g_set_error(error,
                        1,
                        0,
                        _("invalid handle: '%s'"),
                        g_variant_get_string(variant, NULL));
            return FALSE;
        }
    }

    variant = g_hash_table_lookup(ht, "kind");
    if (variant) {
        *kind = g_variant_dup_string(variant, NULL);
        if (NM_IN_STRSET(*kind, "ingress", "clsact")) {
            if (*parent == TC_H_UNSPEC)
                *parent = TC_H_INGRESS;
            if (*handle == TC_H_UNSPEC)
                *handle = TC_H_MAKE(TC_H_INGRESS, 0);
        }
    }

    if (*parent == TC_H_UNSPEC) {
        if (*kind) {
            g_free(*kind);
            *kind = NULL;
        }
        g_set_error_literal(error, 1, 0, _("parent not specified."));
        return FALSE;
    }

    variant = g_hash_table_lookup(ht, "");
    if (variant)
        *rest = g_variant_dup_string(variant, NULL);

    return TRUE;
}

/**
 * nm_utils_tc_qdisc_from_str:
 * @str: the string representation of a qdisc
 * @error: location of the error
 *
 * Parses the tc style string qdisc representation of the queueing
 * discipline to a %NMTCQdisc instance. Supports a subset of the tc language.
 *
 * Returns: the %NMTCQdisc or %NULL
 *
 * Since: 1.12
 */
NMTCQdisc *
nm_utils_tc_qdisc_from_str(const char *str, GError **error)
{
    guint32                        handle  = TC_H_UNSPEC;
    guint32                        parent  = TC_H_UNSPEC;
    gs_free char                  *kind    = NULL;
    gs_free char                  *rest    = NULL;
    NMTCQdisc                     *qdisc   = NULL;
    gs_unref_hashtable GHashTable *options = NULL;
    GHashTableIter                 iter;
    gpointer                       key, value;
    guint                          i;

    nm_assert(str);
    nm_assert(!error || !*error);

    if (!_tc_read_common_opts(str, &handle, &parent, &kind, &rest, error))
        return NULL;

    for (i = 0; rest && tc_qdisc_attribute_spec[i]; i++) {
        if (nm_streq(tc_qdisc_attribute_spec[i]->kind, kind)) {
            options = nm_utils_parse_variant_attributes(rest,
                                                        ' ',
                                                        ' ',
                                                        FALSE,
                                                        tc_qdisc_attribute_spec[i]->attrs,
                                                        error);
            if (!options)
                return NULL;
            break;
        }
    }
    nm_clear_g_free(&rest);

    if (options) {
        value = g_hash_table_lookup(options, "");
        if (value)
            rest = g_variant_dup_string(value, NULL);
    }

    if (rest) {
        g_set_error(error, 1, 0, _("unsupported qdisc option: '%s'."), rest);
        return NULL;
    }

    qdisc = nm_tc_qdisc_new(kind, parent, error);
    if (!qdisc)
        return NULL;

    nm_tc_qdisc_set_handle(qdisc, handle);

    if (options) {
        g_hash_table_iter_init(&iter, options);
        while (g_hash_table_iter_next(&iter, &key, &value))
            nm_tc_qdisc_set_attribute(qdisc, key, g_variant_ref_sink(value));
    }

    return qdisc;
}

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

static const NMVariantAttributeSpec *const tc_action_simple_attribute_spec[] = {
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("sdata", G_VARIANT_TYPE_BYTESTRING, ),
    NULL,
};

static const NMVariantAttributeSpec *const tc_action_mirred_attribute_spec[] = {
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("egress", G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("ingress", G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("mirror", G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("redirect", G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("dev", G_VARIANT_TYPE_STRING, ),
    NULL,
};

static const NMVariantAttributeSpec *const tc_action_attribute_spec[] = {
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("kind", G_VARIANT_TYPE_STRING, .no_value = TRUE, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("",
                                     G_VARIANT_TYPE_STRING,
                                     .no_value      = TRUE,
                                     .consumes_rest = TRUE, ),
    NULL,
};

static gboolean
_string_append_tc_action(GString *string, NMTCAction *action, GError **error)
{
    const char                          *kind = nm_tc_action_get_kind(action);
    gs_free char                        *str  = NULL;
    const NMVariantAttributeSpec *const *attrs;

    if (nm_streq(kind, "simple"))
        attrs = tc_action_simple_attribute_spec;
    else if (nm_streq(kind, "mirred"))
        attrs = tc_action_mirred_attribute_spec;
    else
        attrs = NULL;

    g_string_append(string, kind);

    str =
        _nm_utils_format_variant_attributes(_nm_tc_action_get_attributes(action), attrs, ' ', ' ');
    if (str) {
        g_string_append_c(string, ' ');
        g_string_append(string, str);
    }

    return TRUE;
}

/**
 * nm_utils_tc_action_to_str:
 * @action: the %NMTCAction
 * @error: location of the error
 *
 * Turns the %NMTCAction into a tc style string representation of the queueing
 * discipline.
 *
 * Returns: formatted string or %NULL
 *
 * Since: 1.12
 */
char *
nm_utils_tc_action_to_str(NMTCAction *action, GError **error)
{
    GString *string;

    string = g_string_sized_new(60);
    if (!_string_append_tc_action(string, action, error)) {
        g_string_free(string, TRUE);
        return NULL;
    }

    return g_string_free(string, FALSE);
}

/**
 * nm_utils_tc_action_from_str:
 * @str: the string representation of a action
 * @error: location of the error
 *
 * Parses the tc style string action representation of the queueing
 * discipline to a %NMTCAction instance. Supports a subset of the tc language.
 *
 * Returns: the %NMTCAction or %NULL
 *
 * Since: 1.12
 */
NMTCAction *
nm_utils_tc_action_from_str(const char *str, GError **error)
{
    const char                          *kind    = NULL;
    const char                          *rest    = NULL;
    nm_auto_unref_tc_action NMTCAction  *action  = NULL;
    gs_unref_hashtable GHashTable       *ht      = NULL;
    gs_unref_hashtable GHashTable       *options = NULL;
    GVariant                            *variant;
    const NMVariantAttributeSpec *const *attrs;

    nm_assert(str);
    nm_assert(!error || !*error);

    ht = nm_utils_parse_variant_attributes(str, ' ', ' ', FALSE, tc_action_attribute_spec, error);
    if (!ht)
        return FALSE;

    variant = g_hash_table_lookup(ht, "kind");
    if (variant) {
        kind = g_variant_get_string(variant, NULL);
    } else {
        g_set_error_literal(error, 1, 0, _("action name missing."));
        return NULL;
    }

    kind = g_variant_get_string(variant, NULL);
    if (nm_streq(kind, "simple"))
        attrs = tc_action_simple_attribute_spec;
    else if (nm_streq(kind, "mirred"))
        attrs = tc_action_mirred_attribute_spec;
    else
        attrs = NULL;

    variant = g_hash_table_lookup(ht, "");
    if (variant)
        rest = g_variant_get_string(variant, NULL);

    action = nm_tc_action_new(kind, error);
    if (!action)
        return NULL;

    if (rest) {
        GHashTableIter iter;
        gpointer       key, value;

        if (!attrs) {
            g_set_error(error, 1, 0, _("unsupported action option: '%s'."), rest);
            return NULL;
        }

        options = nm_utils_parse_variant_attributes(rest, ' ', ' ', FALSE, attrs, error);
        if (!options)
            return NULL;

        g_hash_table_iter_init(&iter, options);
        while (g_hash_table_iter_next(&iter, &key, &value))
            nm_tc_action_set_attribute(action, key, g_variant_ref_sink(value));
    }

    return g_steal_pointer(&action);
}

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

/**
 * _nm_utils_string_append_tc_tfilter_rest:
 * @string: the string to write the formatted tfilter to
 * @tfilter: the %NMTCTfilter
 *
 * This formats the rest of the tfilter string but the parent. Useful to format
 * the keyfile value and nowhere else.
 * Use nm_utils_tc_tfilter_to_str() that also includes the parent instead.
 */
gboolean
_nm_utils_string_append_tc_tfilter_rest(GString *string, NMTCTfilter *tfilter, GError **error)
{
    guint32     handle = nm_tc_tfilter_get_handle(tfilter);
    const char *kind   = nm_tc_tfilter_get_kind(tfilter);
    NMTCAction *action;

    if (handle != TC_H_UNSPEC) {
        g_string_append(string, "handle ");
        _string_append_tc_handle(string, handle);
        g_string_append_c(string, ' ');
    }

    g_string_append(string, kind);

    action = nm_tc_tfilter_get_action(tfilter);
    if (action) {
        g_string_append(string, " action ");
        if (!_string_append_tc_action(string, action, error))
            return FALSE;
    }

    return TRUE;
}

/**
 * nm_utils_tc_tfilter_to_str:
 * @tfilter: the %NMTCTfilter
 * @error: location of the error
 *
 * Turns the %NMTCTfilter into a tc style string representation of the queueing
 * discipline.
 *
 * Returns: formatted string or %NULL
 *
 * Since: 1.12
 */
char *
nm_utils_tc_tfilter_to_str(NMTCTfilter *tfilter, GError **error)
{
    GString *string;

    string = g_string_sized_new(60);

    _nm_utils_string_append_tc_parent(string, "parent", nm_tc_tfilter_get_parent(tfilter));
    if (!_nm_utils_string_append_tc_tfilter_rest(string, tfilter, error)) {
        g_string_free(string, TRUE);
        return NULL;
    }

    return g_string_free(string, FALSE);
}

static const NMVariantAttributeSpec *const tc_tfilter_attribute_spec[] = {
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("action", G_VARIANT_TYPE_BOOLEAN, .no_value = TRUE, ),
    NM_VARIANT_ATTRIBUTE_SPEC_DEFINE("",
                                     G_VARIANT_TYPE_STRING,
                                     .no_value      = TRUE,
                                     .consumes_rest = TRUE, ),
    NULL,
};

/**
 * nm_utils_tc_tfilter_from_str:
 * @str: the string representation of a tfilter
 * @error: location of the error
 *
 * Parses the tc style string tfilter representation of the queueing
 * discipline to a %NMTCTfilter instance. Supports a subset of the tc language.
 *
 * Returns: the %NMTCTfilter or %NULL
 *
 * Since: 1.12
 */
NMTCTfilter *
nm_utils_tc_tfilter_from_str(const char *str, GError **error)
{
    guint32                             handle     = TC_H_UNSPEC;
    guint32                             parent     = TC_H_UNSPEC;
    gs_free char                       *kind       = NULL;
    gs_free char                       *rest       = NULL;
    nm_auto_unref_tc_action NMTCAction *action     = NULL;
    const char                         *extra_opts = NULL;
    NMTCTfilter                        *tfilter    = NULL;
    gs_unref_hashtable GHashTable      *ht         = NULL;
    GVariant                           *variant;

    nm_assert(str);
    nm_assert(!error || !*error);

    if (!_tc_read_common_opts(str, &handle, &parent, &kind, &rest, error))
        return NULL;

    if (rest) {
        ht = nm_utils_parse_variant_attributes(rest,
                                               ' ',
                                               ' ',
                                               FALSE,
                                               tc_tfilter_attribute_spec,
                                               error);
        if (!ht)
            return NULL;

        variant = g_hash_table_lookup(ht, "");
        if (variant)
            extra_opts = g_variant_get_string(variant, NULL);

        if (g_hash_table_contains(ht, "action")) {
            action = nm_utils_tc_action_from_str(extra_opts, error);
            if (!action) {
                g_prefix_error(error, _("invalid action: "));
                return NULL;
            }
        } else {
            g_set_error(error, 1, 0, _("unsupported tfilter option: '%s'."), rest);
            return NULL;
        }
    }

    tfilter = nm_tc_tfilter_new(kind, parent, error);
    if (!tfilter)
        return NULL;

    nm_tc_tfilter_set_handle(tfilter, handle);
    if (action)
        nm_tc_tfilter_set_action(tfilter, action);

    return tfilter;
}

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

extern const NMVariantAttributeSpec *const _nm_sriov_vf_attribute_spec[];

/**
 * nm_utils_sriov_vf_to_str:
 * @vf: the %NMSriovVF
 * @omit_index: if %TRUE, the VF index will be omitted from output string
 * @error: location to store the error on failure
 *
 * Converts a SR-IOV virtual function object to its string representation.
 *
 * Returns: a newly allocated string or %NULL on error
 *
 * Since: 1.14
 */
char *
nm_utils_sriov_vf_to_str(const NMSriovVF *vf, gboolean omit_index, GError **error)
{
    gs_free NMUtilsNamedValue *values = NULL;
    gs_free const char       **names  = NULL;
    const guint               *vlan_ids;
    guint                      num_vlans, num_attrs;
    guint                      i;
    GString                   *str;

    str = g_string_new("");
    if (!omit_index)
        g_string_append_printf(str, "%u", nm_sriov_vf_get_index(vf));

    names     = nm_sriov_vf_get_attribute_names(vf);
    num_attrs = names ? g_strv_length((char **) names) : 0;
    values    = g_new0(NMUtilsNamedValue, num_attrs);

    for (i = 0; i < num_attrs; i++) {
        values[i].name      = names[i];
        values[i].value_ptr = nm_sriov_vf_get_attribute(vf, names[i]);
    }

    if (num_attrs > 0) {
        if (!omit_index)
            g_string_append_c(str, ' ');
        _nm_utils_format_variant_attributes_full(str, values, num_attrs, NULL, ' ', '=');
    }

    vlan_ids = nm_sriov_vf_get_vlan_ids(vf, &num_vlans);
    if (num_vlans != 0) {
        g_string_append(str, " vlans");
        for (i = 0; i < num_vlans; i++) {
            guint32               qos;
            NMSriovVFVlanProtocol protocol;

            qos      = nm_sriov_vf_get_vlan_qos(vf, vlan_ids[i]);
            protocol = nm_sriov_vf_get_vlan_protocol(vf, vlan_ids[i]);

            g_string_append_c(str, i == 0 ? '=' : ';');

            g_string_append_printf(str, "%u", vlan_ids[i]);

            if (qos != 0 || protocol != NM_SRIOV_VF_VLAN_PROTOCOL_802_1Q) {
                g_string_append_printf(str,
                                       ".%u%s",
                                       (unsigned) qos,
                                       protocol == NM_SRIOV_VF_VLAN_PROTOCOL_802_1Q ? "" : ".ad");
            }
        }
    }

    return g_string_free(str, FALSE);
}

gboolean
_nm_sriov_vf_parse_vlans(NMSriovVF *vf, const char *str, GError **error)
{
    gs_free const char **vlans = NULL;
    guint                i;

    vlans = nm_strsplit_set(str, ";");
    if (!vlans) {
        g_set_error_literal(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_FAILED,
                            "empty VF VLAN");
        return FALSE;
    }

    for (i = 0; vlans[i]; i++) {
        gs_strfreev char **params = NULL;
        guint              id     = G_MAXUINT;
        gint64             qos    = -1;

        /* we accept leading/trailing whitespace around vlans[1]. Hence
         * the nm_str_skip_leading_spaces() and g_strchomp() below.
         *
         * However, we don't accept any whitespace inside the specifier.
         * Hence the NM_STRCHAR_ALL() checks. */

        params = g_strsplit(nm_str_skip_leading_spaces(vlans[i]), ".", 3);
        if (!params || !params[0] || *params[0] == '\0') {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_FAILED,
                                "empty VF VLAN");
            return FALSE;
        }

        if (!params[1])
            g_strchomp(params[0]);
        if (NM_STRCHAR_ALL(params[0], ch, ch == 'x' || g_ascii_isdigit(ch)))
            id = _nm_utils_ascii_str_to_int64(params[0], 0, 0, 4095, G_MAXUINT);
        if (id == G_MAXUINT) {
            g_set_error(error,
                        NM_CONNECTION_ERROR,
                        NM_CONNECTION_ERROR_FAILED,
                        "invalid VF VLAN id '%s'",
                        params[0]);
            return FALSE;
        }
        if (!nm_sriov_vf_add_vlan(vf, id)) {
            g_set_error(error,
                        NM_CONNECTION_ERROR,
                        NM_CONNECTION_ERROR_FAILED,
                        "duplicate VLAN id %u",
                        id);
            return FALSE;
        }

        if (!params[1])
            continue;

        if (!params[2])
            g_strchomp(params[1]);
        if (NM_STRCHAR_ALL(params[1], ch, ch == 'x' || g_ascii_isdigit(ch)))
            qos = _nm_utils_ascii_str_to_int64(params[1], 0, 0, G_MAXUINT32, -1);
        if (qos == -1) {
            g_set_error(error,
                        NM_CONNECTION_ERROR,
                        NM_CONNECTION_ERROR_FAILED,
                        "invalid VF VLAN QoS '%s'",
                        params[1]);
            return FALSE;
        }
        nm_sriov_vf_set_vlan_qos(vf, id, qos);

        if (!params[2])
            continue;

        g_strchomp(params[2]);

        if (nm_streq(params[2], "ad"))
            nm_sriov_vf_set_vlan_protocol(vf, id, NM_SRIOV_VF_VLAN_PROTOCOL_802_1AD);
        else if (nm_streq(params[2], "q"))
            nm_sriov_vf_set_vlan_protocol(vf, id, NM_SRIOV_VF_VLAN_PROTOCOL_802_1Q);
        else {
            g_set_error(error,
                        NM_CONNECTION_ERROR,
                        NM_CONNECTION_ERROR_FAILED,
                        "invalid VF VLAN protocol '%s'",
                        params[2]);
            return FALSE;
        }
    }

    return TRUE;
}

/**
 * nm_utils_sriov_vf_from_str:
 * @str: the input string
 * @error: location to store the error on failure
 *
 * Converts a string to a SR-IOV virtual function object.
 *
 * Returns: (transfer full): the virtual function object
 *
 * Since: 1.14
 */
NMSriovVF *
nm_utils_sriov_vf_from_str(const char *str, GError **error)
{
    gs_free char *index_free = NULL;
    const char   *detail;

    g_return_val_if_fail(str, NULL);
    g_return_val_if_fail(!error || !*error, NULL);

    while (*str == ' ')
        str++;

    detail = strchr(str, ' ');
    if (detail) {
        str = nm_strndup_a(200, str, detail - str, &index_free);
        detail++;
    }

    return _nm_utils_sriov_vf_from_strparts(str, detail, FALSE, error);
}

NMSriovVF *
_nm_utils_sriov_vf_from_strparts(const char *index,
                                 const char *detail,
                                 gboolean    ignore_unknown,
                                 GError    **error)
{
    NMSriovVF                     *vf;
    guint32                        n_index;
    GHashTableIter                 iter;
    char                          *key;
    GVariant                      *variant;
    gs_unref_hashtable GHashTable *ht = NULL;

    n_index = _nm_utils_ascii_str_to_int64(index, 10, 0, G_MAXUINT32, 0);
    if (errno) {
        g_set_error_literal(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_FAILED,
                            "invalid index");
        return NULL;
    }

    vf = nm_sriov_vf_new(n_index);
    if (detail) {
        ht = nm_utils_parse_variant_attributes(detail,
                                               ' ',
                                               '=',
                                               ignore_unknown,
                                               _nm_sriov_vf_attribute_spec,
                                               error);
        if (!ht) {
            nm_sriov_vf_unref(vf);
            return NULL;
        }

        if ((variant = g_hash_table_lookup(ht, "vlans"))) {
            if (!_nm_sriov_vf_parse_vlans(vf, g_variant_get_string(variant, NULL), error)) {
                nm_sriov_vf_unref(vf);
                return NULL;
            }
            g_hash_table_remove(ht, "vlans");
        }

        g_hash_table_iter_init(&iter, ht);
        while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &variant))
            nm_sriov_vf_set_attribute(vf, key, g_variant_ref_sink(variant));
    }

    return vf;
}

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

/**
 * nm_utils_uuid_generate:
 *
 * Returns: a newly allocated UUID suitable for use as the #NMSettingConnection
 * object's #NMSettingConnection:id: property.  Should be freed with g_free()
 **/
char *
nm_utils_uuid_generate(void)
{
    return nm_uuid_generate_random_str_malloc();
}

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

gboolean
_nm_utils_check_file(const char               *filename,
                     gint64                    check_owner,
                     NMUtilsCheckFilePredicate check_file,
                     gpointer                  user_data,
                     struct stat              *out_st,
                     GError                  **error)
{
    struct stat st_backup;

    if (!out_st)
        out_st = &st_backup;

    if (stat(filename, out_st) != 0) {
        int errsv = errno;

        g_set_error(error,
                    NM_VPN_PLUGIN_ERROR,
                    NM_VPN_PLUGIN_ERROR_FAILED,
                    _("failed stat file %s: %s"),
                    filename,
                    nm_strerror_native(errsv));
        return FALSE;
    }

    /* ignore non-files. */
    if (!S_ISREG(out_st->st_mode)) {
        g_set_error(error,
                    NM_VPN_PLUGIN_ERROR,
                    NM_VPN_PLUGIN_ERROR_FAILED,
                    _("not a file (%s)"),
                    filename);
        return FALSE;
    }

    /* with check_owner enabled, check that the file belongs to the
     * owner or root. */
    if (check_owner >= 0 && (out_st->st_uid != 0 && (gint64) out_st->st_uid != check_owner)) {
        g_set_error(error,
                    NM_VPN_PLUGIN_ERROR,
                    NM_VPN_PLUGIN_ERROR_FAILED,
                    _("invalid file owner %d for %s"),
                    out_st->st_uid,
                    filename);
        return FALSE;
    }

    /* with check_owner enabled, check that the file cannot be modified
     * by other users (except root). */
    if (check_owner >= 0 && NM_FLAGS_ANY(out_st->st_mode, S_IWGRP | S_IWOTH | S_ISUID)) {
        g_set_error(error,
                    NM_VPN_PLUGIN_ERROR,
                    NM_VPN_PLUGIN_ERROR_FAILED,
                    _("file permissions for %s"),
                    filename);
        return FALSE;
    }

    if (check_file && !check_file(filename, out_st, user_data, error)) {
        if (error && !*error) {
            g_set_error(error,
                        NM_VPN_PLUGIN_ERROR,
                        NM_VPN_PLUGIN_ERROR_FAILED,
                        _("reject %s"),
                        filename);
        }
        return FALSE;
    }

    return TRUE;
}

gboolean
_nm_utils_check_module_file(const char               *name,
                            int                       check_owner,
                            NMUtilsCheckFilePredicate check_file,
                            gpointer                  user_data,
                            GError                  **error)
{
    if (!g_path_is_absolute(name)) {
        g_set_error(error,
                    NM_VPN_PLUGIN_ERROR,
                    NM_VPN_PLUGIN_ERROR_FAILED,
                    _("path is not absolute (%s)"),
                    name);
        return FALSE;
    }

    /* Set special error code if the file doesn't exist.
     * The VPN package might be split into separate packages,
     * so it could be correct that the plugin file is missing.
     *
     * Note that nm-applet checks for this error code to fail
     * gracefully. */
    if (!g_file_test(name, G_FILE_TEST_EXISTS)) {
        g_set_error(error,
                    G_FILE_ERROR,
                    G_FILE_ERROR_NOENT,
                    _("Plugin file does not exist (%s)"),
                    name);
        return FALSE;
    }

    if (!g_file_test(name, G_FILE_TEST_IS_REGULAR)) {
        g_set_error(error,
                    NM_VPN_PLUGIN_ERROR,
                    NM_VPN_PLUGIN_ERROR_FAILED,
                    _("Plugin is not a valid file (%s)"),
                    name);
        return FALSE;
    }

    if (g_str_has_suffix(name, ".la")) {
        /* g_module_open() treats files that end with .la special.
         * We don't want to parse the libtool archive. Just error out. */
        g_set_error(error,
                    NM_VPN_PLUGIN_ERROR,
                    NM_VPN_PLUGIN_ERROR_FAILED,
                    _("libtool archives are not supported (%s)"),
                    name);
        return FALSE;
    }

    return _nm_utils_check_file(name, check_owner, check_file, user_data, NULL, error);
}

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

/**
 * nm_utils_file_search_in_paths:
 * @progname: the helper program name, like "iptables"
 *   Must be a non-empty string, without path separator (/).
 * @try_first: (nullable): a custom path to try first before searching.
 *   It is silently ignored if it is empty or not an absolute path.
 * @paths: (nullable): a %NULL terminated list of search paths.
 *   Can be empty or %NULL, in which case only @try_first is checked.
 * @file_test_flags: the flags passed to g_file_test() when searching
 *   for @progname. Set it to 0 to skip the g_file_test().
 * @predicate: (scope call) (closure user_data): if given, pass the file name to this function
 *   for additional checks. This check is performed after the check for
 *   @file_test_flags. You cannot omit both @file_test_flags and @predicate.
 * @user_data: (nullable): user data for @predicate function.
 * @error: on failure, set a "not found" error %G_IO_ERROR %G_IO_ERROR_NOT_FOUND.
 *
 * Searches for a @progname file in a list of search @paths.
 *
 * Returns: (transfer none): the full path to the helper, if found, or %NULL if not found.
 *   The returned string is not owned by the caller, but later
 *   invocations of the function might overwrite it.
 */
const char *
nm_utils_file_search_in_paths(const char                       *progname,
                              const char                       *try_first,
                              const char *const                *paths,
                              GFileTest                         file_test_flags,
                              NMUtilsFileSearchInPathsPredicate predicate,
                              gpointer                          user_data,
                              GError                          **error)
{
    g_return_val_if_fail(!error || !*error, NULL);
    g_return_val_if_fail(progname && progname[0] && !strchr(progname, '/'), NULL);
    g_return_val_if_fail(file_test_flags || predicate, NULL);

    /* Only consider @try_first if it is a valid, absolute path. This makes
     * it simpler to pass in a path from configure checks. */
    if (try_first && try_first[0] == '/'
        && (file_test_flags == 0 || g_file_test(try_first, file_test_flags))
        && (!predicate || predicate(try_first, user_data)))
        return g_intern_string(try_first);

    if (paths && paths[0]) {
        nm_auto_str_buf NMStrBuf strbuf =
            NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_104, FALSE);

        for (; *paths; paths++) {
            const char *path = *paths;
            const char *s;

            if (!path[0])
                continue;

            nm_str_buf_reset(&strbuf);
            nm_str_buf_append(&strbuf, path);
            nm_str_buf_ensure_trailing_c(&strbuf, '/');
            s = nm_str_buf_append0(&strbuf, progname);

            if ((file_test_flags == 0 || g_file_test(s, file_test_flags))
                && (!predicate || predicate(s, user_data)))
                return g_intern_string(s);
        }
    }

    g_set_error(error,
                G_IO_ERROR,
                G_IO_ERROR_NOT_FOUND,
                _("Could not find \"%s\" binary"),
                progname);
    return NULL;
}

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

/* Band, channel/frequency stuff for wireless */
struct cf_pair {
    guint32 chan;
    guint32 freq;
};

static const struct cf_pair a_table[] = {
    /* A band */
    {7, 5035},   {8, 5040},   {9, 5045},   {11, 5055},  {12, 5060},  {16, 5080},  {34, 5170},
    {36, 5180},  {38, 5190},  {40, 5200},  {42, 5210},  {44, 5220},  {46, 5230},  {48, 5240},
    {50, 5250},  {52, 5260},  {56, 5280},  {58, 5290},  {60, 5300},  {64, 5320},  {100, 5500},
    {104, 5520}, {108, 5540}, {112, 5560}, {116, 5580}, {120, 5600}, {124, 5620}, {128, 5640},
    {132, 5660}, {136, 5680}, {140, 5700}, {149, 5745}, {152, 5760}, {153, 5765}, {157, 5785},
    {160, 5800}, {161, 5805}, {165, 5825}, {183, 4915}, {184, 4920}, {185, 4925}, {187, 4935},
    {188, 4945}, {192, 4960}, {196, 4980}, {0, 0}};

static const guint a_table_freqs[G_N_ELEMENTS(a_table)] = {
    /* A band */
    5035, 5040, 5045, 5055, 5060, 5080, 5170, 5180, 5190, 5200, 5210, 5220, 5230, 5240, 5250, 5260,
    5280, 5290, 5300, 5320, 5500, 5520, 5540, 5560, 5580, 5600, 5620, 5640, 5660, 5680, 5700, 5745,
    5760, 5765, 5785, 5800, 5805, 5825, 4915, 4920, 4925, 4935, 4945, 4960, 4980, 0,
};

static const struct cf_pair bg_table[] = {
    /* B/G band */
    {1, 2412},
    {2, 2417},
    {3, 2422},
    {4, 2427},
    {5, 2432},
    {6, 2437},
    {7, 2442},
    {8, 2447},
    {9, 2452},
    {10, 2457},
    {11, 2462},
    {12, 2467},
    {13, 2472},
    {14, 2484},
    {0, 0}};

static const guint bg_table_freqs[G_N_ELEMENTS(bg_table)] = {
    /* B/G band */
    2412,
    2417,
    2422,
    2427,
    2432,
    2437,
    2442,
    2447,
    2452,
    2457,
    2462,
    2467,
    2472,
    2484,
    0,
};

/**
 * nm_utils_wifi_freq_to_channel:
 * @freq: frequency
 *
 * Utility function to translate a Wi-Fi frequency to its corresponding channel.
 *
 * Returns: the channel represented by the frequency or 0
 **/
guint32
nm_utils_wifi_freq_to_channel(guint32 freq)
{
    int i = 0;

    if (freq > 4900) {
        while (a_table[i].freq && (a_table[i].freq != freq))
            i++;
        return a_table[i].chan;
    }

    while (bg_table[i].freq && (bg_table[i].freq != freq))
        i++;
    return bg_table[i].chan;
}

/**
 * nm_utils_wifi_freq_to_band:
 * @freq: frequency
 *
 * Utility function to translate a Wi-Fi frequency to its corresponding band.
 *
 * Returns: the band containing the frequency or NULL if freq is invalid
 **/
const char *
nm_utils_wifi_freq_to_band(guint32 freq)
{
    if (freq >= 4915 && freq <= 5825)
        return "a";
    else if (freq >= 2412 && freq <= 2484)
        return "bg";

    return NULL;
}

/**
 * nm_utils_wifi_channel_to_freq:
 * @channel: channel
 * @band: frequency band for wireless ("a" or "bg")
 *
 * Utility function to translate a Wi-Fi channel to its corresponding frequency.
 *
 * Returns: the frequency represented by the channel of the band,
 *          or -1 when the freq is invalid, or 0 when the band
 *          is invalid
 **/
guint32
nm_utils_wifi_channel_to_freq(guint32 channel, const char *band)
{
    int i;

    g_return_val_if_fail(band, 0);

    if (nm_streq(band, "a")) {
        for (i = 0; a_table[i].chan; i++) {
            if (a_table[i].chan == channel)
                return a_table[i].freq;
        }
        return ((guint32) -1);
    }

    if (nm_streq(band, "bg")) {
        for (i = 0; bg_table[i].chan; i++) {
            if (bg_table[i].chan == channel)
                return bg_table[i].freq;
        }
        return ((guint32) -1);
    }

    return 0;
}

/**
 * nm_utils_wifi_find_next_channel:
 * @channel: current channel
 * @direction: whether going downward (0 or less) or upward (1 or more)
 * @band: frequency band for wireless ("a" or "bg")
 *
 * Utility function to find out next/previous Wi-Fi channel for a channel.
 *
 * Returns: the next channel in the specified direction or 0
 **/
guint32
nm_utils_wifi_find_next_channel(guint32 channel, int direction, char *band)
{
    size_t                a_size  = G_N_ELEMENTS(a_table);
    size_t                bg_size = G_N_ELEMENTS(bg_table);
    const struct cf_pair *pair;

    if (nm_streq(band, "a")) {
        if (channel < a_table[0].chan)
            return a_table[0].chan;
        if (channel > a_table[a_size - 2].chan)
            return a_table[a_size - 2].chan;
        pair = &a_table[0];
    } else if (nm_streq(band, "bg")) {
        if (channel < bg_table[0].chan)
            return bg_table[0].chan;
        if (channel > bg_table[bg_size - 2].chan)
            return bg_table[bg_size - 2].chan;
        pair = &bg_table[0];
    } else
        g_return_val_if_reached(0);

    while (pair->chan) {
        if (channel == pair->chan)
            return channel;
        if ((channel < (pair + 1)->chan) && (channel > pair->chan)) {
            if (direction > 0)
                return (pair + 1)->chan;
            else
                return pair->chan;
        }
        pair++;
    }
    return 0;
}

/**
 * nm_utils_wifi_is_channel_valid:
 * @channel: channel
 * @band: frequency band for wireless ("a" or "bg")
 *
 * Utility function to verify Wi-Fi channel validity.
 *
 * Returns: %TRUE or %FALSE
 **/
gboolean
nm_utils_wifi_is_channel_valid(guint32 channel, const char *band)
{
    guint32 freq;

    freq = nm_utils_wifi_channel_to_freq(channel, band);

    return !NM_IN_SET(freq, 0u, (guint32) -1);
}

#define _nm_assert_wifi_freqs(table, table_freqs)                                  \
    G_STMT_START                                                                   \
    {                                                                              \
        if (NM_MORE_ASSERT_ONCE(5)) {                                              \
            int i, j;                                                              \
                                                                                   \
            G_STATIC_ASSERT(G_N_ELEMENTS(table) > 0);                              \
            G_STATIC_ASSERT(G_N_ELEMENTS(table) == G_N_ELEMENTS(table_freqs));     \
                                                                                   \
            for (i = 0; i < (int) G_N_ELEMENTS(table); i++) {                      \
                nm_assert((i == G_N_ELEMENTS(table) - 1) == (table[i].chan == 0)); \
                nm_assert((i == G_N_ELEMENTS(table) - 1) == (table[i].freq == 0)); \
                nm_assert(table[i].freq == table_freqs[i]);                        \
                for (j = 0; j < i; j++) {                                          \
                    nm_assert(table[j].chan != table[i].chan);                     \
                    nm_assert(table[j].freq != table[i].freq);                     \
                }                                                                  \
            }                                                                      \
        }                                                                          \
    }                                                                              \
    G_STMT_END

/**
 * nm_utils_wifi_2ghz_freqs:
 *
 * Utility function to return 2.4 GHz Wi-Fi frequencies (802.11bg band).
 *
 * Returns: zero-terminated array of frequencies numbers (in MHz)
 *
 * Since: 1.2
 **/
const guint *
nm_utils_wifi_2ghz_freqs(void)
{
    _nm_assert_wifi_freqs(bg_table, bg_table_freqs);
    return bg_table_freqs;
}

/**
 * nm_utils_wifi_5ghz_freqs:
 *
 * Utility function to return 5 GHz Wi-Fi frequencies (802.11a band).
 *
 * Returns: zero-terminated array of frequencies numbers (in MHz)
 *
 * Since: 1.2
 **/
const guint *
nm_utils_wifi_5ghz_freqs(void)
{
    _nm_assert_wifi_freqs(a_table, a_table_freqs);
    return a_table_freqs;
}

/**
 * nm_utils_wifi_strength_bars:
 * @strength: the access point strength, from 0 to 100
 *
 * Converts @strength into a 4-character-wide graphical representation of
 * strength suitable for printing to stdout.
 *
 * Previous versions used to take a guess at the terminal type and possibly
 * return a wide UTF-8 encoded string. Now it always returns a 7-bit
 * clean strings of one to 0 to 4 asterisks. Users that actually need
 * the functionality are encouraged to make their implementations instead.
 *
 * Returns: the graphical representation of the access point strength
 */
const char *
nm_utils_wifi_strength_bars(guint8 strength)
{
    if (strength > 80)
        return "****";
    else if (strength > 55)
        return "*** ";
    else if (strength > 30)
        return "**  ";
    else if (strength > 5)
        return "*   ";
    else
        return "    ";
}

gboolean
_nm_property_variant_to_gvalue(GVariant *src_value, GValue *dst_value)
{
    GValue   tmp = G_VALUE_INIT;
    gboolean success;

    g_dbus_gvariant_to_gvalue(src_value, &tmp);
    if (G_VALUE_TYPE(&tmp) == G_VALUE_TYPE(dst_value)) {
        *dst_value = tmp;
        return TRUE;
    }

    success = g_value_transform(&tmp, dst_value);
    g_value_unset(&tmp);
    return success;
}

/**
 * nm_utils_hwaddr_len:
 * @type: the type of address; either <literal>ARPHRD_ETHER</literal> or
 * <literal>ARPHRD_INFINIBAND</literal>
 *
 * Returns the length in octets of a hardware address of type @type.
 *
 * Before 1.28, it was an error to call this function with any value other than
 * <literal>ARPHRD_ETHER</literal> or <literal>ARPHRD_INFINIBAND</literal>.
 *
 * Returns: the length or zero if the type is unrecognized.
 */
gsize
nm_utils_hwaddr_len(int type)
{
    switch (type) {
    case ARPHRD_ETHER:
        return ETH_ALEN;
    case ARPHRD_INFINIBAND:
        return INFINIBAND_ALEN;
    default:
        return 0;
    }
}

/**
 * nm_utils_hexstr2bin:
 * @hex: a string of hexadecimal characters with optional ':' separators
 *
 * Converts a hexadecimal string @hex into an array of bytes.  The optional
 * separator ':' may be used between single or pairs of hexadecimal characters,
 * eg "00:11" or "0:1".  Any "0x" at the beginning of @hex is ignored.  @hex
 * may not start or end with ':'.
 *
 * Returns: (transfer full): the converted bytes, or %NULL on error
 */
GBytes *
nm_utils_hexstr2bin(const char *hex)
{
    guint8 *buffer;
    gsize   len;

    buffer = nm_utils_hexstr2bin_alloc(hex, TRUE, FALSE, ":", 0, &len);
    if (!buffer)
        return NULL;
    buffer = g_realloc(buffer, len);
    return g_bytes_new_take(buffer, len);
}

/**
 * nm_utils_hwaddr_atoba:
 * @asc: the ASCII representation of a hardware address
 * @length: the expected length in bytes of the result
 *
 * Parses @asc and converts it to binary form in a #GByteArray. See
 * nm_utils_hwaddr_aton() if you don't want a #GByteArray.
 *
 * Returns: (transfer full): a new #GByteArray, or %NULL if @asc couldn't
 * be parsed
 */
GByteArray *
nm_utils_hwaddr_atoba(const char *asc, gsize length)
{
    GByteArray *ba;

    g_return_val_if_fail(asc, NULL);
    g_return_val_if_fail(length > 0 && length <= NM_UTILS_HWADDR_LEN_MAX, NULL);

    ba = g_byte_array_sized_new(length);
    g_byte_array_set_size(ba, length);
    if (!_nm_utils_hwaddr_aton_exact(asc, ba->data, length))
        goto fail;

    return ba;
fail:
    g_byte_array_unref(ba);
    return NULL;
}

/**
 * nm_utils_hwaddr_aton:
 * @asc: the ASCII representation of a hardware address
 * @buffer: (type guint8) (array length=length): buffer to store the result into
 * @length: the expected length in bytes of the result and
 * the size of the buffer in bytes.
 *
 * Parses @asc and converts it to binary form in @buffer.
 * Bytes in @asc can be separated by colons (:), or hyphens (-), but not mixed.
 *
 * Returns: @buffer, or %NULL if @asc couldn't be parsed
 *   or would be shorter or longer than @length.
 */
guint8 *
nm_utils_hwaddr_aton(const char *asc, gpointer buffer, gsize length)
{
    g_return_val_if_fail(asc, NULL);
    g_return_val_if_fail(buffer, NULL);
    g_return_val_if_fail(length > 0 && length <= NM_UTILS_HWADDR_LEN_MAX, NULL);

    return _nm_utils_hwaddr_aton_exact(asc, buffer, length);
}

/**
 * nm_utils_bin2hexstr:
 * @src: (type guint8) (array length=len): an array of bytes
 * @len: the length of the @src array
 * @final_len: an index where to cut off the returned string, or -1
 *
 * Converts the byte array @src into a hexadecimal string. If @final_len is
 * greater than -1, the returned string is terminated at that index
 * (returned_string[final_len] == '\0'),
 *
 * Returns: (transfer full): the textual form of @bytes
 */
char *
nm_utils_bin2hexstr(gconstpointer src, gsize len, int final_len)
{
    gsize buflen = (len * 2) + 1;

    g_return_val_if_fail(src != NULL, NULL);
    g_return_val_if_fail(len > 0 && (buflen - 1) / 2 == len, NULL);
    g_return_val_if_fail(final_len < 0 || (gsize) final_len < buflen, NULL);

    return _nm_utils_bin2hexstr(src, len, final_len);
}

/**
 * nm_utils_hwaddr_ntoa:
 * @addr: (type guint8) (array length=length): a binary hardware address
 * @length: the length of @addr
 *
 * Converts @addr to textual form.
 *
 * Returns: (transfer full): the textual form of @addr
 */
char *
nm_utils_hwaddr_ntoa(gconstpointer addr, gsize length)
{
    g_return_val_if_fail(addr, g_strdup(""));
    g_return_val_if_fail(length > 0, g_strdup(""));

    return nm_utils_bin2hexstr_full(addr, length, ':', TRUE, NULL);
}

/**
 * nm_utils_hwaddr_valid:
 * @asc: the ASCII representation of a hardware address
 * @length: the length of address that @asc is expected to convert to
 *   (or -1 to accept any length up to %NM_UTILS_HWADDR_LEN_MAX)
 *
 * Parses @asc to see if it is a valid hardware address of the given
 * length.
 *
 * Returns: %TRUE if @asc appears to be a valid hardware address
 *   of the indicated length, %FALSE if not.
 */
gboolean
nm_utils_hwaddr_valid(const char *asc, gssize length)
{
    guint8 buf[NM_UTILS_HWADDR_LEN_MAX];
    gsize  l;

    g_return_val_if_fail(asc != NULL, FALSE);
    g_return_val_if_fail(length >= -1 && length <= NM_UTILS_HWADDR_LEN_MAX, FALSE);

    if (length == 0)
        return FALSE;

    if (!_nm_utils_hwaddr_aton(asc, buf, sizeof(buf), &l))
        return FALSE;

    return length == -1 || length == (gssize) l;
}

/**
 * nm_utils_hwaddr_canonical:
 * @asc: the ASCII representation of a hardware address
 * @length: the length of address that @asc is expected to convert to
 *   (or -1 to accept any length up to %NM_UTILS_HWADDR_LEN_MAX)
 *
 * Parses @asc to see if it is a valid hardware address of the given
 * length, and if so, returns it in canonical form (uppercase, with
 * leading 0s as needed, and with colons rather than hyphens).
 *
 * Returns: (transfer full): the canonicalized address if @asc appears to
 *   be a valid hardware address of the indicated length, %NULL if not.
 */
char *
nm_utils_hwaddr_canonical(const char *asc, gssize length)
{
    guint8 buf[NM_UTILS_HWADDR_LEN_MAX];
    gsize  l;

    g_return_val_if_fail(asc, NULL);
    g_return_val_if_fail(length == -1 || (length > 0 && length <= NM_UTILS_HWADDR_LEN_MAX), NULL);

    if (!_nm_utils_hwaddr_aton(asc, buf, sizeof(buf), &l))
        return NULL;
    if (length != -1 && length != (gssize) l)
        return NULL;
    return nm_utils_hwaddr_ntoa(buf, l);
}

/* This is used to possibly canonicalize values passed to MAC address property
 * setters. Unlike nm_utils_hwaddr_canonical(), it accepts %NULL, and if you
 * pass it an invalid MAC address, it just returns that string rather than
 * returning %NULL (so that we can return a proper error from verify() later).
 */
char *
_nm_utils_hwaddr_canonical_or_invalid(const char *mac, gssize length)
{
    char *canonical;

    if (!mac)
        return NULL;

    canonical = nm_utils_hwaddr_canonical(mac, length);
    if (canonical)
        return canonical;
    else
        return g_strdup(mac);
}

char *
_nm_utils_ipaddr_canonical_or_invalid(int addr_family, const char *ip, gboolean map_zero_to_null)
{
    NMIPAddr addr_bin;

    nm_assert_addr_family_or_unspec(addr_family);

    if (!ip)
        return NULL;

    if (!nm_inet_parse_bin(addr_family, ip, &addr_family, &addr_bin))
        return g_strdup(ip);

    if (map_zero_to_null && nm_ip_addr_is_null(addr_family, &addr_bin))
        return NULL;

    return nm_inet_ntop_dup(addr_family, &addr_bin);
}

/*
 * Determine if given Ethernet address is link-local
 *
 * Returns: %TRUE if @mac is link local
 * reserved addr (01:80:c2:00:00:0X) per IEEE 802.1Q 8.6.3 Frame filtering, %FALSE if not.
 */
gboolean
_nm_utils_hwaddr_link_local_valid(const char *mac)
{
    guint8              mac_net[ETH_ALEN];
    static const guint8 eth_reserved_addr_base[] = {0x01, 0x80, 0xc2, 0x00, 0x00};

    if (!mac)
        return FALSE;

    if (!nm_utils_hwaddr_aton(mac, mac_net, ETH_ALEN))
        return FALSE;

    if (memcmp(mac_net, eth_reserved_addr_base, ETH_ALEN - 1) || (mac_net[5] & 0xF0))
        return FALSE;

    if (mac_net[5] == 1     /* 802.3x Pause address */
        || mac_net[5] == 2  /* 802.3ad Slow protocols */
        || mac_net[5] == 3) /* 802.1X PAE address */
        return FALSE;

    return TRUE;
}

/**
 * nm_utils_hwaddr_matches:
 * @hwaddr1: (nullable): pointer to a binary or ASCII hardware address, or %NULL
 * @hwaddr1_len: size of @hwaddr1, or -1 if @hwaddr1 is ASCII
 * @hwaddr2: (nullable): pointer to a binary or ASCII hardware address, or %NULL
 * @hwaddr2_len: size of @hwaddr2, or -1 if @hwaddr2 is ASCII
 *
 * Generalized hardware address comparison function. Tests if @hwaddr1 and
 * @hwaddr2 "equal" (or more precisely, "equivalent"), with several advantages
 * over a simple memcmp():
 *
 *   1. If @hwaddr1_len or @hwaddr2_len is -1, then the corresponding address is
 *      assumed to be ASCII rather than binary, and will be converted to binary
 *      before being compared.
 *
 *   2. If @hwaddr1 or @hwaddr2 is %NULL, it is treated instead as though it was
 *      a zero-filled buffer @hwaddr1_len or @hwaddr2_len bytes long.
 *
 *   3. If @hwaddr1 and @hwaddr2 are InfiniBand hardware addresses (that is, if
 *      they are <literal>INFINIBAND_ALEN</literal> bytes long in binary form)
 *      then only the last 8 bytes are compared, since those are the only bytes
 *      that actually identify the hardware. (The other 12 bytes will change
 *      depending on the configuration of the InfiniBand fabric that the device
 *      is connected to.)
 *
 * If a passed-in ASCII hardware address cannot be parsed, or would parse to an
 * address larger than %NM_UTILS_HWADDR_LEN_MAX, then it will silently fail to
 * match. (This means that externally-provided address strings do not need to be
 * sanity-checked before comparing them against known good addresses; they are
 * guaranteed to not match if they are invalid.)
 *
 * Returns: %TRUE if @hwaddr1 and @hwaddr2 are equivalent, %FALSE if they are
 *   different (or either of them is invalid).
 */
gboolean
nm_utils_hwaddr_matches(gconstpointer hwaddr1,
                        gssize        hwaddr1_len,
                        gconstpointer hwaddr2,
                        gssize        hwaddr2_len)
{
    guint8 buf1[NM_UTILS_HWADDR_LEN_MAX], buf2[NM_UTILS_HWADDR_LEN_MAX];
    gsize  l;

    if (hwaddr1_len == -1) {
        if (hwaddr1 == NULL) {
            hwaddr1_len = 0;
        } else if (_nm_utils_hwaddr_aton(hwaddr1, buf1, sizeof(buf1), &l)) {
            hwaddr1     = buf1;
            hwaddr1_len = l;
        } else {
            g_return_val_if_fail(hwaddr2_len == -1
                                     || (hwaddr2_len > 0 && hwaddr2_len <= NM_UTILS_HWADDR_LEN_MAX),
                                 FALSE);
            return FALSE;
        }
    } else {
        g_return_val_if_fail(hwaddr1_len > 0 && hwaddr1_len <= NM_UTILS_HWADDR_LEN_MAX, FALSE);

        if (!hwaddr1) {
            memset(buf1, 0, hwaddr1_len);
            hwaddr1 = buf1;
        }
    }

    if (hwaddr2_len == -1) {
        if (hwaddr2 == NULL)
            l = 0;
        else if (!_nm_utils_hwaddr_aton(hwaddr2, buf2, sizeof(buf2), &l))
            return FALSE;
        if (l != hwaddr1_len)
            return FALSE;
        hwaddr2 = buf2;
    } else {
        g_return_val_if_fail(hwaddr2_len > 0 && hwaddr2_len <= NM_UTILS_HWADDR_LEN_MAX, FALSE);

        if (hwaddr2_len != hwaddr1_len)
            return FALSE;

        if (!hwaddr2) {
            memset(buf2, 0, hwaddr2_len);
            hwaddr2 = buf2;
        }
    }

    if (G_UNLIKELY(hwaddr1_len <= 0 || hwaddr1_len > NM_UTILS_HWADDR_LEN_MAX)) {
        /* Only valid addresses can compare equal. In particular,
         * addresses that are too long or of zero bytes, never
         * compare equal. */
        return FALSE;
    }

    if (hwaddr1_len == INFINIBAND_ALEN) {
        hwaddr1     = &((guint8 *) hwaddr1)[INFINIBAND_ALEN - 8];
        hwaddr2     = &((guint8 *) hwaddr2)[INFINIBAND_ALEN - 8];
        hwaddr1_len = 8;
    }

    return !memcmp(hwaddr1, hwaddr2, hwaddr1_len);
}

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

GVariant *
nm_utils_hwaddr_to_dbus(const char *str)
{
    guint8 buf[NM_UTILS_HWADDR_LEN_MAX];
    gsize  len;

    if (!str)
        return NULL;
    if (!_nm_utils_hwaddr_aton(str, buf, sizeof(buf), &len))
        return NULL;

    return nm_g_variant_new_ay(buf, len);
}

GVariant *
_nm_sett_info_prop_to_dbus_fcn_cloned_mac_address(_NM_SETT_INFO_PROP_TO_DBUS_FCN_ARGS _nm_nil)
{
    gs_free char *addr = NULL;

    nm_assert(nm_streq(property_info->name, "cloned-mac-address"));

    g_object_get(setting, "cloned-mac-address", &addr, NULL);
    return nm_utils_hwaddr_to_dbus(addr);
}

gboolean
_nm_sett_info_prop_from_dbus_fcn_cloned_mac_address(_NM_SETT_INFO_PROP_FROM_DBUS_FCN_ARGS _nm_nil)
{
    gsize         length;
    const guint8 *array;
    char         *str;

    nm_assert(nm_streq0(property_info->name, "cloned-mac-address"));

    if (!_nm_setting_use_legacy_property(setting,
                                         connection_dict,
                                         "cloned-mac-address",
                                         "assigned-mac-address")) {
        *out_is_modified = FALSE;
        return TRUE;
    }

    length = 0;
    array  = g_variant_get_fixed_array(value, &length, 1);

    if (!length) {
        *out_is_modified = FALSE;
        return TRUE;
    }

    str = nm_utils_hwaddr_ntoa(array, length);
    g_object_set(setting, "cloned-mac-address", str, NULL);
    g_free(str);
    return TRUE;
}

gboolean
_nm_sett_info_prop_missing_from_dbus_fcn_cloned_mac_address(
    _NM_SETT_INFO_PROP_MISSING_FROM_DBUS_FCN_ARGS _nm_nil)
{
    nm_assert(nm_streq0(property, "cloned-mac-address"));
    return TRUE;
}

static GVariant *
assigned_mac_address_to_dbus(_NM_SETT_INFO_PROP_TO_DBUS_FCN_ARGS _nm_nil)
{
    gs_free char *addr = NULL;

    if (!_nm_connection_serialize_non_secret(flags))
        return NULL;

    nm_assert(nm_streq0(property_info->name, "assigned-mac-address"));

    g_object_get(setting, "cloned-mac-address", &addr, NULL);

    /* Before introducing the extended "cloned-mac-address" (and its D-Bus
     * field "assigned-mac-address"), libnm's nm_utils_hwaddr_to_dbus()
     * would drop invalid values as it was unable to serialize them.
     *
     * Now, we would like to send invalid values as "assigned-mac-address"
     * over D-Bus and let the server reject them.
     *
     * However, clients used to set the cloned-mac-address property
     * to "" and it just worked as the value was not serialized in
     * an ill form.
     *
     * To preserve that behavior, serialize "" as NULL.
     */

    return addr && addr[0] ? g_variant_new_take_string(g_steal_pointer(&addr)) : NULL;
}

static gboolean
assigned_mac_address_from_dbus(_NM_SETT_INFO_PROP_FROM_DBUS_FCN_ARGS _nm_nil)
{
    nm_assert(nm_streq0(property_info->name, "assigned-mac-address"));

    if (_nm_setting_use_legacy_property(setting,
                                        connection_dict,
                                        "cloned-mac-address",
                                        "assigned-mac-address")) {
        *out_is_modified = FALSE;
        return TRUE;
    }

    g_object_set(setting,
                 "cloned-mac-address",
                 nm_str_not_empty(g_variant_get_string(value, NULL)),
                 NULL);
    return TRUE;
}

const NMSettInfoPropertType nm_sett_info_propert_type_assigned_mac_address =
    NM_SETT_INFO_PROPERT_TYPE_DBUS_INIT(G_VARIANT_TYPE_STRING,
                                        .compare_fcn   = _nm_setting_property_compare_fcn_ignore,
                                        .to_dbus_fcn   = assigned_mac_address_to_dbus,
                                        .from_dbus_fcn = assigned_mac_address_from_dbus, );

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

/* Validate secret-flags. Most settings don't validate them, which is a bug.
 * But we possibly cannot enforce a strict validation now.
 *
 * For new settings, they shall validate the secret-flags strictly. */
gboolean
_nm_utils_secret_flags_validate(NMSettingSecretFlags secret_flags,
                                const char          *setting_name,
                                const char          *property_name,
                                NMSettingSecretFlags disallowed_flags,
                                GError             **error)
{
    if (secret_flags == NM_SETTING_SECRET_FLAG_NONE)
        return TRUE;

    if (NM_FLAGS_ANY(secret_flags, ~NM_SETTING_SECRET_FLAG_ALL)) {
        g_set_error_literal(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("unknown secret flags"));
        if (setting_name)
            g_prefix_error(error, "%s.%s: ", setting_name, property_name);
        return FALSE;
    }

    if (!nm_utils_is_power_of_two(secret_flags)) {
        g_set_error_literal(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("conflicting secret flags"));
        if (setting_name)
            g_prefix_error(error, "%s.%s: ", setting_name, property_name);
        return FALSE;
    }

    if (NM_FLAGS_ANY(secret_flags, disallowed_flags)) {
        if (NM_FLAGS_HAS(secret_flags, NM_SETTING_SECRET_FLAG_NOT_REQUIRED)) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("secret flags must not be \"not-required\""));
            if (setting_name)
                g_prefix_error(error, "%s.%s: ", setting_name, property_name);
            return FALSE;
        }
        g_set_error_literal(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("unsupported secret flags"));
        if (setting_name)
            g_prefix_error(error, "%s.%s: ", setting_name, property_name);
        return FALSE;
    }

    return TRUE;
}

gboolean
_nm_utils_wps_method_validate(NMSettingWirelessSecurityWpsMethod wps_method,
                              const char                        *setting_name,
                              const char                        *property_name,
                              gboolean                           wps_required,
                              GError                           **error)
{
    if (wps_method > NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_PIN) {
        g_set_error_literal(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("property is invalid"));
        g_prefix_error(error, "%s.%s: ", setting_name, property_name);
        return FALSE;
    }

    if (NM_FLAGS_HAS(wps_method, NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_DISABLED)) {
        if (wps_method != NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_DISABLED) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("can't be simultaneously disabled and enabled"));
            g_prefix_error(error, "%s.%s: ", setting_name, property_name);
            return FALSE;
        }
        if (wps_required) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("WPS is required"));
            g_prefix_error(error, "%s.%s: ", setting_name, property_name);
            return FALSE;
        }
    }

    return TRUE;
}

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

static char *
_split_word(char *s)
{
    /* takes @s and truncates the string on the first white-space.
     * then it returns the first word afterwards (again seeking
     * over leading white-space). */
    for (; s[0]; s++) {
        if (g_ascii_isspace(s[0])) {
            s[0] = '\0';
            s++;
            while (g_ascii_isspace(s[0]))
                s++;
            return s;
        }
    }
    return s;
}

gboolean
_nm_utils_generate_mac_address_mask_parse(const char         *value,
                                          struct ether_addr  *out_mask,
                                          struct ether_addr **out_ouis,
                                          gsize              *out_ouis_len,
                                          GError            **error)
{
    gs_free char          *s_free = NULL;
    char                  *s, *s_next;
    struct ether_addr      mask;
    gs_unref_array GArray *ouis = NULL;

    g_return_val_if_fail(!error || !*error, FALSE);

    if (!value || !*value) {
        /* NULL and "" are valid values and both mean the default
         * "q */
        if (out_mask) {
            memset(out_mask, 0, sizeof(*out_mask));
            out_mask->ether_addr_octet[0] |= 0x02;
        }
        NM_SET_OUT(out_ouis, NULL);
        NM_SET_OUT(out_ouis_len, 0);
        return TRUE;
    }

    s_free = g_strdup(value);
    s      = s_free;

    /* skip over leading whitespace */
    while (g_ascii_isspace(s[0]))
        s++;

    /* parse the first mask */
    s_next = _split_word(s);
    if (!nm_utils_hwaddr_aton(s, &mask, ETH_ALEN)) {
        g_set_error(error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_UNKNOWN,
                    _("not a valid ethernet MAC address for mask at position %lld"),
                    (long long) (s - s_free));
        return FALSE;
    }

    if (s_next[0]) {
        ouis = g_array_sized_new(FALSE, FALSE, sizeof(struct ether_addr), 4);

        do {
            struct ether_addr *new;

            s      = s_next;
            s_next = _split_word(s);

            new = nm_g_array_append_new(ouis, struct ether_addr);
            if (!nm_utils_hwaddr_aton(s, new, ETH_ALEN)) {
                g_set_error(error,
                            NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            _("not a valid ethernet MAC address #%u at position %lld"),
                            ouis->len,
                            (long long) (s - s_free));
                return FALSE;
            }
        } while (s_next[0]);
    }

    NM_SET_OUT(out_mask, mask);
    NM_SET_OUT(out_ouis_len, ouis ? ouis->len : 0);
    NM_SET_OUT(out_ouis,
               ouis ? ((struct ether_addr *) g_array_free(g_steal_pointer(&ouis), FALSE)) : NULL);
    return TRUE;
}

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

gboolean
nm_utils_is_valid_iface_name_utf8safe(const char *utf8safe_name)
{
    gs_free gpointer bin_to_free = NULL;
    gconstpointer    bin;
    gsize            len;

    g_return_val_if_fail(utf8safe_name, FALSE);

    bin = nm_utils_buf_utf8safe_unescape(utf8safe_name,
                                         NM_UTILS_STR_UTF8_SAFE_FLAG_NONE,
                                         &len,
                                         &bin_to_free);

    if (bin_to_free) {
        /* some unescaping happened... */

        if (len != strlen(bin)) {
            /* there are embedded NUL chars. Invalid. */
            return FALSE;
        }
    }

    return nm_utils_ifname_valid_kernel(bin, NULL);
}

/**
 * nm_utils_is_valid_iface_name:
 * @name: (nullable): Name of interface
 * @error: location to store the error occurring, or %NULL to ignore
 *
 * Validate the network interface name.
 *
 * This function is a 1:1 copy of the kernel's interface validation
 * function in net/core/dev.c.
 *
 * Returns: %TRUE if interface name is valid, otherwise %FALSE is returned.
 *
 * Before 1.20, this function did not accept %NULL as @name argument. If you
 *   want to run against older versions of libnm, don't pass %NULL.
 *
 * Since: 1.6
 */
gboolean
nm_utils_is_valid_iface_name(const char *name, GError **error)
{
    g_return_val_if_fail(!error || !*error, FALSE);

    return nm_utils_ifname_valid_kernel(name, error);
}

/**
 * nm_utils_iface_valid_name:
 * @name: (nullable): Name of interface
 *
 * Validate the network interface name.
 *
 * Deprecated: 1.6: Use nm_utils_is_valid_iface_name() instead, with better error reporting.
 *
 * Returns: %TRUE if interface name is valid, otherwise %FALSE is returned.
 *
 * Before 1.20, this function did not accept %NULL as @name argument. If you
 *   want to run against older versions of libnm, don't pass %NULL.
 */
gboolean
nm_utils_iface_valid_name(const char *name)
{
    return nm_utils_is_valid_iface_name(name, NULL);
}

/**
 * nm_utils_is_uuid:
 * @str: (nullable): a string that might be a UUID
 *
 * Checks if @str is a UUID
 *
 * Returns: %TRUE if @str is a UUID, %FALSE if not
 *
 * In older versions, nm_utils_is_uuid() did not accept %NULL as @str
 * argument. Don't pass %NULL if you run against older versions of libnm.
 *
 * Deprecated: 1.32: older versions of NetworkManager had a wrong
 *   understanding of what makes a valid UUID. This function can thus
 *   accept some inputs as valid, which in fact are not valid UUIDs.
 */
gboolean
nm_utils_is_uuid(const char *str)
{
    return nm_uuid_is_valid_nmlegacy(str);
}

static _nm_thread_local char _nm_utils_inet_ntop_buffer[NM_INET_ADDRSTRLEN];

/**
 * nm_utils_inet4_ntop: (skip)
 * @inaddr: the address that should be converted to string.
 * @dst: the destination buffer, it must contain at least
 *  <literal>INET_ADDRSTRLEN</literal> or %NM_INET_ADDRSTRLEN
 *  characters. If set to %NULL, it will return a pointer to an internal, static
 *  buffer (shared with nm_utils_inet6_ntop()).  Beware, that the internal
 *  buffer will be overwritten with ever new call of nm_utils_inet4_ntop() or
 *  nm_utils_inet6_ntop() that does not provide its own @dst buffer. Since
 *  1.28, the internal buffer is thread local and thus thread safe. Before
 *  it was not thread safe. When in doubt, pass your own
 *  @dst buffer to avoid these issues.
 *
 * Wrapper for inet_ntop.
 *
 * Returns: the input buffer @dst, or a pointer to an
 *  internal, static buffer. This function cannot fail.
 **/
const char *
nm_utils_inet4_ntop(in_addr_t inaddr, char *dst)
{
    /* relying on the static buffer (by leaving @dst as %NULL) is discouraged.
     * Don't do that!
     *
     * However, still support it to be lenient against mistakes and because
     * this is public API of libnm. */
    return nm_inet4_ntop(inaddr, dst ?: _nm_utils_inet_ntop_buffer);
}

/**
 * nm_utils_inet6_ntop: (skip)
 * @in6addr: the address that should be converted to string.
 * @dst: the destination buffer, it must contain at least
 *  <literal>INET6_ADDRSTRLEN</literal> or %NM_INET_ADDRSTRLEN
 *  characters. If set to %NULL, it will return a pointer to an internal, static
 *  buffer (shared with nm_utils_inet4_ntop()).  Beware, that the internal
 *  buffer will be overwritten with ever new call of nm_utils_inet4_ntop() or
 *  nm_utils_inet6_ntop() that does not provide its own @dst buffer. Since
 *  1.28, the internal buffer is thread local and thus thread safe. Before
 *  it was not thread safe. When in doubt, pass your own
 *  @dst buffer to avoid these issues.
 *
 * Wrapper for inet_ntop.
 *
 * Returns: the input buffer @dst, or a pointer to an
 *  internal, static buffer. %NULL is not allowed as @in6addr,
 *  otherwise, this function cannot fail.
 **/
const char *
nm_utils_inet6_ntop(const struct in6_addr *in6addr, char *dst)
{
    /* relying on the static buffer (by leaving @dst as %NULL) is discouraged.
     * Don't do that!
     *
     * However, still support it to be lenient against mistakes and because
     * this is public API of libnm. */
    g_return_val_if_fail(in6addr, NULL);
    return nm_inet6_ntop(in6addr, dst ?: _nm_utils_inet_ntop_buffer);
}

/**
 * nm_utils_ipaddr_valid:
 * @family: <literal>AF_INET</literal> or <literal>AF_INET6</literal>, or
 *   <literal>AF_UNSPEC</literal> to accept either
 * @ip: an IP address
 *
 * Checks if @ip contains a valid IP address of the given family.
 *
 * Returns: %TRUE or %FALSE
 */
gboolean
nm_utils_ipaddr_valid(int family, const char *ip)
{
    g_return_val_if_fail(family == AF_INET || family == AF_INET6 || family == AF_UNSPEC, FALSE);

    return nm_inet_is_valid(family, ip);
}

/**
 * _nm_utils_dhcp_duid_valid:
 * @duid: the candidate DUID
 *
 * Checks if @duid string contains either a special duid value ("ll",
 * "llt", "lease" or the "stable" variants) or a valid hex DUID.
 *
 * Returns: %TRUE or %FALSE
 */
gboolean
_nm_utils_dhcp_duid_valid(const char *duid, GBytes **out_duid_bin)
{
    guint8 duid_arr[128 + 2];
    gsize  duid_len;

    NM_SET_OUT(out_duid_bin, NULL);

    if (!duid)
        return FALSE;

    if (NM_IN_STRSET(duid, "lease", "llt", "ll", "stable-llt", "stable-ll", "stable-uuid")) {
        return TRUE;
    }

    if (nm_utils_hexstr2bin_full(duid,
                                 FALSE,
                                 FALSE,
                                 FALSE,
                                 ":",
                                 0,
                                 duid_arr,
                                 sizeof(duid_arr),
                                 &duid_len)) {
        /* MAX DUID length is 128 octects + the type code (2 octects). */
        if (duid_len > 2 && duid_len <= (128 + 2)) {
            NM_SET_OUT(out_duid_bin, g_bytes_new(duid_arr, duid_len));
            return TRUE;
        }
    }

    return FALSE;
}

/**
 * nm_utils_check_virtual_device_compatibility:
 * @virtual_type: a virtual connection type
 * @other_type: a connection type to test against @virtual_type
 *
 * Determines if a connection of type @virtual_type can (in the
 * general case) work with connections of type @other_type.
 *
 * If @virtual_type is %NM_TYPE_SETTING_VLAN, then this checks if
 * @other_type is a valid type for the parent of a VLAN.
 *
 * If @virtual_type is a "controller" type (eg, %NM_TYPE_SETTING_BRIDGE),
 * then this checks if @other_type is a valid type for a port of that
 * controller.
 *
 * Note that even if this returns %TRUE it is not guaranteed that
 * <emphasis>every</emphasis> connection of type @other_type is
 * compatible with @virtual_type; it may depend on the exact
 * configuration of the two connections, or on the capabilities of an
 * underlying device driver.
 *
 * Returns: %TRUE or %FALSE
 */
gboolean
nm_utils_check_virtual_device_compatibility(GType virtual_type, GType other_type)
{
    g_return_val_if_fail(_nm_setting_type_get_base_type_priority(virtual_type)
                             != NM_SETTING_PRIORITY_INVALID,
                         FALSE);
    g_return_val_if_fail(_nm_setting_type_get_base_type_priority(other_type)
                             != NM_SETTING_PRIORITY_INVALID,
                         FALSE);

    if (virtual_type == NM_TYPE_SETTING_BOND) {
        return NM_IN_SET(other_type,
                         NM_TYPE_SETTING_BOND,
                         NM_TYPE_SETTING_BRIDGE,
                         NM_TYPE_SETTING_INFINIBAND,
                         NM_TYPE_SETTING_TEAM,
                         NM_TYPE_SETTING_VLAN,
                         NM_TYPE_SETTING_WIRED,
                         NM_TYPE_SETTING_WIRELESS);
    }
    if (virtual_type == NM_TYPE_SETTING_BRIDGE) {
        return NM_IN_SET(other_type,
                         NM_TYPE_SETTING_BOND,
                         NM_TYPE_SETTING_TEAM,
                         NM_TYPE_SETTING_VLAN,
                         NM_TYPE_SETTING_WIRED);
    }
    if (virtual_type == NM_TYPE_SETTING_TEAM) {
        return NM_IN_SET(other_type,
                         NM_TYPE_SETTING_BOND,
                         NM_TYPE_SETTING_BRIDGE,
                         NM_TYPE_SETTING_TEAM,
                         NM_TYPE_SETTING_VLAN,
                         NM_TYPE_SETTING_WIRED);
    }
    if (virtual_type == NM_TYPE_SETTING_VLAN) {
        return NM_IN_SET(other_type,
                         NM_TYPE_SETTING_BOND,
                         NM_TYPE_SETTING_BRIDGE,
                         NM_TYPE_SETTING_TEAM,
                         NM_TYPE_SETTING_VLAN,
                         NM_TYPE_SETTING_WIRED,
                         NM_TYPE_SETTING_WIRELESS);
    }

    return FALSE;
}

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

/**
 * nm_utils_bond_mode_int_to_string:
 * @mode: bonding mode as a numeric value
 *
 * Convert bonding mode from integer value to descriptive name.
 * See https://www.kernel.org/doc/Documentation/networking/bonding.txt for
 * available modes.
 *
 * Returns: bonding mode string, or NULL on error
 *
 * Since: 1.2
 */
const char *
nm_utils_bond_mode_int_to_string(int mode)
{
    return _nm_setting_bond_mode_to_string(mode);
}

/**
 * nm_utils_bond_mode_string_to_int:
 * @mode: bonding mode as string
 *
 * Convert bonding mode from string representation to numeric value.
 * See https://www.kernel.org/doc/Documentation/networking/bonding.txt for
 * available modes.
 * The @mode string can be either a descriptive name or a number (as string).
 *
 * Returns: numeric bond mode, or -1 on error
 *
 * Since: 1.2
 */
int
nm_utils_bond_mode_string_to_int(const char *mode)
{
    return _nm_setting_bond_mode_from_string(mode);
}

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

#define STRSTRDICTKEY_V1_SET  0x01
#define STRSTRDICTKEY_V2_SET  0x02
#define STRSTRDICTKEY_ALL_SET 0x03

struct _NMUtilsStrStrDictKey {
    char type;
    char data[];
};

guint
_nm_utils_strstrdictkey_hash(gconstpointer a)
{
    const NMUtilsStrStrDictKey *k = a;
    const char                 *p;
    NMHashState                 h;

    nm_hash_init(&h, 76642997u);
    if (k) {
        if (((int) k->type) & ~STRSTRDICTKEY_ALL_SET)
            g_return_val_if_reached(0);

        nm_hash_update_val(&h, k->type);
        if (k->type & STRSTRDICTKEY_ALL_SET) {
            p = strchr(k->data, '\0');
            if (k->type == STRSTRDICTKEY_ALL_SET) {
                /* the key contains two strings. Continue... */
                p = strchr(p + 1, '\0');
            }
            if (p != k->data)
                nm_hash_update(&h, k->data, p - k->data);
        }
    }
    return nm_hash_complete(&h);
}

gboolean
_nm_utils_strstrdictkey_equal(gconstpointer a, gconstpointer b)
{
    const NMUtilsStrStrDictKey *k1 = a;
    const NMUtilsStrStrDictKey *k2 = b;

    if (k1 == k2)
        return TRUE;
    if (!k1 || !k2)
        return FALSE;

    if (k1->type != k2->type)
        return FALSE;

    if (k1->type & STRSTRDICTKEY_ALL_SET) {
        if (!nm_streq(k1->data, k2->data))
            return FALSE;

        if (k1->type == STRSTRDICTKEY_ALL_SET) {
            gsize l = strlen(k1->data) + 1;

            return nm_streq(&k1->data[l], &k2->data[l]);
        }
    }

    return TRUE;
}

NMUtilsStrStrDictKey *
_nm_utils_strstrdictkey_create(const char *v1, const char *v2)
{
    char                  type = 0;
    gsize                 l1 = 0, l2 = 0;
    NMUtilsStrStrDictKey *k;

    if (!v1 && !v2)
        return g_malloc0(1);

    /* we need to distinguish between ("",NULL) and (NULL,"").
     * Thus, in @type we encode which strings we have present
     * as not-NULL. */
    if (v1) {
        type |= STRSTRDICTKEY_V1_SET;
        l1 = strlen(v1) + 1;
    }
    if (v2) {
        type |= STRSTRDICTKEY_V2_SET;
        l2 = strlen(v2) + 1;
    }

    k       = g_malloc(G_STRUCT_OFFSET(NMUtilsStrStrDictKey, data) + l1 + l2);
    k->type = type;
    if (v1)
        memcpy(&k->data[0], v1, l1);
    if (v2)
        memcpy(&k->data[l1], v2, l2);

    return k;
}

static gboolean
validate_dns_option(const char                 *name,
                    gboolean                    numeric,
                    int                         addr_family,
                    const NMUtilsDNSOptionDesc *option_descs)
{
    const NMUtilsDNSOptionDesc *desc;

    if (!option_descs)
        return !!*name;

    for (desc = option_descs; desc->name; desc++) {
        if (!nm_streq(name, desc->name))
            continue;
        if ((!!numeric) != (!!desc->numeric))
            continue;
        if (addr_family != AF_UNSPEC) {
            if (desc->ipv6_only && addr_family != AF_INET6)
                continue;
        }
        return TRUE;
    }

    return FALSE;
}

/**
 * _nm_utils_dns_option_validate:
 * @option: option string
 * @out_name: (out) (optional) (nullable): the option name
 * @out_value: (out) (optional): the option value
 * @addr_family: AF_INET/AF_INET6 to only allow options for the specified address
 *   family. AF_UNSPEC to allow either. This argument is ignored, if @option_descs
 *   is NULL.
 * @option_descs: (nullable): an array of NMUtilsDNSOptionDesc which describes the
 * valid options
 *
 * Parses a DNS option in the form "name" or "name:number" and, if
 * @option_descs is not NULL, checks that the option conforms to one
 * of the provided descriptors. If @option_descs is NULL @ipv6 is
 * not considered.
 *
 * Returns: %TRUE when the parsing was successful and the option is valid,
 * %FALSE otherwise
 */
gboolean
_nm_utils_dns_option_validate(const char                 *option,
                              char                      **out_name,
                              long                       *out_value,
                              int                         addr_family,
                              const NMUtilsDNSOptionDesc *option_descs)
{
    gs_free char *option0_free = NULL;
    const char   *option0;
    const char   *option1;
    const char   *delim;
    long          option1_num;

    g_return_val_if_fail(option != NULL, FALSE);

    nm_assert_addr_family_or_unspec(addr_family);

    NM_SET_OUT(out_name, NULL);
    NM_SET_OUT(out_value, -1);

    if (!option[0])
        return FALSE;

    delim = strchr(option, ':');
    if (!delim) {
        if (!validate_dns_option(option, FALSE, addr_family, option_descs))
            return FALSE;
        NM_SET_OUT(out_name, g_strdup(option));
        return TRUE;
    }

    option1 = &delim[1];

    if (!option1[0])
        return FALSE;
    if (!NM_STRCHAR_ALL(option1, ch, g_ascii_isdigit(ch)))
        return FALSE;

    option0 = nm_strndup_a(300, option, delim - option, &option0_free);

    if (!validate_dns_option(option0, TRUE, addr_family, option_descs))
        return FALSE;

    option1_num = _nm_utils_ascii_str_to_int64(option1, 10, 0, G_MAXINT32, -1);
    if (option1_num == -1)
        return FALSE;

    NM_SET_OUT(out_name, g_steal_pointer(&option0_free) ?: g_strdup(option0));
    NM_SET_OUT(out_value, option1_num);
    return TRUE;
}

/**
 * _nm_utils_dns_option_find_idx:
 * @strv: an array of strings of length @strv_len
 * @strv_len: the length of @strv, or -1 for a NULL terminated strv array.
 * @option: a dns option string
 *
 * Searches for an option in an array of strings. The match is
 * performed only the option name; the option value is ignored.
 *
 * Returns: the index of the option in the array or -1 if was not
 * found.
 */
gssize
_nm_utils_dns_option_find_idx(const char *const *strv, gssize strv_len, const char *option)
{
    gs_free char *option_name = NULL;
    gsize         l;
    gsize         i;

    if (strv_len >= 0)
        l = strv_len;
    else
        l = NM_PTRARRAY_LEN(strv);

    if (l == 0)
        return -1;

    if (!_nm_utils_dns_option_validate(option, &option_name, NULL, AF_UNSPEC, NULL))
        return -1;

    for (i = 0; i < l; i++) {
        const char   *str      = strv[i];
        gs_free char *tmp_name = NULL;

        if (_nm_utils_dns_option_validate(str, &tmp_name, NULL, AF_UNSPEC, NULL)) {
            if (nm_streq(tmp_name, option_name))
                return i;
        }
    }

    return -1;
}

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

/**
 * nm_utils_enum_to_str:
 * @type: the %GType of the enum
 * @value: the value to be translated
 *
 * Converts an enum value to its string representation. If the enum is a
 * %G_TYPE_FLAGS the function returns a comma-separated list of matching values.
 * If the value has no corresponding string representation, it is converted
 * to a number. For enums it is converted to a decimal number, for flags
 * to an (unsigned) hex number.
 *
 * Returns: a newly allocated string or %NULL
 *
 * Since: 1.2
 */
char *
nm_utils_enum_to_str(GType type, int value)
{
    return _nm_utils_enum_to_str_full(type, value, ", ", NULL);
}

/**
 * nm_utils_enum_from_str:
 * @type: the %GType of the enum
 * @str: the input string
 * @out_value: (out) (optional): the output value
 * @err_token: (out) (optional) (nullable) (transfer full): location to store
 *   the first unrecognized token
 *
 * Converts a string to the matching enum value.
 *
 * If the enum is a %G_TYPE_FLAGS the function returns the logical OR of values
 * matching the comma-separated tokens in the string; if an unknown token is found
 * the function returns %FALSE and stores a pointer to a newly allocated string
 * containing the unrecognized token in @err_token.
 *
 * Returns: %TRUE if the conversion was successful, %FALSE otherwise
 *
 * Since: 1.2
 */
gboolean
nm_utils_enum_from_str(GType type, const char *str, int *out_value, char **err_token)
{
    return _nm_utils_enum_from_str_full(type, str, out_value, err_token, NULL);
}

/**
 * nm_utils_enum_get_values:
 * @type: the %GType of the enum
 * @from: the first element to be returned
 * @to: the last element to be returned
 *
 * Returns the list of possible values for a given enum.
 *
 * Returns: (transfer container): a NULL-terminated dynamically-allocated array of static strings
 * or %NULL on error
 *
 * Since: 1.2
 */
const char **
nm_utils_enum_get_values(GType type, int from, int to)
{
    return _nm_utils_enum_get_values(type, from, to);
}

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

static gboolean
_nm_utils_is_json_object_no_validation(const char *str, GError **error)
{
    nm_assert(str);

    /* libjansson also requires only utf-8 encoding. */
    if (!g_utf8_validate(str, -1, NULL)) {
        g_set_error_literal(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("not valid utf-8"));
        return FALSE;
    }
    while (g_ascii_isspace(str[0]))
        str++;

    /* do some very basic validation to see if this might be a JSON object. */
    if (str[0] == '{') {
        gsize l;

        l = strlen(str) - 1;
        while (l > 0 && g_ascii_isspace(str[l]))
            l--;

        if (str[l] == '}')
            return TRUE;
    }

    g_set_error_literal(error,
                        NM_CONNECTION_ERROR,
                        NM_CONNECTION_ERROR_INVALID_PROPERTY,
                        _("is not a JSON object"));
    return FALSE;
}

/**
 * nm_utils_is_json_object:
 * @str: the JSON string to test
 * @error: optional error reason
 *
 * Returns: whether the passed string is valid JSON.
 *   If libnm is not compiled with libjansson support, this check will
 *   also return %TRUE for possibly invalid inputs. If that is a problem
 *   for you, you must validate the JSON yourself.
 *
 * Since: 1.6
 */
gboolean
nm_utils_is_json_object(const char *str, GError **error)
{
    nm_auto_decref_json nm_json_t *json = NULL;
    const NMJsonVt                *vt;
    nm_json_error_t                jerror;

    g_return_val_if_fail(!error || !*error, FALSE);

    if (!str || !str[0]) {
        g_set_error_literal(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            str ? _("value is NULL") : _("value is empty"));
        return FALSE;
    }

    if (!(vt = nm_json_vt()))
        return _nm_utils_is_json_object_no_validation(str, error);

    json = vt->nm_json_loads(str, NM_JSON_REJECT_DUPLICATES, &jerror);
    if (!json) {
        g_set_error(error,
                    NM_CONNECTION_ERROR,
                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
                    _("invalid JSON at position %d (%s)"),
                    jerror.position,
                    jerror.text);
        return FALSE;
    }

    /* valid JSON (depending on the definition) can also be a literal.
     * Here we only allow objects. */
    if (!nm_json_is_object(json)) {
        g_set_error_literal(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("is not a JSON object"));
        return FALSE;
    }

    return TRUE;
}

static char *
attribute_unescape(const char *start, const char *end)
{
    char *ret, *dest;

    nm_assert(start <= end);
    dest = ret = g_malloc(end - start + 1);

    for (; start < end && *start; start++) {
        if (*start == '\\') {
            start++;
            if (!*start)
                break;
        }
        *dest++ = *start;
    }
    *dest = '\0';

    return ret;
}

gboolean
_nmtst_variant_attribute_spec_assert_sorted(const NMVariantAttributeSpec *const *array, gsize len)
{
    gsize i;

    g_assert(array);
    g_assert(len > 0);
    g_assert_cmpint(len, ==, NM_PTRARRAY_LEN(array));

    for (i = 0; i < len; i++) {
        nm_assert(array[i]->name);
        nm_assert(array[i]->name[0]);
        if (i > 0)
            nm_assert(strcmp(array[i - 1]->name, array[i]->name) < 0);
    }
    nm_assert(!array[i]);

    return TRUE;
}

const NMVariantAttributeSpec *
_nm_variant_attribute_spec_find_binary_search(const NMVariantAttributeSpec *const *array,
                                              gsize                                len,
                                              const char                          *name)
{
    gssize idx;

    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(NMVariantAttributeSpec, name) == 0);

    idx =
        nm_ptrarray_find_bsearch((gconstpointer *) array, len, &name, nm_strcmp_p_with_data, NULL);
    if (idx < 0)
        return NULL;
    return array[idx];
}

/**
 * nm_utils_parse_variant_attributes:
 * @string: the input string
 * @attr_separator: the attribute separator character
 * @key_value_separator: character separating key and values
 * @ignore_unknown: whether unknown attributes should be ignored
 * @spec: the attribute format specifiers
 * @error: location to store the error on failure
 *
 * Parse attributes from a string.
 *
 * Returns: (transfer full) (element-type utf8 GVariant): a #GHashTable mapping
 * attribute names to #GVariant values. Warning: the variant are still floating
 * references, owned by the hash table. If you take a reference, ensure to sink
 * the one of the hash table first.
 *
 * Since: 1.8
 */
GHashTable *
nm_utils_parse_variant_attributes(const char                          *string,
                                  char                                 attr_separator,
                                  char                                 key_value_separator,
                                  gboolean                             ignore_unknown,
                                  const NMVariantAttributeSpec *const *spec,
                                  GError                             **error)
{
    gs_unref_hashtable GHashTable       *ht  = NULL;
    const char                          *ptr = string, *start = NULL, *sep;
    GVariant                            *variant;
    const NMVariantAttributeSpec *const *s;

    g_return_val_if_fail(string, NULL);
    g_return_val_if_fail(attr_separator, NULL);
    g_return_val_if_fail(key_value_separator, NULL);
    g_return_val_if_fail(!error || !*error, NULL);

    ht = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, (GDestroyNotify) g_variant_unref);

    while (TRUE) {
        gs_free char *name = NULL, *value = NULL;

        if (!start)
            start = ptr;
        if (*ptr == '\\') {
            ptr++;
            if (!*ptr) {
                g_set_error_literal(error,
                                    NM_CONNECTION_ERROR,
                                    NM_CONNECTION_ERROR_FAILED,
                                    _("unterminated escape sequence"));
                return NULL;
            }
            goto next;
        }
        if (*ptr == attr_separator || *ptr == '\0') {
            if (ptr == start) {
                /* multiple separators */
                start = NULL;
                goto next;
            }

            /* Find the key-value separator */
            for (sep = start; sep != ptr; sep++) {
                if (*sep == '\\') {
                    sep++;
                    if (!*sep) {
                        g_set_error_literal(error,
                                            NM_CONNECTION_ERROR,
                                            NM_CONNECTION_ERROR_FAILED,
                                            _("unterminated escape sequence"));
                        return NULL;
                    }
                }
                if (*sep == key_value_separator)
                    break;
            }

            name = attribute_unescape(start, sep);

            for (s = spec; *s; s++) {
                if (g_hash_table_contains(ht, (*s)->name))
                    continue;
                if (nm_streq(name, (*s)->name))
                    break;
                if ((*s)->no_value && g_variant_type_equal((*s)->type, G_VARIANT_TYPE_STRING))
                    break;
            }

            if (!*s) {
                if (ignore_unknown)
                    goto next;
                else {
                    g_set_error(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_FAILED,
                                _("unknown attribute '%s'"),
                                name);
                    return NULL;
                }
            }

            if ((*s)->no_value) {
                if ((*s)->consumes_rest) {
                    value = g_strdup(start);
                    ptr   = strchr(start, '\0');
                } else {
                    value = g_steal_pointer(&name);
                }
            } else {
                if (*sep != key_value_separator) {
                    g_set_error(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_FAILED,
                                _("missing key-value separator '%c' after '%s'"),
                                key_value_separator,
                                name);
                    return NULL;
                }

                /* The attribute and key/value separators are the same. Look for the next one. */
                if (ptr == sep)
                    goto next;

                value = attribute_unescape(sep + 1, ptr);
            }

            if (g_variant_type_equal((*s)->type, G_VARIANT_TYPE_UINT32)) {
                gint64 num = _nm_utils_ascii_str_to_int64(value, 10, 0, G_MAXUINT32, -1);

                if (num == -1) {
                    g_set_error(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_FAILED,
                                _("invalid uint32 value '%s' for attribute '%s'"),
                                value,
                                (*s)->name);
                    return NULL;
                }
                variant = g_variant_new_uint32(num);
            } else if (g_variant_type_equal((*s)->type, G_VARIANT_TYPE_INT32)) {
                gint64 num =
                    _nm_utils_ascii_str_to_int64(value, 10, G_MININT32, G_MAXINT32, G_MAXINT64);

                if (num == G_MAXINT64) {
                    g_set_error(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_FAILED,
                                _("invalid int32 value '%s' for attribute '%s'"),
                                value,
                                (*s)->name);
                    return NULL;
                }
                variant = g_variant_new_int32(num);
            } else if (g_variant_type_equal((*s)->type, G_VARIANT_TYPE_UINT64)) {
                guint64 num = _nm_utils_ascii_str_to_uint64(value, 10, 0, G_MAXUINT64, G_MAXUINT64);

                if (num == G_MAXUINT64 && errno != 0) {
                    g_set_error(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_FAILED,
                                _("invalid uint64 value '%s' for attribute '%s'"),
                                value,
                                (*s)->name);
                    return NULL;
                }
                variant = g_variant_new_uint64(num);
            } else if (g_variant_type_equal((*s)->type, G_VARIANT_TYPE_BYTE)) {
                gint64 num = _nm_utils_ascii_str_to_int64(value, 10, 0, G_MAXUINT8, -1);

                if (num == -1) {
                    g_set_error(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_FAILED,
                                _("invalid uint8 value '%s' for attribute '%s'"),
                                value,
                                (*s)->name);
                    return NULL;
                }
                variant = g_variant_new_byte((guchar) num);
            } else if (g_variant_type_equal((*s)->type, G_VARIANT_TYPE_BOOLEAN)) {
                int b;

                b = (*s)->no_value ? TRUE : _nm_utils_ascii_str_to_bool(value, -1);
                if (b == -1) {
                    g_set_error(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_FAILED,
                                _("invalid boolean value '%s' for attribute '%s'"),
                                value,
                                (*s)->name);
                    return NULL;
                }
                variant = g_variant_new_boolean(b);
            } else if (g_variant_type_equal((*s)->type, G_VARIANT_TYPE_STRING)) {
                variant = g_variant_new_take_string(g_steal_pointer(&value));
            } else if (g_variant_type_equal((*s)->type, G_VARIANT_TYPE_BYTESTRING)) {
                variant = g_variant_new_bytestring(value);
            } else {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_FAILED,
                            _("unsupported attribute '%s' of type '%s'"),
                            (*s)->name,
                            (char *) (*s)->type);
                return NULL;
            }

            g_hash_table_insert(ht, g_strdup((*s)->name), variant);
            start = NULL;
        }
next:
        if (*ptr == '\0')
            break;
        ptr++;
    }

    return g_steal_pointer(&ht);
}

/**
 * nm_utils_format_variant_attributes:
 * @attributes: (element-type utf8 GVariant): a #GHashTable mapping attribute names to #GVariant values
 * @attr_separator: the attribute separator character
 * @key_value_separator: character separating key and values
 *
 * Format attributes to a string.
 *
 * Returns: (transfer full): the string representing attributes, or %NULL
 *    in case there are no attributes
 *
 * Since: 1.8
 */
char *
nm_utils_format_variant_attributes(GHashTable *attributes,
                                   char        attr_separator,
                                   char        key_value_separator)
{
    return _nm_utils_format_variant_attributes(attributes,
                                               NULL,
                                               attr_separator,
                                               key_value_separator);
}

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

/**
 * nm_utils_get_timestamp_msec:
 *
 * Gets current time in milliseconds of CLOCK_BOOTTIME.
 *
 * Returns: time in milliseconds
 *
 * Since: 1.12
 */
gint64
nm_utils_get_timestamp_msec(void)
{
    gint64 ts;

    ts = nm_utils_clock_gettime_msec(CLOCK_BOOTTIME);
    if (ts >= 0)
        return ts;

    if (ts == -EINVAL) {
        /* The fallback to CLOCK_MONOTONIC is taken only if we're running on a
         * criminally old kernel, prior to 2.6.39 (released on 18 May, 2011).
         * That happens during buildcheck on old builders, we don't expect to
         * be actually runs on kernels that old. */
        ts = nm_utils_clock_gettime_msec(CLOCK_MONOTONIC);
        if (ts >= 0)
            return ts;
    }

    g_return_val_if_reached(-1);
}

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

/**
 * nm_utils_version:
 *
 * Returns: the version ID of the libnm version. That is, the %NM_VERSION
 *   at runtime.
 *
 * Since: 1.6
 */
guint
nm_utils_version(void)
{
    return NM_VERSION;
}

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

NM_UTILS_FLAGS2STR_DEFINE(nm_bluetooth_capability_to_string,
                          NMBluetoothCapabilities,
                          NM_UTILS_FLAGS2STR(NM_BT_CAPABILITY_NONE, "NONE"),
                          NM_UTILS_FLAGS2STR(NM_BT_CAPABILITY_DUN, "DUN"),
                          NM_UTILS_FLAGS2STR(NM_BT_CAPABILITY_NAP, "NAP"), );

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

/**
 * nm_utils_base64secret_decode:
 * @base64_key: the (possibly invalid) base64 encode key.
 * @required_key_len: the expected (binary) length of the key after
 *   decoding. If the length does not match, the validation fails.
 * @out_key: (out) (optional): an optional output buffer for the binary
 *   key. If given, it will be filled with exactly @required_key_len
 *   bytes.
 *
 * Returns: %TRUE if the input key is a valid base64 encoded key
 *   with @required_key_len bytes.
 *
 * Since: 1.16
 */
gboolean
nm_utils_base64secret_decode(const char *base64_key, gsize required_key_len, guint8 *out_key)
{
    nm_auto_free guint8 *bin_arr = NULL;
    gsize                base64_key_len;
    gsize                bin_len;
    int                  r;

    if (!base64_key)
        return FALSE;

    base64_key_len = strlen(base64_key);

    r = nm_unbase64mem_full(base64_key, base64_key_len, TRUE, &bin_arr, &bin_len);
    if (r < 0)
        return FALSE;
    if (bin_len != required_key_len) {
        nm_explicit_bzero(bin_arr, bin_len);
        return FALSE;
    }

    if (out_key)
        memcpy(out_key, bin_arr, required_key_len);

    nm_explicit_bzero(bin_arr, bin_len);
    return TRUE;
}

gboolean
nm_utils_base64secret_normalize(const char *base64_key,
                                gsize       required_key_len,
                                char      **out_base64_key_norm)
{
    gs_free guint8 *buf_free = NULL;
    guint8          buf_static[200];
    guint8         *buf;

    if (required_key_len > sizeof(buf_static)) {
        buf_free = g_new(guint8, required_key_len);
        buf      = buf_free;
    } else
        buf = buf_static;

    if (!nm_utils_base64secret_decode(base64_key, required_key_len, buf)) {
        NM_SET_OUT(out_base64_key_norm, NULL);
        return FALSE;
    }

    NM_SET_OUT(out_base64_key_norm, g_base64_encode(buf, required_key_len));
    nm_explicit_bzero(buf, required_key_len);
    return TRUE;
}

GVariant *
_nm_utils_bridge_vlans_to_dbus(_NM_SETT_INFO_PROP_TO_DBUS_FCN_ARGS _nm_nil)
{
    gs_unref_ptrarray GPtrArray *vlans = NULL;
    GVariantBuilder              builder;
    guint                        i;
    const char                  *property_name = property_info->name;

    nm_assert(property_name);

    g_object_get(setting, property_name, &vlans, NULL);
    g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}"));

    if (vlans) {
        for (i = 0; i < vlans->len; i++) {
            NMBridgeVlan   *vlan = vlans->pdata[i];
            GVariantBuilder vlan_builder;
            guint16         vid_start, vid_end;

            nm_bridge_vlan_get_vid_range(vlan, &vid_start, &vid_end);

            g_variant_builder_init(&vlan_builder, G_VARIANT_TYPE_VARDICT);
            g_variant_builder_add(&vlan_builder,
                                  "{sv}",
                                  "vid-start",
                                  g_variant_new_uint16(vid_start));
            g_variant_builder_add(&vlan_builder, "{sv}", "vid-end", g_variant_new_uint16(vid_end));
            g_variant_builder_add(&vlan_builder,
                                  "{sv}",
                                  "pvid",
                                  g_variant_new_boolean(nm_bridge_vlan_is_pvid(vlan)));
            g_variant_builder_add(&vlan_builder,
                                  "{sv}",
                                  "untagged",
                                  g_variant_new_boolean(nm_bridge_vlan_is_untagged(vlan)));
            g_variant_builder_add(&builder, "a{sv}", &vlan_builder);
        }
    }

    return g_variant_builder_end(&builder);
}

gboolean
_nm_utils_bridge_vlans_from_dbus(_NM_SETT_INFO_PROP_FROM_DBUS_FCN_ARGS _nm_nil)
{
    gs_unref_ptrarray GPtrArray *vlans = NULL;
    GVariantIter                 vlan_iter;
    GVariant                    *vlan_var;

    g_return_val_if_fail(g_variant_is_of_type(value, G_VARIANT_TYPE("aa{sv}")), FALSE);

    vlans = g_ptr_array_new_with_free_func((GDestroyNotify) nm_bridge_vlan_unref);
    g_variant_iter_init(&vlan_iter, value);
    while (g_variant_iter_next(&vlan_iter, "@a{sv}", &vlan_var)) {
        _nm_unused gs_unref_variant GVariant *var_unref = vlan_var;
        NMBridgeVlan                         *vlan;
        guint16                               vid_start, vid_end;
        gboolean                              pvid = FALSE, untagged = FALSE;

        if (!g_variant_lookup(vlan_var, "vid-start", "q", &vid_start))
            continue;
        if (vid_start < NM_BRIDGE_VLAN_VID_MIN || vid_start > NM_BRIDGE_VLAN_VID_MAX)
            continue;

        if (!g_variant_lookup(vlan_var, "vid-end", "q", &vid_end))
            continue;
        if (vid_end < NM_BRIDGE_VLAN_VID_MIN || vid_end > NM_BRIDGE_VLAN_VID_MAX)
            continue;
        if (vid_start > vid_end)
            continue;

        if (!g_variant_lookup(vlan_var, "pvid", "b", &pvid))
            pvid = FALSE;
        if (pvid && vid_start != vid_end)
            continue;
        if (!g_variant_lookup(vlan_var, "untagged", "b", &untagged))
            untagged = FALSE;

        vlan = nm_bridge_vlan_new(vid_start, vid_end);
        nm_bridge_vlan_set_untagged(vlan, untagged);
        nm_bridge_vlan_set_pvid(vlan, pvid);
        g_ptr_array_add(vlans, vlan);
    }

    g_object_set(setting, property_info->name, vlans, NULL);

    return TRUE;
}

NMTernary
_nm_utils_bridge_compare_vlans(GPtrArray *vlans_a, GPtrArray *vlans_b)
{
    guint l = nm_g_ptr_array_len(vlans_a);
    guint i;

    if (l != nm_g_ptr_array_len(vlans_b))
        return FALSE;
    for (i = 0; i < l; i++) {
        if (nm_bridge_vlan_cmp(vlans_a->pdata[i], vlans_b->pdata[i]))
            return FALSE;
    }
    return TRUE;
}

gboolean
_nm_utils_bridge_vlan_verify_list(GPtrArray  *vlans,
                                  gboolean    check_normalizable,
                                  GError    **error,
                                  const char *setting,
                                  const char *property)
{
    guint                          i;
    gs_unref_hashtable GHashTable *h          = NULL;
    gboolean                       pvid_found = FALSE;

    if (!vlans || vlans->len <= 1)
        return TRUE;

    if (check_normalizable) {
        guint16 vid_prev_end, vid_start, vid_end;

        nm_assert(_nm_utils_bridge_vlan_verify_list(vlans, FALSE, NULL, setting, property));

        nm_bridge_vlan_get_vid_range(vlans->pdata[0], NULL, &vid_prev_end);
        for (i = 1; i < vlans->len; i++) {
            const NMBridgeVlan *vlan = vlans->pdata[i];

            nm_bridge_vlan_get_vid_range(vlan, &vid_start, &vid_end);

            if (vid_prev_end > vid_start) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("Bridge VLANs %d and %d are not sorted by ascending vid"),
                            vid_prev_end,
                            vid_start);
                g_prefix_error(error, "%s.%s: ", setting, property);
                return FALSE;
            }

            vid_prev_end = vid_end;
        }
        return TRUE;
    }

    h = g_hash_table_new(nm_direct_hash, NULL);
    for (i = 0; i < vlans->len; i++) {
        NMBridgeVlan *vlan = vlans->pdata[i];
        guint16       v, vid_start, vid_end;

        nm_bridge_vlan_get_vid_range(vlan, &vid_start, &vid_end);

        for (v = vid_start; v <= vid_end; v++) {
            if (!g_hash_table_add(h, GUINT_TO_POINTER(v))) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("duplicate bridge VLAN vid %u"),
                            v);
                g_prefix_error(error, "%s.%s: ", setting, property);
                return FALSE;
            }
        }

        if (nm_bridge_vlan_is_pvid(vlan)) {
            if (vid_start != vid_end || pvid_found) {
                g_set_error_literal(error,
                                    NM_CONNECTION_ERROR,
                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                    _("only one VLAN can be the PVID"));
                g_prefix_error(error, "%s.%s: ", setting, property);
                return FALSE;
            }
            pvid_found = TRUE;
        }
    }

    return TRUE;
}

GVariant *
_nm_utils_ranges_to_dbus(_NM_SETT_INFO_PROP_TO_DBUS_FCN_ARGS _nm_nil)
{
    gs_unref_ptrarray GPtrArray *ranges = NULL;
    GVariantBuilder              builder;
    const char                  *property_name = property_info->name;
    guint                        i;

    nm_assert(property_name);

    g_object_get(setting, property_name, &ranges, NULL);
    g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}"));

    if (ranges) {
        for (i = 0; i < ranges->len; i++) {
            NMRange        *range = ranges->pdata[i];
            GVariantBuilder range_builder;

            g_variant_builder_init(&range_builder, G_VARIANT_TYPE_VARDICT);
            g_variant_builder_add(&range_builder,
                                  "{sv}",
                                  "start",
                                  g_variant_new_uint64(range->start));
            g_variant_builder_add(&range_builder, "{sv}", "end", g_variant_new_uint64(range->end));

            g_variant_builder_add(&builder, "a{sv}", &range_builder);
        }
    }

    return g_variant_builder_end(&builder);
}

gboolean
_nm_utils_ranges_from_dbus(_NM_SETT_INFO_PROP_FROM_DBUS_FCN_ARGS _nm_nil)
{
    gs_unref_ptrarray GPtrArray *ranges = NULL;
    GVariantIter                 iter;
    GVariant                    *range_var;

    g_return_val_if_fail(g_variant_is_of_type(value, G_VARIANT_TYPE("aa{sv}")), FALSE);

    ranges = g_ptr_array_new_with_free_func((GDestroyNotify) nm_range_unref);
    g_variant_iter_init(&iter, value);
    while (g_variant_iter_next(&iter, "@a{sv}", &range_var)) {
        _nm_unused gs_unref_variant GVariant *var_unref = range_var;
        gint64                                start;
        gint64                                end;

        if (!g_variant_lookup(range_var, "start", "t", &start))
            continue;
        if (!g_variant_lookup(range_var, "end", "t", &end))
            continue;
        if (start > end)
            continue;

        g_ptr_array_add(ranges, nm_range_new(start, end));
    }

    g_object_set(setting, property_info->name, ranges, NULL);

    return TRUE;
}

NMTernary
_nm_utils_ranges_cmp(_NM_SETT_INFO_PROP_COMPARE_FCN_ARGS _nm_nil)
{
    const GPtrArray *ranges_a = NULL;
    const GPtrArray *ranges_b = NULL;
    guint            len;
    guint            i;

    if (nm_streq0(nm_setting_get_name(set_a), NM_SETTING_OVS_PORT_SETTING_NAME)
        && nm_streq0(property_info->name, NM_SETTING_OVS_PORT_TRUNKS)) {
        ranges_a = _nm_setting_ovs_port_get_trunks_arr(NM_SETTING_OVS_PORT(set_a));
        if (set_b)
            ranges_b = _nm_setting_ovs_port_get_trunks_arr(NM_SETTING_OVS_PORT(set_b));
    } else {
        nm_assert_not_reached();
    }

    len = nm_g_ptr_array_len(ranges_a);
    if (len != nm_g_ptr_array_len(ranges_b))
        return FALSE;
    for (i = 0; i < len; i++) {
        if (nm_range_cmp(ranges_a->pdata[i], ranges_b->pdata[i]))
            return FALSE;
    }

    return TRUE;
}

gboolean
_nm_utils_iaid_verify(const char *str, gint64 *out_value)
{
    gint64  i64;
    guint32 u32;

    NM_SET_OUT(out_value, -1);

    if (!str || !str[0])
        return FALSE;

    if (NM_IAID_IS_SPECIAL(str))
        return TRUE;

    if (NM_STRCHAR_ALL(str, ch, g_ascii_isxdigit(ch) || NM_IN_SET(ch, 'x', ':'))) {
        if ((i64 = _nm_utils_ascii_str_to_int64(str, 0, 0, G_MAXUINT32, -1)) != -1) {
            NM_SET_OUT(out_value, i64);
            return TRUE;
        }

        if (nm_dhcp_iaid_from_hexstr(str, &u32)) {
            NM_SET_OUT(out_value, u32);
            return TRUE;
        }
    }

    return FALSE;
}

gboolean
_nm_utils_validate_dhcp_hostname_flags(NMDhcpHostnameFlags flags, int addr_family, GError **error)
{
    NMDhcpHostnameFlags unknown;

    unknown = flags;
    unknown &= ~(NM_DHCP_HOSTNAME_FLAG_FQDN_ENCODED | NM_DHCP_HOSTNAME_FLAG_FQDN_SERV_UPDATE
                 | NM_DHCP_HOSTNAME_FLAG_FQDN_NO_UPDATE | NM_DHCP_HOSTNAME_FLAG_FQDN_CLEAR_FLAGS);
    if (unknown) {
        g_set_error(error,
                    NM_CONNECTION_ERROR,
                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
                    _("unknown flags 0x%x"),
                    (guint) unknown);
        return FALSE;
    }

    if (NM_FLAGS_ALL(flags,
                     NM_DHCP_HOSTNAME_FLAG_FQDN_NO_UPDATE
                         | NM_DHCP_HOSTNAME_FLAG_FQDN_SERV_UPDATE)) {
        g_set_error_literal(
            error,
            NM_CONNECTION_ERROR,
            NM_CONNECTION_ERROR_INVALID_PROPERTY,
            _("'fqdn-no-update' and 'fqdn-serv-update' flags cannot be set at the same time"));
        return FALSE;
    }

    if (NM_FLAGS_HAS(flags, NM_DHCP_HOSTNAME_FLAG_FQDN_CLEAR_FLAGS)
        && NM_FLAGS_ANY(flags,
                        NM_DHCP_HOSTNAME_FLAG_FQDN_SERV_UPDATE | NM_DHCP_HOSTNAME_FLAG_FQDN_ENCODED
                            | NM_DHCP_HOSTNAME_FLAG_FQDN_NO_UPDATE)) {
        g_set_error_literal(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("'fqdn-clear-flags' flag is incompatible with other FQDN flags"));
        return FALSE;
    }

    if (addr_family == AF_INET6 && (flags & NM_DHCP_HOSTNAME_FLAG_FQDN_ENCODED)) {
        g_set_error_literal(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("DHCPv6 does not support the E (encoded) FQDN flag"));
        return FALSE;
    }

    return TRUE;
}

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

/**
 * nm_utils_ensure_gtypes:
 *
 * This ensures that all NMSetting GTypes are created. For example,
 * after this call, g_type_from_name("NMSettingConnection") will work.
 *
 * This cannot fail and does nothing if the type already exists.
 *
 * Since: 1.42
 */
void
nm_utils_ensure_gtypes(void)
{
    NMMetaSettingType meta_type;

    for (meta_type = 0; meta_type < _NM_META_SETTING_TYPE_NUM; meta_type++)
        nm_meta_setting_infos[meta_type].get_setting_gtype();
}

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

typedef struct {
    GPid       pid;
    GSource   *child_watch_source;
    GMainLoop *loop;
    GError    *error;

    int child_stdout;
    int child_stderr;

    GSource *output_source;
    GSource *error_source;

    NMStrBuf output_buffer;
    NMStrBuf error_buffer;
} HelperInfo;

static void
helper_complete(HelperInfo *info, GError *error_take)
{
    if (error_take) {
        if (!info->error)
            info->error = error_take;
        else
            g_error_free(error_take);
    }

    if (info->output_source || info->error_source || info->pid != -1) {
        /* Wait that the pipe is closed and process has terminated */
        return;
    }

    if (info->error && info->error_buffer.len > 0) {
        /* Prefer the message from stderr as it's more informative */
        g_error_free(info->error);
        info->error = g_error_new(NM_CONNECTION_ERROR,
                                  NM_CONNECTION_ERROR_FAILED,
                                  "%s",
                                  nm_str_buf_get_str(&info->error_buffer));
    }

    g_main_loop_quit(info->loop);
}

static gboolean
helper_have_err_data(int fd, GIOCondition condition, gpointer user_data)
{
    HelperInfo *info = user_data;
    gssize      n_read;
    GError     *error = NULL;

    n_read = nm_utils_fd_read(fd, &info->error_buffer);

    if (n_read > 0)
        return G_SOURCE_CONTINUE;

    nm_clear_g_source_inst(&info->error_source);
    nm_clear_fd(&info->child_stderr);

    if (n_read < 0) {
        error = g_error_new(NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            "read from process returned %d (%s)",
                            (int) -n_read,
                            nm_strerror_native((int) -n_read));
    }

    helper_complete(info, error);
    return G_SOURCE_CONTINUE;
}

static gboolean
helper_have_data(int fd, GIOCondition condition, gpointer user_data)
{
    HelperInfo *info = user_data;
    gssize      n_read;
    GError     *error = NULL;

    n_read = nm_utils_fd_read(fd, &info->output_buffer);

    if (n_read > 0)
        return G_SOURCE_CONTINUE;

    nm_clear_g_source_inst(&info->output_source);
    nm_clear_fd(&info->child_stdout);

    if (n_read < 0) {
        error = g_error_new(NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            "read from process returned %d (%s)",
                            (int) -n_read,
                            nm_strerror_native((int) -n_read));
    }

    helper_complete(info, error);
    return G_SOURCE_CONTINUE;
}

static void
helper_child_terminated(GPid pid, int status, gpointer user_data)
{
    HelperInfo   *info        = user_data;
    gs_free char *status_desc = NULL;
    GError       *error       = NULL;

    info->pid = -1;
    nm_clear_g_source_inst(&info->child_watch_source);

    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
        if (!status_desc)
            status_desc = nm_utils_get_process_exit_status_desc(status);
        error =
            g_error_new(NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "helper process %s", status_desc);
    }

    helper_complete(info, error);
}

#define RUN_CERT_DIR NMRUNDIR "/cert"

/**
 * nm_utils_copy_cert_as_user:
 * @filename: the file name of the certificate or key to copy
 * @user: the user to impersonate when reading the file
 * @error: (nullable): return location for a #GError, or %NULL
 *
 * Reads @filename on behalf of user @user and writes the
 * content to a new file in /run/NetworkManager/cert/.
 * The new file has permission 600 and is owned by root.
 *
 * This function is useful for VPN plugins that run as root and need
 * to verify that the user owning the connection (the one listed in the
 * connection.permissions property) can access the file.
 *
 * Returns: (transfer full): the name of the new temporary file. Or %NULL
 *   if an error occurred, including when the given user can't access the
 *   file.
 *
 * Since: 1.56
 */
char *
nm_utils_copy_cert_as_user(const char *filename, const char *user, GError **error)
{
    gs_unref_bytes GBytes *bytes      = NULL;
    char                   dst_path[] = RUN_CERT_DIR "/XXXXXX";
    HelperInfo             info       = {
                          .child_stdout = -1,
                          .child_stderr = -1,
    };
    GMainContext *context;
    int           fd = -1;

    g_return_val_if_fail(filename, NULL);
    g_return_val_if_fail(user, NULL);
    g_return_val_if_fail(!error || !*error, NULL);

    if (geteuid() != 0) {
        g_set_error_literal(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("This function needs to be called by root"));
        return NULL;
    }

    if (!g_spawn_async_with_pipes(
            "/",
            (char **)
                NM_MAKE_STRV(LIBEXECDIR "/nm-libnm-helper", "read-file-as-user", filename, user),
            (char **) NM_MAKE_STRV(),
            G_SPAWN_CLOEXEC_PIPES | G_SPAWN_DO_NOT_REAP_CHILD,
            NULL,
            NULL,
            &info.pid,
            NULL,
            &info.child_stdout,
            &info.child_stderr,
            error)) {
        return NULL;
    }

    context   = g_main_context_new();
    info.loop = g_main_loop_new(context, FALSE);

    /* Watch process */
    info.child_watch_source = nm_g_child_watch_source_new(info.pid,
                                                          G_PRIORITY_DEFAULT,
                                                          helper_child_terminated,
                                                          &info,
                                                          NULL);
    g_source_attach(info.child_watch_source, context);

    /* Watch stdout */
    info.output_buffer = NM_STR_BUF_INIT(0, FALSE);
    info.output_source = nm_g_unix_fd_source_new(info.child_stdout,
                                                 G_IO_IN | G_IO_ERR | G_IO_HUP,
                                                 G_PRIORITY_DEFAULT,
                                                 helper_have_data,
                                                 &info,
                                                 NULL);
    g_source_attach(info.output_source, context);

    /* Watch stderr */
    info.error_buffer = NM_STR_BUF_INIT(0, FALSE);
    info.error_source = nm_g_unix_fd_source_new(info.child_stderr,
                                                G_IO_IN | G_IO_ERR | G_IO_HUP,
                                                G_PRIORITY_DEFAULT,
                                                helper_have_err_data,
                                                &info,
                                                NULL);
    g_source_attach(info.error_source, context);

    /* Wait termination */
    g_main_loop_run(info.loop);
    g_clear_pointer(&info.loop, g_main_loop_unref);
    g_clear_pointer(&context, g_main_context_unref);

    if (info.error) {
        nm_str_buf_destroy(&info.output_buffer);
        nm_str_buf_destroy(&info.error_buffer);
        g_propagate_error(error, g_steal_pointer(&info.error));
        return NULL;
    }

    /* Write the data to a new file */

    bytes = g_bytes_new(nm_str_buf_get_str_unsafe(&info.output_buffer), info.output_buffer.len);
    nm_str_buf_destroy(&info.output_buffer);
    nm_str_buf_destroy(&info.error_buffer);

    mkdir(RUN_CERT_DIR, 0600);
    fd = mkstemp(dst_path);
    if (fd < 0) {
        g_set_error_literal(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("Failure creating the temporary file"));
        return NULL;
    }
    nm_close(fd);

    if (!nm_utils_file_set_contents(dst_path,
                                    g_bytes_get_data(bytes, NULL),
                                    g_bytes_get_size(bytes),
                                    0600,
                                    NULL,
                                    NULL,
                                    NULL,
                                    error)) {
        return NULL;
    }

    return g_strdup(dst_path);
}
