about summary refs log tree commit diff
path: root/src/libnm-glib-aux
diff options
context:
space:
mode:
authorJeremy Bicha <jeremy.bicha@canonical.com>2022-08-18 08:31:29 -0400
committerJeremy Bicha <jeremy.bicha@canonical.com>2022-08-18 08:31:29 -0400
commitb0887dd4d035acc0d84aa859748d36891548eaaf (patch)
treec4dc0dae50954f3c8fb600aa9e7cfaabeb80deb0 /src/libnm-glib-aux
parent1a62dcfdc0470be37743914da69fe0b0677c6bfb (diff)
parent4741f1a52215c7ba466140912084d6906185bef3 (diff)
Merge branch 'debian/master' into ubuntu/master
Diffstat (limited to 'src/libnm-glib-aux')
-rw-r--r--src/libnm-glib-aux/nm-default-glib.h44
-rw-r--r--src/libnm-glib-aux/nm-enum-utils.c2
-rw-r--r--src/libnm-glib-aux/nm-errno.c1
-rw-r--r--src/libnm-glib-aux/nm-errno.h1
-rw-r--r--src/libnm-glib-aux/nm-glib.h27
-rw-r--r--src/libnm-glib-aux/nm-hash-utils.c2
-rw-r--r--src/libnm-glib-aux/nm-hash-utils.h24
-rw-r--r--src/libnm-glib-aux/nm-io-utils.c285
-rw-r--r--src/libnm-glib-aux/nm-io-utils.h11
-rw-r--r--src/libnm-glib-aux/nm-logging-fwd.h11
-rw-r--r--src/libnm-glib-aux/nm-macros-internal.h20
-rw-r--r--src/libnm-glib-aux/nm-random-utils.c340
-rw-r--r--src/libnm-glib-aux/nm-random-utils.h10
-rw-r--r--src/libnm-glib-aux/nm-ref-string.h2
-rw-r--r--src/libnm-glib-aux/nm-shared-utils.c743
-rw-r--r--src/libnm-glib-aux/nm-shared-utils.h277
-rw-r--r--src/libnm-glib-aux/nm-str-buf.h120
-rw-r--r--src/libnm-glib-aux/nm-test-utils.h3
-rw-r--r--src/libnm-glib-aux/nm-uuid.c40
-rw-r--r--src/libnm-glib-aux/tests/test-shared-general.c846
20 files changed, 2488 insertions, 321 deletions
diff --git a/src/libnm-glib-aux/nm-default-glib.h b/src/libnm-glib-aux/nm-default-glib.h
index 9d04ddc4..f39e16cc 100644
--- a/src/libnm-glib-aux/nm-default-glib.h
+++ b/src/libnm-glib-aux/nm-default-glib.h
@@ -33,27 +33,27 @@
 #if NM_MORE_ASSERTS == 0
 #ifndef G_DISABLE_CAST_CHECKS
 /* Unless compiling with G_DISABLE_CAST_CHECKS, glib performs type checking
-         * during G_VARIANT_TYPE() via g_variant_type_checked_(). This is not necessary
-         * because commonly this cast is needed during something like
-         *
-         *   g_variant_builder_init (&props, G_VARIANT_TYPE ("a{sv}"));
-         *
-         * Note that in if the variant type would be invalid, the check still
-         * wouldn't make the buggy code magically work. Instead of passing a
-         * bogus type string (bad), it would pass %NULL to g_variant_builder_init()
-         * (also bad).
-         *
-         * Also, a function like g_variant_builder_init() already validates
-         * the input type via something like
-         *
-         *   g_return_if_fail (g_variant_type_is_container (type));
-         *
-         * So, by having G_VARIANT_TYPE() also validate the type, we validate
-         * twice, whereas the first validation is rather pointless because it
-         * doesn't prevent the function to be called with invalid arguments.
-         *
-         * Just patch G_VARIANT_TYPE() to perform no check.
-         */
+ * during G_VARIANT_TYPE() via g_variant_type_checked_(). This is not necessary
+ * because commonly this cast is needed during something like
+ *
+ *   g_variant_builder_init (&props, G_VARIANT_TYPE ("a{sv}"));
+ *
+ * Note that in if the variant type would be invalid, the check still
+ * wouldn't make the buggy code magically work. Instead of passing a
+ * bogus type string (bad), it would pass %NULL to g_variant_builder_init()
+ * (also bad).
+ *
+ * Also, a function like g_variant_builder_init() already validates
+ * the input type via something like
+ *
+ *   g_return_if_fail (g_variant_type_is_container (type));
+ *
+ * So, by having G_VARIANT_TYPE() also validate the type, we validate
+ * twice, whereas the first validation is rather pointless because it
+ * doesn't prevent the function to be called with invalid arguments.
+ *
+ * Just patch G_VARIANT_TYPE() to perform no check.
+ */
 #undef G_VARIANT_TYPE
 #define G_VARIANT_TYPE(type_string) ((const GVariantType *) (type_string))
 #endif
@@ -63,8 +63,6 @@
 
 #include "nm-gassert-patch.h"
 
-#include "libnm-std-aux/nm-std-aux.h"
-#include "libnm-std-aux/nm-std-utils.h"
 #include "libnm-glib-aux/nm-macros-internal.h"
 #include "libnm-glib-aux/nm-shared-utils.h"
 #include "libnm-glib-aux/nm-errno.h"
diff --git a/src/libnm-glib-aux/nm-enum-utils.c b/src/libnm-glib-aux/nm-enum-utils.c
index 3b9b7e8d..b7f7a3f6 100644
--- a/src/libnm-glib-aux/nm-enum-utils.c
+++ b/src/libnm-glib-aux/nm-enum-utils.c
@@ -142,7 +142,7 @@ _nm_utils_enum_to_str_full(GType                       type,
 
         flags_separator = flags_separator ?: " ";
 
-        nm_str_buf_init(&strbuf, 16, FALSE);
+        strbuf = NM_STR_BUF_INIT(16, FALSE);
 
         for (; value_infos && value_infos->nick; value_infos++) {
             nm_assert(_enum_is_valid_flags_nick(value_infos->nick));
diff --git a/src/libnm-glib-aux/nm-errno.c b/src/libnm-glib-aux/nm-errno.c
index 14d93d64..b76386a6 100644
--- a/src/libnm-glib-aux/nm-errno.c
+++ b/src/libnm-glib-aux/nm-errno.c
@@ -28,6 +28,7 @@ static NM_UTILS_LOOKUP_STR_DEFINE(
     NM_UTILS_LOOKUP_STR_ITEM(NME_NL_ATTRSIZE, "NME_NL_ATTRSIZE"),
     NM_UTILS_LOOKUP_STR_ITEM(NME_NL_BAD_SOCK, "NME_NL_BAD_SOCK"),
     NM_UTILS_LOOKUP_STR_ITEM(NME_NL_DUMP_INTR, "NME_NL_DUMP_INTR"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_NL_MSG_INVAL, "NME_NL_MSG_INVAL"),
     NM_UTILS_LOOKUP_STR_ITEM(NME_NL_MSG_OVERFLOW, "NME_NL_MSG_OVERFLOW"),
     NM_UTILS_LOOKUP_STR_ITEM(NME_NL_MSG_TOOSHORT, "NME_NL_MSG_TOOSHORT"),
     NM_UTILS_LOOKUP_STR_ITEM(NME_NL_MSG_TRUNC, "NME_NL_MSG_TRUNC"),
diff --git a/src/libnm-glib-aux/nm-errno.h b/src/libnm-glib-aux/nm-errno.h
index 62c8379f..f893a528 100644
--- a/src/libnm-glib-aux/nm-errno.h
+++ b/src/libnm-glib-aux/nm-errno.h
@@ -39,6 +39,7 @@ enum _NMErrno {
 
     /* netlink errors. */
     NME_NL_SEQ_MISMATCH,
+    NME_NL_MSG_INVAL,
     NME_NL_MSG_TRUNC,
     NME_NL_MSG_TOOSHORT,
     NME_NL_DUMP_INTR,
diff --git a/src/libnm-glib-aux/nm-glib.h b/src/libnm-glib-aux/nm-glib.h
index f3be3b32..2dc86d7b 100644
--- a/src/libnm-glib-aux/nm-glib.h
+++ b/src/libnm-glib-aux/nm-glib.h
@@ -307,6 +307,33 @@ _nm_g_ptr_array_insert(GPtrArray *array, int index_, gpointer data)
 
 /*****************************************************************************/
 
+#if !GLIB_CHECK_VERSION(2, 54, 0)
+static inline gboolean
+g_ptr_array_find(GPtrArray *haystack, gconstpointer needle, guint *index_)
+{
+    guint i;
+    g_return_val_if_fail(haystack, FALSE);
+
+    for (i = 0; i < haystack->len; i++) {
+        if (haystack->pdata[i] == needle) {
+            if (index_)
+                *index_ = i;
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+#else
+#define g_ptr_array_find(haystack, needle, index_)  \
+    ({                                              \
+        G_GNUC_BEGIN_IGNORE_DEPRECATIONS            \
+        g_ptr_array_find(haystack, needle, index_); \
+        G_GNUC_END_IGNORE_DEPRECATIONS              \
+    })
+#endif
+
+/*****************************************************************************/
+
 #if !GLIB_CHECK_VERSION(2, 40, 0)
 static inline gboolean
 _g_key_file_save_to_file(GKeyFile *key_file, const char *filename, GError **error)
diff --git a/src/libnm-glib-aux/nm-hash-utils.c b/src/libnm-glib-aux/nm-hash-utils.c
index 68b33998..941aba01 100644
--- a/src/libnm-glib-aux/nm-hash-utils.c
+++ b/src/libnm-glib-aux/nm-hash-utils.c
@@ -45,7 +45,7 @@ again:
             guint8 _extra_entropy[3 * HASH_KEY_SIZE];
         } t_arr;
 
-        nm_utils_random_bytes(&t_arr, sizeof(t_arr));
+        nm_random_get_bytes(&t_arr, sizeof(t_arr));
 
         /* We only initialize one random hash key. So we can spend some effort
          * of getting this right. For one, we collect more random bytes than
diff --git a/src/libnm-glib-aux/nm-hash-utils.h b/src/libnm-glib-aux/nm-hash-utils.h
index b7ee4dd1..dffa828b 100644
--- a/src/libnm-glib-aux/nm-hash-utils.h
+++ b/src/libnm-glib-aux/nm-hash-utils.h
@@ -122,26 +122,26 @@ nm_hash_update_bool(NMHashState *state, bool val)
     nm_hash_update(state, &val, sizeof(val));
 }
 
-#define _NM_HASH_COMBINE_BOOLS_OP(x, n) ((x) ? NM_BIT((n)) : 0u)
+#define _NM_HASH_COMBINE_BOOLS_OP(x, n, op_arg) ((x) ? NM_BIT((n)) : 0ull)
 
-#define NM_HASH_COMBINE_BOOLS(type, ...)                                               \
-    ((type) (NM_STATIC_ASSERT_EXPR_1(NM_NARG(__VA_ARGS__) <= 8 * sizeof(type))         \
-                 ? (NM_VA_ARGS_FOREACH(, , |, _NM_HASH_COMBINE_BOOLS_OP, __VA_ARGS__)) \
-                 : 0))
+#define NM_HASH_COMBINE_BOOLS(type, ...)                                                 \
+    ((type) (NM_STATIC_ASSERT_EXPR_1(NM_NARG(__VA_ARGS__) <= 8 * sizeof(type))           \
+                 ? (NM_VA_ARGS_FOREACH(, , |, _NM_HASH_COMBINE_BOOLS_OP, , __VA_ARGS__)) \
+                 : 0ull))
 
 #define nm_hash_update_bools(state, ...) \
     nm_hash_update_val(state, NM_HASH_COMBINE_BOOLS(guint8, __VA_ARGS__))
 
-#define _NM_HASH_COMBINE_VALS_TYPE_OP(x, idx) typeof(x) _v##idx;
-#define _NM_HASH_COMBINE_VALS_INIT_OP(x, idx) ._v##idx = (x),
+#define _NM_HASH_COMBINE_VALS_TYPE_OP(x, idx, op_arg) typeof(x) _v##idx;
+#define _NM_HASH_COMBINE_VALS_INIT_OP(x, idx, op_arg) ._v##idx = (x),
 
 /* NM_HASH_COMBINE_VALS() is faster then nm_hash_update_val() as it combines multiple
  * calls to nm_hash_update() using a packed structure. */
-#define NM_HASH_COMBINE_VALS(var, ...)                                       \
-    const struct _nm_packed {                                                \
-        NM_VA_ARGS_FOREACH(, , , _NM_HASH_COMBINE_VALS_TYPE_OP, __VA_ARGS__) \
-    } var _nm_alignas(guint64) = {                                           \
-        NM_VA_ARGS_FOREACH(, , , _NM_HASH_COMBINE_VALS_INIT_OP, __VA_ARGS__)}
+#define NM_HASH_COMBINE_VALS(var, ...)                                         \
+    const struct _nm_packed {                                                  \
+        NM_VA_ARGS_FOREACH(, , , _NM_HASH_COMBINE_VALS_TYPE_OP, , __VA_ARGS__) \
+    } var _nm_alignas(max_align_t) = {                                         \
+        NM_VA_ARGS_FOREACH(, , , _NM_HASH_COMBINE_VALS_INIT_OP, , __VA_ARGS__)}
 
 /* nm_hash_update_vals() is faster then nm_hash_update_val() as it combines multiple
  * calls to nm_hash_update() using a packed structure. */
diff --git a/src/libnm-glib-aux/nm-io-utils.c b/src/libnm-glib-aux/nm-io-utils.c
index 503f044f..0823a16c 100644
--- a/src/libnm-glib-aux/nm-io-utils.c
+++ b/src/libnm-glib-aux/nm-io-utils.c
@@ -723,3 +723,288 @@ nm_sd_notify(const char *state)
 
     return 0;
 }
+
+/*****************************************************************************/
+
+#define SHELL_NEED_ESCAPE "\"\\`$"
+
+int
+nm_parse_env_file_full(
+    const char *contents,
+    int (*push)(unsigned line, const char *key, const char *value, void *userdata),
+    void *userdata)
+{
+    gsize                    last_value_whitespace = G_MAXSIZE;
+    gsize                    last_key_whitespace   = G_MAXSIZE;
+    nm_auto_str_buf NMStrBuf key                   = NM_STR_BUF_INIT(0, FALSE);
+    nm_auto_str_buf NMStrBuf value                 = NM_STR_BUF_INIT(0, FALSE);
+    unsigned                 line                  = 1;
+    int                      r;
+    enum {
+        PRE_KEY,
+        KEY,
+        PRE_VALUE,
+        VALUE,
+        VALUE_ESCAPE,
+        SINGLE_QUOTE_VALUE,
+        DOUBLE_QUOTE_VALUE,
+        DOUBLE_QUOTE_VALUE_ESCAPE,
+        COMMENT,
+        COMMENT_ESCAPE
+    } state = PRE_KEY;
+
+    /* Copied and adjusted from systemd's parse_env_file_internal().
+     * https://github.com/systemd/systemd/blob/6247128902ca71ee2ad406cf69af04ea389d3d27/src/basic/env-file.c#L15 */
+
+    nm_assert(push);
+
+    if (!contents)
+        return -ENOENT;
+
+    for (const char *p = contents; *p; p++) {
+        char c = *p;
+
+        switch (state) {
+        case PRE_KEY:
+            if (NM_IN_SET(c, '#', ';'))
+                state = COMMENT;
+            else if (!nm_ascii_is_whitespace(c)) {
+                state               = KEY;
+                last_key_whitespace = G_MAXSIZE;
+                nm_str_buf_append_c(&key, c);
+            }
+            break;
+
+        case KEY:
+            if (nm_ascii_is_newline(c)) {
+                state = PRE_KEY;
+                line++;
+                nm_str_buf_reset(&key);
+            } else if (c == '=') {
+                state                 = PRE_VALUE;
+                last_value_whitespace = G_MAXSIZE;
+            } else {
+                if (!nm_ascii_is_whitespace(c))
+                    last_key_whitespace = G_MAXSIZE;
+                else if (last_key_whitespace == G_MAXSIZE)
+                    last_key_whitespace = key.len;
+                nm_str_buf_append_c(&key, c);
+            }
+            break;
+
+        case PRE_VALUE:
+            if (nm_ascii_is_newline(c)) {
+                state = PRE_KEY;
+                line++;
+
+                /* strip trailing whitespace from key */
+                if (last_key_whitespace != G_MAXSIZE)
+                    nm_str_buf_get_str_unsafe(&key)[last_key_whitespace] = 0;
+
+                r = push(line,
+                         nm_str_buf_get_str(&key),
+                         nm_str_buf_get_str(&value) ?: "",
+                         userdata);
+                if (r < 0)
+                    return r;
+
+                nm_str_buf_reset(&key);
+                nm_str_buf_reset(&value);
+            } else if (c == '\'')
+                state = SINGLE_QUOTE_VALUE;
+            else if (c == '"')
+                state = DOUBLE_QUOTE_VALUE;
+            else if (c == '\\')
+                state = VALUE_ESCAPE;
+            else if (!nm_ascii_is_whitespace(c)) {
+                state = VALUE;
+                nm_str_buf_append_c(&value, c);
+            }
+
+            break;
+
+        case VALUE:
+            if (nm_ascii_is_newline(c)) {
+                state = PRE_KEY;
+                line++;
+
+                /* Chomp off trailing whitespace from value */
+                if (last_value_whitespace != G_MAXSIZE)
+                    nm_str_buf_get_str_unsafe(&value)[last_value_whitespace] = 0;
+
+                /* strip trailing whitespace from key */
+                if (last_key_whitespace != G_MAXSIZE)
+                    nm_str_buf_get_str_unsafe(&key)[last_key_whitespace] = 0;
+
+                r = push(line,
+                         nm_str_buf_get_str(&key),
+                         nm_str_buf_get_str(&value) ?: "",
+                         userdata);
+                if (r < 0)
+                    return r;
+
+                nm_str_buf_reset(&key);
+                nm_str_buf_reset(&value);
+            } else if (c == '\\') {
+                state                 = VALUE_ESCAPE;
+                last_value_whitespace = G_MAXSIZE;
+            } else {
+                if (!nm_ascii_is_whitespace(c))
+                    last_value_whitespace = G_MAXSIZE;
+                else if (last_value_whitespace == G_MAXSIZE)
+                    last_value_whitespace = value.len;
+                nm_str_buf_append_c(&value, c);
+            }
+            break;
+
+        case VALUE_ESCAPE:
+            state = VALUE;
+            if (!nm_ascii_is_newline(c)) {
+                /* Escaped newlines we eat up entirely */
+                nm_str_buf_append_c(&value, c);
+            }
+            break;
+
+        case SINGLE_QUOTE_VALUE:
+            if (c == '\'')
+                state = PRE_VALUE;
+            else
+                nm_str_buf_append_c(&value, c);
+            break;
+
+        case DOUBLE_QUOTE_VALUE:
+            if (c == '"')
+                state = PRE_VALUE;
+            else if (c == '\\')
+                state = DOUBLE_QUOTE_VALUE_ESCAPE;
+            else
+                nm_str_buf_append_c(&value, c);
+            break;
+
+        case DOUBLE_QUOTE_VALUE_ESCAPE:
+            state = DOUBLE_QUOTE_VALUE;
+            if (strchr(SHELL_NEED_ESCAPE, c)) {
+                /* If this is a char that needs escaping, just unescape it. */
+                nm_str_buf_append_c(&value, c);
+            } else if (c != '\n') {
+                /* If other char than what needs escaping, keep the "\" in place, like the
+                 * real shell does. */
+                nm_str_buf_append_c(&value, '\\', c);
+            }
+            /* Escaped newlines (aka "continuation lines") are eaten up entirely */
+            break;
+
+        case COMMENT:
+            if (c == '\\')
+                state = COMMENT_ESCAPE;
+            else if (nm_ascii_is_newline(c)) {
+                state = PRE_KEY;
+                line++;
+            }
+            break;
+
+        case COMMENT_ESCAPE:
+            state = COMMENT;
+            break;
+        }
+    }
+
+    if (NM_IN_SET(state,
+                  PRE_VALUE,
+                  VALUE,
+                  VALUE_ESCAPE,
+                  SINGLE_QUOTE_VALUE,
+                  DOUBLE_QUOTE_VALUE,
+                  DOUBLE_QUOTE_VALUE_ESCAPE)) {
+        if (state == VALUE)
+            if (last_value_whitespace != G_MAXSIZE)
+                nm_str_buf_get_str_unsafe(&value)[last_value_whitespace] = 0;
+
+        /* strip trailing whitespace from key */
+        if (last_key_whitespace != G_MAXSIZE)
+            nm_str_buf_get_str_unsafe(&key)[last_key_whitespace] = 0;
+
+        r = push(line, nm_str_buf_get_str(&key), nm_str_buf_get_str(&value) ?: "", userdata);
+        if (r < 0)
+            return r;
+    }
+
+    return 0;
+}
+
+/*****************************************************************************/
+
+static int
+check_utf8ness_and_warn(const char *key, const char *value)
+{
+    /* Taken from systemd's check_utf8ness_and_warn()
+     * https://github.com/systemd/systemd/blob/6247128902ca71ee2ad406cf69af04ea389d3d27/src/basic/env-file.c#L273 */
+
+    if (!g_utf8_validate(key, -1, NULL))
+        return -EINVAL;
+
+    if (!g_utf8_validate(value, -1, NULL))
+        return -EINVAL;
+
+    return 0;
+}
+
+static int
+parse_env_file_push(unsigned line, const char *key, const char *value, void *userdata)
+{
+    const char *k;
+    va_list    *ap = userdata;
+    va_list     aq;
+    int         r;
+
+    r = check_utf8ness_and_warn(key, value);
+    if (r < 0)
+        return r;
+
+    va_copy(aq, *ap);
+
+    while ((k = va_arg(aq, const char *))) {
+        char **v;
+
+        v = va_arg(aq, char **);
+        if (nm_streq(key, k)) {
+            va_end(aq);
+            g_free(*v);
+            *v = g_strdup(value);
+            return 1;
+        }
+    }
+
+    va_end(aq);
+    return 0;
+}
+
+int
+nm_parse_env_filev(const char *contents, va_list ap)
+{
+    va_list aq;
+    int     r;
+
+    /* Copied from systemd's parse_env_filev().
+     * https://github.com/systemd/systemd/blob/6247128902ca71ee2ad406cf69af04ea389d3d27/src/basic/env-file.c#L333 */
+
+    va_copy(aq, ap);
+    r = nm_parse_env_file_full(contents, parse_env_file_push, &aq);
+    va_end(aq);
+    return r;
+}
+
+int
+nm_parse_env_file_sentinel(const char *contents, ...)
+{
+    va_list ap;
+    int     r;
+
+    /* Copied from systemd's parse_env_file_sentinel().
+     * https://github.com/systemd/systemd/blob/6247128902ca71ee2ad406cf69af04ea389d3d27/src/basic/env-file.c#L347 */
+
+    va_start(ap, contents);
+    r = nm_parse_env_filev(contents, ap);
+    va_end(ap);
+    return r;
+}
diff --git a/src/libnm-glib-aux/nm-io-utils.h b/src/libnm-glib-aux/nm-io-utils.h
index a8503986..ef015153 100644
--- a/src/libnm-glib-aux/nm-io-utils.h
+++ b/src/libnm-glib-aux/nm-io-utils.h
@@ -77,4 +77,15 @@ int nm_io_sockaddr_un_set(struct sockaddr_un *ret, NMOptionBool is_abstract, con
 
 int nm_sd_notify(const char *state);
 
+/*****************************************************************************/
+
+int nm_parse_env_file_full(
+    const char *contents,
+    int (*push)(unsigned line, const char *key, const char *value, void *userdata),
+    void *userdata);
+
+int nm_parse_env_filev(const char *contents, va_list ap);
+int nm_parse_env_file_sentinel(const char *contents, ...) G_GNUC_NULL_TERMINATED;
+#define nm_parse_env_file(contents, ...) nm_parse_env_file_sentinel((contents), __VA_ARGS__, NULL)
+
 #endif /* __NM_IO_UTILS_H__ */
diff --git a/src/libnm-glib-aux/nm-logging-fwd.h b/src/libnm-glib-aux/nm-logging-fwd.h
index 0e715c50..72e5723c 100644
--- a/src/libnm-glib-aux/nm-logging-fwd.h
+++ b/src/libnm-glib-aux/nm-logging-fwd.h
@@ -59,7 +59,16 @@ typedef enum {
     LOGD_IP   = LOGD_IP4 | LOGD_IP6,
 
 #define LOGD_DHCPX(is_ipv4) ((is_ipv4) ? LOGD_DHCP4 : LOGD_DHCP6)
-#define LOGD_IPX(is_ipv4)   ((is_ipv4) ? LOGD_IP4 : LOGD_IP6)
+
+#define LOGD_DHCP_af(addr_family)                                                               \
+    ({                                                                                          \
+        const int _addr_family_1 = (addr_family);                                               \
+                                                                                                \
+        (_addr_family_1 == AF_UNSPEC ? LOGD_DHCP                                                \
+                                     : (NM_IS_IPv4(_addr_family_1) ? LOGD_DHCP4 : LOGD_DHCP6)); \
+    })
+
+#define LOGD_IPX(is_ipv4) ((is_ipv4) ? LOGD_IP4 : LOGD_IP6)
 
 } NMLogDomain;
 
diff --git a/src/libnm-glib-aux/nm-macros-internal.h b/src/libnm-glib-aux/nm-macros-internal.h
index 7cc8ac97..f2a4461e 100644
--- a/src/libnm-glib-aux/nm-macros-internal.h
+++ b/src/libnm-glib-aux/nm-macros-internal.h
@@ -1053,6 +1053,24 @@ nm_g_variant_equal(GVariant *a, GVariant *b)
  * the kernel command line. */
 #define NM_ASCII_WHITESPACES " \n\t\r"
 
+static inline gboolean
+nm_ascii_is_whitespace(char ch)
+{
+    /* Checks whether @ch is in NM_ASCII_WHITESPACES.
+     * Similar to g_ascii_isspace(), however this one does not accept '\f'.
+     * This is the same as systemd's strchr(WHITESPACE, ch). */
+    return NM_IN_SET(ch, ' ', '\n', '\t', '\r');
+}
+
+#define NM_ASCII_NEWLINE "\n\r"
+
+static inline gboolean
+nm_ascii_is_newline(char ch)
+{
+    /* This is the same as systemd's (!!strchr(NEWLINE, ch)). */
+    return NM_IN_SET(ch, '\n', '\t');
+}
+
 #define nm_str_skip_leading_spaces(str)                          \
     ({                                                           \
         typeof(*(str))              *_str_sls        = (str);    \
@@ -1177,7 +1195,7 @@ _NM_IN_STRSET_EVAL_op_streq_ascii_case(const char *x1, const char *x)
     return x && g_ascii_strcasecmp(x1, x) == 0;
 }
 
-#define _NM_IN_STRSET_EVAL_OP_STREQ_ASCII_CASE(x, idx) \
+#define _NM_IN_STRSET_EVAL_OP_STREQ_ASCII_CASE(x, idx, op_arg) \
     _NM_IN_STRSET_EVAL_op_streq_ascii_case(_x1, x)
 #define NM_IN_STRSET_ASCII_CASE(x1, ...) \
     _NM_IN_STRSET_EVAL(||, _NM_IN_STRSET_EVAL_OP_STREQ_ASCII_CASE, x1, __VA_ARGS__)
diff --git a/src/libnm-glib-aux/nm-random-utils.c b/src/libnm-glib-aux/nm-random-utils.c
index 3a8ba64f..93eee7c4 100644
--- a/src/libnm-glib-aux/nm-random-utils.c
+++ b/src/libnm-glib-aux/nm-random-utils.c
@@ -10,6 +10,7 @@
 #include <fcntl.h>
 #include <sys/auxv.h>
 #include <sys/syscall.h>
+#include <poll.h>
 
 #if USE_SYS_RANDOM_H
 #include <sys/random.h>
@@ -34,18 +35,121 @@
 #define GRND_INSECURE 0x04
 #endif
 
-#if !HAVE_GETRANDOM && defined(SYS_getrandom)
-static int
+#if !HAVE_GETRANDOM
+static ssize_t
 getrandom(void *buf, size_t buflen, unsigned flags)
 {
+#if defined(SYS_getrandom)
     return syscall(SYS_getrandom, buf, buflen, flags);
+#else
+    errno = ENOSYS;
+    return -1;
+#endif
 }
-#undef HAVE_GETRANDOM
-#define HAVE_GETRANDOM 1
 #endif
 
 /*****************************************************************************/
 
+static ssize_t
+_getrandom(void *buf, size_t buflen, unsigned flags)
+{
+    static int have_getrandom = TRUE;
+    ssize_t    l;
+    int        errsv;
+
+    nm_assert(buflen > 0);
+
+    /* This calls getrandom() and either returns the positive
+     * success or an negative errno. ENOSYS means getrandom()
+     * call is not supported. That result is cached and we don't retry. */
+
+    if (!have_getrandom)
+        return -ENOSYS;
+
+    l = getrandom(buf, buflen, flags);
+    if (l > 0)
+        return l;
+    if (l == 0)
+        return -EIO;
+    errsv = errno;
+    if (errsv == ENOSYS)
+        have_getrandom = FALSE;
+    return -errsv;
+}
+
+static ssize_t
+_getrandom_insecure(void *buf, size_t buflen)
+{
+    static int have_grnd_insecure = TRUE;
+    ssize_t    l;
+
+    /* GRND_INSECURE was added recently. We catch EINVAL
+     * if kernel does not support the flag (and cache it). */
+
+    if (!have_grnd_insecure)
+        return -EINVAL;
+
+    l = _getrandom(buf, buflen, GRND_INSECURE);
+
+    if (l == -EINVAL)
+        have_grnd_insecure = FALSE;
+
+    return l;
+}
+
+static ssize_t
+_getrandom_best_effort(void *buf, size_t buflen)
+{
+    ssize_t l;
+
+    /* To get best-effort bytes, we would use GRND_INSECURE (and we try that
+     * first). However, not all kernel versions support that, so we fallback
+     * to GRND_NONBLOCK.
+     *
+     * Granted, this is called from a fallback path where we have no entropy
+     * already, it's unlikely that GRND_NONBLOCK would succeed. Still... */
+    l = _getrandom_insecure(buf, buflen);
+    if (l != -EINVAL)
+        return l;
+
+    return _getrandom(buf, buflen, GRND_NONBLOCK);
+}
+
+static int
+_random_check_entropy(gboolean block)
+{
+    static gboolean   seen_high_quality = FALSE;
+    nm_auto_close int fd                = -1;
+    int               r;
+
+    /* We come here because getrandom() gave ENOSYS. We will fallback to /dev/urandom,
+     * but the caller wants to know whether we have high quality numbers. Poll
+     * /dev/random to find out. */
+
+    if (seen_high_quality) {
+        /* We cache the positive result. Once kernel has entropy, we will get
+         * good random numbers. */
+        return 1;
+    }
+
+    fd = open("/dev/random", O_RDONLY | O_CLOEXEC | O_NOCTTY);
+    if (fd < 0)
+        return -errno;
+
+    r = nm_utils_fd_wait_for_event(fd, POLLIN, block ? -1 : 0);
+
+    if (r <= 0) {
+        nm_assert(r < 0 || !block);
+        return r;
+    }
+
+    nm_assert(r == 1);
+    seen_high_quality = TRUE;
+    return 1;
+}
+
+/*****************************************************************************/
+
 typedef struct _nm_packed {
     uintptr_t heap_ptr;
     uintptr_t stack_ptr;
@@ -72,7 +176,8 @@ typedef struct _nm_packed {
         guint8  u8[NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2];
         guint32 u32[((NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2) + 3) / 4];
     } rand_vals;
-    GRand *rand;
+    guint8 rand_vals_getrandom[16];
+    gint64 rand_vals_timestamp;
 } BadRandState;
 
 static void
@@ -106,18 +211,7 @@ _bad_random_init_seed(BadRandSeed *seed)
         memcpy(&seed->auxval, p_at_random, 16);
     }
 
-#if HAVE_GETRANDOM
-    {
-        ssize_t r;
-
-        /* This is likely to fail, because we already failed a moment earlier. Still, give
-         * it a try. */
-        r = getrandom(seed->getrandom_buf,
-                      sizeof(seed->getrandom_buf),
-                      GRND_INSECURE | GRND_NONBLOCK);
-        (void) r;
-    }
-#endif
+    _getrandom_best_effort(seed->getrandom_buf, sizeof(seed->getrandom_buf));
 
     seed->now_bootime = nm_utils_clock_gettime_nsec(CLOCK_BOOTTIME);
     seed->now_real    = g_get_real_time();
@@ -168,6 +262,10 @@ _bad_random_bytes(guint8 *buf, gsize n)
             nm_utils_checksum_get_digest(sum, gl_state.sha_digest.full);
         }
 
+        _getrandom_best_effort(gl_state.rand_vals_getrandom, sizeof(gl_state.rand_vals_getrandom));
+
+        gl_state.rand_vals_timestamp = nm_utils_clock_gettime_nsec(CLOCK_BOOTTIME);
+
         while (TRUE) {
             int i;
 
@@ -197,103 +295,155 @@ _bad_random_bytes(guint8 *buf, gsize n)
     }
 }
 
+/*****************************************************************************/
+
 /**
- * nm_utils_random_bytes:
+ * nm_random_get_bytes_full:
  * @p: the buffer to fill
  * @n: the number of bytes to write to @p.
+ * @out_high_quality: (allow-none) (out): whether the returned
+ *   random bytes are of high quality.
  *
- * Uses getrandom() or reads /dev/urandom to fill the buffer
- * with random data. If all fails, as last fallback it uses
- * GRand to fill the buffer with pseudo random numbers.
- * The function always succeeds in writing some random numbers
- * to the buffer. The return value of FALSE indicates that the
- * obtained bytes are probably not of good randomness.
- *
- * Returns: whether the written bytes are good. If you
- * don't require good randomness, you can ignore the return
- * value.
- *
- * Note that if calling getrandom() fails because there is not enough
- * entropy (at early boot), the function will read /dev/urandom.
- * Which of course, still has low entropy, and cause kernel to log
- * a warning.
+ * - will never block
+ * - will always produce some numbers, but they may not
+ *   be of high quality.
+ * - Whether they are of high quality, you can know via @out_high_quality.
+ * - will always try hard to produce high quality numbers, and on success
+ *   they are as good as nm_random_get_crypto_bytes().
  */
-gboolean
-nm_utils_random_bytes(void *p, size_t n)
+void
+nm_random_get_bytes_full(void *p, size_t n, gboolean *out_high_quality)
 {
     int      fd;
     int      r;
-    gboolean has_high_quality = TRUE;
-    guint8  *buf              = p;
+    gboolean has_high_quality;
+    ssize_t  l;
 
-    g_return_val_if_fail(p, FALSE);
-    g_return_val_if_fail(n > 0, FALSE);
+    if (n == 0) {
+        NM_SET_OUT(out_high_quality, TRUE);
+        return;
+    }
 
-#if HAVE_GETRANDOM
-    {
-        static gboolean have_syscall = TRUE;
-
-        if (have_syscall) {
-            ssize_t r2;
-            int     errsv;
-
-            r2 = getrandom(buf, n, GRND_NONBLOCK);
-            if (r2 >= 0) {
-                if ((size_t) r2 == n)
-                    return TRUE;
-
-                /* no or partial read. There is not enough entropy.
-                 * Fill the rest reading with the fallback code and remember
-                 * that some bits are not high quality. */
-                nm_assert((size_t) r2 < n);
-                buf += r2;
-                n -= r2;
-
-                /* At this point, we don't want to read /dev/urandom, because
-                 * the entropy pool is low (early boot?), and asking for more
-                 * entropy causes kernel messages to be logged.
-                 *
-                 * Note that we fall back to _bad_random_bytes(), which (among others) seeds
-                 * itself with g_rand_new(). That also will read /dev/urandom, but as
-                 * we do that only once, we don't care. But in general, we are here in
-                 * a situation where we want to avoid reading /dev/urandom too much. */
-                goto out_bad_random;
-            }
-            errsv = errno;
-            if (errsv == ENOSYS) {
-                /* no support for getrandom(). We don't know whether
-                 * we /dev/urandom will give us good quality. Assume yes. */
-                have_syscall = FALSE;
-            } else if (errsv == EAGAIN) {
-                /* No entropy. We avoid reading /dev/urandom. */
-                goto out_bad_random;
-            } else {
-                /* Unknown error, likely no entropy. We'll read /dev/urandom below, but we don't
-                 * have high-quality randomness. */
-                has_high_quality = FALSE;
+    g_return_if_fail(p);
+
+again_getrandom:
+    l = _getrandom(p, n, GRND_NONBLOCK);
+    if (l > 0) {
+        if ((size_t) l == n) {
+            NM_SET_OUT(out_high_quality, TRUE);
+            return;
+        }
+        p = ((uint8_t *) p) + l;
+        n -= l;
+        goto again_getrandom;
+    }
+
+    /* getrandom() failed. Fallback to read /dev/urandom. */
+
+    if (l == -ENOSYS) {
+        /* no support for getrandom(). */
+        if (out_high_quality) {
+            /* The caller wants to know whether we have high quality. Poll /dev/random
+             * to find out. */
+            has_high_quality = (_random_check_entropy(FALSE) > 0);
+        } else {
+            /* The value doesn't matter in this case. It will be unused. */
+            has_high_quality = FALSE;
+        }
+    } else {
+        /* Any other failure of getrandom() means we don't have high quality. */
+        has_high_quality = FALSE;
+        if (l == -EAGAIN) {
+            /* getrandom(GRND_NONBLOCK) failed because lack of entropy. Retry with GRND_INSECURE. */
+            for (;;) {
+                l = _getrandom_insecure(p, n);
+                if (l > 0) {
+                    if ((size_t) l == n) {
+                        NM_SET_OUT(out_high_quality, FALSE);
+                        return;
+                    }
+                    p = ((uint8_t *) p) + l;
+                    n -= l;
+                    continue;
+                }
+                /* Any error. Fallback to /dev/urandom. */
+                break;
             }
         }
     }
-#endif
 
-fd_open:
+again_open:
     fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC | O_NOCTTY);
     if (fd < 0) {
         if (errno == EINTR)
-            goto fd_open;
-        goto out_bad_random;
+            goto again_open;
+    } else {
+        r = nm_utils_fd_read_loop_exact(fd, p, n, TRUE);
+        nm_close(fd);
+        if (r >= 0) {
+            NM_SET_OUT(out_high_quality, has_high_quality);
+            return;
+        }
     }
-    r = nm_utils_fd_read_loop_exact(fd, buf, n, TRUE);
-    nm_close(fd);
-    if (r >= 0)
-        return has_high_quality;
 
-out_bad_random:
     /* we failed to fill the bytes reading from /dev/urandom.
-     * Fill the bits using our pseudo random numbers.
-     *
-     * We don't have good quality.
+     * Fill the bits using our fallback approach (which obviously
+     * cannot give high quality random).
      */
-    _bad_random_bytes(buf, n);
-    return FALSE;
+    _bad_random_bytes(p, n);
+    NM_SET_OUT(out_high_quality, FALSE);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_random_get_crypto_bytes:
+ * @p: the buffer to fill
+ * @n: the number of bytes to fill
+ *
+ * - can fail (in which case a negative number is returned
+ *   and the output buffer is undefined).
+ * - will block trying to get high quality random numbers.
+ */
+int
+nm_random_get_crypto_bytes(void *p, size_t n)
+{
+    nm_auto_close int fd = -1;
+    ssize_t           l;
+    int               r;
+
+    if (n == 0)
+        return 0;
+
+    nm_assert(p);
+
+again_getrandom:
+    l = _getrandom(p, n, 0);
+    if (l > 0) {
+        if ((size_t) l == n)
+            return 0;
+        p = (uint8_t *) p + l;
+        n -= l;
+        goto again_getrandom;
+    }
+
+    if (l != -ENOSYS) {
+        /* We got a failure, but getrandom seems to be working in principle. We
+         * won't get good numbers. Fail. */
+        return l;
+    }
+
+    /* getrandom() failed with ENOSYS. Fallback to reading /dev/urandom. */
+
+    r = _random_check_entropy(TRUE);
+    if (r < 0)
+        return r;
+    if (r == 0)
+        return nm_assert_unreachable_val(-EIO);
+
+    fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC | O_NOCTTY);
+    if (fd < 0)
+        return -errno;
+
+    return nm_utils_fd_read_loop_exact(fd, p, n, FALSE);
 }
diff --git a/src/libnm-glib-aux/nm-random-utils.h b/src/libnm-glib-aux/nm-random-utils.h
index d0eae103..ab8aee1b 100644
--- a/src/libnm-glib-aux/nm-random-utils.h
+++ b/src/libnm-glib-aux/nm-random-utils.h
@@ -6,6 +6,14 @@
 #ifndef __NM_RANDOM_UTILS_H__
 #define __NM_RANDOM_UTILS_H__
 
-gboolean nm_utils_random_bytes(void *p, size_t n);
+void nm_random_get_bytes_full(void *p, size_t n, gboolean *out_high_quality);
+
+static inline void
+nm_random_get_bytes(void *p, size_t n)
+{
+    nm_random_get_bytes_full(p, n, NULL);
+}
+
+int nm_random_get_crypto_bytes(void *p, size_t n);
 
 #endif /* __NM_RANDOM_UTILS_H__ */
diff --git a/src/libnm-glib-aux/nm-ref-string.h b/src/libnm-glib-aux/nm-ref-string.h
index 3363bce0..eb3c38de 100644
--- a/src/libnm-glib-aux/nm-ref-string.h
+++ b/src/libnm-glib-aux/nm-ref-string.h
@@ -119,7 +119,7 @@ nm_ref_string_cmp(NMRefString *a, NMRefString *b)
     NM_CMP_SELF(a, b);
 
     /* It would be cheaper to first compare by length. But this
-     * way we get a nicer, ASCIIbethical sort order. */
+     * way we get a nicer, ASCIIbetical sort order. */
     NM_CMP_DIRECT_MEMCMP(a->str, b->str, NM_MIN(a->len, b->len));
     NM_CMP_DIRECT(a->len, b->len);
     return nm_assert_unreachable_val(0);
diff --git a/src/libnm-glib-aux/nm-shared-utils.c b/src/libnm-glib-aux/nm-shared-utils.c
index e6ee04d7..624f9a3e 100644
--- a/src/libnm-glib-aux/nm-shared-utils.c
+++ b/src/libnm-glib-aux/nm-shared-utils.c
@@ -44,6 +44,15 @@ G_STATIC_ASSERT(_nm_alignof(struct in_addr) <= _nm_alignof(NMIPAddr));
 G_STATIC_ASSERT(_nm_alignof(struct in6_addr) <= _nm_alignof(NMIPAddr));
 G_STATIC_ASSERT(_nm_alignof(NMEtherAddr) <= _nm_alignof(NMIPAddr));
 
+int
+nm_ip_addr_cmp_for_sort(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+    /* This is a compare function that can be used for sorting IP addresses.
+     * Essentially, it calls memcmp(). @user_data must be GINT_TO_POINTER(addr_family).
+     * @a and @b must be either pointers to in_addr_t, struct in6_addr or NMIPAddr. */
+    return nm_ip_addr_cmp(GPOINTER_TO_INT(user_data), a, b);
+}
+
 /* this initializes a struct in_addr/in6_addr and allows for untrusted
  * arguments (like unsuitable @addr_family or @src_len). It's almost safe
  * in the sense that it verifies input arguments strictly. Also, it
@@ -137,6 +146,18 @@ G_STATIC_ASSERT(ETH_ALEN == sizeof(NMEtherAddr));
 
 G_STATIC_ASSERT(_nm_alignof(struct ether_addr) <= _nm_alignof(NMEtherAddr));
 
+NMEtherAddr *
+nm_ether_addr_from_string(NMEtherAddr *addr, const char *str)
+{
+    nm_assert(addr);
+
+    if (!str || !_nm_utils_hwaddr_aton_exact(str, addr, ETH_ALEN)) {
+        *addr = NM_ETHER_ADDR_INIT(0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
+        return NULL;
+    }
+
+    return addr;
+}
 /*****************************************************************************/
 
 /**
@@ -994,12 +1015,22 @@ nm_utils_ip_is_site_local(int addr_family, const void *address)
         return (addr4 & 0xff000000) == 0x0a000000 || (addr4 & 0xfff00000) == 0xac100000
                || (addr4 & 0xffff0000) == 0xc0a80000;
     case AF_INET6:
+        /* IN6_IS_ADDR_SITELOCAL() is for deprecated fec0::/10 addresses (see rfc3879, 4.).
+         * Note that for unique local IPv6 addresses (ULA, fc00::/7) this returns false,
+         * which may or may not be a bug. */
         return IN6_IS_ADDR_SITELOCAL(address);
     default:
         g_return_val_if_reached(FALSE);
     }
 }
 
+gboolean
+nm_utils_ip6_is_ula(const struct in6_addr *address)
+{
+    /* Unique local IPv6 address (ULA) fc00::/7 */
+    return (address->s6_addr32[0] & htonl(0xfe000000u)) == htonl(0xfc000000u);
+}
+
 /*****************************************************************************/
 
 static gboolean
@@ -2288,9 +2319,11 @@ nm_utils_escaped_tokens_options_split(char *str, const char **out_key, const cha
 char **
 nm_utils_strsplit_quoted(const char *str)
 {
-    gs_unref_ptrarray GPtrArray *arr     = NULL;
-    gs_free char                *str_out = NULL;
-    CharLookupTable              ch_lookup;
+    char          **arr       = NULL;
+    gsize           arr_len   = 0;
+    gsize           arr_alloc = 0;
+    gs_free char   *str_out   = NULL;
+    CharLookupTable ch_lookup;
 
     nm_assert(str);
 
@@ -2347,19 +2380,32 @@ nm_utils_strsplit_quoted(const char *str)
             str++;
         }
 
-        if (!arr)
-            arr = g_ptr_array_new();
-        g_ptr_array_add(arr, g_strndup(str_out, j));
+        if (arr_len >= arr_alloc) {
+            if (arr_alloc == 0)
+                arr_alloc = 4;
+            else
+                arr_alloc *= 2;
+            arr = g_realloc(arr, sizeof(char *) * arr_alloc);
+        }
+
+        arr[arr_len++] = g_strndup(str_out, j);
     }
 
     if (!arr)
         return g_new0(char *, 1);
 
-    g_ptr_array_add(arr, NULL);
-
     /* We want to return an optimally sized strv array, with no excess
      * memory allocated. Hence, clone once more. */
-    return nm_memdup(arr->pdata, sizeof(char *) * arr->len);
+
+    if (arr_len + 1u != arr_alloc) {
+        gs_free char **arr_old = arr;
+
+        arr = g_new(char *, arr_len + 1u);
+        memcpy(arr, arr_old, sizeof(char *) * arr_len);
+    }
+
+    arr[arr_len] = NULL;
+    return arr;
 }
 
 /*****************************************************************************/
@@ -3008,7 +3054,7 @@ nm_utils_buf_utf8safe_unescape(const char             *str,
         return str;
     }
 
-    nm_str_buf_init(&strbuf, len + 1u, FALSE);
+    strbuf = NM_STR_BUF_INIT(len + 1u, FALSE);
 
     nm_str_buf_append_len(&strbuf, str, s - str);
     str = s;
@@ -3175,7 +3221,7 @@ nm_utils_buf_utf8safe_escape(gconstpointer           buf,
             return str;
     }
 
-    nm_str_buf_init(&strbuf, buflen + 5, NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_SECRET));
+    strbuf = NM_STR_BUF_INIT(buflen + 5, NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_SECRET));
 
     s = str;
     do {
@@ -3347,6 +3393,8 @@ nm_utils_fd_wait_for_event(int fd, int event, gint64 timeout_nsec)
     struct timespec ts, *pts;
     int             r;
 
+    nm_assert(fd >= 0);
+
     if (timeout_nsec < 0)
         pts = NULL;
     else {
@@ -3360,6 +3408,13 @@ nm_utils_fd_wait_for_event(int fd, int event, gint64 timeout_nsec)
         return -NM_ERRNO_NATIVE(errno);
     if (r == 0)
         return 0;
+
+    nm_assert(r == 1);
+    nm_assert(pollfd.revents > 0);
+
+    if (pollfd.revents & POLLNVAL)
+        return nm_assert_unreachable_val(-EBADF);
+
     return pollfd.revents;
 }
 
@@ -3442,51 +3497,6 @@ nm_utils_named_value_clear_with_g_free(NMUtilsNamedValue *val)
 
 G_STATIC_ASSERT(G_STRUCT_OFFSET(NMUtilsNamedValue, name) == 0);
 
-NMUtilsNamedValue *
-nm_utils_named_values_from_strdict_full(GHashTable         *hash,
-                                        guint              *out_len,
-                                        GCompareDataFunc    compare_func,
-                                        gpointer            user_data,
-                                        NMUtilsNamedValue  *provided_buffer,
-                                        guint               provided_buffer_len,
-                                        NMUtilsNamedValue **out_allocated_buffer)
-{
-    GHashTableIter     iter;
-    NMUtilsNamedValue *values;
-    guint              i, len;
-
-    nm_assert(provided_buffer_len == 0 || provided_buffer);
-    nm_assert(!out_allocated_buffer || !*out_allocated_buffer);
-
-    if (!hash || !(len = g_hash_table_size(hash))) {
-        NM_SET_OUT(out_len, 0);
-        return NULL;
-    }
-
-    if (provided_buffer_len >= len + 1) {
-        /* the buffer provided by the caller is large enough. Use it. */
-        values = provided_buffer;
-    } else {
-        /* allocate a new buffer. */
-        values = g_new(NMUtilsNamedValue, len + 1);
-        NM_SET_OUT(out_allocated_buffer, values);
-    }
-
-    i = 0;
-    g_hash_table_iter_init(&iter, hash);
-    while (g_hash_table_iter_next(&iter, (gpointer *) &values[i].name, &values[i].value_ptr))
-        i++;
-    nm_assert(i == len);
-    values[i].name      = NULL;
-    values[i].value_ptr = NULL;
-
-    if (compare_func)
-        nm_utils_named_value_list_sort(values, len, compare_func, user_data);
-
-    NM_SET_OUT(out_len, len);
-    return values;
-}
-
 gssize
 nm_utils_named_value_list_find(const NMUtilsNamedValue *arr,
                                gsize                    len,
@@ -3636,6 +3646,52 @@ nm_utils_hash_values_to_array(GHashTable      *hash,
     return arr;
 }
 
+NMUtilsNamedValue *
+nm_utils_hash_to_array_full(GHashTable         *hash,
+                            guint              *out_len,
+                            GCompareDataFunc    compare_func,
+                            gpointer            user_data,
+                            NMUtilsNamedValue  *provided_buffer,
+                            guint               provided_buffer_len,
+                            NMUtilsNamedValue **out_allocated_buffer)
+{
+    GHashTableIter     iter;
+    NMUtilsNamedValue *values;
+    guint              len;
+    guint              i;
+
+    nm_assert(provided_buffer_len == 0 || provided_buffer);
+    nm_assert(!out_allocated_buffer || !*out_allocated_buffer);
+
+    if (!hash || ((len = g_hash_table_size(hash)) == 0)) {
+        NM_SET_OUT(out_len, 0);
+        return NULL;
+    }
+
+    if (provided_buffer_len >= len + 1) {
+        /* the buffer provided by the caller is large enough. Use it. */
+        values = provided_buffer;
+    } else {
+        /* allocate a new buffer. */
+        values = g_new(NMUtilsNamedValue, len + 1);
+        NM_SET_OUT(out_allocated_buffer, values);
+    }
+
+    i = 0;
+    g_hash_table_iter_init(&iter, hash);
+    while (g_hash_table_iter_next(&iter, &values[i].name_ptr, &values[i].value_ptr))
+        i++;
+    nm_assert(i == len);
+    values[i].name_ptr  = NULL;
+    values[i].value_ptr = NULL;
+
+    if (compare_func && len > 1)
+        g_qsort_with_data(values, len, sizeof(NMUtilsNamedValue), compare_func, user_data);
+
+    NM_SET_OUT(out_len, len);
+    return values;
+}
+
 /*****************************************************************************/
 
 /**
@@ -5850,17 +5906,29 @@ _nm_str_buf_ensure_size(NMStrBuf *strbuf, gsize new_size, gboolean reserve_exact
         new_size = nm_utils_get_next_realloc_size(!strbuf->_priv_do_bzero_mem, new_size);
     }
 
-    strbuf->_priv_str       = nm_secret_mem_realloc(strbuf->_priv_str,
-                                              strbuf->_priv_do_bzero_mem,
-                                              strbuf->_priv_allocated,
-                                              new_size);
+    if (strbuf->_priv_malloced) {
+        strbuf->_priv_str = nm_secret_mem_realloc(strbuf->_priv_str,
+                                                  strbuf->_priv_do_bzero_mem,
+                                                  strbuf->_priv_allocated,
+                                                  new_size);
+    } else {
+        char *old = strbuf->_priv_str;
+
+        strbuf->_priv_str = g_malloc(new_size);
+        if (strbuf->_priv_len > 0) {
+            memcpy(strbuf->_priv_str, old, strbuf->_priv_len);
+            if (strbuf->_priv_do_bzero_mem)
+                nm_explicit_bzero(old, strbuf->_priv_len);
+        }
+        strbuf->_priv_malloced = TRUE;
+    }
     strbuf->_priv_allocated = new_size;
 }
 
 void
-nm_str_buf_append_printf(NMStrBuf *strbuf, const char *format, ...)
+nm_str_buf_append_printfv(NMStrBuf *strbuf, const char *format, va_list args)
 {
-    va_list args;
+    va_list args_copy;
     gsize   available;
     int     l;
 
@@ -5870,12 +5938,12 @@ nm_str_buf_append_printf(NMStrBuf *strbuf, const char *format, ...)
 
     nm_assert(available < G_MAXULONG);
 
-    va_start(args, format);
+    va_copy(args_copy, args);
     l = g_vsnprintf(strbuf->_priv_allocated > 0 ? &strbuf->_priv_str[strbuf->_priv_len] : NULL,
                     available,
                     format,
-                    args);
-    va_end(args);
+                    args_copy);
+    va_end(args_copy);
 
     nm_assert(l >= 0);
     nm_assert(l < G_MAXINT);
@@ -5890,9 +5958,9 @@ nm_str_buf_append_printf(NMStrBuf *strbuf, const char *format, ...)
 
         nm_str_buf_maybe_expand(strbuf, l2, FALSE);
 
-        va_start(args, format);
-        l = g_vsnprintf(&strbuf->_priv_str[strbuf->_priv_len], l2, format, args);
-        va_end(args);
+        va_copy(args_copy, args);
+        l = g_vsnprintf(&strbuf->_priv_str[strbuf->_priv_len], l2, format, args_copy);
+        va_end(args_copy);
 
         nm_assert(l >= 0);
         nm_assert((gsize) l == l2 - 1u);
@@ -6594,7 +6662,7 @@ nm_utils_validate_hostname(const char *hostname)
     if (dot)
         return FALSE;
 
-    return (p - hostname <= HOST_NAME_MAX);
+    return (p - hostname <= NM_HOST_NAME_MAX);
 }
 
 /*****************************************************************************/
@@ -6723,3 +6791,540 @@ nm_g_main_context_can_acquire(GMainContext *context)
     g_main_context_release(context);
     return TRUE;
 }
+
+/*****************************************************************************/
+
+int
+nm_unbase64char(char c)
+{
+    /* copied from systemd's unbase64char():
+     * https://github.com/systemd/systemd/blob/688efe7703328c5a0251fafac55757b8864a9f9a/src/basic/hexdecoct.c#L539 */
+
+    switch (c) {
+    case 'A' ... 'Z':
+        return c - 'A';
+    case 'a' ... 'z':
+        return (c - 'a') + ('Z' - 'A' + 1);
+    case '0' ... '9':
+        return (c - '0') + (('Z' - 'A' + 1) + ('z' - 'a' + 1));
+    case '+':
+        return ('Z' - 'A' + 1) + ('z' - 'a' + 1) + ('9' - '0' + 1);
+    case '/':
+        return ('Z' - 'A' + 1) + ('z' - 'a' + 1) + ('9' - '0' + 1) + 1;
+    case '=':
+        /* The padding is a different kind of base64 character. Return
+         * a special error code for it. */
+        return -ERANGE;
+    default:
+        return -EINVAL;
+    }
+}
+
+static int
+unbase64_next(const char **p, size_t *l)
+{
+    int ret;
+
+    nm_assert(p);
+    nm_assert(l);
+
+    /* copied from systemd's unbase64_next():
+     * https://github.com/systemd/systemd/blob/688efe7703328c5a0251fafac55757b8864a9f9a/src/basic/hexdecoct.c#L709 */
+
+    /* Find the next non-whitespace character, and decode it. If we find padding, we return it as INT_MAX. We
+     * greedily skip all preceding and all following whitespace. */
+
+    for (;;) {
+        if (*l == 0)
+            return -EPIPE;
+
+        if (!nm_ascii_is_whitespace(**p))
+            break;
+
+        /* Skip leading whitespace */
+        (*p)++;
+        (*l)--;
+    }
+
+    ret = nm_unbase64char(**p);
+    if (ret < 0) {
+        nm_assert(NM_IN_SET(ret, -EINVAL, -ERANGE));
+        if (ret != -ERANGE)
+            return ret;
+    }
+
+    for (;;) {
+        (*p)++;
+        (*l)--;
+
+        if (*l == 0)
+            break;
+        if (!nm_ascii_is_whitespace(**p))
+            break;
+
+        /* Skip following whitespace */
+    }
+
+    nm_assert(ret == -ERANGE || ret >= 0);
+    return ret;
+}
+
+/**
+ * nm_unbase64mem_full:
+ * @p: a valid base64 string. Whitespace is ignored, but invalid encodings
+ *   will cause the function to fail.
+ * @l: the length of @p. @p is not treated as NUL terminated string but
+ *   merely as a buffer of ascii characters.
+ * @secure: whether the temporary memory will be cleared to avoid leaving
+ *   secrets in memory (see also nm_explicit_bzero()).
+ * @mem: (transfer full): the decoded buffer on success.
+ * @len: the length of @mem on success.
+ *
+ * glib provides g_base64_decode(), but that does not report any errors
+ * from invalid encodings. Our own implementation (based on systemd code)
+ * rejects invalid inputs.
+ *
+ * Returns: a non-negative code on success. Invalid encoding let the
+ *   function fail.
+ */
+int
+nm_unbase64mem_full(const char *p, gsize l, gboolean secure, guint8 **ret, gsize *ret_size)
+{
+    gs_free uint8_t *buf = NULL;
+    const char      *x;
+    guint8          *z;
+    gsize            len;
+    int              r;
+
+    /* copied from systemd's unbase64mem_full():
+     * https://github.com/systemd/systemd/blob/688efe7703328c5a0251fafac55757b8864a9f9a/src/basic/hexdecoct.c#L751 */
+
+    nm_assert(p || l == 0);
+
+    if (l == G_MAXSIZE)
+        l = strlen(p);
+
+    /* A group of four input bytes needs three output bytes, in case of padding we need to add two or three extra
+     * bytes. Note that this calculation is an upper boundary, as we ignore whitespace while decoding */
+    len = (l / 4) * 3 + (l % 4 != 0 ? (l % 4) - 1 : 0);
+
+    buf = g_malloc(len + 1);
+
+    for (x = p, z = buf;;) {
+        int a; /* a == 00XXXXXX */
+        int b; /* b == 00YYYYYY */
+        int c; /* c == 00ZZZZZZ */
+        int d; /* d == 00WWWWWW */
+
+        a = unbase64_next(&x, &l);
+        if (a < 0) {
+            if (a == -EPIPE) /* End of string */
+                break;
+            if (a == -ERANGE) { /* Padding is not allowed at the beginning of a 4ch block */
+                r = -EINVAL;
+                goto on_failure;
+            }
+            r = a;
+            goto on_failure;
+        }
+
+        b = unbase64_next(&x, &l);
+        if (b < 0) {
+            if (b == -ERANGE) {
+                /* Padding is not allowed at the second character of a 4ch block either */
+                r = -EINVAL;
+                goto on_failure;
+            }
+            r = b;
+            goto on_failure;
+        }
+
+        c = unbase64_next(&x, &l);
+        if (c < 0) {
+            if (c != -ERANGE) {
+                r = c;
+                goto on_failure;
+            }
+        }
+
+        d = unbase64_next(&x, &l);
+        if (d < 0) {
+            if (d != -ERANGE) {
+                r = d;
+                goto on_failure;
+            }
+        }
+
+        if (c == -ERANGE) { /* Padding at the third character */
+
+            if (d != -ERANGE) { /* If the third character is padding, the fourth must be too */
+                r = -EINVAL;
+                goto on_failure;
+            }
+
+            /* b == 00YY0000 */
+            if (b & 15) {
+                r = -EINVAL;
+                goto on_failure;
+            }
+
+            if (l > 0) { /* Trailing rubbish? */
+                r = -ENAMETOOLONG;
+                goto on_failure;
+            }
+
+            *(z++) = (uint8_t) a << 2 | (uint8_t) (b >> 4); /* XXXXXXYY */
+            break;
+        }
+
+        if (d == -ERANGE) {
+            /* c == 00ZZZZ00 */
+            if (c & 3) {
+                r = -EINVAL;
+                goto on_failure;
+            }
+
+            if (l > 0) { /* Trailing rubbish? */
+                r = -ENAMETOOLONG;
+                goto on_failure;
+            }
+
+            *(z++) = (uint8_t) a << 2 | (uint8_t) b >> 4; /* XXXXXXYY */
+            *(z++) = (uint8_t) b << 4 | (uint8_t) c >> 2; /* YYYYZZZZ */
+            break;
+        }
+
+        *(z++) = (uint8_t) a << 2 | (uint8_t) b >> 4; /* XXXXXXYY */
+        *(z++) = (uint8_t) b << 4 | (uint8_t) c >> 2; /* YYYYZZZZ */
+        *(z++) = (uint8_t) c << 6 | (uint8_t) d;      /* ZZWWWWWW */
+    }
+
+    *z = '\0';
+
+    NM_SET_OUT(ret_size, (gsize) (z - buf));
+    NM_SET_OUT(ret, g_steal_pointer(&buf));
+    return 0;
+
+on_failure:
+    if (secure)
+        nm_explicit_bzero(buf, len);
+    return r;
+}
+
+/*****************************************************************************/
+
+static const char *
+skip_slash_or_dot(const char *p)
+{
+    for (; !nm_str_is_empty(p);) {
+        if (p[0] == '/') {
+            p += 1;
+            continue;
+        }
+        if (p[0] == '.' && p[1] == '/') {
+            p += 2;
+            continue;
+        }
+        break;
+    }
+    return p;
+}
+
+int
+nm_path_find_first_component(const char **p, gboolean accept_dot_dot, const char **ret)
+{
+    const char *q, *first, *end_first, *next;
+    size_t      len;
+
+    /* Copied from systemd's path_compare()
+     * https://github.com/systemd/systemd/blob/bc85f8b51d962597360e982811e674c126850f56/src/basic/path-util.c#L809 */
+
+    nm_assert(p);
+
+    /* When a path is input, then returns the pointer to the first component and its length, and
+     * move the input pointer to the next component or nul. This skips both over any '/'
+     * immediately *before* and *after* the first component before returning.
+     *
+     * Examples
+     *   Input:  p: "//.//aaa///bbbbb/cc"
+     *   Output: p: "bbbbb///cc"
+     *           ret: "aaa///bbbbb/cc"
+     *           return value: 3 (== strlen("aaa"))
+     *
+     *   Input:  p: "aaa//"
+     *   Output: p: (pointer to NUL)
+     *           ret: "aaa//"
+     *           return value: 3 (== strlen("aaa"))
+     *
+     *   Input:  p: "/", ".", ""
+     *   Output: p: (pointer to NUL)
+     *           ret: NULL
+     *           return value: 0
+     *
+     *   Input:  p: NULL
+     *   Output: p: NULL
+     *           ret: NULL
+     *           return value: 0
+     *
+     *   Input:  p: "(too long component)"
+     *   Output: return value: -EINVAL
+     *
+     *   (when accept_dot_dot is false)
+     *   Input:  p: "//..//aaa///bbbbb/cc"
+     *   Output: return value: -EINVAL
+     */
+
+    q = *p;
+
+    first = skip_slash_or_dot(q);
+    if (nm_str_is_empty(first)) {
+        *p = first;
+        if (ret)
+            *ret = NULL;
+        return 0;
+    }
+    if (nm_streq(first, ".")) {
+        *p = first + 1;
+        if (ret)
+            *ret = NULL;
+        return 0;
+    }
+
+    end_first = strchrnul(first, '/');
+    len       = end_first - first;
+
+    if (len > NAME_MAX)
+        return -EINVAL;
+    if (!accept_dot_dot && len == 2 && first[0] == '.' && first[1] == '.')
+        return -EINVAL;
+
+    next = skip_slash_or_dot(end_first);
+
+    *p = next + (nm_streq(next, ".") ? 1 : 0);
+    if (ret)
+        *ret = first;
+    return len;
+}
+
+int
+nm_path_compare(const char *a, const char *b)
+{
+    /* Copied from systemd's path_compare()
+     * https://github.com/systemd/systemd/blob/bc85f8b51d962597360e982811e674c126850f56/src/basic/path-util.c#L415 */
+
+    /* Order NULL before non-NULL */
+    NM_CMP_SELF(a, b);
+
+    /* A relative path and an absolute path must not compare as equal.
+     * Which one is sorted before the other does not really matter.
+     * Here a relative path is ordered before an absolute path. */
+    NM_CMP_DIRECT(nm_path_is_absolute(a), nm_path_is_absolute(b));
+
+    for (;;) {
+        const char *aa, *bb;
+        int         j, k;
+
+        j = nm_path_find_first_component(&a, TRUE, &aa);
+        k = nm_path_find_first_component(&b, TRUE, &bb);
+
+        if (j < 0 || k < 0) {
+            /* When one of paths is invalid, order invalid path after valid one. */
+            NM_CMP_DIRECT(j < 0, k < 0);
+
+            /* fallback to use strcmp() if both paths are invalid. */
+            NM_CMP_DIRECT_STRCMP(a, b);
+            return 0;
+        }
+
+        /* Order prefixes first: "/foo" before "/foo/bar" */
+        if (j == 0) {
+            if (k == 0)
+                return 0;
+            return -1;
+        }
+        if (k == 0)
+            return 1;
+
+        /* Alphabetical sort: "/foo/aaa" before "/foo/b" */
+        NM_CMP_DIRECT_MEMCMP(aa, bb, NM_MIN(j, k));
+
+        /* Sort "/foo/a" before "/foo/aaa" */
+        NM_CMP_DIRECT(j, k);
+    }
+}
+
+char *
+nm_path_startswith_full(const char *path, const char *prefix, gboolean accept_dot_dot)
+{
+    /* Copied from systemd's path_startswith_full()
+     * https://github.com/systemd/systemd/blob/bc85f8b51d962597360e982811e674c126850f56/src/basic/path-util.c#L375 */
+
+    nm_assert(path);
+    nm_assert(prefix);
+
+    /* Returns a pointer to the start of the first component after the parts matched by
+     * the prefix, iff
+     * - both paths are absolute or both paths are relative,
+     * and
+     * - each component in prefix in turn matches a component in path at the same position.
+     * An empty string will be returned when the prefix and path are equivalent.
+     *
+     * Returns NULL otherwise.
+     */
+
+    if ((path[0] == '/') != (prefix[0] == '/'))
+        return NULL;
+
+    for (;;) {
+        const char *p, *q;
+        int         r, k;
+
+        r = nm_path_find_first_component(&path, accept_dot_dot, &p);
+        if (r < 0)
+            return NULL;
+
+        k = nm_path_find_first_component(&prefix, accept_dot_dot, &q);
+        if (k < 0)
+            return NULL;
+
+        if (k == 0)
+            return (char *) (p ?: path);
+
+        if (r != k)
+            return NULL;
+
+        if (strncmp(p, q, r) != 0)
+            return NULL;
+    }
+}
+
+char *
+nm_path_simplify(char *path)
+{
+    bool  add_slash = false;
+    char *f         = path;
+    int   r;
+
+    /* Copied from systemd's path_simplify()
+     * https://github.com/systemd/systemd/blob/bc85f8b51d962597360e982811e674c126850f56/src/basic/path-util.c#L325 */
+
+    nm_assert(path);
+
+    /* Removes redundant inner and trailing slashes. Also removes unnecessary dots.
+     * Modifies the passed string in-place.
+     *
+     * ///foo//./bar/.   becomes /foo/bar
+     * .//./foo//./bar/. becomes foo/bar
+     */
+
+    if (path[0] == '\0')
+        return path;
+
+    if (nm_path_is_absolute(path))
+        f++;
+
+    for (const char *p = f;;) {
+        const char *e;
+
+        r = nm_path_find_first_component(&p, TRUE, &e);
+        if (r == 0)
+            break;
+
+        if (add_slash)
+            *f++ = '/';
+
+        if (r < 0) {
+            /* if path is invalid, then refuse to simplify remaining part. */
+            memmove(f, p, strlen(p) + 1);
+            return path;
+        }
+
+        memmove(f, e, r);
+        f += r;
+
+        add_slash = TRUE;
+    }
+
+    /* Special rule, if we stripped everything, we need a "." for the current directory. */
+    if (f == path)
+        *f++ = '.';
+
+    *f = '\0';
+    return path;
+}
+
+/*****************************************************************************/
+
+static gboolean
+valid_ldh_char(char c)
+{
+    /* "LDH" → "Letters, digits, hyphens", as per RFC 5890, Section 2.3.1 */
+
+    return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-';
+}
+
+/**
+ * nm_hostname_is_valid:
+ * @s: the hostname to check.
+ * @trailing_dot: Accept trailing dot on multi-label names.
+ *
+ * Return: %TRUE if valid.
+ */
+gboolean
+nm_hostname_is_valid(const char *s, gboolean trailing_dot)
+{
+    unsigned    n_dots = 0;
+    const char *p;
+    gboolean    dot;
+    gboolean    hyphen;
+
+    /* Copied from systemd's hostname_is_valid()
+     * https://github.com/systemd/systemd/blob/bc85f8b51d962597360e982811e674c126850f56/src/basic/hostname-util.c#L85 */
+
+    /* Check if s looks like a valid hostname or FQDN. This does not do full DNS validation, but only
+     * checks if the name is composed of allowed characters and the length is not above the maximum
+     * allowed by Linux (c.f. dns_name_is_valid()). A trailing dot is allowed if
+     * VALID_HOSTNAME_TRAILING_DOT flag is set and at least two components are present in the name. Note
+     * that due to the restricted charset and length this call is substantially more conservative than
+     * dns_name_is_valid(). Doesn't accept empty hostnames, hostnames with leading dots, and hostnames
+     * with multiple dots in a sequence. Doesn't allow hyphens at the beginning or end of label. */
+
+    if (nm_str_is_empty(s))
+        return FALSE;
+
+    for (p = s, dot = hyphen = TRUE; *p; p++)
+        if (*p == '.') {
+            if (dot || hyphen)
+                return FALSE;
+
+            dot    = TRUE;
+            hyphen = FALSE;
+            n_dots++;
+
+        } else if (*p == '-') {
+            if (dot)
+                return FALSE;
+
+            dot    = FALSE;
+            hyphen = TRUE;
+
+        } else {
+            if (!valid_ldh_char(*p))
+                return FALSE;
+
+            dot    = FALSE;
+            hyphen = FALSE;
+        }
+
+    if (dot && (n_dots < 2 || !trailing_dot))
+        return FALSE;
+    if (hyphen)
+        return FALSE;
+
+    /* Note that HOST_NAME_MAX is 64 on Linux, but DNS allows domain names up to
+     * 255 characters */
+    if (p - s > NM_HOST_NAME_MAX)
+        return FALSE;
+
+    return TRUE;
+}
diff --git a/src/libnm-glib-aux/nm-shared-utils.h b/src/libnm-glib-aux/nm-shared-utils.h
index 52cf1d4c..3d8eaced 100644
--- a/src/libnm-glib-aux/nm-shared-utils.h
+++ b/src/libnm-glib-aux/nm-shared-utils.h
@@ -243,11 +243,16 @@ extern const NMIPAddr nm_ip_addr_zero;
 static inline int
 nm_ip_addr_cmp(int addr_family, gconstpointer a, gconstpointer b)
 {
+    /* Note that @a and @b are not required to be full NMIPAddr unions.
+     * Depending on @addr_family, they can also be only in_addr_t or
+     * struct in6_addr. */
     NM_CMP_SELF(a, b);
     NM_CMP_DIRECT_MEMCMP(a, b, nm_utils_addr_family_to_size(addr_family));
     return 0;
 }
 
+int nm_ip_addr_cmp_for_sort(gconstpointer a, gconstpointer b, gpointer user_data);
+
 static inline gboolean
 nm_ip_addr_equal(int addr_family, gconstpointer a, gconstpointer b)
 {
@@ -277,8 +282,15 @@ nm_ip_addr_set(int addr_family, gpointer dst, gconstpointer src)
     nm_assert(dst);
     nm_assert(src);
 
-    /* this MUST use memcpy() (or similar means) to support unaligned src/dst pointers. */
+    /* this MUST use memcpy() to support unaligned src/dst pointers. */
     memcpy(dst, src, nm_utils_addr_family_to_size(addr_family));
+
+    /* Note that @dst is not necessarily a NMIPAddr, it could also be just
+     * an in_addr_t/struct in6_addr. We thus can only set the bytes that
+     * we know are present based on the address family.
+     *
+     * Using this function to initialize an NMIPAddr union (for IPv4) leaves
+     * uninitalized bytes. Avoid that by using nm_ip_addr_init() instead. */
 }
 
 static inline NMIPAddr
@@ -291,6 +303,8 @@ nm_ip_addr_init(int addr_family, gconstpointer src)
 
     G_STATIC_ASSERT_EXPR(sizeof(NMIPAddr) == sizeof(struct in6_addr));
 
+    /* this MUST use memcpy() to support unaligned src/dst pointers. */
+
     if (NM_IS_IPv4(addr_family)) {
         memcpy(&a, src, sizeof(in_addr_t));
 
@@ -312,12 +326,6 @@ gboolean nm_ip_addr_set_from_untrusted(int           addr_family,
 gboolean
 nm_ip_addr_set_from_variant(int addr_family, gpointer dst, GVariant *variant, int *out_addr_family);
 
-static inline gboolean
-nm_ip4_addr_is_localhost(in_addr_t addr4)
-{
-    return (addr4 & htonl(0xFF000000u)) == htonl(0x7F000000u);
-}
-
 static inline gconstpointer
 nm_ip_addr_from_packed_array(int addr_family, gconstpointer ipaddr_arr, gsize idx)
 {
@@ -389,6 +397,17 @@ gboolean nm_utils_get_ipv6_interface_identifier(NMLinkType          link_type,
 
 /*****************************************************************************/
 
+static inline guint32
+_nm_utils_ip4_netmask_to_prefix(in_addr_t subnetmask)
+{
+    G_STATIC_ASSERT_EXPR(__SIZEOF_INT__ == 4);
+    G_STATIC_ASSERT_EXPR(sizeof(int) == 4);
+    G_STATIC_ASSERT_EXPR(sizeof(guint) == 4);
+    G_STATIC_ASSERT_EXPR(sizeof(subnetmask) == 4);
+
+    return ((subnetmask != 0u) ? (guint32) (32 - __builtin_ctz(ntohl(subnetmask))) : 0u);
+}
+
 /**
  * _nm_utils_ip4_prefix_to_netmask:
  * @prefix: a CIDR prefix
@@ -485,11 +504,20 @@ nm_utils_ip_address_same_prefix(int           addr_family,
 /*****************************************************************************/
 
 gboolean nm_utils_ip_is_site_local(int addr_family, const void *address);
+gboolean nm_utils_ip6_is_ula(const struct in6_addr *address);
 
 /*****************************************************************************/
 
-#define NM_IPV4LL_NETWORK ((in_addr_t) (htonl(0xA9FE0000lu)))
-#define NM_IPV4LL_NETMASK ((in_addr_t) (htonl(0xFFFF0000lu)))
+#define NM_IPV4LL_NETWORK ((in_addr_t) htonl(0xA9FE0000lu))
+#define NM_IPV4LL_NETMASK ((in_addr_t) htonl(0xFFFF0000lu))
+
+static inline gboolean
+nm_utils_ip4_address_is_loopback(in_addr_t addr)
+{
+    /* There is also IN_LOOPBACK() in <linux/in.h>, but there the
+     * argument is in host order not `in_addr_t`. */
+    return (addr & htonl(0xFF000000u)) == htonl(0x7F000000u);
+}
 
 static inline gboolean
 nm_utils_ip4_address_is_link_local(in_addr_t addr)
@@ -623,6 +651,16 @@ nm_utils_is_separator(const char c)
 
 /*****************************************************************************/
 
+static inline GBytes *
+nm_g_bytes_ref(GBytes *b)
+{
+    if (b)
+        g_bytes_ref(b);
+    return b;
+}
+
+/*****************************************************************************/
+
 GBytes *nm_g_bytes_get_empty(void);
 
 GBytes *nm_g_bytes_new_from_str(const char *str);
@@ -1977,6 +2015,7 @@ typedef struct {
         NMUtilsNamedEntry named_entry;
         const char       *name;
         char             *name_mutable;
+        gpointer          name_ptr;
     };
     union {
         const char *value_str;
@@ -1990,14 +2029,28 @@ typedef struct {
         .name = (n), .value_ptr = (v)   \
     }
 
-NMUtilsNamedValue *
-nm_utils_named_values_from_strdict_full(GHashTable         *hash,
-                                        guint              *out_len,
-                                        GCompareDataFunc    compare_func,
-                                        gpointer            user_data,
-                                        NMUtilsNamedValue  *provided_buffer,
-                                        guint               provided_buffer_len,
-                                        NMUtilsNamedValue **out_allocated_buffer);
+NMUtilsNamedValue *nm_utils_hash_to_array_full(GHashTable         *hash,
+                                               guint              *out_len,
+                                               GCompareDataFunc    compare_func,
+                                               gpointer            user_data,
+                                               NMUtilsNamedValue  *provided_buffer,
+                                               guint               provided_buffer_len,
+                                               NMUtilsNamedValue **out_allocated_buffer);
+
+#define nm_utils_named_values_from_strdict_full(hash,                 \
+                                                out_len,              \
+                                                compare_func,         \
+                                                user_data,            \
+                                                provided_buffer,      \
+                                                provided_buffer_len,  \
+                                                out_allocated_buffer) \
+    nm_utils_hash_to_array_full((hash),                               \
+                                (out_len),                            \
+                                (compare_func),                       \
+                                (user_data),                          \
+                                (provided_buffer),                    \
+                                (provided_buffer_len),                \
+                                (out_allocated_buffer))
 
 #define nm_utils_named_values_from_strdict(hash, out_len, array, out_allocated_buffer) \
     nm_utils_named_values_from_strdict_full((hash),                                    \
@@ -2038,6 +2091,29 @@ gpointer *nm_utils_hash_values_to_array(GHashTable      *hash,
                                         gpointer         user_data,
                                         guint           *out_len);
 
+static inline NMUtilsNamedValue *
+nm_utils_hash_to_array(GHashTable      *hash,
+                       GCompareDataFunc compare_func,
+                       gpointer         user_data,
+                       guint           *out_len)
+{
+    return nm_utils_hash_to_array_full(hash, out_len, compare_func, user_data, NULL, 0, NULL);
+}
+
+#define nm_utils_hash_to_array_with_buffer(hash,                 \
+                                           out_len,              \
+                                           compare_func,         \
+                                           user_data,            \
+                                           array,                \
+                                           out_allocated_buffer) \
+    nm_utils_hash_to_array_full((hash),                          \
+                                (out_len),                       \
+                                (compare_func),                  \
+                                (user_data),                     \
+                                (array),                         \
+                                G_N_ELEMENTS(array),             \
+                                (out_allocated_buffer))
+
 static inline const char **
 nm_strdict_get_keys(const GHashTable *hash, gboolean sorted, guint *out_length)
 {
@@ -2146,38 +2222,57 @@ nm_g_array_unref(GArray *arr)
         g_array_unref(arr);
 }
 
-#define nm_g_array_first(arr, type)   \
-    ({                                \
-        GArray *const _arr = (arr);   \
-        guint         _len;           \
-                                      \
-        nm_assert(_arr);              \
-        _len = _arr->len;             \
-        nm_assert(_len > 0);          \
-        &g_array_index(arr, type, 0); \
+#define nm_g_array_first(arr, Type)                                \
+    ({                                                             \
+        GArray *const _arr = (arr);                                \
+                                                                   \
+        nm_assert(_arr);                                           \
+        nm_assert(sizeof(Type) == g_array_get_element_size(_arr)); \
+        nm_assert(_arr->len > 0);                                  \
+                                                                   \
+        &g_array_index(arr, Type, 0);                              \
     })
 
-#define nm_g_array_last(arr, type)            \
-    ({                                        \
-        GArray *const _arr = (arr);           \
-        guint         _len;                   \
-                                              \
-        nm_assert(_arr);                      \
-        _len = _arr->len;                     \
-        nm_assert(_len > 0);                  \
-        &g_array_index(arr, type, _len - 1u); \
+#define nm_g_array_last(arr, Type)                                 \
+    ({                                                             \
+        GArray *const _arr = (arr);                                \
+                                                                   \
+        nm_assert(_arr);                                           \
+        nm_assert(sizeof(Type) == g_array_get_element_size(_arr)); \
+        nm_assert(_arr->len > 0);                                  \
+                                                                   \
+        &g_array_index(arr, Type, _arr->len - 1u);                 \
     })
 
-#define nm_g_array_append_new(arr, type)   \
-    ({                                     \
-        GArray *const _arr = (arr);        \
-        guint         _len;                \
-                                           \
-        nm_assert(_arr);                   \
-        _len = _arr->len;                  \
-        nm_assert(_len < G_MAXUINT);       \
-        g_array_set_size(_arr, _len + 1u); \
-        &g_array_index(arr, type, _len);   \
+/* Similar to g_array_index(). The differences are
+ * - this does nm_assert() checks that the arguments are valid.
+ * - returns a pointer to the element. */
+#define nm_g_array_index_p(arr, Type, idx)                            \
+    ({                                                                \
+        GArray *const _arr_55 = (arr);                                \
+        const guint   _idx_55 = (idx);                                \
+                                                                      \
+        nm_assert(_arr_55);                                           \
+        nm_assert(sizeof(Type) == g_array_get_element_size(_arr_55)); \
+        nm_assert(_idx_55 < _arr_55->len);                            \
+                                                                      \
+        &g_array_index(_arr_55, Type, _idx_55);                       \
+    })
+
+#define nm_g_array_append_new(arr, Type)                           \
+    ({                                                             \
+        GArray *const _arr = (arr);                                \
+        guint         _len;                                        \
+                                                                   \
+        nm_assert(_arr);                                           \
+        nm_assert(sizeof(Type) == g_array_get_element_size(_arr)); \
+                                                                   \
+        _len = _arr->len;                                          \
+                                                                   \
+        nm_assert(_len < G_MAXUINT);                               \
+                                                                   \
+        g_array_set_size(_arr, _len + 1u);                         \
+        &g_array_index(arr, Type, _len);                           \
     })
 
 /*****************************************************************************/
@@ -2311,6 +2406,25 @@ nm_g_hash_table_contains(GHashTable *hash, gconstpointer key)
     return hash ? g_hash_table_contains(hash, key) : FALSE;
 }
 
+#define nm_g_hash_table_contains_any(hash, ...)                              \
+    ({                                                                       \
+        GHashTable *const   _hash   = (hash);                                \
+        gconstpointer const _keys[] = {__VA_ARGS__};                         \
+        int                 _i_key;                                          \
+        gboolean            _contains = FALSE;                               \
+                                                                             \
+        if (_hash) {                                                         \
+            for (_i_key = 0; _i_key < (int) G_N_ELEMENTS(_keys); _i_key++) { \
+                if (g_hash_table_contains(_hash, _keys[_i_key])) {           \
+                    _contains = TRUE;                                        \
+                    break;                                                   \
+                }                                                            \
+            }                                                                \
+        }                                                                    \
+                                                                             \
+        _contains;                                                           \
+    })
+
 static inline gboolean
 nm_g_hash_table_remove(GHashTable *hash, gconstpointer key)
 {
@@ -2765,6 +2879,8 @@ nm_ether_addr_to_string(const NMEtherAddr *ether_addr, char sbuf[static(sizeof(N
 #define nm_ether_addr_to_string_a(ether_addr) \
     nm_ether_addr_to_string((ether_addr), g_alloca(sizeof(NMEtherAddr) * 3))
 
+NMEtherAddr *nm_ether_addr_from_string(NMEtherAddr *addr, const char *str);
+
 guint8 *nm_utils_hexstr2bin_full(const char *hexstr,
                                  gboolean    allow_0x_prefix,
                                  gboolean    delimiter_required,
@@ -3054,10 +3170,14 @@ gboolean nm_utils_ifname_valid(const char *name, NMUtilsIfaceType type, GError *
 static inline GArray *
 nm_strvarray_ensure(GArray **p)
 {
+    nm_assert(p);
+
     if (!*p) {
         *p = g_array_new(TRUE, FALSE, sizeof(char *));
         g_array_set_clear_func(*p, nm_indirect_g_free);
-    }
+    } else
+        nm_assert(g_array_get_element_size(*p) == sizeof(char *));
+
     return *p;
 }
 
@@ -3066,6 +3186,9 @@ nm_strvarray_add(GArray *array, const char *str)
 {
     char *s;
 
+    nm_assert(array);
+    nm_assert(g_array_get_element_size(array) == sizeof(char *));
+
     s = g_strdup(str);
     g_array_append_val(array, s);
 }
@@ -3073,15 +3196,14 @@ nm_strvarray_add(GArray *array, const char *str)
 static inline const char *
 nm_strvarray_get_idx(GArray *array, guint idx)
 {
-    nm_assert(array);
-    nm_assert(idx < array->len);
-
-    return g_array_index(array, const char *, idx);
+    return *nm_g_array_index_p(array, const char *, idx);
 }
 
 static inline const char *const *
 nm_strvarray_get_strv_non_empty(GArray *arr, guint *length)
 {
+    nm_assert(!arr || g_array_get_element_size(arr) == sizeof(char *));
+
     if (!arr || arr->len == 0) {
         NM_SET_OUT(length, 0);
         return NULL;
@@ -3096,6 +3218,8 @@ nm_strvarray_get_strv_non_empty_dup(GArray *arr, guint *length)
 {
     const char *const *strv;
 
+    nm_assert(!arr || g_array_get_element_size(arr) == sizeof(char *));
+
     if (!arr || arr->len == 0) {
         NM_SET_OUT(length, 0);
         return NULL;
@@ -3114,6 +3238,8 @@ nm_strvarray_get_strv(GArray **arr, guint *length)
         return (const char *const *) arr;
     }
 
+    nm_assert(g_array_get_element_size(*arr) == sizeof(char *));
+
     NM_SET_OUT(length, (*arr)->len);
     return &g_array_index(*arr, const char *, 0);
 }
@@ -3125,6 +3251,8 @@ nm_strvarray_set_strv(GArray **array, const char *const *strv)
 
     array_old = g_steal_pointer(array);
 
+    nm_assert(!array_old || g_array_get_element_size(array_old) == sizeof(char *));
+
     if (!strv || !strv[0])
         return;
 
@@ -3141,6 +3269,7 @@ nm_strvarray_find_first(GArray *strv, const char *needle)
     nm_assert(needle);
 
     if (strv) {
+        nm_assert(g_array_get_element_size(strv) == sizeof(char *));
         for (i = 0; i < strv->len; i++) {
             if (nm_streq(needle, g_array_index(strv, const char *, i)))
                 return i;
@@ -3224,6 +3353,12 @@ char *_nm_utils_format_variant_attributes(GHashTable                          *a
 
 /*****************************************************************************/
 
+/* glibc defines HOST_NAME_MAX as 64. Also Linux' sethostname() enforces
+ * that (__NEW_UTS_LEN). However, musl sets this to 255.
+ *
+ * At some places, we want to follow Linux. Hardcode our own define. */
+#define NM_HOST_NAME_MAX 64
+
 gboolean nm_utils_is_localhost(const char *name);
 
 gboolean nm_utils_is_specific_hostname(const char *name);
@@ -3276,4 +3411,46 @@ gboolean nm_utils_validate_hostname(const char *hostname);
 
 void nm_utils_thread_local_register_destroy(gpointer tls_data, GDestroyNotify destroy_notify);
 
+/*****************************************************************************/
+
+int nm_unbase64char(char c);
+int nm_unbase64mem_full(const char *p, gsize l, gboolean secure, guint8 **ret, gsize *ret_size);
+
+/*****************************************************************************/
+
+static inline gboolean
+nm_path_is_absolute(const char *p)
+{
+    /* Copied from systemd's path_is_absolute()
+     * https://github.com/systemd/systemd/blob/bc85f8b51d962597360e982811e674c126850f56/src/basic/path-util.h#L50 */
+
+    nm_assert(p);
+    return p[0] == '/';
+}
+
+int nm_path_find_first_component(const char **p, gboolean accept_dot_dot, const char **ret);
+
+int nm_path_compare(const char *a, const char *b);
+
+static inline gboolean
+nm_path_equal(const char *a, const char *b)
+{
+    return nm_path_compare(a, b) == 0;
+}
+
+char *nm_path_simplify(char *path);
+
+char *
+nm_path_startswith_full(const char *path, const char *prefix, gboolean accept_dot_dot) _nm_pure;
+
+static inline char *
+nm_path_startswith(const char *path, const char *prefix)
+{
+    return nm_path_startswith_full(path, prefix, TRUE);
+}
+
+/*****************************************************************************/
+
+gboolean nm_hostname_is_valid(const char *s, gboolean trailing_dot);
+
 #endif /* __NM_SHARED_UTILS_H__ */
diff --git a/src/libnm-glib-aux/nm-str-buf.h b/src/libnm-glib-aux/nm-str-buf.h
index 47d1f055..652bc96b 100644
--- a/src/libnm-glib-aux/nm-str-buf.h
+++ b/src/libnm-glib-aux/nm-str-buf.h
@@ -26,6 +26,7 @@ typedef struct _NMStrBuf {
     };
 
     bool _priv_do_bzero_mem;
+    bool _priv_malloced;
 } NMStrBuf;
 
 /*****************************************************************************/
@@ -36,29 +37,56 @@ _nm_str_buf_assert(const NMStrBuf *strbuf)
     nm_assert(strbuf);
     nm_assert((!!strbuf->_priv_str) == (strbuf->_priv_allocated > 0));
     nm_assert(strbuf->_priv_len <= strbuf->_priv_allocated);
+    nm_assert(!strbuf->_priv_malloced || strbuf->_priv_str);
 }
 
 static inline NMStrBuf
-NM_STR_BUF_INIT(gsize allocated, gboolean do_bzero_mem)
+NM_STR_BUF_INIT_FULL(char    *str,
+                     gsize    len,
+                     gsize    allocated,
+                     gboolean malloced,
+                     gboolean do_bzero_mem)
 {
     NMStrBuf strbuf = {
-        ._priv_str          = allocated ? g_malloc(allocated) : NULL,
+        ._priv_str          = allocated > 0 ? str : NULL,
         ._priv_allocated    = allocated,
-        ._priv_len          = 0,
+        ._priv_len          = len,
         ._priv_do_bzero_mem = do_bzero_mem,
+        ._priv_malloced     = allocated > 0 && malloced,
     };
 
+    _nm_str_buf_assert(&strbuf);
+
     return strbuf;
 }
 
-static inline void
-nm_str_buf_init(NMStrBuf *strbuf, gsize len, bool do_bzero_mem)
+static inline NMStrBuf
+NM_STR_BUF_INIT(gsize allocated, gboolean do_bzero_mem)
 {
-    nm_assert(strbuf);
-    *strbuf = NM_STR_BUF_INIT(len, do_bzero_mem);
-    _nm_str_buf_assert(strbuf);
+    return NM_STR_BUF_INIT_FULL(allocated > 0 ? g_malloc(allocated) : NULL,
+                                0,
+                                allocated,
+                                allocated > 0,
+                                do_bzero_mem);
 }
 
+#define NM_STR_BUF_INIT_A(size, do_bzero_mem)                                               \
+    NM_STR_BUF_INIT_FULL(                                                                   \
+        g_alloca(size),                                                                     \
+        0,                                                                                  \
+        NM_STATIC_ASSERT_EXPR_1((size) > 0 && (size) <= NM_UTILS_GET_NEXT_REALLOC_SIZE_488) \
+            ? (size)                                                                        \
+            : 0,                                                                            \
+        FALSE,                                                                              \
+        (do_bzero_mem));
+
+#define NM_STR_BUF_INIT_ARR(arr, do_bzero_mem)                                                    \
+    NM_STR_BUF_INIT_FULL((arr),                                                                   \
+                         0,                                                                       \
+                         NM_STATIC_ASSERT_EXPR_1(sizeof(arr) > sizeof(char *)) ? sizeof(arr) : 0, \
+                         FALSE,                                                                   \
+                         (do_bzero_mem));
+
 void _nm_str_buf_ensure_size(NMStrBuf *strbuf, gsize new_size, gboolean reserve_exact);
 
 static inline void
@@ -263,7 +291,27 @@ nm_str_buf_append0(NMStrBuf *strbuf, const char *str)
     return nm_str_buf_append_len0(strbuf, str, strlen(str));
 }
 
-void nm_str_buf_append_printf(NMStrBuf *strbuf, const char *format, ...) _nm_printf(2, 3);
+void nm_str_buf_append_printfv(NMStrBuf *strbuf, const char *format, va_list args) _nm_printf(2, 0);
+
+/* Warning, this is not a function-like macro. That is, you must
+ * evaluate it in a place where you would otherwise call va_start(). */
+#define nm_str_buf_append_printfv_eval(strbuf, format, va_start_last) \
+    ({                                                                \
+        NMStrBuf *const _strbuf = (strbuf);                           \
+        va_list         _ap;                                          \
+                                                                      \
+        va_start(_ap, (va_start_last));                               \
+        nm_str_buf_append_printfv(_strbuf, (format), _ap);            \
+        va_end(_ap);                                                  \
+                                                                      \
+        _strbuf;                                                      \
+    })
+
+static inline void _nm_printf(2, 3)
+    nm_str_buf_append_printf(NMStrBuf *strbuf, const char *format, ...)
+{
+    nm_str_buf_append_printfv_eval(strbuf, format, format);
+}
 
 static inline void
 nm_str_buf_ensure_trailing_c(NMStrBuf *strbuf, char ch)
@@ -359,10 +407,10 @@ static inline gboolean
 nm_str_buf_is_initalized(NMStrBuf *strbuf)
 {
     nm_assert(strbuf);
-#if NM_MORE_ASSERTS
-    if (strbuf->_priv_str)
-        _nm_str_buf_assert(strbuf);
-#endif
+    if (NM_MORE_ASSERTS > 0) {
+        if (strbuf->_priv_str)
+            _nm_str_buf_assert(strbuf);
+    }
     return !!strbuf->_priv_str;
 }
 
@@ -399,6 +447,29 @@ nm_str_buf_get_str(NMStrBuf *strbuf)
     return strbuf->_priv_str;
 }
 
+/**
+ * nm_str_buf_get_str_unsafe:
+ * @strbuf: the buffer
+ *
+ * Usually, NMStrBuf is used to construct NUL terminated strings. But
+ * while constructing the buffer (nm_str_buf_append*()), it does
+ * not NUL terminate the buffer yet. Only nm_str_buf_get_str()
+ * and nm_str_buf_finalize() ensure that the returned string is
+ * actually NUL terminated.
+ *
+ * NMStrBuf can also be used for binary data, or you might not
+ * require the NUL termination. In that case, nm_str_buf_get_str_unsafe()
+ * will give you the pointer, but you must not rely on it being NUL
+ * terminated. This is the "unsafe" part of it.
+ *
+ * The returned string is of course initialized up to length "strbuf->len"
+ * and allocated with "strbuf->allocated" bytes.
+ *
+ * If currently no buffer is allocated, %NULL is returned.
+ *
+ * Returns: (transfer none): very similar to nm_str_buf_get_str(),
+ *   except that the result is no guaranteed to be NUL terminated.
+ */
 static inline char *
 nm_str_buf_get_str_unsafe(NMStrBuf *strbuf)
 {
@@ -440,9 +511,11 @@ nm_str_buf_get_char(const NMStrBuf *strbuf, gsize index)
  * Returns: (transfer full): the string of the buffer
  *   which must be freed by the caller. The @strbuf
  *   is afterwards in undefined state, though it can be
- *   reused after nm_str_buf_init().
- *   Note that if no string is allocated yet (after nm_str_buf_init() with
- *   length zero), this will return %NULL. */
+ *   reused after resetting with NM_STR_BUF_INIT().
+ *   Note that if no string is allocated yet (after NM_STR_BUF_INIT() with
+ *   length zero), this will return %NULL.
+ *
+ *   If the buffer was not malloced before, it will be malloced now. */
 static inline char *
 nm_str_buf_finalize(NMStrBuf *strbuf, gsize *out_len)
 {
@@ -453,6 +526,16 @@ nm_str_buf_finalize(NMStrBuf *strbuf, gsize *out_len)
     if (!strbuf->_priv_str)
         return NULL;
 
+    if (!strbuf->_priv_malloced) {
+        char *str = g_steal_pointer(&strbuf->_priv_str);
+        char *result;
+
+        result = g_strndup(str, strbuf->_priv_len);
+        if (strbuf->_priv_do_bzero_mem)
+            nm_explicit_bzero(str, strbuf->_priv_len);
+        return result;
+    }
+
     nm_str_buf_maybe_expand(strbuf, 1, TRUE);
     strbuf->_priv_str[strbuf->_priv_len] = '\0';
 
@@ -484,7 +567,7 @@ nm_str_buf_finalize_to_gbytes(NMStrBuf *strbuf)
  *
  * Frees the associated memory of @strbuf. The buffer
  * afterwards is in undefined state, but can be re-initialized
- * with nm_str_buf_init().
+ * with NM_STR_BUF_INIT().
  */
 static inline void
 nm_str_buf_destroy(NMStrBuf *strbuf)
@@ -494,7 +577,8 @@ nm_str_buf_destroy(NMStrBuf *strbuf)
     _nm_str_buf_assert(strbuf);
     if (strbuf->_priv_do_bzero_mem)
         nm_explicit_bzero(strbuf->_priv_str, strbuf->_priv_len);
-    g_free(strbuf->_priv_str);
+    if (strbuf->_priv_malloced)
+        g_free(strbuf->_priv_str);
 
     /* the buffer is in invalid state afterwards, however, we clear it
      * so far, that nm_auto_str_buf is happy when calling
diff --git a/src/libnm-glib-aux/nm-test-utils.h b/src/libnm-glib-aux/nm-test-utils.h
index 2dfe9e32..83702070 100644
--- a/src/libnm-glib-aux/nm-test-utils.h
+++ b/src/libnm-glib-aux/nm-test-utils.h
@@ -185,7 +185,7 @@
 #define nmtst_assert_strv(strv, ...)                              \
     G_STMT_START                                                  \
     {                                                             \
-        const char *const *const _strv  = (strv);                 \
+        const char *const *const _strv  = NM_CAST_STRV_CC(strv);  \
         const char *const        _exp[] = {__VA_ARGS__, NULL};    \
         const gsize              _n     = G_N_ELEMENTS(_exp) - 1; \
         gsize                    _i;                              \
@@ -196,6 +196,7 @@
             g_assert(_exp[_i]);                                   \
             g_assert_cmpstr(_strv[_i], ==, _exp[_i]);             \
         }                                                         \
+        g_assert(!_strv[_n]);                                     \
     }                                                             \
     G_STMT_END
 
diff --git a/src/libnm-glib-aux/nm-uuid.c b/src/libnm-glib-aux/nm-uuid.c
index ff0649d0..53e8b78c 100644
--- a/src/libnm-glib-aux/nm-uuid.c
+++ b/src/libnm-glib-aux/nm-uuid.c
@@ -115,12 +115,12 @@ nm_uuid_generate_random(NMUuid *out_uuid)
 
     /* See also, systemd's id128_make_v4_uuid() */
 
-    /* nm_utils_random_bytes() is supposed to try hard to give good
+    /* nm_random_get_bytes() is supposed to try hard to give good
      * randomness. If it fails, it still makes an effort to fill
      * random data into the buffer. There is not much we can do about
      * that case, except making sure that it does not happen in the
      * first place. */
-    nm_utils_random_bytes(out_uuid, sizeof(*out_uuid));
+    nm_random_get_bytes(out_uuid, sizeof(*out_uuid));
 
     /* Set the four most significant bits (bits 12 through 15) of the
      * time_hi_and_version field to the 4-bit version number from
@@ -202,28 +202,25 @@ nm_uuid_is_valid_nm(const char *str,
 
     /* @out_normalized_str is only set, if normalization was necessary
      * and possible. The caller cannot request @out_normalized_str, without
-     * also getting @out_normalized. */
+     * also requesting @out_normalized. Otherwise, they couldn't know whether
+     * a normalized string was returned. */
     nm_assert(!out_normalized_str || out_normalized);
 
     if (!str)
         return FALSE;
 
     if (nm_uuid_parse_full(str, &uuid, &is_normalized)) {
-        /* Note that:
-         *   @is_normalized means that "str" contains a normalized UUID
-         *   @out_normalized: indicates whether str requires normalization
-         *     and whether @out_normalized_str was set to contain the normalized
-         *     UUID.
-         * With this, we get the slightly odd assignment: */
-        NM_SET_OUT(out_normalized, !is_normalized);
-
-        if (!is_normalized && out_normalized_str) {
-            /* we need to normalize the UUID */
-            nm_uuid_unparse(&uuid, out_normalized_str);
+        if (is_normalized) {
+            /* @str is already normalized. No need to normalize again, so
+             * @out_normalized is FALSE. */
+            NM_SET_OUT(out_normalized, FALSE);
+        } else {
+            NM_SET_OUT(out_normalized, TRUE);
+            if (out_normalized_str) {
+                /* we need to normalize the UUID */
+                nm_uuid_unparse(&uuid, out_normalized_str);
+            }
         }
-
-        /* regardless whether normalization was necessary, the UUID is
-         * essentially valid. */
         return TRUE;
     }
 
@@ -235,24 +232,23 @@ nm_uuid_is_valid_nm(const char *str,
          * are made lower case first. */
         NM_SET_OUT(out_normalized, TRUE);
         if (out_normalized_str) {
-            char str_lower[40 + 1];
+            char str_lower[40];
             int  i;
 
-            nm_assert(strlen(str) < G_N_ELEMENTS(str_lower));
+            nm_assert(strlen(str) <= G_N_ELEMENTS(str_lower));
 
             /* normalize first to lower-case. */
             for (i = 0; str[i]; i++) {
                 nm_assert(i < G_N_ELEMENTS(str_lower));
                 str_lower[i] = g_ascii_tolower(str[i]);
             }
-            nm_assert(i < G_N_ELEMENTS(str_lower));
-            str_lower[i] = '\0';
+            nm_assert(i <= G_N_ELEMENTS(str_lower));
 
             /* The namespace UUID is chosen randomly. */
             nm_uuid_generate_from_string(
                 &uuid,
                 str_lower,
-                -1,
+                i,
                 NM_UUID_TYPE_VERSION5,
                 &NM_UUID_INIT(4e, 72, f7, 09, ca, 95, 44, 05, 90, 53, 1f, 43, 29, 4a, 61, 8c));
             nm_uuid_unparse(&uuid, out_normalized_str);
diff --git a/src/libnm-glib-aux/tests/test-shared-general.c b/src/libnm-glib-aux/tests/test-shared-general.c
index e7fd2b6b..7d36d64a 100644
--- a/src/libnm-glib-aux/tests/test-shared-general.c
+++ b/src/libnm-glib-aux/tests/test-shared-general.c
@@ -10,6 +10,7 @@
 #include "libnm-glib-aux/nm-str-buf.h"
 #include "libnm-glib-aux/nm-time-utils.h"
 #include "libnm-glib-aux/nm-ref-string.h"
+#include "libnm-glib-aux/nm-io-utils.h"
 
 #include "libnm-glib-aux/nm-test-utils.h"
 
@@ -93,7 +94,7 @@ test_nmhash(void)
 {
     int rnd;
 
-    nm_utils_random_bytes(&rnd, sizeof(rnd));
+    nm_random_get_bytes(&rnd, sizeof(rnd));
 
     g_assert(nm_hash_val(555, 4) != 0);
 }
@@ -116,8 +117,8 @@ test_make_strv(void)
     const char *const *v2a  = NM_MAKE_STRV("a", "b");
     const char *const *v2b  = NM_MAKE_STRV("a", "b", );
     const char *const  v3[] = {
-        "a",
-        "b",
+         "a",
+         "b",
     };
     const char *const *v4b = NM_MAKE_STRV("a", _make_strv_foo(), );
 
@@ -250,14 +251,14 @@ test_nm_strndup_a(void)
 /*****************************************************************************/
 
 static void
-test_nm_ip4_addr_is_localhost(void)
+test_nm_utils_ip4_address_is_loopback(void)
 {
-    g_assert(nm_ip4_addr_is_localhost(nmtst_inet4_from_string("127.0.0.0")));
-    g_assert(nm_ip4_addr_is_localhost(nmtst_inet4_from_string("127.0.0.1")));
-    g_assert(nm_ip4_addr_is_localhost(nmtst_inet4_from_string("127.5.0.1")));
-    g_assert(!nm_ip4_addr_is_localhost(nmtst_inet4_from_string("126.5.0.1")));
-    g_assert(!nm_ip4_addr_is_localhost(nmtst_inet4_from_string("128.5.0.1")));
-    g_assert(!nm_ip4_addr_is_localhost(nmtst_inet4_from_string("129.5.0.1")));
+    g_assert(nm_utils_ip4_address_is_loopback(nmtst_inet4_from_string("127.0.0.0")));
+    g_assert(nm_utils_ip4_address_is_loopback(nmtst_inet4_from_string("127.0.0.1")));
+    g_assert(nm_utils_ip4_address_is_loopback(nmtst_inet4_from_string("127.5.0.1")));
+    g_assert(!nm_utils_ip4_address_is_loopback(nmtst_inet4_from_string("126.5.0.1")));
+    g_assert(!nm_utils_ip4_address_is_loopback(nmtst_inet4_from_string("128.5.0.1")));
+    g_assert(!nm_utils_ip4_address_is_loopback(nmtst_inet4_from_string("129.5.0.1")));
 }
 
 /*****************************************************************************/
@@ -781,6 +782,9 @@ test_nm_utils_get_next_realloc_size(void)
         {NM_UTILS_GET_NEXT_REALLOC_SIZE_232,
          NM_UTILS_GET_NEXT_REALLOC_SIZE_232,
          NM_UTILS_GET_NEXT_REALLOC_SIZE_232},
+        {NM_UTILS_GET_NEXT_REALLOC_SIZE_488,
+         NM_UTILS_GET_NEXT_REALLOC_SIZE_488,
+         NM_UTILS_GET_NEXT_REALLOC_SIZE_488},
         {NM_UTILS_GET_NEXT_REALLOC_SIZE_1000,
          NM_UTILS_GET_NEXT_REALLOC_SIZE_1000,
          NM_UTILS_GET_NEXT_REALLOC_SIZE_1000},
@@ -913,27 +917,58 @@ test_nm_str_buf(void)
 {
     guint i_run;
 
-    for (i_run = 0; TRUE; i_run++) {
-        nm_auto_str_buf NMStrBuf      strbuf = {};
-        nm_auto_free_gstring GString *gstr   = NULL;
+    for (i_run = 0; i_run < 1000; i_run++) {
+        char                          stack_buf[1024];
+        nm_auto_str_buf NMStrBuf      strbuf;
+        nm_auto_free_gstring GString *gstr = NULL;
         int                           i, j, k;
         int                           c;
 
-        nm_str_buf_init(&strbuf, nmtst_get_rand_uint32() % 200u + 1u, nmtst_get_rand_bool());
+        switch (nmtst_get_rand_uint32() % 10) {
+        case 0:
+            memset(&strbuf, 0, sizeof(strbuf));
+            break;
+        case 1 ... 4:
+            strbuf = NM_STR_BUF_INIT_FULL(stack_buf,
+                                          0,
+                                          nmtst_get_rand_uint32() % sizeof(stack_buf),
+                                          FALSE,
+                                          nmtst_get_rand_bool());
+            break;
+        default:
+            strbuf = NM_STR_BUF_INIT(nmtst_get_rand_uint32() % 200u + 1u, nmtst_get_rand_bool());
+            break;
+        }
 
-        if (i_run < 1000) {
-            c = nmtst_get_rand_word_length(NULL);
-            for (i = 0; i < c; i++)
-                nm_str_buf_append_c(&strbuf, '0' + (i % 10));
-            gstr = g_string_new(nm_str_buf_get_str(&strbuf));
-            j    = nmtst_get_rand_uint32() % (strbuf.len + 1);
-            k    = nmtst_get_rand_uint32() % (strbuf.len - j + 2) - 1;
+        c = nmtst_get_rand_word_length(NULL);
+        for (i = 0; i < c; i++)
+            nm_str_buf_append_c(&strbuf, '0' + (i % 10));
+        gstr = g_string_new(nm_str_buf_get_str(&strbuf));
+        j    = nmtst_get_rand_uint32() % (strbuf.len + 1);
+        k    = nmtst_get_rand_uint32() % (strbuf.len - j + 2) - 1;
 
-            nm_str_buf_erase(&strbuf, j, k, nmtst_get_rand_bool());
-            g_string_erase(gstr, j, k);
+        nm_str_buf_erase(&strbuf, j, k, nmtst_get_rand_bool());
+        g_string_erase(gstr, j, k);
+        if (gstr->str[0])
             g_assert_cmpstr(gstr->str, ==, nm_str_buf_get_str(&strbuf));
+        else
+            g_assert(NM_IN_STRSET(nm_str_buf_get_str(&strbuf), NULL, ""));
+    }
+
+    for (i_run = 0; i_run < 50; i_run++) {
+        char                     stack_buf[20];
+        nm_auto_str_buf NMStrBuf strbuf = NM_STR_BUF_INIT_ARR(stack_buf, nmtst_get_rand_bool());
+
+        nm_str_buf_append_c_len(&strbuf, 'a', nmtst_get_rand_uint32() % (sizeof(stack_buf) * 2));
+        if (strbuf.len <= sizeof(stack_buf)) {
+            g_assert(stack_buf == nm_str_buf_get_str_unsafe(&strbuf));
         } else
-            return;
+            g_assert(stack_buf != nm_str_buf_get_str_unsafe(&strbuf));
+
+        if (strbuf.len < sizeof(stack_buf)) {
+            g_assert(stack_buf == nm_str_buf_get_str(&strbuf));
+        } else
+            g_assert(stack_buf != nm_str_buf_get_str(&strbuf));
     }
 }
 
@@ -1417,6 +1452,755 @@ test_nm_ascii(void)
 
 /*****************************************************************************/
 
+static int
+_env_file_push_cb(unsigned line, const char *key, const char *value, void *user_data)
+{
+    char ***strv = user_data;
+    char   *s_line;
+    gsize   key_l;
+    gsize   strv_l;
+    gsize   i;
+
+    g_assert(strv);
+    g_assert(key);
+    g_assert(key[0]);
+    g_assert(!strchr(key, '='));
+    g_assert(value);
+
+    key_l = strlen(key);
+
+    s_line = g_strconcat(key, "=", value, NULL);
+
+    strv_l = 0;
+    if (*strv) {
+        const char *s;
+
+        for (i = 0; (s = (*strv)[i]); i++) {
+            if (g_str_has_prefix(s, key) && s[key_l] == '=') {
+                g_free((*strv)[i]);
+                (*strv)[i] = s_line;
+                return 0;
+            }
+        }
+        strv_l = i;
+    }
+
+    *strv               = g_realloc(*strv, sizeof(char *) * (strv_l + 2));
+    (*strv)[strv_l]     = s_line;
+    (*strv)[strv_l + 1] = NULL;
+
+    return 0;
+}
+
+static void
+test_parse_env_file(void)
+{
+    gs_strfreev char **data = NULL;
+    gs_free char      *arg1 = NULL;
+    gs_free char      *arg2 = NULL;
+    int                r;
+
+#define env_file_1                  \
+    "a=a\n"                         \
+    "a=b\n"                         \
+    "a=b\n"                         \
+    "a=a\n"                         \
+    "b=b\\\n"                       \
+    "c\n"                           \
+    "d= d\\\n"                      \
+    "e  \\\n"                       \
+    "f  \n"                         \
+    "g=g\\ \n"                      \
+    "h= ąęół\\ śćńźżµ \n" \
+    "i=i\\"
+    r = nm_parse_env_file_full(env_file_1, _env_file_push_cb, &data);
+    g_assert_cmpint(r, ==, 0);
+    nmtst_assert_strv(data, "a=a", "b=bc", "d=de  f", "g=g ", "h=ąęół śćńźżµ", "i=i");
+    nm_clear_pointer(&data, g_strfreev);
+
+    r = nm_parse_env_file(env_file_1, "a", &arg1);
+    g_assert_cmpint(r, ==, 0);
+    g_assert_cmpstr(arg1, ==, "a");
+    nm_clear_g_free(&arg1);
+
+    r = nm_parse_env_file(env_file_1, "a", &arg1, "d", &arg2);
+    g_assert_cmpint(r, ==, 0);
+    g_assert_cmpstr(arg1, ==, "a");
+    g_assert_cmpstr(arg2, ==, "de  f");
+    nm_clear_g_free(&arg1);
+    nm_clear_g_free(&arg2);
+
+#define env_file_2 "a=a\\\n"
+    r = nm_parse_env_file_full(env_file_2, _env_file_push_cb, &data);
+    g_assert_cmpint(r, ==, 0);
+    nmtst_assert_strv(data, "a=a");
+    nm_clear_pointer(&data, g_strfreev);
+
+#define env_file_3                                              \
+    "#SPAMD_ARGS=\"-d --socketpath=/var/lib/bulwark/spamd \\\n" \
+    "#--nouser-config                                     \\\n" \
+    "normal=line                                          \\\n" \
+    ";normal=ignored                                      \\\n" \
+    "normal_ignored                                       \\\n" \
+    "normal ignored                                       \\\n"
+    r = nm_parse_env_file_full(env_file_3, _env_file_push_cb, &data);
+    g_assert_cmpint(r, ==, 0);
+    g_assert(!data);
+
+#define env_file_4                          \
+    "# Generated\n"                         \
+    "\n"                                    \
+    "HWMON_MODULES=\"coretemp f71882fg\"\n" \
+    "\n"                                    \
+    "# For compatibility reasons\n"         \
+    "\n"                                    \
+    "MODULE_0=coretemp\n"                   \
+    "MODULE_1=f71882fg"
+    r = nm_parse_env_file_full(env_file_4, _env_file_push_cb, &data);
+    g_assert_cmpint(r, ==, 0);
+    nmtst_assert_strv(data,
+                      "HWMON_MODULES=coretemp f71882fg",
+                      "MODULE_0=coretemp",
+                      "MODULE_1=f71882fg");
+    nm_clear_pointer(&data, g_strfreev);
+
+#define env_file_5 \
+    "a=\n"         \
+    "b="
+    r = nm_parse_env_file_full(env_file_5, _env_file_push_cb, &data);
+    g_assert_cmpint(r, ==, 0);
+    nmtst_assert_strv(data, "a=", "b=");
+    nm_clear_pointer(&data, g_strfreev);
+
+#define env_file_6                \
+    "a=\\ \\n \\t \\x \\y \\' \n" \
+    "b= \\$'                  \n" \
+    "c= ' \\n\\t\\$\\`\\\\\n"     \
+    "'   \n"                      \
+    "d= \" \\n\\t\\$\\`\\\\\n"    \
+    "\"   \n"
+    r = nm_parse_env_file_full(env_file_6, _env_file_push_cb, &data);
+    g_assert_cmpint(r, ==, 0);
+    nmtst_assert_strv(data, "a= n t x y '", "b=$'", "c= \\n\\t\\$\\`\\\\\n", "d= \\n\\t$`\\\n");
+    nm_clear_pointer(&data, g_strfreev);
+}
+
+/*****************************************************************************/
+
+static void
+test_unbase64char(void)
+{
+    static const int expected[128] = {
+        [0] = -1,   [1] = -1,   [2] = -1,   [3] = -1,   [4] = -1,   [5] = -1,   [6] = -1,
+        [7] = -1,   [8] = -1,   [9] = -1,   [10] = -1,  [11] = -1,  [12] = -1,  [13] = -1,
+        [14] = -1,  [15] = -1,  [16] = -1,  [17] = -1,  [18] = -1,  [19] = -1,  [20] = -1,
+        [21] = -1,  [22] = -1,  [23] = -1,  [24] = -1,  [25] = -1,  [26] = -1,  [27] = -1,
+        [28] = -1,  [29] = -1,  [30] = -1,  [31] = -1,  [32] = -1,  [33] = -1,  [34] = -1,
+        [35] = -1,  [36] = -1,  [37] = -1,  [38] = -1,  [39] = -1,  [40] = -1,  [41] = -1,
+        [42] = -1,  ['+'] = 62, [44] = -1,  [45] = -1,  [46] = -1,  ['/'] = 63, ['0'] = 52,
+        ['1'] = 53, ['2'] = 54, ['3'] = 55, ['4'] = 56, ['5'] = 57, ['6'] = 58, ['7'] = 59,
+        ['8'] = 60, ['9'] = 61, [58] = -1,  [59] = -1,  [60] = -1,  [61] = -1,  [62] = -1,
+        [63] = -1,  [64] = -1,  ['A'] = 0,  ['B'] = 1,  ['C'] = 2,  ['D'] = 3,  ['E'] = 4,
+        ['F'] = 5,  ['G'] = 6,  ['H'] = 7,  ['I'] = 8,  ['J'] = 9,  ['K'] = 10, ['L'] = 11,
+        ['M'] = 12, ['N'] = 13, ['O'] = 14, ['P'] = 15, ['Q'] = 16, ['R'] = 17, ['S'] = 18,
+        ['T'] = 19, ['U'] = 20, ['V'] = 21, ['W'] = 22, ['X'] = 23, ['Y'] = 24, ['Z'] = 25,
+        [91] = -1,  [92] = -1,  [93] = -1,  [94] = -1,  [95] = -1,  [96] = -1,  ['a'] = 26,
+        ['b'] = 27, ['c'] = 28, ['d'] = 29, ['e'] = 30, ['f'] = 31, ['g'] = 32, ['h'] = 33,
+        ['i'] = 34, ['j'] = 35, ['k'] = 36, ['l'] = 37, ['m'] = 38, ['n'] = 39, ['o'] = 40,
+        ['p'] = 41, ['q'] = 42, ['r'] = 43, ['s'] = 44, ['t'] = 45, ['u'] = 46, ['v'] = 47,
+        ['w'] = 48, ['x'] = 49, ['y'] = 50, ['z'] = 51, [123] = -1, [124] = -1, [125] = -1,
+        [126] = -1, [127] = -1,
+    };
+    int i;
+
+    /* Copied from systemd's TEST(unbase64char)
+     * https://github.com/systemd/systemd/blob/688efe7703328c5a0251fafac55757b8864a9f9a/src/test/test-hexdecoct.c#L44 */
+
+    g_assert_cmpint(nm_unbase64char('A'), ==, 0);
+    g_assert_cmpint(nm_unbase64char('Z'), ==, 25);
+    g_assert_cmpint(nm_unbase64char('a'), ==, 26);
+    g_assert_cmpint(nm_unbase64char('z'), ==, 51);
+    g_assert_cmpint(nm_unbase64char('0'), ==, 52);
+    g_assert_cmpint(nm_unbase64char('9'), ==, 61);
+    g_assert_cmpint(nm_unbase64char('+'), ==, 62);
+    g_assert_cmpint(nm_unbase64char('/'), ==, 63);
+    g_assert_cmpint(nm_unbase64char('='), ==, -ERANGE);
+    g_assert_cmpint(nm_unbase64char('\0'), ==, -EINVAL);
+    g_assert_cmpint(nm_unbase64char('\1'), ==, -EINVAL);
+    g_assert_cmpint(nm_unbase64char('\x7F'), ==, -EINVAL);
+    g_assert_cmpint(nm_unbase64char('\x80'), ==, -EINVAL);
+    g_assert_cmpint(nm_unbase64char('\xFF'), ==, -EINVAL);
+
+    for (i = 0; i < 256; i++) {
+        int base64;
+
+        base64 = nm_unbase64char((char) i);
+
+        if (base64 < 0) {
+            if (((char) i) == '=')
+                g_assert_cmpint(base64, ==, -ERANGE);
+            else
+                g_assert_cmpint(base64, ==, -EINVAL);
+            base64 = -1;
+        }
+
+        if (i >= G_N_ELEMENTS(expected)) {
+            g_assert_cmpint(base64, ==, -1);
+            continue;
+        }
+        g_assert_cmpint(base64, ==, expected[i]);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+test_unbase64mem1(void)
+{
+    nm_auto_str_buf NMStrBuf encoded_wrapped = NM_STR_BUF_INIT(400, FALSE);
+    uint8_t                  data[4096];
+    int                      i_run;
+
+    /* Copied from systemd's TEST(base64mem_linebreak)
+     * https://github.com/systemd/systemd/blob/688efe7703328c5a0251fafac55757b8864a9f9a/src/test/test-hexdecoct.c#L280 */
+
+    for (i_run = 0; i_run < 20; i_run++) {
+        gs_free char   *encoded = NULL;
+        gs_free guint8 *decoded = NULL;
+        gsize           decoded_size;
+        guint64         n;
+        guint64         m;
+        guint64         i;
+        guint64         j;
+        gssize          l;
+        int             r;
+
+        /* Try a bunch of differently sized blobs */
+        n = nmtst_get_rand_uint64() % sizeof(data);
+        nmtst_rand_buf(NULL, data, n);
+
+        /* Break at various different columns */
+        m = 1 + (nmtst_get_rand_uint64() % (n + 5));
+
+        encoded = g_base64_encode(data, n);
+        g_assert(encoded);
+        l = strlen(encoded);
+
+        nm_str_buf_reset(&encoded_wrapped);
+        for (i = 0, j = 0; i < l; i++, j++) {
+            if (j == m) {
+                nm_str_buf_append_c(&encoded_wrapped, '\n');
+                j = 0;
+            }
+            nm_str_buf_append_c(&encoded_wrapped, encoded[i]);
+        }
+
+        g_assert_cmpint(strlen(nm_str_buf_get_str(&encoded_wrapped)), ==, encoded_wrapped.len);
+
+        r = nm_unbase64mem_full(nm_str_buf_get_str(&encoded_wrapped),
+                                nmtst_get_rand_bool() ? SIZE_MAX : encoded_wrapped.len,
+                                nmtst_get_rand_bool(),
+                                &decoded,
+                                &decoded_size);
+        g_assert_cmpint(r, >=, 0);
+        g_assert_cmpmem(data, n, decoded, decoded_size);
+
+        for (j = 0; j < encoded_wrapped.len; j++)
+            g_assert((nm_str_buf_get_str(&encoded_wrapped)[j] == '\n') == (j % (m + 1) == m));
+    }
+}
+
+/*****************************************************************************/
+
+static void
+_assert_unbase64mem(const char *input, const char *output, int ret)
+{
+    gs_free guint8 *buffer = NULL;
+    gsize           size   = 0;
+    int             r;
+
+    r = nm_unbase64mem_full(input, SIZE_MAX, nmtst_get_rand_bool(), &buffer, &size);
+    g_assert_cmpint(r, ==, ret);
+
+    if (ret >= 0) {
+        g_assert_cmpmem(buffer, size, output, strlen(output));
+        g_assert_cmpint(((const char *) buffer)[size], ==, '\0');
+    } else {
+        g_assert(!buffer);
+        g_assert_cmpint(size, ==, 0);
+    }
+}
+
+static void
+test_unbase64mem2(void)
+{
+    /* Copied from systemd's TEST(unbase64mem)
+     * https://github.com/systemd/systemd/blob/688efe7703328c5a0251fafac55757b8864a9f9a/src/test/test-hexdecoct.c#L324 */
+
+    _assert_unbase64mem("", "", 0);
+    _assert_unbase64mem("Zg==", "f", 0);
+    _assert_unbase64mem("Zm8=", "fo", 0);
+    _assert_unbase64mem("Zm9v", "foo", 0);
+    _assert_unbase64mem("Zm9vYg==", "foob", 0);
+    _assert_unbase64mem("Zm9vYmE=", "fooba", 0);
+    _assert_unbase64mem("Zm9vYmFy", "foobar", 0);
+
+    _assert_unbase64mem(" ", "", 0);
+    _assert_unbase64mem(" \n\r ", "", 0);
+    _assert_unbase64mem("    Zg\n==       ", "f", 0);
+    _assert_unbase64mem(" Zm 8=\r", "fo", 0);
+    _assert_unbase64mem("  Zm9\n\r\r\nv   ", "foo", 0);
+    _assert_unbase64mem(" Z m9vYg==\n\r", "foob", 0);
+    _assert_unbase64mem(" Zm 9vYmE=   ", "fooba", 0);
+    _assert_unbase64mem("   Z m9v    YmFy   ", "foobar", 0);
+
+    _assert_unbase64mem("A", NULL, -EPIPE);
+    _assert_unbase64mem("A====", NULL, -EINVAL);
+    _assert_unbase64mem("AAB==", NULL, -EINVAL);
+    _assert_unbase64mem(" A A A B = ", NULL, -EINVAL);
+    _assert_unbase64mem(" Z m 8 = q u u x ", NULL, -ENAMETOOLONG);
+}
+
+/*****************************************************************************/
+
+static void
+_test_unbase64mem_mem(const char *base64, const guint8 *expected_arr, gsize expected_len)
+{
+    gs_free char        *expected_base64 = NULL;
+    int                  r;
+    nm_auto_free guint8 *exp2_arr = NULL;
+    nm_auto_free guint8 *exp3_arr = NULL;
+    gsize                exp2_len;
+    gsize                exp3_len;
+
+    expected_base64 = g_base64_encode(expected_arr, expected_len);
+
+    r = nm_unbase64mem_full(expected_base64, strlen(expected_base64), TRUE, &exp2_arr, &exp2_len);
+    g_assert_cmpint(r, ==, 0);
+    g_assert_cmpmem(expected_arr, expected_len, exp2_arr, exp2_len);
+
+    if (!nm_streq(base64, expected_base64)) {
+        r = nm_unbase64mem_full(base64, strlen(base64), TRUE, &exp3_arr, &exp3_len);
+        g_assert_cmpint(r, ==, 0);
+        g_assert_cmpmem(expected_arr, expected_len, exp3_arr, exp3_len);
+    }
+}
+
+#define _test_unbase64mem(base64, expected_str) \
+    _test_unbase64mem_mem(base64, (const guint8 *) "" expected_str "", NM_STRLEN(expected_str))
+
+static void
+_test_unbase64mem_inval(const char *base64)
+{
+    gs_free guint8 *exp_arr = NULL;
+    gsize           exp_len = 0;
+    int             r;
+
+    r = nm_unbase64mem_full(base64, strlen(base64), TRUE, &exp_arr, &exp_len);
+    g_assert_cmpint(r, <, 0);
+    g_assert(!exp_arr);
+    g_assert(exp_len == 0);
+}
+
+static void
+test_unbase64mem3(void)
+{
+    gs_free char *rnd_base64 = NULL;
+    guint8        rnd_buf[30];
+    guint         i, rnd_len;
+
+    _test_unbase64mem("", "");
+    _test_unbase64mem("  ", "");
+    _test_unbase64mem(" Y Q == ", "a");
+    _test_unbase64mem(" Y   WJjZGV mZ 2g = ", "abcdefgh");
+    _test_unbase64mem_inval(" Y   %WJjZGV mZ 2g = ");
+    _test_unbase64mem_inval(" Y   %WJjZGV mZ 2g = a");
+    _test_unbase64mem("YQ==", "a");
+    _test_unbase64mem_inval("YQ==a");
+
+    rnd_len = nmtst_get_rand_uint32() % sizeof(rnd_buf);
+    for (i = 0; i < rnd_len; i++)
+        rnd_buf[i] = nmtst_get_rand_uint32() % 256;
+    rnd_base64 = g_base64_encode(rnd_buf, rnd_len);
+    _test_unbase64mem_mem(rnd_base64, rnd_buf, rnd_len);
+}
+
+/*****************************************************************************/
+
+static void
+assert_path_compare(const char *a, const char *b, int expected)
+{
+    int r;
+
+    g_assert(NM_IN_SET(expected, -1, 0, 1));
+
+    g_assert_cmpint(nm_path_compare(a, a), ==, 0);
+    g_assert_cmpint(nm_path_compare(b, b), ==, 0);
+
+    r = nm_path_compare(a, b);
+    g_assert_cmpint(r, ==, expected);
+    r = nm_path_compare(b, a);
+    g_assert_cmpint(r, ==, -expected);
+
+    g_assert(nm_path_equal(a, a) == 1);
+    g_assert(nm_path_equal(b, b) == 1);
+    g_assert(nm_path_equal(a, b) == (expected == 0));
+    g_assert(nm_path_equal(b, a) == (expected == 0));
+}
+
+static void
+test_path_compare(void)
+{
+    /* Copied from systemd.
+     * https://github.com/systemd/systemd/blob/bc85f8b51d962597360e982811e674c126850f56/src/test/test-path-util.c#L126 */
+
+    assert_path_compare("/goo", "/goo", 0);
+    assert_path_compare("/goo", "/goo", 0);
+    assert_path_compare("//goo", "/goo", 0);
+    assert_path_compare("//goo/////", "/goo", 0);
+    assert_path_compare("goo/////", "goo", 0);
+    assert_path_compare("/goo/boo", "/goo//boo", 0);
+    assert_path_compare("//goo/boo", "/goo/boo//", 0);
+    assert_path_compare("//goo/././//./boo//././//", "/goo/boo//.", 0);
+    assert_path_compare("/.", "//.///", 0);
+    assert_path_compare("/x", "x/", 1);
+    assert_path_compare("x/", "/", -1);
+    assert_path_compare("/x/./y", "x/y", 1);
+    assert_path_compare("/x/./y", "/x/y", 0);
+    assert_path_compare("/x/./././y", "/x/y/././.", 0);
+    assert_path_compare("./x/./././y", "./x/y/././.", 0);
+    assert_path_compare(".", "./.", 0);
+    assert_path_compare(".", "././.", 0);
+    assert_path_compare("./..", ".", 1);
+    assert_path_compare("x/.y", "x/y", -1);
+    assert_path_compare("foo", "/foo", -1);
+    assert_path_compare("/foo", "/foo/bar", -1);
+    assert_path_compare("/foo/aaa", "/foo/b", -1);
+    assert_path_compare("/foo/aaa", "/foo/b/a", -1);
+    assert_path_compare("/foo/a", "/foo/aaa", -1);
+    assert_path_compare("/foo/a/b", "/foo/aaa", -1);
+}
+
+/*****************************************************************************/
+
+static void
+test_path_equal(void)
+{
+#define _path_equal_check(path, expected)           \
+    G_STMT_START                                    \
+    {                                               \
+        const char   *_path0    = (path);           \
+        const char   *_expected = (expected);       \
+        gs_free char *_path     = g_strdup(_path0); \
+        const char   *_path_result;                 \
+                                                    \
+        _path_result = nm_path_simplify(_path);     \
+        g_assert(_path_result == _path);            \
+        g_assert_cmpstr(_path, ==, _expected);      \
+    }                                               \
+    G_STMT_END
+
+    _path_equal_check("", "");
+    _path_equal_check(".", ".");
+    _path_equal_check("..", "..");
+    _path_equal_check("/..", "/..");
+    _path_equal_check("//..", "/..");
+    _path_equal_check("/.", "/");
+    _path_equal_check("./", ".");
+    _path_equal_check("./.", ".");
+    _path_equal_check(".///.", ".");
+    _path_equal_check(".///./", ".");
+    _path_equal_check(".////", ".");
+    _path_equal_check("//..//foo/", "/../foo");
+    _path_equal_check("///foo//./bar/.", "/foo/bar");
+    _path_equal_check(".//./foo//./bar/.", "foo/bar");
+}
+
+/*****************************************************************************/
+
+static void
+assert_path_find_first_component(const char        *path,
+                                 gboolean           accept_dot_dot,
+                                 const char *const *expected,
+                                 int                ret)
+{
+    const char *p;
+
+    for (p = path;;) {
+        const char *e;
+        int         r;
+
+        r = nm_path_find_first_component(&p, accept_dot_dot, &e);
+        if (r <= 0) {
+            if (r == 0) {
+                if (path)
+                    g_assert(p == path + strlen(path));
+                else
+                    g_assert(!p);
+                g_assert(!e);
+            }
+            g_assert(r == ret);
+            g_assert(!expected || !*expected);
+            return;
+        }
+
+        g_assert(e);
+        g_assert(strcspn(e, "/") == (size_t) r);
+        g_assert(strlen(*expected) == (size_t) r);
+        g_assert(strncmp(e, *expected++, r) == 0);
+    }
+}
+
+static void
+test_path_find_first_component(void)
+{
+    gs_free char *hoge = NULL;
+    char          foo[NAME_MAX * 2];
+
+    /* Copied from systemd.
+     * https://github.com/systemd/systemd/blob/bc85f8b51d962597360e982811e674c126850f56/src/test/test-path-util.c#L631 */
+
+    assert_path_find_first_component(NULL, false, NULL, 0);
+    assert_path_find_first_component("", false, NULL, 0);
+    assert_path_find_first_component("/", false, NULL, 0);
+    assert_path_find_first_component(".", false, NULL, 0);
+    assert_path_find_first_component("./", false, NULL, 0);
+    assert_path_find_first_component("./.", false, NULL, 0);
+    assert_path_find_first_component("..", false, NULL, -EINVAL);
+    assert_path_find_first_component("/..", false, NULL, -EINVAL);
+    assert_path_find_first_component("./..", false, NULL, -EINVAL);
+    assert_path_find_first_component("////./././//.", false, NULL, 0);
+    assert_path_find_first_component("a/b/c", false, NM_MAKE_STRV("a", "b", "c"), 0);
+    assert_path_find_first_component("././//.///aa/bbb//./ccc",
+                                     false,
+                                     NM_MAKE_STRV("aa", "bbb", "ccc"),
+                                     0);
+    assert_path_find_first_component("././//.///aa/.../../bbb//./ccc/.",
+                                     false,
+                                     NM_MAKE_STRV("aa", "..."),
+                                     -EINVAL);
+    assert_path_find_first_component("//./aaa///.//./.bbb/..///c.//d.dd///..eeee/.",
+                                     false,
+                                     NM_MAKE_STRV("aaa", ".bbb"),
+                                     -EINVAL);
+    assert_path_find_first_component("a/foo./b", false, NM_MAKE_STRV("a", "foo.", "b"), 0);
+
+    assert_path_find_first_component(NULL, true, NULL, 0);
+    assert_path_find_first_component("", true, NULL, 0);
+    assert_path_find_first_component("/", true, NULL, 0);
+    assert_path_find_first_component(".", true, NULL, 0);
+    assert_path_find_first_component("./", true, NULL, 0);
+    assert_path_find_first_component("./.", true, NULL, 0);
+    assert_path_find_first_component("..", true, NM_MAKE_STRV(".."), 0);
+    assert_path_find_first_component("/..", true, NM_MAKE_STRV(".."), 0);
+    assert_path_find_first_component("./..", true, NM_MAKE_STRV(".."), 0);
+    assert_path_find_first_component("////./././//.", true, NULL, 0);
+    assert_path_find_first_component("a/b/c", true, NM_MAKE_STRV("a", "b", "c"), 0);
+    assert_path_find_first_component("././//.///aa/bbb//./ccc",
+                                     true,
+                                     NM_MAKE_STRV("aa", "bbb", "ccc"),
+                                     0);
+    assert_path_find_first_component("././//.///aa/.../../bbb//./ccc/.",
+                                     true,
+                                     NM_MAKE_STRV("aa", "...", "..", "bbb", "ccc"),
+                                     0);
+    assert_path_find_first_component("//./aaa///.//./.bbb/..///c.//d.dd///..eeee/.",
+                                     true,
+                                     NM_MAKE_STRV("aaa", ".bbb", "..", "c.", "d.dd", "..eeee"),
+                                     0);
+    assert_path_find_first_component("a/foo./b", true, NM_MAKE_STRV("a", "foo.", "b"), 0);
+
+    memset(foo, 'a', sizeof(foo) - 1);
+    foo[sizeof(foo) - 1] = '\0';
+
+    assert_path_find_first_component(foo, false, NULL, -EINVAL);
+    assert_path_find_first_component(foo, true, NULL, -EINVAL);
+
+    hoge = g_strjoin("", "a/b/c/", foo, "//d/e/.//f/", NULL);
+    g_assert(hoge);
+
+    assert_path_find_first_component(hoge, false, NM_MAKE_STRV("a", "b", "c"), -EINVAL);
+    assert_path_find_first_component(hoge, true, NM_MAKE_STRV("a", "b", "c"), -EINVAL);
+}
+
+/*****************************************************************************/
+
+static void
+assert_path_startswith(const char *path,
+                       const char *prefix,
+                       const char *skipped,
+                       const char *expected)
+{
+    const char *p;
+
+    p = nm_path_startswith(path, prefix);
+    g_assert_cmpstr(p, ==, expected);
+    if (p) {
+        gs_free char *q = NULL;
+
+        g_assert(skipped);
+        q = g_strjoin("", skipped, p, NULL);
+        g_assert_cmpstr(q, ==, path);
+        g_assert(p == path + strlen(skipped));
+    } else
+        g_assert(!skipped);
+}
+
+static void
+test_path_startswith(void)
+{
+    assert_path_startswith("/foo/bar/barfoo/", "/foo", "/foo/", "bar/barfoo/");
+    assert_path_startswith("/foo/bar/barfoo/", "/foo/", "/foo/", "bar/barfoo/");
+    assert_path_startswith("/foo/bar/barfoo/", "/", "/", "foo/bar/barfoo/");
+    assert_path_startswith("/foo/bar/barfoo/", "////", "/", "foo/bar/barfoo/");
+    assert_path_startswith("/foo/bar/barfoo/", "/foo//bar/////barfoo///", "/foo/bar/barfoo/", "");
+    assert_path_startswith("/foo/bar/barfoo/", "/foo/bar/barfoo////", "/foo/bar/barfoo/", "");
+    assert_path_startswith("/foo/bar/barfoo/", "/foo/bar///barfoo/", "/foo/bar/barfoo/", "");
+    assert_path_startswith("/foo/bar/barfoo/", "/foo////bar/barfoo/", "/foo/bar/barfoo/", "");
+    assert_path_startswith("/foo/bar/barfoo/", "////foo/bar/barfoo/", "/foo/bar/barfoo/", "");
+    assert_path_startswith("/foo/bar/barfoo/", "/foo/bar/barfoo", "/foo/bar/barfoo/", "");
+
+    assert_path_startswith("/foo/bar/barfoo/", "/foo/bar/barfooa/", NULL, NULL);
+    assert_path_startswith("/foo/bar/barfoo/", "/foo/bar/barfooa", NULL, NULL);
+    assert_path_startswith("/foo/bar/barfoo/", "", NULL, NULL);
+    assert_path_startswith("/foo/bar/barfoo/", "/bar/foo", NULL, NULL);
+    assert_path_startswith("/foo/bar/barfoo/", "/f/b/b/", NULL, NULL);
+    assert_path_startswith("/foo/bar/barfoo/", "/foo/bar/barfo", NULL, NULL);
+    assert_path_startswith("/foo/bar/barfoo/", "/foo/bar/bar", NULL, NULL);
+    assert_path_startswith("/foo/bar/barfoo/", "/fo", NULL, NULL);
+}
+
+/*****************************************************************************/
+
+static void
+assert_path_simplify(const char *in, const char *out)
+{
+    gs_free char *p = NULL;
+
+    g_assert(in);
+    p = g_strdup(in);
+    nm_path_simplify(p);
+    g_assert_cmpstr(p, ==, out);
+}
+
+static void
+test_path_simplify(void)
+{
+    gs_free char *hoge     = NULL;
+    gs_free char *hoge_out = NULL;
+    char          foo[NAME_MAX * 2];
+
+    assert_path_simplify("", "");
+    assert_path_simplify("aaa/bbb////ccc", "aaa/bbb/ccc");
+    assert_path_simplify("//aaa/.////ccc", "/aaa/ccc");
+    assert_path_simplify("///", "/");
+    assert_path_simplify("///.//", "/");
+    assert_path_simplify("///.//.///", "/");
+    assert_path_simplify("////.././///../.", "/../..");
+    assert_path_simplify(".", ".");
+    assert_path_simplify("./", ".");
+    assert_path_simplify(".///.//./.", ".");
+    assert_path_simplify(".///.//././/", ".");
+    assert_path_simplify("//./aaa///.//./.bbb/..///c.//d.dd///..eeee/.",
+                         "/aaa/.bbb/../c./d.dd/..eeee");
+    assert_path_simplify("//./aaa///.//./.bbb/..///c.//d.dd///..eeee/..",
+                         "/aaa/.bbb/../c./d.dd/..eeee/..");
+    assert_path_simplify(".//./aaa///.//./.bbb/..///c.//d.dd///..eeee/..",
+                         "aaa/.bbb/../c./d.dd/..eeee/..");
+    assert_path_simplify("..//./aaa///.//./.bbb/..///c.//d.dd///..eeee/..",
+                         "../aaa/.bbb/../c./d.dd/..eeee/..");
+
+    memset(foo, 'a', sizeof(foo) - 1);
+    foo[sizeof(foo) - 1] = '\0';
+
+    assert_path_simplify(foo, foo);
+
+    hoge = g_strjoin("", "/", foo, NULL);
+    g_assert(hoge);
+    assert_path_simplify(hoge, hoge);
+    nm_clear_g_free(&hoge);
+
+    hoge =
+        g_strjoin("", "a////.//././//./b///././/./c/////././//./", foo, "//.//////d/e/.//f/", NULL);
+    g_assert(hoge);
+
+    hoge_out = g_strjoin("", "a/b/c/", foo, "//.//////d/e/.//f/", NULL);
+    g_assert(hoge_out);
+
+    assert_path_simplify(hoge, hoge_out);
+}
+
+/*****************************************************************************/
+
+static void
+test_hostname_is_valid(void)
+{
+    g_assert(nm_hostname_is_valid("foobar", FALSE));
+    g_assert(nm_hostname_is_valid("foobar.com", FALSE));
+    g_assert(!nm_hostname_is_valid("foobar.com.", FALSE));
+    g_assert(nm_hostname_is_valid("fooBAR", FALSE));
+    g_assert(nm_hostname_is_valid("fooBAR.com", FALSE));
+    g_assert(!nm_hostname_is_valid("fooBAR.", FALSE));
+    g_assert(!nm_hostname_is_valid("fooBAR.com.", FALSE));
+    g_assert(!nm_hostname_is_valid("fööbar", FALSE));
+    g_assert(!nm_hostname_is_valid("", FALSE));
+    g_assert(!nm_hostname_is_valid(".", FALSE));
+    g_assert(!nm_hostname_is_valid("..", FALSE));
+    g_assert(!nm_hostname_is_valid("foobar.", FALSE));
+    g_assert(!nm_hostname_is_valid(".foobar", FALSE));
+    g_assert(!nm_hostname_is_valid("foo..bar", FALSE));
+    g_assert(!nm_hostname_is_valid("foo.bar..", FALSE));
+
+    G_STATIC_ASSERT_EXPR(NM_HOST_NAME_MAX <= HOST_NAME_MAX);
+
+#define _assert_hostname_length(n, valid)         \
+    G_STMT_START                                  \
+    {                                             \
+        const gsize   _n = (n);                   \
+        gs_free char *_h = g_strnfill(_n, 'x');   \
+        gboolean      _valid;                     \
+                                                  \
+        _valid = nm_hostname_is_valid(_h, FALSE); \
+        g_assert_cmpint(_valid, ==, (valid));     \
+    }                                             \
+    G_STMT_END
+
+    _assert_hostname_length(NM_HOST_NAME_MAX - 10, TRUE);
+    _assert_hostname_length(NM_HOST_NAME_MAX - 1, TRUE);
+    _assert_hostname_length(NM_HOST_NAME_MAX, TRUE);
+    _assert_hostname_length(NM_HOST_NAME_MAX + 1, FALSE);
+    _assert_hostname_length(NM_HOST_NAME_MAX + 10, FALSE);
+
+    g_assert(!nm_hostname_is_valid(
+        "au-xph5-rvgrdsb5hcxc-47et3a5vvkrc-server-wyoz4elpdpe3.openstack.local",
+        FALSE));
+
+    g_assert(nm_hostname_is_valid("foobar", TRUE));
+    g_assert(nm_hostname_is_valid("foobar.com", TRUE));
+    g_assert(nm_hostname_is_valid("foobar.com.", TRUE));
+    g_assert(nm_hostname_is_valid("fooBAR", TRUE));
+    g_assert(nm_hostname_is_valid("fooBAR.com", TRUE));
+    g_assert(!nm_hostname_is_valid("fooBAR.", TRUE));
+    g_assert(nm_hostname_is_valid("fooBAR.com.", TRUE));
+    g_assert(!nm_hostname_is_valid("fööbar", TRUE));
+    g_assert(!nm_hostname_is_valid("", TRUE));
+    g_assert(!nm_hostname_is_valid(".", TRUE));
+    g_assert(!nm_hostname_is_valid("..", TRUE));
+    g_assert(!nm_hostname_is_valid("foobar.", TRUE));
+    g_assert(!nm_hostname_is_valid(".foobar", TRUE));
+    g_assert(!nm_hostname_is_valid("foo..bar", TRUE));
+    g_assert(!nm_hostname_is_valid("foo.bar..", TRUE));
+    g_assert(
+        nm_hostname_is_valid("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+                             TRUE));
+    g_assert(
+        !nm_hostname_is_valid("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+                              "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+                              TRUE));
+}
+
+/*****************************************************************************/
+
 NMTST_DEFINE();
 
 int
@@ -1431,7 +2215,8 @@ main(int argc, char **argv)
     g_test_add_func("/general/test_nm_make_strv", test_make_strv);
     g_test_add_func("/general/test_nm_strdup_int", test_nm_strdup_int);
     g_test_add_func("/general/test_nm_strndup_a", test_nm_strndup_a);
-    g_test_add_func("/general/test_nm_ip4_addr_is_localhost", test_nm_ip4_addr_is_localhost);
+    g_test_add_func("/general/test_nm_utils_ip4_address_is_loopback",
+                    test_nm_utils_ip4_address_is_loopback);
     g_test_add_func("/general/test_nm_utils_ip4_prefix_to_netmask",
                     test_nm_utils_ip4_prefix_to_netmask);
     g_test_add_func("/general/test_unaligned", test_unaligned);
@@ -1450,6 +2235,17 @@ main(int argc, char **argv)
     g_test_add_func("/general/test_utils_hashtable_cmp", test_utils_hashtable_cmp);
     g_test_add_func("/general/test_nm_g_source_sentinel", test_nm_g_source_sentinel);
     g_test_add_func("/general/test_nm_ascii", test_nm_ascii);
+    g_test_add_func("/general/test_parse_env_file", test_parse_env_file);
+    g_test_add_func("/general/test_unbase64char", test_unbase64char);
+    g_test_add_func("/general/test_unbase64mem1", test_unbase64mem1);
+    g_test_add_func("/general/test_unbase64mem2", test_unbase64mem2);
+    g_test_add_func("/general/test_unbase64mem3", test_unbase64mem3);
+    g_test_add_func("/general/test_path_compare", test_path_compare);
+    g_test_add_func("/general/test_path_equal", test_path_equal);
+    g_test_add_func("/general/test_path_find_first_component", test_path_find_first_component);
+    g_test_add_func("/general/test_path_startswith", test_path_startswith);
+    g_test_add_func("/general/test_path_simplify", test_path_simplify);
+    g_test_add_func("/general/test_hostname_is_valid", test_hostname_is_valid);
 
     return g_test_run();
 }