summary refs log tree commit diff
path: root/libnm-core
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2020-06-29 22:15:58 +0200
committerMichael Biebl <biebl@debian.org>2020-06-29 22:15:58 +0200
commit10ae7d8cd706062742d0cdb1803d49909aef9e06 (patch)
treecb89e8b475cec18f22b1abfe45f1d63da7e3f440 /libnm-core
parenta54ac63bbf9b2c71026ac9028a8ffaf186cf3c82 (diff)
New upstream version 1.25.91 upstream/1.25.91
Diffstat (limited to 'libnm-core')
-rw-r--r--libnm-core/nm-core-internal.h2
-rw-r--r--libnm-core/nm-core-types.h1
-rw-r--r--libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.c30
-rw-r--r--libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.h4
-rw-r--r--libnm-core/nm-setting-ip-config.c57
-rw-r--r--libnm-core/nm-setting-ip-config.h1
-rw-r--r--libnm-core/nm-setting-match.c52
-rw-r--r--libnm-core/tests/test-general.c234
8 files changed, 354 insertions, 27 deletions
diff --git a/libnm-core/nm-core-internal.h b/libnm-core/nm-core-internal.h
index 6d7661b9..1d67b094 100644
--- a/libnm-core/nm-core-internal.h
+++ b/libnm-core/nm-core-internal.h
@@ -347,7 +347,7 @@ const char **_nm_ip_address_get_attribute_names (const NMIPAddress *addr, gboole
 
 void _nm_setting_wired_clear_s390_options (NMSettingWired *setting);
 
-gboolean _nm_ip_route_attribute_validate_all (const NMIPRoute *route);
+gboolean _nm_ip_route_attribute_validate_all (const NMIPRoute *route, GError **error);
 const char **_nm_ip_route_get_attribute_names (const NMIPRoute *route, gboolean sorted, guint *out_length);
 GHashTable *_nm_ip_route_get_attributes (NMIPRoute *route);
 
diff --git a/libnm-core/nm-core-types.h b/libnm-core/nm-core-types.h
index ce59ec03..0237e676 100644
--- a/libnm-core/nm-core-types.h
+++ b/libnm-core/nm-core-types.h
@@ -65,7 +65,6 @@ typedef struct _NMSettingWirelessSecurity NMSettingWirelessSecurity;
 typedef struct _NMSettingWpan             NMSettingWpan;
 typedef struct _NMSimpleConnection        NMSimpleConnection;
 
-NM_AVAILABLE_IN_1_26
 typedef gboolean (*NMUtilsPredicateStr) (const char *str);
 
 #endif  /* __NM_CORE_TYPES_H__ */
diff --git a/libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.c b/libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.c
index df2f2e77..8d2ea09c 100644
--- a/libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.c
+++ b/libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.c
@@ -6,6 +6,8 @@
 
 #include "nm-common-macros.h"
 
+#include <linux/rtnetlink.h>
+
 /*****************************************************************************/
 
 gboolean
@@ -178,3 +180,31 @@ nm_client_permission_result_to_string (NMClientPermissionResult permission)
 	nm_assert_not_reached ();
 	return NULL;
 }
+
+NM_UTILS_STRING_TABLE_LOOKUP_DEFINE (
+	nm_utils_route_type_by_name,
+	guint8,
+	{ nm_assert (name); },
+	{ return RTN_UNSPEC; },
+	{ "blackhole",   RTN_BLACKHOLE   },
+	{ "broadcast",   RTN_BROADCAST   },
+	{ "local",       RTN_LOCAL       },
+	{ "multicast",   RTN_MULTICAST   },
+	{ "nat",         RTN_NAT         },
+	{ "prohibit",    RTN_PROHIBIT    },
+	{ "throw",       RTN_THROW       },
+	{ "unicast",     RTN_UNICAST     },
+	{ "unreachable", RTN_UNREACHABLE },
+);
+
+NM_UTILS_ENUM2STR_DEFINE (nm_utils_route_type2str, guint8,
+	NM_UTILS_ENUM2STR (RTN_BLACKHOLE, "blackhole"),
+	NM_UTILS_ENUM2STR (RTN_BROADCAST, "broadcast"),
+	NM_UTILS_ENUM2STR (RTN_LOCAL, "local"),
+	NM_UTILS_ENUM2STR (RTN_MULTICAST, "multicast"),
+	NM_UTILS_ENUM2STR (RTN_NAT, "nat"),
+	NM_UTILS_ENUM2STR (RTN_PROHIBIT, "prohibit"),
+	NM_UTILS_ENUM2STR (RTN_THROW, "throw"),
+	NM_UTILS_ENUM2STR (RTN_UNICAST, "unicast"),
+	NM_UTILS_ENUM2STR (RTN_UNREACHABLE, "unreachable"),
+);
diff --git a/libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.h b/libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.h
index a35a5e15..afc214d4 100644
--- a/libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.h
+++ b/libnm-core/nm-libnm-core-intern/nm-libnm-core-utils.h
@@ -112,4 +112,8 @@ NMClientPermission nm_auth_permission_from_string (const char *str);
 NMClientPermissionResult nm_client_permission_result_from_string (const char *nm);
 const char *nm_client_permission_result_to_string (NMClientPermissionResult permission);
 
+guint8 nm_utils_route_type_by_name (const char *name);
+
+const char *nm_utils_route_type2str (guint8 val, char *buf, gsize len);
+
 #endif /* __NM_LIBNM_SHARED_UTILS_H__ */
diff --git a/libnm-core/nm-setting-ip-config.c b/libnm-core/nm-setting-ip-config.c
index 09a34283..bc811291 100644
--- a/libnm-core/nm-setting-ip-config.c
+++ b/libnm-core/nm-setting-ip-config.c
@@ -1214,6 +1214,7 @@ static const NMVariantAttributeSpec *const ip_route_attribute_spec[] = {
 	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE (NM_IP_ROUTE_ATTRIBUTE_SRC,           G_VARIANT_TYPE_STRING,  .v4 = TRUE, .v6 = TRUE, .str_type = 'a', ),
 	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE (NM_IP_ROUTE_ATTRIBUTE_TABLE,         G_VARIANT_TYPE_UINT32,  .v4 = TRUE, .v6 = TRUE,                  ),
 	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE (NM_IP_ROUTE_ATTRIBUTE_TOS,           G_VARIANT_TYPE_BYTE,    .v4 = TRUE,                              ),
+	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE (NM_IP_ROUTE_ATTRIBUTE_TYPE,          G_VARIANT_TYPE_STRING,  .v4 = TRUE, .v6 = TRUE, .str_type = 'T', ),
 	NM_VARIANT_ATTRIBUTE_SPEC_DEFINE (NM_IP_ROUTE_ATTRIBUTE_WINDOW,        G_VARIANT_TYPE_UINT32,  .v4 = TRUE, .v6 = TRUE,                  ),
 	NULL,
 };
@@ -1339,6 +1340,18 @@ nm_ip_route_attribute_validate  (const char *name,
 			}
 			break;
 		}
+		case 'T': /* route type. */
+			if (!NM_IN_SET (nm_utils_route_type_by_name (string),
+			                RTN_UNICAST,
+			                RTN_LOCAL)) {
+				g_set_error (error,
+				             NM_CONNECTION_ERROR,
+				             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+				             _("%s is not a valid route type"),
+				             string);
+				return FALSE;
+			}
+			break;
 		default:
 			break;
 		}
@@ -1348,22 +1361,48 @@ nm_ip_route_attribute_validate  (const char *name,
 }
 
 gboolean
-_nm_ip_route_attribute_validate_all (const NMIPRoute *route)
+_nm_ip_route_attribute_validate_all (const NMIPRoute *route, GError **error)
 {
 	GHashTableIter iter;
 	const char *key;
 	GVariant *val;
+	guint8 u8;
 
 	g_return_val_if_fail (route, FALSE);
+	g_return_val_if_fail (!error || !*error, FALSE);
 
 	if (!route->attributes)
 		return TRUE;
 
 	g_hash_table_iter_init (&iter, route->attributes);
 	while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &val)) {
-		if (!nm_ip_route_attribute_validate (key, val, route->family, NULL, NULL))
+		if (!nm_ip_route_attribute_validate (key, val, route->family, NULL, error))
 			return FALSE;
 	}
+
+	if ((val = g_hash_table_lookup (route->attributes,
+	                                NM_IP_ROUTE_ATTRIBUTE_TYPE))) {
+		nm_assert (g_variant_is_of_type (val, G_VARIANT_TYPE_STRING));
+		u8 = nm_utils_route_type_by_name (g_variant_get_string (val, NULL));
+
+		if (   u8 == RTN_LOCAL
+		    && route->family == AF_INET
+		    && (val = g_hash_table_lookup (route->attributes, NM_IP_ROUTE_ATTRIBUTE_SCOPE))) {
+			nm_assert (g_variant_is_of_type (val, G_VARIANT_TYPE_BYTE));
+			u8 = g_variant_get_byte (val);
+
+			if (!NM_IN_SET(u8,
+			               RT_SCOPE_HOST,
+			               RT_SCOPE_NOWHERE)) {
+				g_set_error (error,
+				             NM_CONNECTION_ERROR,
+				             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+				             _("route scope is invalid"));
+				return FALSE;
+			}
+		}
+	}
+
 	return TRUE;
 }
 
@@ -5020,6 +5059,7 @@ verify (NMSetting *setting, NMConnection *connection, GError **error)
 
 	/* Validate routes */
 	for (i = 0; i < priv->routes->len; i++) {
+		gs_free_error GError *local = NULL;
 		NMIPRoute *route = (NMIPRoute *) priv->routes->pdata[i];
 
 		if (nm_ip_route_get_family (route) != NM_SETTING_IP_CONFIG_GET_FAMILY (setting)) {
@@ -5031,6 +5071,19 @@ verify (NMSetting *setting, NMConnection *connection, GError **error)
 			g_prefix_error (error, "%s.%s: ", nm_setting_get_name (setting), NM_SETTING_IP_CONFIG_ROUTES);
 			return FALSE;
 		}
+
+		if (!_nm_ip_route_attribute_validate_all (route, &local)) {
+			g_set_error (error,
+			             NM_CONNECTION_ERROR,
+			             NM_CONNECTION_ERROR_INVALID_PROPERTY,
+			             _("invalid attribute: %s"),
+			             local->message);
+			g_prefix_error (error,
+			                "%s.%s: ",
+			                nm_setting_get_name (setting),
+			                NM_SETTING_IP_CONFIG_ROUTES);
+			return FALSE;
+		}
 	}
 
 	if (priv->routing_rules) {
diff --git a/libnm-core/nm-setting-ip-config.h b/libnm-core/nm-setting-ip-config.h
index ae77480c..0c655d26 100644
--- a/libnm-core/nm-setting-ip-config.h
+++ b/libnm-core/nm-setting-ip-config.h
@@ -164,6 +164,7 @@ gboolean     nm_ip_route_attribute_validate  (const char *name,
 #define NM_IP_ROUTE_ATTRIBUTE_SRC            "src"
 #define NM_IP_ROUTE_ATTRIBUTE_TABLE          "table"
 #define NM_IP_ROUTE_ATTRIBUTE_TOS            "tos"
+#define NM_IP_ROUTE_ATTRIBUTE_TYPE           "type"
 #define NM_IP_ROUTE_ATTRIBUTE_WINDOW         "window"
 
 /*****************************************************************************/
diff --git a/libnm-core/nm-setting-match.c b/libnm-core/nm-setting-match.c
index 8fb513e5..940cc68d 100644
--- a/libnm-core/nm-setting-match.c
+++ b/libnm-core/nm-setting-match.c
@@ -659,16 +659,16 @@ get_property (GObject *object, guint prop_id,
 
 	switch (prop_id) {
 	case PROP_INTERFACE_NAME:
-		g_value_set_boxed (value, nm_strvarray_get_strv (&self->interface_name, NULL));
+		g_value_set_boxed (value, nm_strvarray_get_strv_non_empty (self->interface_name, NULL));
 		break;
 	case PROP_KERNEL_COMMAND_LINE:
-		g_value_set_boxed (value, nm_strvarray_get_strv (&self->kernel_command_line, NULL));
+		g_value_set_boxed (value, nm_strvarray_get_strv_non_empty (self->kernel_command_line, NULL));
 		break;
 	case PROP_DRIVER:
-		g_value_set_boxed (value, nm_strvarray_get_strv (&self->driver, NULL));
+		g_value_set_boxed (value, nm_strvarray_get_strv_non_empty (self->driver, NULL));
 		break;
 	case PROP_PATH:
-		g_value_set_boxed (value, nm_strvarray_get_strv (&self->path, NULL));
+		g_value_set_boxed (value, nm_strvarray_get_strv_non_empty (self->path, NULL));
 		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@@ -796,6 +796,7 @@ finalize (GObject *object)
 	nm_clear_pointer (&self->interface_name, g_array_unref);
 	nm_clear_pointer (&self->kernel_command_line, g_array_unref);
 	nm_clear_pointer (&self->driver, g_array_unref);
+	nm_clear_pointer (&self->path, g_array_unref);
 
 	G_OBJECT_CLASS (nm_setting_match_parent_class)->finalize (object);
 }
@@ -816,13 +817,19 @@ nm_setting_match_class_init (NMSettingMatchClass *klass)
 	 * NMSettingMatch:interface-name
 	 *
 	 * A list of interface names to match. Each element is a shell wildcard
-	 * pattern.  When an element is prefixed with exclamation mark (!) the
-	 * condition is inverted.
+	 * pattern.
 	 *
-	 * A candidate interface name is considered matching when both these
-	 * conditions are satisfied: (a) any of the elements not prefixed with '!'
-	 * matches or there aren't such elements; (b) none of the elements
-	 * prefixed with '!' match.
+	 * An element can be prefixed with a pipe symbol (|) or an ampersand (&).
+	 * The former means that the element is optional and the latter means that
+	 * it is mandatory. If there are any optional elements, than the match
+	 * evaluates to true if at least one of the optional element matches
+	 * (logical OR). If there are any mandatory elements, then they all
+	 * must match (logical AND). By default, an element is optional. This means
+	 * that an element "foo" behaves the same as "|foo". An element can also be inverted
+	 * with exclamation mark (!) between the pipe symbol (or the ampersand) and before
+	 * the pattern. Note that "!foo" is a shortcut for the mandatory match "&!foo". Finally,
+	 * a backslash can be used at the beginning of the element (after the optional special characters)
+	 * to escape the start of the pattern. For example, "&\\!a" is an mandatory match for literally "!a".
 	 *
 	 * Since: 1.14
 	 **/
@@ -844,6 +851,10 @@ nm_setting_match_class_init (NMSettingMatchClass *klass)
 	 * of an assignment. In the latter case, the exact assignment is looked for
 	 * with right and left hand side matching.
 	 *
+	 * See NMSettingMatch:interface-name for how special characters '|', '&',
+	 * '!' and '\\' are used for optional and mandatory matches and inverting the
+	 * pattern.
+	 *
 	 * Since: 1.26
 	 **/
 	obj_properties[PROP_KERNEL_COMMAND_LINE] =
@@ -857,11 +868,10 @@ nm_setting_match_class_init (NMSettingMatchClass *klass)
 	 * NMSettingMatch:driver
 	 *
 	 * A list of driver names to match. Each element is a shell wildcard pattern.
-	 * When an element is prefixed with exclamation mark (!) the condition is
-	 * inverted. A candidate driver name is considered matching when both these
-	 * conditions are satisfied: (a) any of the elements not prefixed with '!'
-	 * matches or there aren't such elements; (b) none of the elements prefixed
-	 * with '!' match.
+	 *
+	 * See NMSettingMatch:interface-name for how special characters '|', '&',
+	 * '!' and '\\' are used for optional and mandatory matches and inverting the
+	 * pattern.
 	 *
 	 * Since: 1.26
 	 **/
@@ -872,7 +882,6 @@ nm_setting_match_class_init (NMSettingMatchClass *klass)
 	                        G_PARAM_READWRITE |
 	                        G_PARAM_STATIC_STRINGS);
 
-
 	/**
 	 * NMSettingMatch:path
 	 *
@@ -890,14 +899,11 @@ nm_setting_match_class_init (NMSettingMatchClass *klass)
 	 * property exported by NetworkManager ("nmcli -f general.path device
 	 * show $dev").
 	 *
-	 * Each element of the list is a shell wildcard pattern. When an
-	 * element is prefixed with exclamation mark (!) the condition is
-	 * inverted.
+	 * Each element of the list is a shell wildcard pattern.
 	 *
-	 * A candidate path is considered matching when both these
-	 * conditions are satisfied: (a) any of the elements not prefixed with '!'
-	 * matches or there aren't such elements; (b) none of the elements
-	 * prefixed with '!' match.
+	 * See NMSettingMatch:interface-name for how special characters '|', '&',
+	 * '!' and '\\' are used for optional and mandatory matches and inverting the
+	 * pattern.
 	 *
 	 * Since: 1.26
 	 **/
diff --git a/libnm-core/tests/test-general.c b/libnm-core/tests/test-general.c
index a6fb7007..f70a7d5a 100644
--- a/libnm-core/tests/test-general.c
+++ b/libnm-core/tests/test-general.c
@@ -13,6 +13,8 @@
 
 #include "nm-std-aux/c-list-util.h"
 #include "nm-glib-aux/nm-enum-utils.h"
+#include "nm-glib-aux/nm-str-buf.h"
+#include "systemd/nm-sd-utils-shared.h"
 
 #include "nm-utils.h"
 #include "nm-setting-private.h"
@@ -2020,6 +2022,11 @@ test_setting_ip_route_attributes (void)
 	TEST_ATTR ("src", string, "1.2.3.0/24", AF_INET,  FALSE, TRUE);
 	TEST_ATTR ("src", string, "fd01::12",   AF_INET6, TRUE,  TRUE);
 
+	TEST_ATTR ("type", string, "local", AF_INET, TRUE, TRUE);
+	TEST_ATTR ("type", string, "local", AF_INET6, TRUE, TRUE);
+	TEST_ATTR ("type", string, "unicast", AF_INET, TRUE, TRUE);
+	TEST_ATTR ("type", string, "unicast", AF_INET6, TRUE, TRUE);
+
 #undef TEST_ATTR
 }
 
@@ -8864,7 +8871,232 @@ test_connection_ovs_ifname (gconstpointer test_data)
 	}
 }
 
+/*****************************************************************************/
+
+static gboolean
+_strsplit_quoted_char_needs_escaping (char ch)
+{
+	return    NM_IN_SET (ch, '\'', '\"', '\\')
+	       || strchr (NM_ASCII_WHITESPACES, ch);
+}
+
+static char *
+_strsplit_quoted_create_str_rand (gssize len)
+{
+	NMStrBuf strbuf = NM_STR_BUF_INIT (nmtst_get_rand_uint32 () % 200, nmtst_get_rand_bool ());
+
+	g_assert (len >= -1);
+
+	if (len == -1)
+		len = nmtst_get_rand_word_length (NULL);
+
+	while (len-- > 0) {
+		char ch;
+
+		ch = nmtst_rand_select ('a', ' ', '\\', '"', '\'', nmtst_get_rand_uint32 () % 255 + 1);
+		g_assert (ch);
+		nm_str_buf_append_c (&strbuf, ch);
+	}
+
+	if (!strbuf.allocated)
+		nm_str_buf_maybe_expand (&strbuf, 1, nmtst_get_rand_bool ());
+	return nm_str_buf_finalize (&strbuf, NULL);
+}
+
+static char **
+_strsplit_quoted_create_strv_rand (void)
+{
+	guint len = nmtst_get_rand_word_length (NULL);
+	char **ptr;
+	guint i;
+
+	ptr = g_new (char *, len + 1);
+	for (i = 0; i < len; i++)
+		ptr[i] = _strsplit_quoted_create_str_rand (-1);
+	ptr[i] = NULL;
+	return ptr;
+}
+
+static char *
+_strsplit_quoted_join_strv_rand (const char *const*strv)
+{
+	NMStrBuf strbuf = NM_STR_BUF_INIT (nmtst_get_rand_uint32 () % 200, nmtst_get_rand_bool ());
+	char *result;
+	gsize l;
+	gsize l2;
+	gsize *p_l2 = nmtst_get_rand_bool () ? &l2 : NULL;
+	gsize i;
+
+	g_assert (strv);
+
+	nm_str_buf_append_c_repeated (&strbuf, ' ', nmtst_get_rand_word_length (NULL) / 4);
+	for (i = 0; strv[i]; i++) {
+		const char *s = strv[i];
+		gsize j;
+		char quote;
+
+		nm_str_buf_append_c_repeated (&strbuf, ' ', 1 + nmtst_get_rand_word_length (NULL) / 4);
+
+		j = 0;
+		quote = '\0';
+		while (TRUE) {
+			char ch = s[j++];
+
+			/* extract_first_word*/
+			if (quote != '\0') {
+				if (ch == '\0') {
+					nm_str_buf_append_c (&strbuf, quote);
+					break;
+				}
+				if (   ch == quote
+				    || ch == '\\'
+				    || nmtst_get_rand_uint32 () % 5 == 0)
+					nm_str_buf_append_c (&strbuf, '\\');
+				nm_str_buf_append_c (&strbuf, ch);
+				if (nmtst_get_rand_uint32 () % 3 == 0) {
+					nm_str_buf_append_c (&strbuf, quote);
+					quote = '\0';
+					goto next_maybe_quote;
+				}
+				continue;
+			}
+
+			if (ch == '\0') {
+				if (s == strv[i]) {
+					quote = nmtst_rand_select ('\'', '"');
+					nm_str_buf_append_c_repeated (&strbuf, quote, 2);
+				}
+				break;
+			}
+
+			if (   _strsplit_quoted_char_needs_escaping (ch)
+			    || nmtst_get_rand_uint32 () % 5 == 0)
+				nm_str_buf_append_c (&strbuf, '\\');
+
+			nm_str_buf_append_c (&strbuf, ch);
+
+next_maybe_quote:
+			if (nmtst_get_rand_uint32 () % 5 == 0) {
+				quote = nmtst_rand_select ('\'', '\"');
+				nm_str_buf_append_c (&strbuf, quote);
+				if (nmtst_get_rand_uint32 () % 5 == 0) {
+					nm_str_buf_append_c (&strbuf, quote);
+					quote = '\0';
+				}
+			}
+		}
+	}
+	nm_str_buf_append_c_repeated (&strbuf, ' ', nmtst_get_rand_word_length (NULL) / 4);
+
+	nm_str_buf_maybe_expand (&strbuf, 1, nmtst_get_rand_bool ());
+
+	l = strbuf.len;
+	result = nm_str_buf_finalize (&strbuf, p_l2);
+	g_assert (!p_l2 || l == *p_l2);
+	g_assert (strlen (result) == l);
+	return result;
+}
+
+static void
+_strsplit_quoted_assert_strv (const char *topic,
+                              const char *str,
+                              const char *const*strv1,
+                              const char *const*strv2)
+{
+	nm_auto_str_buf NMStrBuf s1 = { };
+	nm_auto_str_buf NMStrBuf s2 = { };
+	gs_free char *str_escaped = NULL;
+	int i;
+
+	g_assert (str);
+	g_assert (strv1);
+	g_assert (strv2);
+
+	if (_nm_utils_strv_equal ((char **) strv1, (char **) strv2))
+		return;
+
+	for (i = 0; strv1[i]; i++) {
+		gs_free char *s = g_strescape (strv1[i], NULL);
+
+		g_print (">>> [%s] strv1[%d] = \"%s\"\n", topic, i, s);
+		if (i > 0)
+			nm_str_buf_append_c (&s1, ' ');
+		nm_str_buf_append_printf (&s1, "\"%s\"", s);
+	}
+
+	for (i = 0; strv2[i]; i++) {
+		gs_free char *s = g_strescape (strv2[i], NULL);
+
+		g_print (">>> [%s] strv2[%d] = \"%s\"\n", topic, i, s);
+		if (i > 0)
+			nm_str_buf_append_c (&s2, ' ');
+		nm_str_buf_append_printf (&s2, "\"%s\"", s);
+	}
+
+	nm_str_buf_maybe_expand (&s1, 1, FALSE);
+	nm_str_buf_maybe_expand (&s2, 1, FALSE);
+
+	str_escaped = g_strescape (str, NULL);
+	g_error ("compared words differs: [%s] str=\"%s\"; strv1=%s; strv2=%s", topic, str_escaped, nm_str_buf_get_str (&s1), nm_str_buf_get_str (&s2));
+}
 
+static void
+_strsplit_quoted_test (const char *str,
+                       const char *const*strv_expected)
+{
+	gs_strfreev char **strv_systemd = NULL;
+	gs_strfreev char **strv_nm = NULL;
+	int r;
+
+	g_assert (str);
+
+	r = nmtst_systemd_extract_first_word_all (str, &strv_systemd);
+	g_assert_cmpint (r, ==, 1);
+	g_assert (strv_systemd);
+
+	if (!strv_expected)
+		strv_expected = (const char *const*) strv_systemd;
+
+	_strsplit_quoted_assert_strv ("systemd", str, strv_expected, (const char *const*) strv_systemd);
+
+	strv_nm = nm_utils_strsplit_quoted (str);
+	g_assert (strv_nm);
+	_strsplit_quoted_assert_strv ("nm", str, strv_expected, (const char *const*) strv_nm);
+}
+
+static void
+test_strsplit_quoted (void)
+{
+	int i_run;
+
+	_strsplit_quoted_test ("", NM_MAKE_STRV ());
+	_strsplit_quoted_test (" ", NM_MAKE_STRV ());
+	_strsplit_quoted_test ("  ", NM_MAKE_STRV ());
+	_strsplit_quoted_test ("  \t", NM_MAKE_STRV ());
+	_strsplit_quoted_test ("a b", NM_MAKE_STRV ("a", "b"));
+	_strsplit_quoted_test ("a\\ b", NM_MAKE_STRV ("a b"));
+	_strsplit_quoted_test (" a\\ \"b\"", NM_MAKE_STRV ("a b"));
+	_strsplit_quoted_test (" a\\ \"b\" c \n", NM_MAKE_STRV ("a b", "c"));
+
+	for (i_run = 0; i_run < 1000; i_run++) {
+		gs_strfreev char **strv = NULL;
+		gs_free char *str = NULL;
+
+		/* create random strv array and join them carefully so that splitting
+		 * them will yield the original value. */
+		strv = _strsplit_quoted_create_strv_rand ();
+		str = _strsplit_quoted_join_strv_rand ((const char *const*) strv);
+		_strsplit_quoted_test (str, (const char *const*) strv);
+	}
+
+	/* Create random words and assert that systemd and our implementation can
+	 * both split them (and in the exact same way). */
+	for (i_run = 0; i_run < 1000; i_run++) {
+		gs_free char *s = _strsplit_quoted_create_str_rand (nmtst_get_rand_uint32 () % 150);
+
+		_strsplit_quoted_test (s, NULL);
+	}
+}
 
 /*****************************************************************************/
 
@@ -9042,5 +9274,7 @@ int main (int argc, char **argv)
 
 	g_test_add_func ("/core/general/test_nm_ip_addr_zero", test_nm_ip_addr_zero);
 
+	g_test_add_func ("/core/general/test_strsplit_quoted", test_strsplit_quoted);
+
 	return g_test_run ();
 }