diff options
author | Thomas Haller <thaller@redhat.com> | 2020-04-04 19:53:52 +0200 |
---|---|---|
committer | Thomas Haller <thaller@redhat.com> | 2020-04-04 19:53:52 +0200 |
commit | 3670aa9bcd182505be1b88c055661ff06e4338e4 (patch) | |
tree | 43b401a0a9b65e252fc0b84b72ddec639c357930 | |
parent | 306414b93d447e915b6de94b9b7410ff7aadc4cf (diff) | |
parent | f28f06bad126a29fb285dd31ad46134dd3a1a48d (diff) | |
download | NetworkManager-3670aa9bcd182505be1b88c055661ff06e4338e4.tar.gz |
libnm,cli: merge branch 'th/libnm-setting-vpn-cleanup'
https://gitlab.freedesktop.org/NetworkManager/NetworkManager/issues/390
https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/merge_requests/453
-rw-r--r-- | NEWS | 4 | ||||
-rw-r--r-- | clients/common/nm-meta-setting-desc.c | 161 | ||||
-rw-r--r-- | libnm-core/nm-setting-vpn.c | 373 | ||||
-rw-r--r-- | libnm-core/nm-utils.c | 48 | ||||
-rw-r--r-- | libnm-core/tests/test-general.c | 346 | ||||
-rw-r--r-- | shared/nm-glib-aux/nm-macros-internal.h | 11 | ||||
-rw-r--r-- | shared/nm-glib-aux/nm-shared-utils.c | 311 | ||||
-rw-r--r-- | shared/nm-glib-aux/nm-shared-utils.h | 144 | ||||
-rw-r--r-- | shared/nm-utils/nm-test-utils.h | 63 |
9 files changed, 1118 insertions, 343 deletions
@@ -8,6 +8,10 @@ The API is subject to change and not guaranteed to be compatible with the later release. USE AT YOUR OWN RISK. NOT RECOMMENDED FOR PRODUCTION USE! +* Accept empty values for VPN data items and secrets. +* nmcli: support backslash escape sequences for vpn.data, vpn.secrets, + bond.options, and ethernet.s390-options. + ============================================= NetworkManager-1.22 Overview of changes since NetworkManager-1.20 diff --git a/clients/common/nm-meta-setting-desc.c b/clients/common/nm-meta-setting-desc.c index c77c1367b7..27f1d2882b 100644 --- a/clients/common/nm-meta-setting-desc.c +++ b/clients/common/nm-meta-setting-desc.c @@ -190,8 +190,6 @@ _value_strsplit (const char *value, gsize *out_len) { gs_free const char **strv = NULL; - gsize i; - gsize len; /* FIXME: some modes should support backslash escaping. * In particular, to distinguish from _value_str_as_index_list(), which @@ -200,43 +198,24 @@ _value_strsplit (const char *value, /* note that all modes remove empty tokens (",", "a,,b", ",,"). */ switch (split_mode) { case VALUE_STRSPLIT_MODE_OBJLIST: - strv = nm_utils_strsplit_set (value, ESCAPED_TOKENS_DELIMITERS); + strv = nm_utils_strsplit_set_full (value, + ESCAPED_TOKENS_DELIMITERS, + NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP); break; case VALUE_STRSPLIT_MODE_MULTILIST: - strv = nm_utils_strsplit_set (value, ESCAPED_TOKENS_WITH_SPACES_DELIMTERS); + strv = nm_utils_strsplit_set_full (value, + ESCAPED_TOKENS_WITH_SPACES_DELIMTERS, + NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP); break; case VALUE_STRSPLIT_MODE_ESCAPED_TOKENS: strv = nm_utils_escaped_tokens_split (value, ESCAPED_TOKENS_DELIMITERS); - NM_SET_OUT (out_len, NM_PTRARRAY_LEN (strv)); - return g_steal_pointer (&strv); + break; case VALUE_STRSPLIT_MODE_ESCAPED_TOKENS_WITH_SPACES: strv = nm_utils_escaped_tokens_split (value, ESCAPED_TOKENS_WITH_SPACES_DELIMTERS); - NM_SET_OUT (out_len, NM_PTRARRAY_LEN (strv)); - return g_steal_pointer (&strv); - default: - nm_assert_not_reached (); break; } - NM_SET_OUT (out_len, 0); - - if (!strv) - return NULL; - - len = 0; - for (i = 0; strv[i]; i++) { - const char *s = strv[i]; - - s = nm_str_skip_leading_spaces (s); - if (s[0] == '\0') - continue; - - g_strchomp ((char *) s); - strv[len++] = s; - } - strv[len] = NULL; - - NM_SET_OUT (out_len, len); + NM_SET_OUT (out_len, NM_PTRARRAY_LEN (strv)); return g_steal_pointer (&strv); } @@ -638,6 +617,8 @@ _env_warn_fcn (const NMMetaEnvironment *environment, #define ARGS_SETTING_INIT_FCN \ const NMMetaSettingInfoEditor *setting_info, NMSetting *setting, NMMetaAccessorSettingInitType init_type +static gboolean _set_fcn_optionlist (ARGS_SET_FCN); + static gboolean _SET_FCN_DO_RESET_DEFAULT (const NMMetaPropertyInfo *property_info, NMMetaAccessorModifier modifier, const char *value) { @@ -904,6 +885,10 @@ _get_fcn_gobject_impl (const NMMetaPropertyInfo *property_info, GString *str; gsize i; + nm_assert ( property_info->setting_info == &nm_meta_setting_infos_editor[NM_META_SETTING_TYPE_WIRED] + && NM_IN_STRSET (property_info->property_name, NM_SETTING_WIRED_S390_OPTIONS)); + nm_assert (property_info->property_type->set_fcn == _set_fcn_optionlist); + strdict = g_value_get_boxed (&val); keys = nm_utils_strdict_get_keys (strdict, TRUE, NULL); if (!keys) @@ -911,12 +896,16 @@ _get_fcn_gobject_impl (const NMMetaPropertyInfo *property_info, str = g_string_new (NULL); for (i = 0; keys[i]; i++) { + const char *key = keys[i]; + const char *v = g_hash_table_lookup (strdict, key); + gs_free char *escaped_key = NULL; + gs_free char *escaped_val = NULL; + if (str->len > 0) g_string_append_c (str, ','); - g_string_append_printf (str, - "%s=%s", - keys[i], - (const char *) g_hash_table_lookup (strdict, keys[i])); + g_string_append (str, nm_utils_escaped_tokens_options_escape_key (key, &escaped_key)); + g_string_append_c (str, '='); + g_string_append (str, nm_utils_escaped_tokens_options_escape_val (v, &escaped_val)); } RETURN_STR_TO_FREE (g_string_free (str, FALSE)); } @@ -1761,17 +1750,6 @@ secret_flags_to_string (guint32 flags, NMMetaAccessorGetType get_type) return g_string_free (flag_str, FALSE); } -static void -vpn_data_item (const char *key, const char *value, gpointer user_data) -{ - GString *ret_str = (GString *) user_data; - - if (ret_str->len != 0) - g_string_append (ret_str, ", "); - - g_string_append_printf (ret_str, "%s = %s", key, value); -} - static const char * _multilist_do_validate (const NMMetaPropertyInfo *property_info, NMSetting *setting, @@ -1919,6 +1897,7 @@ _set_fcn_optionlist (ARGS_SET_FCN) { gs_free const char **strv = NULL; gs_free const char **strv_val = NULL; + gsize strv_len; gsize i, nstrv; nm_assert (!error || !*error); @@ -1927,24 +1906,16 @@ _set_fcn_optionlist (ARGS_SET_FCN) return _gobject_property_reset_default (setting, property_info->property_name); nstrv = 0; - strv = nm_utils_strsplit_set (value, ","); + strv = nm_utils_escaped_tokens_options_split_list (value); if (strv) { - strv_val = g_new (const char *, NM_PTRARRAY_LEN (strv)); + strv_len = NM_PTRARRAY_LEN (strv); + + strv_val = g_new (const char *, strv_len); for (i = 0; strv[i]; i++) { const char *opt_name; const char *opt_value; - opt_name = nm_str_skip_leading_spaces (strv[i]); - - /* FIXME: support backslash escaping for the option list. */ - opt_value = strchr (opt_name, '='); - if (opt_value) { - ((char *) opt_value)[0] = '\0'; - opt_value++; - opt_value = nm_str_skip_leading_spaces (opt_value); - g_strchomp ((char *) opt_value); - } - g_strchomp ((char *) opt_name); + nm_utils_escaped_tokens_options_split ((char *) strv[i], &opt_name, &opt_value); if ( property_info->property_type->values_fcn || property_info->property_typ_data->values_static) { @@ -2315,33 +2286,41 @@ static gconstpointer _get_fcn_bond_options (ARGS_GET_FCN) { NMSettingBond *s_bond = NM_SETTING_BOND (setting); - GString *bond_options_s; - int i; + GString *str; + guint32 i, len; RETURN_UNSUPPORTED_GET_TYPE (); - bond_options_s = g_string_new (NULL); - for (i = 0; i < nm_setting_bond_get_num_options (s_bond); i++) { - const char *key, *value; - gs_free char *tmp_value = NULL; + str = g_string_new (NULL); + len = nm_setting_bond_get_num_options (s_bond); + for (i = 0; i < len; i++) { + const char *key; + const char *val; + gs_free char *val_tmp = NULL; char *p; + gs_free char *escaped_key = NULL; + gs_free char *escaped_val = NULL; - nm_setting_bond_get_option (s_bond, i, &key, &value); + nm_setting_bond_get_option (s_bond, i, &key, &val); - if (nm_streq0 (key, NM_SETTING_BOND_OPTION_ARP_IP_TARGET)) { - value = tmp_value = g_strdup (value); - for (p = tmp_value; p && *p; p++) { + if (nm_streq (key, NM_SETTING_BOND_OPTION_ARP_IP_TARGET)) { + val_tmp = g_strdup (val); + for (p = val_tmp; p && *p; p++) { if (*p == ',') *p = ' '; } + val = val_tmp; } - g_string_append_printf (bond_options_s, "%s=%s,", key, value); + if (str->len > 0u) + g_string_append_c (str, ','); + g_string_append (str, nm_utils_escaped_tokens_options_escape_key (key, &escaped_key)); + g_string_append_c (str, '='); + g_string_append (str, nm_utils_escaped_tokens_options_escape_val (val, &escaped_val)); } - g_string_truncate (bond_options_s, bond_options_s->len-1); /* chop off trailing ',' */ - NM_SET_OUT (out_is_default, bond_options_s->len == 0); - RETURN_STR_TO_FREE (g_string_free (bond_options_s, FALSE)); + NM_SET_OUT (out_is_default, str->len == 0); + RETURN_STR_TO_FREE (g_string_free (str, FALSE)); } static gboolean @@ -3815,32 +3794,36 @@ _set_fcn_vlan_xgress_priority_map (ARGS_SET_FCN) return TRUE; } -static gconstpointer -_get_fcn_vpn_data (ARGS_GET_FCN) +static void +_vpn_options_callback (const char *key, const char *val, gpointer user_data) { - NMSettingVpn *s_vpn = NM_SETTING_VPN (setting); - GString *data_item_str; + GString *str = user_data; + gs_free char *escaped_key = NULL; + gs_free char *escaped_val = NULL; - RETURN_UNSUPPORTED_GET_TYPE (); + if (str->len > 0u) + g_string_append (str, ", "); - data_item_str = g_string_new (NULL); - nm_setting_vpn_foreach_data_item (s_vpn, &vpn_data_item, data_item_str); - NM_SET_OUT (out_is_default, data_item_str->len == 0); - RETURN_STR_TO_FREE (g_string_free (data_item_str, FALSE)); + g_string_append (str, nm_utils_escaped_tokens_options_escape_key (key, &escaped_key)); + g_string_append (str, " = "); + g_string_append (str, nm_utils_escaped_tokens_options_escape_val (val, &escaped_val)); } static gconstpointer -_get_fcn_vpn_secrets (ARGS_GET_FCN) +_get_fcn_vpn_options (ARGS_GET_FCN) { NMSettingVpn *s_vpn = NM_SETTING_VPN (setting); - GString *secret_str; + GString *str; RETURN_UNSUPPORTED_GET_TYPE (); - secret_str = g_string_new (NULL); - nm_setting_vpn_foreach_secret (s_vpn, &vpn_data_item, secret_str); - NM_SET_OUT (out_is_default, secret_str->len == 0); - RETURN_STR_TO_FREE (g_string_free (secret_str, FALSE)); + str = g_string_new (NULL); + if (nm_streq (property_info->property_name, NM_SETTING_VPN_SECRETS)) + nm_setting_vpn_foreach_secret (s_vpn, _vpn_options_callback, str); + else + nm_setting_vpn_foreach_data_item (s_vpn, _vpn_options_callback, str); + NM_SET_OUT (out_is_default, str->len == 0); + RETURN_STR_TO_FREE (g_string_free (str, FALSE)); } static gboolean @@ -6884,25 +6867,23 @@ static const NMMetaPropertyInfo *const property_infos_VPN[] = { ), PROPERTY_INFO_WITH_DESC (NM_SETTING_VPN_DATA, .property_type = DEFINE_PROPERTY_TYPE ( - .get_fcn = _get_fcn_vpn_data, + .get_fcn = _get_fcn_vpn_options, .set_fcn = _set_fcn_optionlist, .set_supports_remove = TRUE, ), .property_typ_data = DEFINE_PROPERTY_TYP_DATA_SUBTYPE (optionlist, .set_fcn = _optionlist_set_fcn_vpn_data, - .no_empty_value = TRUE, ), ), PROPERTY_INFO_WITH_DESC (NM_SETTING_VPN_SECRETS, .is_secret = TRUE, .property_type = DEFINE_PROPERTY_TYPE ( - .get_fcn = _get_fcn_vpn_secrets, + .get_fcn = _get_fcn_vpn_options, .set_fcn = _set_fcn_optionlist, .set_supports_remove = TRUE, ), .property_typ_data = DEFINE_PROPERTY_TYP_DATA_SUBTYPE (optionlist, .set_fcn = _optionlist_set_fcn_vpn_secrets, - .no_empty_value = TRUE, ), ), PROPERTY_INFO_WITH_DESC (NM_SETTING_VPN_PERSISTENT, diff --git a/libnm-core/nm-setting-vpn.c b/libnm-core/nm-setting-vpn.c index 05fca918ff..baca3f3222 100644 --- a/libnm-core/nm-setting-vpn.c +++ b/libnm-core/nm-setting-vpn.c @@ -81,6 +81,23 @@ G_DEFINE_TYPE (NMSettingVpn, nm_setting_vpn, NM_TYPE_SETTING) /*****************************************************************************/ +static GHashTable * +_ensure_strdict (GHashTable **p_hash, gboolean for_secrets) +{ + if (!*p_hash) { + *p_hash = g_hash_table_new_full (nm_str_hash, + g_str_equal, + g_free, + for_secrets + ? (GDestroyNotify) nm_free_secret + : g_free); + } + return *p_hash; +} + + +/*****************************************************************************/ + /** * nm_setting_vpn_get_service_type: * @setting: the #NMSettingVpn @@ -139,30 +156,39 @@ nm_setting_vpn_get_num_data_items (NMSettingVpn *setting) { g_return_val_if_fail (NM_IS_SETTING_VPN (setting), 0); - return g_hash_table_size (NM_SETTING_VPN_GET_PRIVATE (setting)->data); + return nm_g_hash_table_size (NM_SETTING_VPN_GET_PRIVATE (setting)->data); } /** * nm_setting_vpn_add_data_item: * @setting: the #NMSettingVpn * @key: a name that uniquely identifies the given value @item - * @item: the value to be referenced by @key + * @item: (allow-none): the value to be referenced by @key * * Establishes a relationship between @key and @item internally in the * setting which may be retrieved later. Should not be used to store passwords * or other secrets, which is what nm_setting_vpn_add_secret() is for. + * + * Before 1.24, @item must not be %NULL and not an empty string. Since 1.24, + * @item can be set to an empty string. It can also be set to %NULL to unset + * the key. In that case, the behavior is as if calling nm_setting_vpn_remove_data_item(). **/ void nm_setting_vpn_add_data_item (NMSettingVpn *setting, const char *key, const char *item) { + if (!item) { + nm_setting_vpn_remove_data_item (setting, key); + return; + } + g_return_if_fail (NM_IS_SETTING_VPN (setting)); g_return_if_fail (key && key[0]); - g_return_if_fail (item && item[0]); - g_hash_table_insert (NM_SETTING_VPN_GET_PRIVATE (setting)->data, - g_strdup (key), g_strdup (item)); + g_hash_table_insert (_ensure_strdict (&NM_SETTING_VPN_GET_PRIVATE (setting)->data, FALSE), + g_strdup (key), + g_strdup (item)); _notify (setting, PROP_DATA); } @@ -180,8 +206,9 @@ const char * nm_setting_vpn_get_data_item (NMSettingVpn *setting, const char *key) { g_return_val_if_fail (NM_IS_SETTING_VPN (setting), NULL); + g_return_val_if_fail (key && key[0], NULL); - return (const char *) g_hash_table_lookup (NM_SETTING_VPN_GET_PRIVATE (setting)->data, key); + return nm_g_hash_table_lookup (NM_SETTING_VPN_GET_PRIVATE (setting)->data, key); } /** @@ -222,63 +249,58 @@ nm_setting_vpn_get_data_keys (NMSettingVpn *setting, gboolean nm_setting_vpn_remove_data_item (NMSettingVpn *setting, const char *key) { - gboolean found; - g_return_val_if_fail (NM_IS_SETTING_VPN (setting), FALSE); - g_return_val_if_fail (key, FALSE); + g_return_val_if_fail (key && key[0], FALSE); - found = g_hash_table_remove (NM_SETTING_VPN_GET_PRIVATE (setting)->data, key); - if (found) + if (nm_g_hash_table_remove (NM_SETTING_VPN_GET_PRIVATE (setting)->data, key)) { _notify (setting, PROP_DATA); - return found; + return TRUE; + } + return FALSE; } static void foreach_item_helper (NMSettingVpn *self, - gboolean is_secrets, + GHashTable **p_hash, NMVpnIterFunc func, gpointer user_data) { - NMSettingVpnPrivate *priv; - guint len, i; + gs_unref_object NMSettingVpn *self_keep_alive = NULL; gs_strfreev char **keys = NULL; - GHashTable *hash; + guint i, len; nm_assert (NM_IS_SETTING_VPN (self)); nm_assert (func); - priv = NM_SETTING_VPN_GET_PRIVATE (self); - - if (is_secrets) { - keys = (char **) nm_setting_vpn_get_secret_keys (self, &len); - hash = priv->secrets; - } else { - keys = (char **) nm_setting_vpn_get_data_keys (self, &len); - hash = priv->data; - } - - if (!len) { + keys = nm_utils_strv_make_deep_copied (nm_utils_strdict_get_keys (*p_hash, + TRUE, + &len)); + if (len == 0u) { nm_assert (!keys); return; } - for (i = 0; i < len; i++) { - nm_assert (keys && keys[i]); - keys[i] = g_strdup (keys[i]); - } - nm_assert (!keys[i]); + if (len > 1u) + self_keep_alive = g_object_ref (self); for (i = 0; i < len; i++) { - const char *value; - - value = g_hash_table_lookup (hash, keys[i]); /* NOTE: note that we call the function with a clone of @key, * not with the actual key from the dictionary. * - * The @value on the other hand, is actually inside our dictionary, - * it's not a clone. However, it might be %NULL, in case the key was - * deleted while iterating. */ - func (keys[i], value, user_data); + * The @value on the other hand, is not cloned but retrieved before + * invoking @func(). That means, if @func() modifies the setting while + * being called, the values are as they currently are, but the + * keys (and their order) were pre-determined before starting to + * invoke the callbacks. + * + * The idea is to give some sensible, stable behavior in case the user + * modifies the settings. Whether this particular behavior is optimal + * is unclear. It's probably a bad idea to modify the settings while + * iterating the values. But at least, it's a safe thing to do and we + * do something sensible. */ + func (keys[i], + nm_g_hash_table_lookup (*p_hash, keys[i]), + user_data); } } @@ -300,7 +322,7 @@ nm_setting_vpn_foreach_data_item (NMSettingVpn *setting, g_return_if_fail (NM_IS_SETTING_VPN (setting)); g_return_if_fail (func); - foreach_item_helper (setting, FALSE, func, user_data); + foreach_item_helper (setting, &NM_SETTING_VPN_GET_PRIVATE (setting)->data, func, user_data); } /** @@ -316,29 +338,38 @@ nm_setting_vpn_get_num_secrets (NMSettingVpn *setting) { g_return_val_if_fail (NM_IS_SETTING_VPN (setting), 0); - return g_hash_table_size (NM_SETTING_VPN_GET_PRIVATE (setting)->secrets); + return nm_g_hash_table_size (NM_SETTING_VPN_GET_PRIVATE (setting)->secrets); } /** * nm_setting_vpn_add_secret: * @setting: the #NMSettingVpn * @key: a name that uniquely identifies the given secret @secret - * @secret: the secret to be referenced by @key + * @secret: (allow-none): the secret to be referenced by @key * * Establishes a relationship between @key and @secret internally in the * setting which may be retrieved later. + * + * Before 1.24, @secret must not be %NULL and not an empty string. Since 1.24, + * @secret can be set to an empty string. It can also be set to %NULL to unset + * the key. In that case, the behavior is as if calling nm_setting_vpn_remove_secret(). **/ void nm_setting_vpn_add_secret (NMSettingVpn *setting, const char *key, const char *secret) { + if (!secret) { + nm_setting_vpn_remove_secret (setting, key); + return; + } + g_return_if_fail (NM_IS_SETTING_VPN (setting)); g_return_if_fail (key && key[0]); - g_return_if_fail (secret && secret[0]); - g_hash_table_insert (NM_SETTING_VPN_GET_PRIVATE (setting)->secrets, - g_strdup (key), g_strdup (secret)); + g_hash_table_insert (_ensure_strdict (&NM_SETTING_VPN_GET_PRIVATE (setting)->secrets, TRUE), + g_strdup (key), + g_strdup (secret)); _notify (setting, PROP_SECRETS); } @@ -356,8 +387,9 @@ const char * nm_setting_vpn_get_secret (NMSettingVpn *setting, const char *key) { g_return_val_if_fail (NM_IS_SETTING_VPN (setting), NULL); + g_return_val_if_fail (key && key[0], NULL); - return (const char *) g_hash_table_lookup (NM_SETTING_VPN_GET_PRIVATE (setting)->secrets, key); + return nm_g_hash_table_lookup (NM_SETTING_VPN_GET_PRIVATE (setting)->secrets, key); } /** @@ -398,15 +430,14 @@ nm_setting_vpn_get_secret_keys (NMSettingVpn *setting, gboolean nm_setting_vpn_remove_secret (NMSettingVpn *setting, const char *key) { - gboolean found; - g_return_val_if_fail (NM_IS_SETTING_VPN (setting), FALSE); - g_return_val_if_fail (key, FALSE); + g_return_val_if_fail (key && key[0], FALSE); - found = g_hash_table_remove (NM_SETTING_VPN_GET_PRIVATE (setting)->secrets, key); - if (found) + if (nm_g_hash_table_remove (NM_SETTING_VPN_GET_PRIVATE (setting)->secrets, key)) { _notify (setting, PROP_SECRETS); - return found; + return TRUE; + } + return FALSE; } /** @@ -427,7 +458,7 @@ nm_setting_vpn_foreach_secret (NMSettingVpn *setting, g_return_if_fail (NM_IS_SETTING_VPN (setting)); g_return_if_fail (func); - foreach_item_helper (setting, TRUE, func, user_data); + foreach_item_helper (setting, &NM_SETTING_VPN_GET_PRIVATE (setting)->secrets, func, user_data); } static gboolean @@ -444,7 +475,7 @@ aggregate (NMSetting *setting, switch (type) { case NM_CONNECTION_AGGREGATE_ANY_SECRETS: - if (g_hash_table_size (priv->secrets) > 0) { + if (nm_g_hash_table_size (priv->secrets) > 0u) { *((gboolean *) arg) = TRUE; return TRUE; } @@ -452,32 +483,36 @@ aggregate (NMSetting *setting, case NM_CONNECTION_AGGREGATE_ANY_SYSTEM_SECRET_FLAGS: - g_hash_table_iter_init (&iter, priv->secrets); - while (g_hash_table_iter_next (&iter, (gpointer *) &key_name, NULL)) { - if (!nm_setting_get_secret_flags (NM_SETTING (setting), key_name, &secret_flags, NULL)) - nm_assert_not_reached (); - if (secret_flags == NM_SETTING_SECRET_FLAG_NONE) { - *((gboolean *) arg) = TRUE; - return TRUE; + if (priv->secrets) { + g_hash_table_iter_init (&iter, priv->secrets); + while (g_hash_table_iter_next (&iter, (gpointer *) &key_name, NULL)) { + if (!nm_setting_get_secret_flags (NM_SETTING (setting), key_name, &secret_flags, NULL)) + nm_assert_not_reached (); + if (secret_flags == NM_SETTING_SECRET_FLAG_NONE) { + *((gboolean *) arg) = TRUE; + return TRUE; + } } } - /* Ok, we have no secrets with system-secret flags. + /* OK, we have no secrets with system-secret flags. * But do we have any secret-flags (without secrets) that indicate system secrets? */ - g_hash_table_iter_init (&iter, priv->data); - while (g_hash_table_iter_next (&iter, (gpointer *) &key_name, NULL)) { - gs_free char *secret_name = NULL; + if (priv->data) { + g_hash_table_iter_init (&iter, priv->data); + while (g_hash_table_iter_next (&iter, (gpointer *) &key_name, NULL)) { + gs_free char *secret_name = NULL; - if (!g_str_has_suffix (key_name, "-flags")) - continue; - secret_name = g_strndup (key_name, strlen (key_name) - NM_STRLEN ("-flags")); - if (secret_name[0] == '\0') - continue; - if (!nm_setting_get_secret_flags (NM_SETTING (setting), secret_name, &secret_flags, NULL)) - nm_assert_not_reached (); - if (secret_flags == NM_SETTING_SECRET_FLAG_NONE) { - *((gboolean *) arg) = TRUE; - return TRUE; + if (!NM_STR_HAS_SUFFIX (key_name, "-flags")) + continue; + secret_name = g_strndup (key_name, strlen (key_name) - NM_STRLEN ("-flags")); + if (secret_name[0] == '\0') + continue; + if (!nm_setting_get_secret_flags (NM_SETTING (setting), secret_name, &secret_flags, NULL)) + nm_assert_not_reached (); + if (secret_flags == NM_SETTING_SECRET_FLAG_NONE) { + *((gboolean *) arg) = TRUE; + return TRUE; + } } } @@ -517,8 +552,7 @@ verify (NMSetting *setting, NMConnection *connection, GError **error) g_prefix_error (error, "%s.%s: ", NM_SETTING_VPN_SETTING_NAME, NM_SETTING_VPN_SERVICE_TYPE); return FALSE; } - - if (!strlen (priv->service_type)) { + if (!priv->service_type[0]) { g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY, @@ -528,7 +562,8 @@ verify (NMSetting *setting, NMConnection *connection, GError **error) } /* default username can be NULL, but can't be zero-length */ - if (priv->user_name && !strlen (priv->user_name)) { + if ( priv->user_name + && !priv->user_name[0]) { g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY, @@ -558,21 +593,15 @@ update_secret_string (NMSetting *setting, { NMSettingVpnPrivate *priv = NM_SETTING_VPN_GET_PRIVATE (setting); - g_return_val_if_fail (key != NULL, NM_SETTING_UPDATE_SECRET_ERROR); - g_return_val_if_fail (value != NULL, NM_SETTING_UPDATE_SECRET_ERROR); - - if (!value || !strlen (value)) { - g_set_error (error, NM_CONNECTION_ERROR, - NM_CONNECTION_ERROR_INVALID_PROPERTY, - _("secret was empty")); - g_prefix_error (error, "%s.%s: ", NM_SETTING_VPN_SETTING_NAME, key); - return NM_SETTING_UPDATE_SECRET_ERROR; - } + g_return_val_if_fail (key && key[0], NM_SETTING_UPDATE_SECRET_ERROR); + g_return_val_if_fail (value, NM_SETTING_UPDATE_SECRET_ERROR); - if (g_strcmp0 (g_hash_table_lookup (priv->secrets, key), value) == 0) + if (nm_streq0 (nm_g_hash_table_lookup (priv->secrets, key), value)) return NM_SETTING_UPDATE_SECRET_SUCCESS_UNCHANGED; - g_hash_table_insert (priv->secrets, g_strdup (key), g_strdup (value)); + g_hash_table_insert (_ensure_strdict (&priv->secrets, TRUE), + g_strdup (key), + g_strdup (value)); return NM_SETTING_UPDATE_SECRET_SUCCESS_MODIFIED; } @@ -591,39 +620,24 @@ update_secret_dict (NMSetting *setting, /* Make sure the items are valid */ g_variant_iter_init (&iter, secrets); while (g_variant_iter_next (&iter, "{&s&s}", &name, &value)) { - if (!name || !strlen (name)) { + if (!name[0]) { g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_SETTING, _("setting contained a secret with an empty name")); g_prefix_error (error, "%s: ", NM_SETTING_VPN_SETTING_NAME); return NM_SETTING_UPDATE_SECRET_ERROR; } - - if (!value || !strlen (value)) { - g_set_error (error, NM_CONNECTION_ERROR, - NM_CONNECTION_ERROR_INVALID_PROPERTY, - _("secret value was empty")); - g_prefix_error (error, "%s.%s: ", NM_SETTING_VPN_SETTING_NAME, name); - return NM_SETTING_UPDATE_SECRET_ERROR; - } } /* Now add the items to the settings' secrets list */ g_variant_iter_init (&iter, secrets); while (g_variant_iter_next (&iter, "{&s&s}", &name, &value)) { - if (value == NULL) { - g_warn_if_fail (value != NULL); - continue; - } - if (strlen (value) == 0) { - g_warn_if_fail (strlen (value) > 0); + if (nm_streq0 (nm_g_hash_table_lookup (priv->secrets, name), value)) continue; - } - if (g_strcmp0 (g_hash_table_lookup (priv->secrets, name), value) == 0) - continue; - - g_hash_table_insert (priv->secrets, g_strdup (name), g_strdup (value)); + g_hash_table_insert (_ensure_strdict (&priv->secrets, TRUE), + g_strdup (name), + g_strdup (value)); result = NM_SETTING_UPDATE_SECRET_SUCCESS_MODIFIED; } @@ -646,7 +660,7 @@ update_one_secret (NMSetting *setting, const char *key, GVariant *value, GError */ success = update_secret_string (setting, key, g_variant_get_string (value, NULL), error); } else if (g_variant_is_of_type (value, G_VARIANT_TYPE ("a{ss}"))) { - if (strcmp (key, NM_SETTING_VPN_SECRETS) != 0) { + if (!nm_streq (key, NM_SETTING_VPN_SECRETS)) { g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_PROPERTY_NOT_SECRET, _("not a secret property")); @@ -728,14 +742,23 @@ get_secret_flags (NMSetting *setting, const char *flags_val; gint64 i64; + nm_assert (secret_name); + + if (!secret_name[0]) { + g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_PROPERTY_NOT_SECRET, + _("secret name cannot be empty")); + return FALSE; + } + flags_key = nm_construct_name_a ("%s-flags", secret_name, &flags_key_free); - if (!g_hash_table_lookup_extended (priv->data, flags_key, NULL, (gpointer *) &flags_val)) { + if ( !priv->data + || !g_hash_table_lookup_extended (priv->data, flags_key, NULL, (gpointer *) &flags_val)) { NM_SET_OUT (out_flags, NM_SETTING_SECRET_FLAG_NONE); /* having no secret flag for the secret is fine, as long as there * is the secret itself... */ - if (!g_hash_table_contains (priv->secrets, secret_name)) { + if (!nm_g_hash_table_lookup (priv->secrets, secret_name)) { g_set_error_literal (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_PROPERTY_NOT_SECRET, @@ -768,7 +791,15 @@ set_secret_flags (NMSetting *setting, NMSettingSecretFlags flags, GError **error) { - g_hash_table_insert (NM_SETTING_VPN_GET_PRIVATE (setting)->data, + nm_assert (secret_name); + + if (!secret_name[0]) { + g_set_error (error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_PROPERTY_NOT_SECRET, + _("secret name cannot be empty")); + return FALSE; + } + + g_hash_table_insert (_ensure_strdict (&NM_SETTING_VPN_GET_PRIVATE (setting)->data, FALSE), g_strdup_printf ("%s-flags", secret_name), g_strdup_printf ("%u", flags)); _notify (NM_SETTING_VPN (setting), PROP_SECRETS); @@ -802,8 +833,12 @@ compare_property_secrets (NMSettingVpn *a, for (run = 0; run < 2; run++) { NMSettingVpn *current_a = (run == 0) ? a : b; NMSettingVpn *current_b = (run == 0) ? b : a; + NMSettingVpnPrivate *priv_a = NM_SETTING_VPN_GET_PRIVATE (current_a); + + if (!priv_a->secrets) + continue; - g_hash_table_iter_init (&iter, NM_SETTING_VPN_GET_PRIVATE (current_a)->secrets); + g_hash_table_iter_init (&iter, priv_a->secrets); while (g_hash_table_iter_next (&iter, (gpointer) &key, (gpointer) &val)) { if (nm_streq0 (val, nm_setting_vpn_get_secret (current_b, key))) @@ -899,11 +934,26 @@ vpn_secrets_from_dbus (NMSetting *setting, NMSettingParseFlags parse_flags, GError **error) { - nm_auto_unset_gvalue GValue object_value = G_VALUE_INIT; + NMSettingVpn *self = NM_SETTING_VPN (setting); + NMSettingVpnPrivate *priv = NM_SETTING_VPN_GET_PRIVATE (self); + gs_unref_hashtable GHashTable *hash_free = NULL; + GVariantIter iter; + const char *key; + const char *val; + + hash_free = g_steal_pointer (&priv->secrets); + + g_variant_iter_init (&iter, value); + while (g_variant_iter_next (&iter, "{&s&s}", &key, &val)) { + if (!key[0]) + continue; + g_hash_table_insert (_ensure_strdict (&priv->secrets, TRUE), + g_strdup (key), + g_strdup (val)); + } - g_value_init (&object_value, G_TYPE_HASH_TABLE); - _nm_utils_strdict_from_dbus (value, &object_value); - return nm_g_object_set_property (G_OBJECT (setting), property, &object_value, error); + _notify (self, PROP_SECRETS); + return TRUE; } static GVariant * @@ -914,29 +964,30 @@ vpn_secrets_to_dbus (const NMSettInfoSetting *sett_info, NMConnectionSerializationFlags flags, const NMConnectionSerializationOptions *options) { - gs_unref_hashtable GHashTable *secrets = NULL; - const char *property_name = sett_info->property_infos[property_idx].name; + NMSettingVpnPrivate *priv = NM_SETTING_VPN_GET_PRIVATE (setting); GVariantBuilder builder; - GHashTableIter iter; - const char *key, *value; - NMSettingSecretFlags secret_flags; + gs_free const char **keys = NULL; + guint i, len; if (NM_FLAGS_HAS (flags, NM_CONNECTION_SERIALIZE_NO_SECRETS)) return NULL; g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{ss}")); - g_object_get (setting, property_name, &secrets, NULL); - - if (secrets) { - g_hash_table_iter_init (&iter, secrets); - while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &value)) { - if (NM_FLAGS_HAS (flags, NM_CONNECTION_SERIALIZE_WITH_SECRETS_AGENT_OWNED)) { - if ( !nm_setting_get_secret_flags (setting, key, &secret_flags, NULL) - || !NM_FLAGS_HAS (secret_flags, NM_SETTING_SECRET_FLAG_AGENT_OWNED)) - continue; - } - g_variant_builder_add (&builder, "{ss}", key, value); + + keys = nm_utils_strdict_get_keys (priv->secrets, TRUE, &len); + for (i = 0; i < len; i++) { + const char *key = keys[i]; + NMSettingSecretFlags secret_flags; + + if (NM_FLAGS_HAS (flags, NM_CONNECTION_SERIALIZE_WITH_SECRETS_AGENT_OWNED)) { + if ( !nm_setting_get_secret_flags (setting, key, &secret_flags, NULL) + || !NM_FLAGS_HAS (secret_flags, NM_SETTING_SECRET_FLAG_AGENT_OWNED)) + continue; } + g_variant_builder_add (&builder, + "{ss}", + key, + g_hash_table_lookup (priv->secrets, key)); } return g_variant_builder_end (&builder); @@ -995,12 +1046,42 @@ set_property (GObject *object, guint prop_id, priv->persistent = g_value_get_boolean (value); break; case PROP_DATA: - g_hash_table_unref (priv->data); - priv->data = _nm_utils_copy_strdict (g_value_get_boxed (value)); - break; - case PROP_SECRETS: - g_hash_table_unref (priv->secrets); - priv->secrets = _nm_utils_copy_strdict (g_value_get_boxed (value)); + case PROP_SECRETS: { + gs_unref_hashtable GHashTable *hash_free = NULL; + GHashTable *src_hash = g_value_get_boxed (value); + GHashTable **p_hash; + const gboolean is_secrets = (prop_id == PROP_SECRETS); + + if (is_secrets) + p_hash = &priv->secrets; + else + p_hash = &priv->data; + + hash_free = g_steal_pointer (p_hash); + + if ( src_hash + && g_hash_table_size (src_hash) > 0) { + GHashTableIter iter; + const char *key; + const char *val; + + g_hash_table_iter_init (&iter, src_hash); + while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &val)) { + if ( !key + || !key[0] + || !val) { + /* NULL keys/values and empty key are not allowed. Usually, we would reject them in verify(), but + * then our nm_setting_vpn_remove_data_item() also doesn't allow empty keys. So, if we failed + * it in verify(), it would be only fixable by setting PROP_DATA again. Instead, + * silently ignore them. */ + continue; + } + g_hash_table_insert (_ensure_strdict (p_hash, is_secrets), + g_strdup (key), + g_strdup (val)); + } + } + } break; case PROP_TIMEOUT: priv->timeout = g_value_get_uint (value); @@ -1016,10 +1097,6 @@ set_property (GObject *object, guint prop_id, static void nm_setting_vpn_init (NMSettingVpn *setting) { - NMSettingVpnPrivate *priv = NM_SETTING_VPN_GET_PRIVATE (setting); - - priv->data = g_hash_table_new_full (nm_str_hash, g_str_equal, g_free, g_free); - priv->secrets = g_hash_table_new_full (nm_str_hash, g_str_equal, g_free, (GDestroyNotify) nm_free_secret); } /** @@ -1042,8 +1119,10 @@ finalize (GObject *object) g_free (priv->service_type); g_free (priv->user_name); - g_hash_table_destroy (priv->data); - g_hash_table_destroy (priv->secrets); + if (priv->data) + g_hash_table_unref (priv->data); + if (priv->secrets) + g_hash_table_unref (priv->secrets); G_OBJECT_CLASS (nm_setting_vpn_parent_class)->finalize (object); } diff --git a/libnm-core/nm-utils.c b/libnm-core/nm-utils.c index 24d9dfcb43..2cfaea6dd4 100644 --- a/libnm-core/nm-utils.c +++ b/libnm-core/nm-utils.c @@ -802,53 +802,7 @@ _nm_utils_hash_values_to_slist (GHashTable *hash) static GVariant * _nm_utils_strdict_to_dbus (const GValue *prop_value) { - GHashTable *hash; - GHashTableIter iter; - const char *key, *value; - GVariantBuilder builder; - guint i, len; - - g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{ss}")); - - hash = g_value_get_boxed (prop_value); - if (!hash) - goto out; - len = g_hash_table_size (hash); - if (!len) - goto out; - - g_hash_table_iter_init (&iter, hash); - if (!g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &value)) - nm_assert_not_reached (); - - if (len == 1) - g_variant_builder_add (&builder, "{ss}", key, value); - else { - gs_free NMUtilsNamedValue *idx_free = NULL; - NMUtilsNamedValue *idx; - - if (len > 300 / sizeof (NMUtilsNamedValue)) { - idx_free = g_new (NMUtilsNamedValue, len); - idx = idx_free; - } else - idx = g_alloca (sizeof (NMUtilsNamedValue) * len); - - i = 0; - do { - idx[i].name = key; - idx[i].value_str = value; - i++; - } while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &value)); - nm_assert (i == len); - - nm_utils_named_value_list_sort (idx, len, NULL, NULL); - - for (i = 0; i < len; i++) - g_variant_builder_add (&builder, "{ss}", idx[i].name, idx[i].value_str); - } - -out: - return g_variant_builder_end (&builder); + return nm_utils_strdict_to_variant_ass (g_value_get_boxed (prop_value)); } void diff --git a/libnm-core/tests/test-general.c b/libnm-core/tests/test-general.c index e0d0d55fa2..138702e4ca 100644 --- a/libnm-core/tests/test-general.c +++ b/libnm-core/tests/test-general.c @@ -659,6 +659,333 @@ test_nm_utils_strsplit_set (void) /*****************************************************************************/ +static char * +_escaped_tokens_create_random_word_full (const char *const*tokens, + gsize n_tokens, + gsize len) +{ + GString *gstr = g_string_new (NULL); + gsize i; + char random_token[2] = { 0 }; + + for (i = 0; i < len; i++) { + const char *token = tokens[nmtst_get_rand_uint32 () % n_tokens]; + + if (!token[0]) { + do { + random_token[0] = nmtst_get_rand_uint32 (); + } while (random_token[0] == '\0'); + token = random_token; + } + g_string_append (gstr, token); + } + + /* reallocate the string, so that we don't have any excess memory from + * the GString buffer. This is so that valgrind may better detect an out + * or range access. */ + return nm_str_realloc (g_string_free (gstr, FALSE)); +} + +/* set to 1 to exclude characters that are annoying to see in the debugger + * and printf() output. */ +#define ESCAPED_TOKENS_ONLY_NICE_CHARS 0 + +static char * +_escaped_tokens_create_random_whitespace (void) +{ + static const char *tokens[] = { + " ", +#if !ESCAPED_TOKENS_ONLY_NICE_CHARS + "\n", + "\t", + "\r", + "\f", +#endif + }; + + return _escaped_tokens_create_random_word_full (tokens, G_N_ELEMENTS (tokens), nmtst_get_rand_word_length (NULL) / 4u); +} + +static char * +_escaped_tokens_create_random_word (void) +{ + static const char *tokens[] = { + "a", + "b", + "c", + " ", + ",", + "=", + "\\", +#if !ESCAPED_TOKENS_ONLY_NICE_CHARS + "\n", + "\f", + ":", + "", +#endif + }; + + return _escaped_tokens_create_random_word_full (tokens, G_N_ELEMENTS (tokens), nmtst_get_rand_word_length (NULL)); +} + +static void +_escaped_tokens_str_append_delimiter (GString *str, + gboolean strict, + gboolean needs_delimiter) +{ + guint len = nmtst_get_rand_word_length (NULL) / 10u; + char *s; + +again: + if (!strict) { + g_string_append (str, (s = _escaped_tokens_create_random_whitespace ())); + nm_clear_g_free (&s); + } + + if (needs_delimiter) + g_string_append_c (str, ','); + + if (!strict) { + g_string_append (str, (s = _escaped_tokens_create_random_whitespace ())); + nm_clear_g_free (&s); + if (len-- > 0) { + needs_delimiter = TRUE; + goto again; + } + } +} + +static void +_escaped_tokens_split (char *str, const char **out_key, const char **out_val) +{ + const char *key; + const char *val; + gsize len = strlen (str); + + g_assert (str); + + nm_utils_escaped_tokens_options_split (str, &key, &val); + g_assert (key); + g_assert (key == str); + if (val) { + g_assert (val > str); + g_assert (val > key); + g_assert (val <= &str[len]); + } + NM_SET_OUT (out_key, key); + NM_SET_OUT (out_val, val); +} + +static void +_escaped_tokens_combine (GString *combined, + const char *key, + const char *val, + gboolean strict, + gboolean allow_append_delimiter_before, + gboolean needs_delimiter_after) +{ + gs_free char *escaped_key = NULL; + gs_free char *escaped_val = NULL; + + if (allow_append_delimiter_before) + _escaped_tokens_str_append_delimiter (combined, strict, FALSE); + g_string_append (combined, nm_utils_escaped_tokens_options_escape_key (key, &escaped_key)); + if (val) { + char *s; + + if (!strict) { + g_string_append (combined, (s = _escaped_tokens_create_random_whitespace ())); + nm_clear_g_free (&s); + } + g_string_append_c (combined, '='); + if (!strict) { + g_string_append (combined, (s = _escaped_tokens_create_random_whitespace ())); + nm_clear_g_free (&s); + } + g_string_append (combined, nm_utils_escaped_tokens_options_escape_val (val, &escaped_val)); + } + _escaped_tokens_str_append_delimiter (combined, strict, needs_delimiter_after); +} + +static void +_escaped_tokens_check_one_impl (const char *expected_key, + const char *expected_val, + const char *expected_combination, + const char *const*other, + gsize n_other) +{ + nm_auto_free_gstring GString *combined = g_string_new (NULL); + gsize i; + + g_assert (expected_key); + g_assert (expected_combination); + g_assert (other); + + _escaped_tokens_combine (combined, + expected_key, + expected_val, + TRUE, + TRUE, + FALSE); + + g_assert_cmpstr (combined->str, ==, expected_combination); + + for (i = 0; i < n_other + 2u; i++) { + nm_auto_free_gstring GString *str0 = NULL; + gs_free const char **strv_split = NULL; + gs_free char *strv_split0 = NULL; + const char *comb; + const char *key; + const char *val; + + if (i == 0) + comb = expected_combination; + else if (i == 1) { + _escaped_tokens_combine (nm_gstring_prepare (&str0), + expected_key, + expected_val, + FALSE, + TRUE, + FALSE); + comb = str0->str; + } else + comb = other[i - 2]; + + strv_split = nm_utils_escaped_tokens_options_split_list (comb); + if (!strv_split) { + g_assert_cmpstr (expected_key, ==, ""); + g_assert_cmpstr (expected_val, ==, NULL); + continue; + } + g_assert (expected_val || expected_key[0]); + + g_assert_cmpuint (NM_PTRARRAY_LEN (strv_split), ==, 1u); + + strv_split0 = g_strdup (strv_split[0]); + + _escaped_tokens_split (strv_split0, &key, &val); + g_assert_cmpstr (key, ==, expected_key); + g_assert_cmpstr (val, ==, expected_val); + } +} + +#define _escaped_tokens_check_one(expected_key, expected_val, expected_combination, ...) \ + _escaped_tokens_check_one_impl (expected_key, expected_val, expected_combination, NM_MAKE_STRV (__VA_ARGS__), NM_NARG (__VA_ARGS__)) + +static void +test_nm_utils_escaped_tokens (void) +{ + int i_run; + + for (i_run = 0; i_run < 1000; i_run++) { + const guint num_options = nmtst_get_rand_word_length (NULL); + gs_unref_ptrarray GPtrArray *options = g_ptr_array_new_with_free_func (g_free); + nm_auto_free_gstring GString *combined = g_string_new (NULL); + gs_free const char **strv_split = NULL; + guint i_option; + guint i; + + /* Generate a list of random words for option key-value pairs. */ + for (i_option = 0; i_option < 2u * num_options; i_option++) { + char *word = NULL; + + if ( i_option % 2u == 1 + && nmtst_get_rand_uint32 () % 5 == 0 + && strlen (options->pdata[options->len - 1]) > 0u) { + /* For some options, leave the value unset and only generate a key. + * + * If key is "", then we cannot do that, because the test below would try + * to append "" to the combined list, which the parser then would drop. + * Only test omitting the value, if strlen() of the key is positive. */ + } else + word = _escaped_tokens_create_random_word (); + g_ptr_array_add (options, word); + } + + /* Combine the options in one comma separated list, with proper escaping. */ + for (i_option = 0; i_option < num_options; i_option++) { + _escaped_tokens_combine (combined, + options->pdata[2u*i_option + 0u], + options->pdata[2u*i_option + 1u], + FALSE, + i_option == 0, + i_option != num_options - 1); + } + + /* ensure that we can split and parse the options without difference. */ + strv_split = nm_utils_escaped_tokens_options_split_list (combined->str); + for (i_option = 0; i_option < num_options; i_option++) { + const char *expected_key = options->pdata[2u*i_option + 0u]; + const char *expected_val = options->pdata[2u*i_option + 1u]; + gs_free char *s_split = i_option < NM_PTRARRAY_LEN (strv_split) ? g_strdup (strv_split[i_option]) : NULL; + const char *key = NULL; + const char *val = NULL; + + if (s_split) + _escaped_tokens_split (s_split, &key, &val); + + if ( !nm_streq0 (key, expected_key) + || !nm_streq0 (val, expected_val)) { + g_print (">>> ASSERTION IS ABOUT TO FAIL for item %5d of %5d\n", i_option, num_options); + g_print (">>> combined = \"%s\"\n", combined->str); + g_print (">>> %c parsed[%5d].key = \"%s\"\n", nm_streq (key, expected_key) ? ' ' : 'X', i_option, key); + g_print (">>> %c parsed[%5d].val = %s%s%s\n", nm_streq0 (val, expected_val) ? ' ' : 'X', i_option, NM_PRINT_FMT_QUOTE_STRING (val)); + for (i = 0; i < num_options; i++) { + g_print (">>> %c original[%5d].key = \"%s\"\n", i == i_option ? '*' : ' ', i, (char *) options->pdata[2u*i + 0u]); + g_print (">>> %c original[%5d].val = %s%s%s\n", i == i_option ? '*' : ' ', i, NM_PRINT_FMT_QUOTE_STRING ((char *) options->pdata[2u*i + 1u])); + } + for (i = 0; i < NM_PTRARRAY_LEN (strv_split); i++) + g_print (">>> split[%5d] = \"%s\"\n", i, strv_split[i]); + } + + g_assert_cmpstr (key, ==, expected_key); + g_assert_cmpstr (val, ==, expected_val); + } + g_assert_cmpint (NM_PTRARRAY_LEN (strv_split), ==, num_options); + + /* Above we show a full round-trip of random option key-value pairs, that they can + * without loss escape, concatenate, split-list, and split. This proofed that every + * option key-value pair can be represented as a combined string and parsed back. + * + * Now, just check that we can also parse arbitrary random words in nm_utils_escaped_tokens_options_split(). + * split() is a non-injective surjective function. As we check the round-trip above for random words, where + * options-split() is the last step, we show that every random word can be the output of the function + * (which shows, the surjective part). + * + * But multiple random input arguments, may map to the same output argument (non-injective). + * Just test whether we can handle random input words without crashing. For that, just use the + * above generate list of random words. + */ + for (i = 0; i < 1u + 2u * i_option; i++) { + gs_free char *str = NULL; + const char *cstr; + + if (i == 0) + cstr = combined->str; + else + cstr = options->pdata[i - 1u]; + if (!cstr) + continue; + + str = g_strdup (cstr); + _escaped_tokens_split (str, NULL, NULL); + } + } + + _escaped_tokens_check_one ("", NULL, ""); + _escaped_tokens_check_one ("", "", "=", " ="); + _escaped_tokens_check_one ("a", "b", "a=b", "a = b"); + _escaped_tokens_check_one ("a\\=", "b\\=", "a\\\\\\==b\\\\=", "a\\\\\\==b\\\\\\="); + _escaped_tokens_check_one ("\\=", "\\=", "\\\\\\==\\\\=", "\\\\\\==\\\\\\="); + _escaped_tokens_check_one (" ", "bb=", "\\ =bb=", "\\ =bb\\="); + _escaped_tokens_check_one (" ", "bb\\=", "\\ =bb\\\\=", "\\ =bb\\\\\\="); + _escaped_tokens_check_one ("a b", "a b", "a b=a b"); + _escaped_tokens_check_one ("a b", "a b", "a b=a b"); + _escaped_tokens_check_one ("a = b", "a = b", "a \\= b=a = b", "a \\= b=a \\= b"); +} + +/*****************************************************************************/ + typedef struct { int val; CList lst; @@ -1172,13 +1499,11 @@ test_setting_vpn_items (void) nm_setting_vpn_add_data_item (s_vpn, "", ""); g_test_assert_expected_messages (); - NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (item && item[0])); - nm_setting_vpn_add_data_item (s_vpn, "foobar1", NULL); - g_test_assert_expected_messages (); - - NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (item && item[0])); nm_setting_vpn_add_data_item (s_vpn, "foobar1", ""); - g_test_assert_expected_messages (); + g_assert_cmpstr (nm_setting_vpn_get_data_item (s_vpn, "foobar1"), ==, ""); + + nm_setting_vpn_add_data_item (s_vpn, "foobar1", NULL); + g_assert_cmpstr (nm_setting_vpn_get_data_item (s_vpn, "foobar1"), ==, NULL); NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0])); nm_setting_vpn_add_data_item (s_vpn, NULL, "blahblah1"); @@ -1199,13 +1524,9 @@ test_setting_vpn_items (void) nm_setting_vpn_add_secret (s_vpn, "", ""); g_test_assert_expected_messages (); - NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (secret && secret[0])); - nm_setting_vpn_add_secret (s_vpn, "foobar1", NULL); - g_test_assert_expected_messages (); - - NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (secret && secret[0])); nm_setting_vpn_add_secret (s_vpn, "foobar1", ""); - g_test_assert_expected_messages (); + + nm_setting_vpn_add_secret (s_vpn, "foobar1", NULL); NMTST_EXPECT_LIBNM_CRITICAL (NMTST_G_RETURN_MSG (key && key[0])); nm_setting_vpn_add_secret (s_vpn, NULL, "blahblah1"); @@ -8559,6 +8880,7 @@ int main (int argc, char **argv) g_test_add_func ("/core/general/test_dedup_multi", test_dedup_multi); g_test_add_func ("/core/general/test_utils_str_utf8safe", test_utils_str_utf8safe); g_test_add_func ("/core/general/test_nm_utils_strsplit_set", test_nm_utils_strsplit_set); + g_test_add_func ("/core/general/test_nm_utils_escaped_tokens", test_nm_utils_escaped_tokens); g_test_add_func ("/core/general/test_nm_in_set", test_nm_in_set); g_test_add_func ("/core/general/test_nm_in_strset", test_nm_in_strset); g_test_add_func ("/core/general/test_setting_vpn_items", test_setting_vpn_items); diff --git a/shared/nm-glib-aux/nm-macros-internal.h b/shared/nm-glib-aux/nm-macros-internal.h index 758a4ec9aa..9e3735d477 100644 --- a/shared/nm-glib-aux/nm-macros-internal.h +++ b/shared/nm-glib-aux/nm-macros-internal.h @@ -1571,6 +1571,17 @@ nm_strstrip_avoid_copy (const char *str, char **str_free) _str_ssac; \ }) +static inline gboolean +nm_str_is_stripped (const char *str) +{ + if (str && str[0]) { + if ( g_ascii_isspace (str[0]) + || g_ascii_isspace (str[strlen (str) - 1])) + return FALSE; + } + return TRUE; +} + /* g_ptr_array_sort()'s compare function takes pointers to the * value. Thus, you cannot use strcmp directly. You can use * nm_strcmp_p(). diff --git a/shared/nm-glib-aux/nm-shared-utils.c b/shared/nm-glib-aux/nm-shared-utils.c index 14e32956db..0e971a7399 100644 --- a/shared/nm-glib-aux/nm-shared-utils.c +++ b/shared/nm-glib-aux/nm-shared-utils.c @@ -444,6 +444,62 @@ nm_utils_gbytes_to_variant_ay (GBytes *bytes) /*****************************************************************************/ +/* Convert a hash table with "char *" keys and values to an "a{ss}" GVariant. + * The keys will be sorted asciibetically. + * Returns a floating reference. + */ +GVariant * +nm_utils_strdict_to_variant_ass (GHashTable *strdict) +{ + GHashTableIter iter; + const char *key, *value; + GVariantBuilder builder; + guint i, len; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{ss}")); + + if (!strdict) + goto out; + len = g_hash_table_size (strdict); + if (!len) + goto out; + + g_hash_table_iter_init (&iter, strdict); + if (!g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &value)) + nm_assert_not_reached (); + + if (len == 1) + g_variant_builder_add (&builder, "{ss}", key, value); + else { + gs_free NMUtilsNamedValue *idx_free = NULL; + NMUtilsNamedValue *idx; + + if (len > 300 / sizeof (NMUtilsNamedValue)) { + idx_free = g_new (NMUtilsNamedValue, len); + idx = idx_free; + } else + idx = g_alloca (sizeof (NMUtilsNamedValue) * len); + + i = 0; + do { + idx[i].name = key; + idx[i].value_str = value; + i++; + } while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &value)); + nm_assert (i == len); + + nm_utils_named_value_list_sort (idx, len, NULL, NULL); + + for (i = 0; i < len; i++) + g_variant_builder_add (&builder, "{ss}", idx[i].name, idx[i].value_str); + } + +out: + return g_variant_builder_end (&builder); +} + +/*****************************************************************************/ + /** * nm_strquote: * @buf: the output buffer of where to write the quoted @str argument. @@ -1441,12 +1497,27 @@ comp_l: /*****************************************************************************/ static void +_char_lookup_table_set_one (guint8 lookup[static 256], + char ch) +{ + lookup[(guint8) ch] = 1; +} + +static void +_char_lookup_table_set_all (guint8 lookup[static 256], + const char *candidates) +{ + while (candidates[0] != '\0') + _char_lookup_table_set_one (lookup, (candidates++)[0]); +} + +static void _char_lookup_table_init (guint8 lookup[static 256], const char *candidates) { memset (lookup, 0, 256); - while (candidates[0] != '\0') - lookup[(guint8) ((candidates++)[0])] = 1; + if (candidates) + _char_lookup_table_set_all (lookup, candidates); } static gboolean @@ -1457,6 +1528,19 @@ _char_lookup_has (const guint8 lookup[static 256], return lookup[(guint8) ch] != 0; } +static gboolean +_char_lookup_has_all (const guint8 lookup[static 256], + const char *candidates) +{ + if (candidates) { + while (candidates[0] != '\0') { + if (!_char_lookup_has (lookup, (candidates++)[0])) + return FALSE; + } + } + return TRUE; +} + /** * nm_utils_strsplit_set_full: * @str: the string to split. @@ -1469,18 +1553,7 @@ _char_lookup_has (const guint8 lookup[static 256], * * Note that for @str %NULL and "", this always returns %NULL too. That differs * from g_strsplit_set(), which would return an empty strv array for "". - * - * Note that g_strsplit_set() returns empty words as well. By default, - * nm_utils_strsplit_set_full() strips all empty tokens (that is, repeated - * delimiters. With %NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, empty tokens - * are not removed. - * - * If @flags has %NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING, delimiters prefixed - * by a backslash are not treated as a separator. Such delimiters and their escape - * character are copied to the current word without unescaping them. In general, - * nm_utils_strsplit_set_full() does not remove any backslash escape characters - * and does not unescaping. It only considers them for skipping to split at - * an escaped delimiter. + * This never returns an empty array. * * Returns: %NULL if @str is %NULL or "". * If @str only contains delimiters and %NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY @@ -1490,7 +1563,7 @@ _char_lookup_has (const guint8 lookup[static 256], * The strings to which the result strv array points to are allocated * after the returned result itself. Don't free the strings themself, * but free everything with g_free(). - * It is however safe and allowed to modify the indiviual strings, + * It is however safe and allowed to modify the individual strings in-place, * like "g_strstrip((char *) iter[0])". */ const char ** @@ -1672,11 +1745,9 @@ done2: /* We no longer need ch_lookup for its original purpose. Modify it, so it * can detect the delimiters, '\\', and (optionally) whitespaces. */ - ch_lookup[((guint8) '\\')] = 1; - if (f_strstrip) { - for (i = 0; NM_ASCII_SPACES[i]; i++) - ch_lookup[((guint8) (NM_ASCII_SPACES[i]))] = 1; - } + _char_lookup_table_set_one (ch_lookup, '\\'); + if (f_strstrip) + _char_lookup_table_set_all (ch_lookup, NM_ASCII_SPACES); for (i_token = 0; ptr[i_token]; i_token++) { s = (char *) ptr[i_token]; @@ -1697,65 +1768,131 @@ done2: /*****************************************************************************/ const char * -nm_utils_escaped_tokens_escape (const char *str, - const char *delimiters, - char **out_to_free) +nm_utils_escaped_tokens_escape_full (const char *str, + const char *delimiters, + const char *delimiters_as_needed, + NMUtilsEscapedTokensEscapeFlags flags, + char **out_to_free) { guint8 ch_lookup[256]; + guint8 ch_lookup_as_needed[256]; + gboolean has_ch_lookup_as_needed = FALSE; char *ret; gsize str_len; gsize alloc_len; gsize n_escapes; gsize i, j; + gboolean escape_leading_space; gboolean escape_trailing_space; + gboolean escape_backslash_as_needed; - if (!delimiters) { - nm_assert (delimiters); - delimiters = NM_ASCII_SPACES; - } + nm_assert ( !delimiters_as_needed + || ( delimiters_as_needed[0] + && NM_FLAGS_HAS (flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_AS_NEEDED))); if (!str || str[0] == '\0') { *out_to_free = NULL; return str; } + str_len = strlen (str); + _char_lookup_table_init (ch_lookup, delimiters); + if ( !delimiters + || NM_FLAGS_HAS (flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_SPACES)) { + flags &= ~( NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE + | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE); + _char_lookup_table_set_all (ch_lookup, NM_ASCII_SPACES); + } + + if (NM_FLAGS_HAS (flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_ALWAYS)) { + _char_lookup_table_set_one (ch_lookup, '\\'); + escape_backslash_as_needed = FALSE; + } else if (_char_lookup_has (ch_lookup, '\\')) + escape_backslash_as_needed = FALSE; + else { + escape_backslash_as_needed = NM_FLAGS_HAS (flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_AS_NEEDED); + if (escape_backslash_as_needed) { + if ( NM_FLAGS_ANY (flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE + | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE) + && !_char_lookup_has_all (ch_lookup, NM_ASCII_SPACES)) { + /* ESCAPE_LEADING_SPACE and ESCAPE_TRAILING_SPACE implies that we escape backslash + * before whitespaces. */ + if (!has_ch_lookup_as_needed) { + has_ch_lookup_as_needed = TRUE; + _char_lookup_table_init (ch_lookup_as_needed, NULL); + } + _char_lookup_table_set_all (ch_lookup_as_needed, NM_ASCII_SPACES); + } + if ( delimiters_as_needed + && !_char_lookup_has_all (ch_lookup, delimiters_as_needed)) { + if (!has_ch_lookup_as_needed) { + has_ch_lookup_as_needed = TRUE; + _char_lookup_table_init (ch_lookup_as_needed, NULL); + } + _char_lookup_table_set_all (ch_lookup_as_needed, delimiters_as_needed); + } + } + } - /* also mark '\\' as requiring escaping. */ - ch_lookup[((guint8) '\\')] = 1; + escape_leading_space = NM_FLAGS_HAS (flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE) + && g_ascii_isspace (str[0]) + && !_char_lookup_has (ch_lookup, str[0]); + if (str_len == 1) + escape_trailing_space = FALSE; + else { + escape_trailing_space = NM_FLAGS_HAS (flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE) + && g_ascii_isspace (str[str_len - 1]) + && !_char_lookup_has (ch_lookup, str[str_len - 1]); + } n_escapes = 0; for (i = 0; str[i] != '\0'; i++) { if (_char_lookup_has (ch_lookup, str[i])) n_escapes++; + else if ( str[i] == '\\' + && escape_backslash_as_needed + && ( _char_lookup_has (ch_lookup, str[i + 1]) + || NM_IN_SET (str[i + 1], '\0', '\\') + || ( has_ch_lookup_as_needed + && _char_lookup_has (ch_lookup_as_needed, str[i + 1])))) + n_escapes++; } + if (escape_leading_space) + n_escapes++; + if (escape_trailing_space) + n_escapes++; - str_len = i; - nm_assert (str_len > 0 && strlen (str) == str_len); - - escape_trailing_space = !_char_lookup_has (ch_lookup, str[str_len - 1]) - && g_ascii_isspace (str[str_len - 1]); - - if ( n_escapes == 0 - && !escape_trailing_space) { + if (n_escapes == 0u) { *out_to_free = NULL; return str; } - alloc_len = str_len + n_escapes + ((gsize) escape_trailing_space) + 1; + alloc_len = str_len + n_escapes + 1u; ret = g_new (char, alloc_len); j = 0; - for (i = 0; str[i] != '\0'; i++) { - if (_char_lookup_has (ch_lookup, str[i])) { - nm_assert (j < alloc_len); + i = 0; + + if (escape_leading_space) { + ret[j++] = '\\'; + ret[j++] = str[i++]; + } + for (; str[i] != '\0'; i++) { + if (_char_lookup_has (ch_lookup, str[i])) + ret[j++] = '\\'; + else if ( str[i] == '\\' + && escape_backslash_as_needed + && ( _char_lookup_has (ch_lookup, str[i + 1]) + || NM_IN_SET (str[i + 1], '\0', '\\') + || ( has_ch_lookup_as_needed + && _char_lookup_has (ch_lookup_as_needed, str[i + 1])))) ret[j++] = '\\'; - } - nm_assert (j < alloc_len); ret[j++] = str[i]; } if (escape_trailing_space) { - nm_assert (!_char_lookup_has (ch_lookup, ret[j - 1]) && g_ascii_isspace (ret[j - 1])); + nm_assert ( !_char_lookup_has (ch_lookup, ret[j - 1]) + && g_ascii_isspace (ret[j - 1])); ret[j] = ret[j - 1]; ret[j - 1] = '\\'; j++; @@ -1763,11 +1900,97 @@ nm_utils_escaped_tokens_escape (const char *str, nm_assert (j == alloc_len - 1); ret[j] = '\0'; + nm_assert (strlen (ret) == j); *out_to_free = ret; return ret; } +/** + * nm_utils_escaped_tokens_options_split: + * @str: the src string. This string will be modified in-place. + * The output values will point into @str. + * @out_key: (allow-none): the returned output key. This will always be set to @str + * itself. @str will be modified to contain only the unescaped, truncated + * key name. + * @out_val: returns the parsed (and unescaped) value or %NULL, if @str contains + * no '=' delimiter. + * + * Honors backslash escaping to parse @str as "key=value" pairs. Optionally, if no '=' + * is present, @out_val will be returned as %NULL. Backslash can be used to escape + * '=', ',', '\\', and ascii whitespace. Other backslash sequences are taken verbatim. + * + * For keys, '=' obviously must be escaped. For values, that is optional because an + * unescaped '=' is just taken verbatim. For example, in a key, the sequence "\\=" + * must be escaped as "\\\\\\=". For the value, that works too, but "\\\\=" is also + * accepted. + * + * Unescaped Space around the key and value are also removed. Space in general must + * not be escaped, unless they are at the beginning or the end of key/value. + */ +void +nm_utils_escaped_tokens_options_split (char *str, + const char **out_key, + const char **out_val) +{ + const char *val = NULL; + gsize i; + gsize j; + gsize last_space_idx; + gboolean last_space_has; + + nm_assert (str); + + i = 0; + while (g_ascii_isspace (str[i])) + i++; + + j = 0; + last_space_idx = 0; + last_space_has = FALSE; + while (str[i] != '\0') { + if (g_ascii_isspace (str[i])) { + if (!last_space_has) { + last_space_has = TRUE; + last_space_idx = j; + } + } else { + if (str[i] == '\\') { + if ( NM_IN_SET (str[i + 1u], '\\', ',', '=') + || g_ascii_isspace (str[i + 1u])) + i++; + } else if (str[i] == '=') { + /* Encounter an unescaped '=' character. When we still parse the key, this + * is the separator we were waiting for. If we are parsing the value, + * we take the character verbatim. */ + if (!val) { + if (last_space_has) { + str[last_space_idx] = '\0'; + j = last_space_idx + 1; + last_space_has = FALSE; + } else + str[j++] = '\0'; + val = &str[j]; + i++; + while (g_ascii_isspace (str[i])) + i++; + continue; + } + } + last_space_has = FALSE; + } + str[j++] = str[i++]; + } + + if (last_space_has) + str[last_space_idx] = '\0'; + else + str[j] = '\0'; + + *out_key = str; + *out_val = val; +} + /*****************************************************************************/ /** diff --git a/shared/nm-glib-aux/nm-shared-utils.h b/shared/nm-glib-aux/nm-shared-utils.h index 1d066451b3..172eaa7d12 100644 --- a/shared/nm-glib-aux/nm-shared-utils.h +++ b/shared/nm-glib-aux/nm-shared-utils.h @@ -393,6 +393,8 @@ gboolean nm_utils_gbytes_equal_mem (GBytes *bytes, GVariant *nm_utils_gbytes_to_variant_ay (GBytes *bytes); +GVariant *nm_utils_strdict_to_variant_ass (GHashTable *strdict); + /*****************************************************************************/ GVariant *nm_utils_gvariant_vardict_filter (GVariant *src, @@ -438,7 +440,25 @@ int nm_utils_dbus_path_cmp (const char *dbus_path_a, const char *dbus_path_b); typedef enum { NM_UTILS_STRSPLIT_SET_FLAGS_NONE = 0, + + /* by default, strsplit will coalesce consecutive delimiters and remove + * them from the result. If this flag is present, empty values are preserved + * and returned. + * + * When combined with %NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP, if a value gets + * empty after strstrip(), it also gets removed. */ NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY = (1u << 0), + + /* %NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING means that delimiters prefixed + * by a backslash are not treated as a separator. Such delimiters and their escape + * character are copied to the current word without unescaping them. In general, + * nm_utils_strsplit_set_full() does not remove any backslash escape characters + * and does no unescaping. It only considers them for skipping to split at + * an escaped delimiter. + * + * If this is combined with (or implied by %NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED), then + * the backslash escapes are removed from the result. + */ NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING = (1u << 1), /* If flag is set, does the same as g_strstrip() on the returned tokens. @@ -478,6 +498,7 @@ typedef enum { * need extra care, and then only if they proceed one of the relevant characters. */ NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED = (1u << 3), + } NMUtilsStrsplitSetFlags; const char **nm_utils_strsplit_set_full (const char *str, @@ -521,9 +542,65 @@ nm_utils_escaped_tokens_split (const char *str, | NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP); } -const char *nm_utils_escaped_tokens_escape (const char *str, - const char *delimiters, - char **out_to_free); +typedef enum { + NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_NONE = 0, + NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_SPACES = (1ull << 0), + NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE = (1ull << 1), + NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE = (1ull << 2), + + /* Backslash characters will be escaped as "\\\\" if they precede another + * character that makes it necessary. Such characters are: + * + * 1) before another '\\' backslash. + * 2) before any delimiter in @delimiters. + * 3) before any delimiter in @delimiters_as_needed. + * 4) before a white space, if ESCAPE_LEADING_SPACE or ESCAPE_TRAILING_SPACE is set. + * 5) before the end of the word + * + * Rule 4) is an extension. It's not immediately clear why with ESCAPE_LEADING_SPACE + * and ESCAPE_TRAILING_SPACE we want *all* backslashes before a white space escaped. + * The reason is, that we obviously want to use ESCAPE_LEADING_SPACE and ESCAPE_TRAILING_SPACE + * in cases, where we later parse the backslash escaped strings back, but allowing to strip + * unescaped white spaces. That means, we want that " a " gets escaped as "\\ a\\ ". + * On the other hand, we also want that " a\\ b " gets escaped as "\\ a\\\\ b\\ ", + * and not "\\ a\\ b\\ ". Because otherwise, the parser would need to treat "\\ " + * differently depending on whether the sequence is at the beginning, end or middle + * of the word. + * + * Rule 5) is also not immediately obvious. When used with ESCAPE_TRAILING_SPACE, + * we clearly want to allow that an escaped word can have arbitrary + * whitespace suffixes. That's why this mode exists. So we must escape "a\\" as + * "a\\\\", so that appending " " does not change the meaning. + * Also without ESCAPE_TRAILING_SPACE, we want in general that we can concatenate + * two escaped words without changing their meaning. If the words would be "a\\" + * and "," (with ',' being a delimiter), then the result must be "a\\\\" and "\\," + * so that the concatenated word ("a\\\\\\,") is still the same. If we would escape + * them instead as "a\\" + "\\,", then the concatenated word would be "a\\\\," and + * different. + * */ + NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_AS_NEEDED = (1ull << 3), + + NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_ALWAYS = (1ull << 4), +} NMUtilsEscapedTokensEscapeFlags; + +const char *nm_utils_escaped_tokens_escape_full (const char *str, + const char *delimiters, + const char *delimiters_as_needed, + NMUtilsEscapedTokensEscapeFlags flags, + char **out_to_free); + +static inline const char * +nm_utils_escaped_tokens_escape (const char *str, + const char *delimiters, + char **out_to_free) +{ + return nm_utils_escaped_tokens_escape_full (str, + delimiters, + NULL, + NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_ALWAYS + | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE, + out_to_free); +} static inline GString * nm_utils_escaped_tokens_escape_gstr_assert (const char *str, @@ -575,6 +652,47 @@ nm_utils_escaped_tokens_escape_gstr (const char *str, /*****************************************************************************/ +static inline const char ** +nm_utils_escaped_tokens_options_split_list (const char *str) +{ + return nm_utils_strsplit_set_full (str, + ",", + NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP + | NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING); +} + +void nm_utils_escaped_tokens_options_split (char *str, + const char **out_key, + const char **out_val); + +static inline const char * +nm_utils_escaped_tokens_options_escape_key (const char *key, + char **out_to_free) +{ + return nm_utils_escaped_tokens_escape_full (key, + ",=", + NULL, + NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_AS_NEEDED + | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE + | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE, + out_to_free); +} + +static inline const char * +nm_utils_escaped_tokens_options_escape_val (const char *val, + char **out_to_free) +{ + return nm_utils_escaped_tokens_escape_full (val, + ",", + "=", + NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_AS_NEEDED + | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE + | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE, + out_to_free); +} + +/*****************************************************************************/ + #define NM_UTILS_CHECKSUM_LENGTH_MD5 16 #define NM_UTILS_CHECKSUM_LENGTH_SHA1 20 #define NM_UTILS_CHECKSUM_LENGTH_SHA256 32 @@ -1307,6 +1425,26 @@ char *nm_utils_g_slist_strlist_join (const GSList *a, const char *separator); /*****************************************************************************/ +static inline guint +nm_g_hash_table_size (GHashTable *hash) +{ + return hash ? g_hash_table_size (hash) : 0u; +} + +static inline gpointer +nm_g_hash_table_lookup (GHashTable *hash, gconstpointer key) +{ + return hash ? g_hash_table_lookup (hash, key) : NULL; +} + +static inline gboolean +nm_g_hash_table_remove (GHashTable *hash, gconstpointer key) +{ + return hash ? g_hash_table_remove (hash, key) : FALSE; +} + +/*****************************************************************************/ + gssize nm_utils_ptrarray_find_binary_search (gconstpointer *list, gsize len, gconstpointer needle, diff --git a/shared/nm-utils/nm-test-utils.h b/shared/nm-utils/nm-test-utils.h index 75af679965..0d03456319 100644 --- a/shared/nm-utils/nm-test-utils.h +++ b/shared/nm-utils/nm-test-utils.h @@ -986,6 +986,69 @@ nmtst_rand_perm_gslist (GRand *rand, GSList *list) /*****************************************************************************/ +/** + * nmtst_get_rand_word_length: + * @rand: (allow-none): #GRand instance or %NULL to use the singleton. + * + * Returns: a random integer >= 0, that most frequently is somewhere between + * 0 and 16, but (with decreasing) probability, it can be larger. This can + * be used when we generate random input for unit tests. + */ +static inline guint +nmtst_get_rand_word_length (GRand *rand) +{ + guint n; + + if (!rand) + rand = nmtst_get_rand (); + + n = 0; + while (TRUE) { + guint32 rnd = g_rand_int (rand); + guint probability; + + /* The following python code implements a random sample with this + * distribution: + * + * def random_histogram(n_tries, scale = None): + * def probability(n_tok): + * import math + * return max(2, math.floor(100 / (2*(n_tok+1)))) + * def n_tokens(): + * import random + * n_tok = 0 + * while True: + * if random.randint(0, 0xFFFFFFFF) % probability(n_tok) == 0: + * return n_tok + * n_tok += 1 + * hist = [] + * i = 0; + * while i < n_tries: + * n_tok = n_tokens() + * while n_tok >= len(hist): + * hist.append(0) + * hist[n_tok] = hist[n_tok] + 1 + * i += 1 + * if scale is not None: + * hist = list([round(x / n_tries * scale) for x in hist]) + * return hist + * + * For example, random_histogram(n_tries = 1000000, scale = 1000) may give + * + * IDX: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29] + * SEEN: [20, 39, 59, 73, 80, 91, 92, 90, 91, 73, 73, 54, 55, 36, 24, 16, 16, 8, 4, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0] + * + * which give a sense of the probability with this individual results are returned. + */ + probability = NM_MAX (2u, (100u / (2u * (n + 1u)))); + if ((rnd % probability) == 0) + return n; + n++; + } +} + +/*****************************************************************************/ + static inline gboolean nmtst_g_source_assert_not_called (gpointer user_data) { |