diff options
Diffstat (limited to 'src/libnm-glib-aux/nm-shared-utils.c')
-rw-r--r-- | src/libnm-glib-aux/nm-shared-utils.c | 5710 |
1 files changed, 5710 insertions, 0 deletions
diff --git a/src/libnm-glib-aux/nm-shared-utils.c b/src/libnm-glib-aux/nm-shared-utils.c new file mode 100644 index 0000000000..9477cc3e74 --- /dev/null +++ b/src/libnm-glib-aux/nm-shared-utils.c @@ -0,0 +1,5710 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2016 Red Hat, Inc. + */ + +#include "libnm-glib-aux/nm-default-glib-i18n-lib.h" + +#include "nm-shared-utils.h" + +#include <pwd.h> +#include <arpa/inet.h> +#include <poll.h> +#include <fcntl.h> +#include <sys/syscall.h> +#include <glib-unix.h> +#include <net/if.h> +#include <net/ethernet.h> + +#include "nm-errno.h" +#include "nm-str-buf.h" + +G_STATIC_ASSERT(sizeof(NMEtherAddr) == 6); +G_STATIC_ASSERT(_nm_alignof(NMEtherAddr) == 1); + +G_STATIC_ASSERT(sizeof(NMUtilsNamedEntry) == sizeof(const char *)); +G_STATIC_ASSERT(G_STRUCT_OFFSET(NMUtilsNamedValue, value_ptr) == sizeof(const char *)); + +/*****************************************************************************/ + +const char _nm_hexchar_table_lower[16] = "0123456789abcdef"; +const char _nm_hexchar_table_upper[16] = "0123456789ABCDEF"; + +const void *const _NM_PTRARRAY_EMPTY[1] = {NULL}; + +/*****************************************************************************/ + +const NMIPAddr nm_ip_addr_zero = {}; + +/* 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 + * uses memcpy() to access @src, so alignment is not an issue. + * + * Only potential pitfalls: + * + * - it allows for @addr_family to be AF_UNSPEC. If that is the case (and the + * caller allows for that), the caller MUST provide @out_addr_family. + * - when setting @dst to an IPv4 address, the trailing bytes are not touched. + * Meaning, if @dst is an NMIPAddr union, only the first bytes will be set. + * If that matter to you, clear @dst before. */ +gboolean +nm_ip_addr_set_from_untrusted(int addr_family, + gpointer dst, + gconstpointer src, + gsize src_len, + int * out_addr_family) +{ + nm_assert(dst); + + switch (addr_family) { + case AF_UNSPEC: + if (!out_addr_family) { + /* when the callers allow undefined @addr_family, they must provide + * an @out_addr_family argument. */ + nm_assert_not_reached(); + return FALSE; + } + switch (src_len) { + case sizeof(struct in_addr): + addr_family = AF_INET; + break; + case sizeof(struct in6_addr): + addr_family = AF_INET6; + break; + default: + return FALSE; + } + break; + case AF_INET: + if (src_len != sizeof(struct in_addr)) + return FALSE; + break; + case AF_INET6: + if (src_len != sizeof(struct in6_addr)) + return FALSE; + break; + default: + /* when the callers allow undefined @addr_family, they must provide + * an @out_addr_family argument. */ + nm_assert(out_addr_family); + return FALSE; + } + + nm_assert(src); + + memcpy(dst, src, src_len); + NM_SET_OUT(out_addr_family, addr_family); + return TRUE; +} + +/*****************************************************************************/ + +G_STATIC_ASSERT(ETH_ALEN == sizeof(struct ether_addr)); +G_STATIC_ASSERT(ETH_ALEN == 6); + +/*****************************************************************************/ + +pid_t +nm_utils_gettid(void) +{ + return (pid_t) syscall(SYS_gettid); +} + +/* Used for asserting that this function is called on the main-thread. + * The main-thread is determined by remembering the thread-id + * of when the function was called the first time. + * + * When forking, the thread-id is again reset upon first call. */ +gboolean +_nm_assert_on_main_thread(void) +{ + G_LOCK_DEFINE_STATIC(lock); + static pid_t seen_tid; + static pid_t seen_pid; + pid_t tid; + pid_t pid; + gboolean success = FALSE; + + tid = nm_utils_gettid(); + nm_assert(tid != 0); + + G_LOCK(lock); + + if (G_LIKELY(tid == seen_tid)) { + /* we don't care about false positives (when the process forked, and the thread-id + * is accidentally re-used) . It's for assertions only. */ + success = TRUE; + } else { + pid = getpid(); + nm_assert(pid != 0); + + if (seen_tid == 0 || seen_pid != pid) { + /* either this is the first time we call the function, or the process + * forked. In both cases, remember the thread-id. */ + seen_tid = tid; + seen_pid = pid; + success = TRUE; + } + } + + G_UNLOCK(lock); + + return success; +} + +/*****************************************************************************/ + +void +nm_utils_strbuf_append_c(char **buf, gsize *len, char c) +{ + switch (*len) { + case 0: + return; + case 1: + (*buf)[0] = '\0'; + *len = 0; + (*buf)++; + return; + default: + (*buf)[0] = c; + (*buf)[1] = '\0'; + (*len)--; + (*buf)++; + return; + } +} + +void +nm_utils_strbuf_append_bin(char **buf, gsize *len, gconstpointer str, gsize str_len) +{ + switch (*len) { + case 0: + return; + case 1: + if (str_len == 0) { + (*buf)[0] = '\0'; + return; + } + (*buf)[0] = '\0'; + *len = 0; + (*buf)++; + return; + default: + if (str_len == 0) { + (*buf)[0] = '\0'; + return; + } + if (str_len >= *len) { + memcpy(*buf, str, *len - 1); + (*buf)[*len - 1] = '\0'; + *buf = &(*buf)[*len]; + *len = 0; + } else { + memcpy(*buf, str, str_len); + *buf = &(*buf)[str_len]; + (*buf)[0] = '\0'; + *len -= str_len; + } + return; + } +} + +void +nm_utils_strbuf_append_str(char **buf, gsize *len, const char *str) +{ + gsize src_len; + + switch (*len) { + case 0: + return; + case 1: + if (!str || !*str) { + (*buf)[0] = '\0'; + return; + } + (*buf)[0] = '\0'; + *len = 0; + (*buf)++; + return; + default: + if (!str || !*str) { + (*buf)[0] = '\0'; + return; + } + src_len = g_strlcpy(*buf, str, *len); + if (src_len >= *len) { + *buf = &(*buf)[*len]; + *len = 0; + } else { + *buf = &(*buf)[src_len]; + *len -= src_len; + } + return; + } +} + +void +nm_utils_strbuf_append(char **buf, gsize *len, const char *format, ...) +{ + char * p = *buf; + va_list args; + int retval; + + if (*len == 0) + return; + + va_start(args, format); + retval = g_vsnprintf(p, *len, format, args); + va_end(args); + + if ((gsize) retval >= *len) { + *buf = &p[*len]; + *len = 0; + } else { + *buf = &p[retval]; + *len -= retval; + } +} + +/** + * nm_utils_strbuf_seek_end: + * @buf: the input/output buffer + * @len: the input/output length of the buffer. + * + * Commonly, one uses nm_utils_strbuf_append*(), to incrementally + * append strings to the buffer. However, sometimes we need to use + * existing API to write to the buffer. + * After doing so, we want to adjust the buffer counter. + * Essentially, + * + * g_snprintf (buf, len, ...); + * nm_utils_strbuf_seek_end (&buf, &len); + * + * is almost the same as + * + * nm_utils_strbuf_append (&buf, &len, ...); + * + * The only difference is the behavior when the string got truncated: + * nm_utils_strbuf_append() will recognize that and set the remaining + * length to zero. + * + * In general, the behavior is: + * + * - if *len is zero, do nothing + * - if the buffer contains a NUL byte within the first *len characters, + * the buffer is pointed to the NUL byte and len is adjusted. In this + * case, the remaining *len is always >= 1. + * In particular, that is also the case if the NUL byte is at the very last + * position ((*buf)[*len -1]). That happens, when the previous operation + * either fit the string exactly into the buffer or the string was truncated + * by g_snprintf(). The difference cannot be determined. + * - if the buffer contains no NUL bytes within the first *len characters, + * write NUL at the last position, set *len to zero, and point *buf past + * the NUL byte. This would happen with + * + * strncpy (buf, long_str, len); + * nm_utils_strbuf_seek_end (&buf, &len). + * + * where strncpy() does truncate the string and not NUL terminate it. + * nm_utils_strbuf_seek_end() would then NUL terminate it. + */ +void +nm_utils_strbuf_seek_end(char **buf, gsize *len) +{ + gsize l; + char *end; + + nm_assert(len); + nm_assert(buf && *buf); + + if (*len <= 1) { + if (*len == 1 && (*buf)[0]) + goto truncate; + return; + } + + end = memchr(*buf, 0, *len); + if (end) { + l = end - *buf; + nm_assert(l < *len); + + *buf = end; + *len -= l; + return; + } + +truncate: + /* hm, no NUL character within len bytes. + * Just NUL terminate the array and consume them + * all. */ + *buf += *len; + (*buf)[-1] = '\0'; + *len = 0; + return; +} + +/*****************************************************************************/ + +GBytes * +nm_gbytes_get_empty(void) +{ + static GBytes *bytes = NULL; + GBytes * b; + +again: + b = g_atomic_pointer_get(&bytes); + if (G_UNLIKELY(!b)) { + b = g_bytes_new_static("", 0); + if (!g_atomic_pointer_compare_and_exchange(&bytes, NULL, b)) { + g_bytes_unref(b); + goto again; + } + } + return b; +} + +GBytes * +nm_g_bytes_new_from_str(const char *str) +{ + gsize l; + + if (!str) + return NULL; + + /* the returned array is guaranteed to have a trailing '\0' + * character *after* the length. */ + + l = strlen(str); + return g_bytes_new_take(nm_memdup(str, l + 1u), l); +} + +/** + * nm_utils_gbytes_equals: + * @bytes: (allow-none): a #GBytes array to compare. Note that + * %NULL is treated like an #GBytes array of length zero. + * @mem_data: the data pointer with @mem_len bytes + * @mem_len: the length of the data pointer + * + * Returns: %TRUE if @bytes contains the same data as @mem_data. As a + * special case, a %NULL @bytes is treated like an empty array. + */ +gboolean +nm_utils_gbytes_equal_mem(GBytes *bytes, gconstpointer mem_data, gsize mem_len) +{ + gconstpointer p; + gsize l; + + if (!bytes) { + /* as a special case, let %NULL GBytes compare identical + * to an empty array. */ + return (mem_len == 0); + } + + p = g_bytes_get_data(bytes, &l); + return l == mem_len + && (mem_len == 0 /* allow @mem_data to be %NULL */ + || memcmp(p, mem_data, mem_len) == 0); +} + +GVariant * +nm_utils_gbytes_to_variant_ay(GBytes *bytes) +{ + const guint8 *p; + gsize l; + + if (!bytes) { + /* for convenience, accept NULL to return an empty variant */ + return g_variant_new_array(G_VARIANT_TYPE_BYTE, NULL, 0); + } + + p = g_bytes_get_data(bytes, &l); + return g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, p, l, 1); +} + +/*****************************************************************************/ + +#define _variant_singleton_get(create_variant) \ + ({ \ + static GVariant *_singleton = NULL; \ + GVariant * _v; \ + \ +again: \ + _v = g_atomic_pointer_get(&_singleton); \ + if (G_UNLIKELY(!_v)) { \ + _v = (create_variant); \ + nm_assert(_v); \ + nm_assert(g_variant_is_floating(_v)); \ + g_variant_ref_sink(_v); \ + if (!g_atomic_pointer_compare_and_exchange(&_singleton, NULL, _v)) { \ + g_variant_unref(_v); \ + goto again; \ + } \ + } \ + _v; \ + }) + +GVariant * +nm_g_variant_singleton_u_0(void) +{ + return _variant_singleton_get(g_variant_new_uint32(0)); +} + +/*****************************************************************************/ + +GHashTable * +nm_utils_strdict_clone(GHashTable *src) +{ + GHashTable * dst; + GHashTableIter iter; + const char * key; + const char * val; + + if (!src) + return NULL; + + dst = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free); + g_hash_table_iter_init(&iter, src); + while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &val)) + g_hash_table_insert(dst, g_strdup(key), g_strdup(val)); + return dst; +} + +/* Convert a hash table with "char *" keys and values to an "a{ss}" GVariant. + * The keys will be sorted asciibetically. + * Returns a floating reference. + */ +GVariant * +nm_utils_strdict_to_variant_ass(GHashTable *strdict) +{ + gs_free NMUtilsNamedValue *values_free = NULL; + NMUtilsNamedValue values_prepared[20]; + const NMUtilsNamedValue * values; + GVariantBuilder builder; + guint i; + guint n; + + values = nm_utils_named_values_from_strdict(strdict, &n, values_prepared, &values_free); + + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{ss}")); + for (i = 0; i < n; i++) { + g_variant_builder_add(&builder, "{ss}", values[i].name, values[i].value_str); + } + return g_variant_builder_end(&builder); +} + +/*****************************************************************************/ + +GVariant * +nm_utils_strdict_to_variant_asv(GHashTable *strdict) +{ + gs_free NMUtilsNamedValue *values_free = NULL; + NMUtilsNamedValue values_prepared[20]; + const NMUtilsNamedValue * values; + GVariantBuilder builder; + guint i; + guint n; + + values = nm_utils_named_values_from_strdict(strdict, &n, values_prepared, &values_free); + + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + for (i = 0; i < n; i++) { + g_variant_builder_add(&builder, + "{sv}", + values[i].name, + g_variant_new_string(values[i].value_str)); + } + return g_variant_builder_end(&builder); +} + +/*****************************************************************************/ + +/** + * nm_strquote: + * @buf: the output buffer of where to write the quoted @str argument. + * @buf_len: the size of @buf. + * @str: (allow-none): the string to quote. + * + * Writes @str to @buf with quoting. The resulting buffer + * is always NUL terminated, unless @buf_len is zero. + * If @str is %NULL, it writes "(null)". + * + * If @str needs to be truncated, the closing quote is '^' instead + * of '"'. + * + * This is similar to nm_strquote_a(), which however uses alloca() + * to allocate a new buffer. Also, here @buf_len is the size of @buf, + * while nm_strquote_a() has the number of characters to print. The latter + * doesn't include the quoting. + * + * Returns: the input buffer with the quoted string. + */ +const char * +nm_strquote(char *buf, gsize buf_len, const char *str) +{ + const char *const buf0 = buf; + + if (!str) { + nm_utils_strbuf_append_str(&buf, &buf_len, "(null)"); + goto out; + } + + if (G_UNLIKELY(buf_len <= 2)) { + switch (buf_len) { + case 2: + *(buf++) = '^'; + /* fall-through */ + case 1: + *(buf++) = '\0'; + break; + } + goto out; + } + + *(buf++) = '"'; + buf_len--; + + nm_utils_strbuf_append_str(&buf, &buf_len, str); + + /* if the string was too long we indicate truncation with a + * '^' instead of a closing quote. */ + if (G_UNLIKELY(buf_len <= 1)) { + switch (buf_len) { + case 1: + buf[-1] = '^'; + break; + case 0: + buf[-2] = '^'; + break; + default: + nm_assert_not_reached(); + break; + } + } else { + nm_assert(buf_len >= 2); + *(buf++) = '"'; + *(buf++) = '\0'; + } + +out: + return buf0; +} + +/*****************************************************************************/ + +char _nm_utils_to_string_buffer[]; + +void +nm_utils_to_string_buffer_init(char **buf, gsize *len) +{ + if (!*buf) { + *buf = _nm_utils_to_string_buffer; + *len = sizeof(_nm_utils_to_string_buffer); + } +} + +gboolean +nm_utils_to_string_buffer_init_null(gconstpointer obj, char **buf, gsize *len) +{ + nm_utils_to_string_buffer_init(buf, len); + if (!obj) { + g_strlcpy(*buf, "(null)", *len); + return FALSE; + } + return TRUE; +} + +/*****************************************************************************/ + +const char * +nm_utils_flags2str(const NMUtilsFlags2StrDesc *descs, + gsize n_descs, + unsigned flags, + char * buf, + gsize len) +{ + gsize i; + char *p; + +#if NM_MORE_ASSERTS > 10 + nm_assert(descs); + nm_assert(n_descs > 0); + for (i = 0; i < n_descs; i++) { + gsize j; + + nm_assert(descs[i].name && descs[i].name[0]); + for (j = 0; j < i; j++) + nm_assert(descs[j].flag != descs[i].flag); + } +#endif + + nm_utils_to_string_buffer_init(&buf, &len); + + if (!len) + return buf; + + buf[0] = '\0'; + p = buf; + if (!flags) { + for (i = 0; i < n_descs; i++) { + if (!descs[i].flag) { + nm_utils_strbuf_append_str(&p, &len, descs[i].name); + break; + } + } + return buf; + } + + for (i = 0; flags && i < n_descs; i++) { + if (descs[i].flag && NM_FLAGS_ALL(flags, descs[i].flag)) { + flags &= ~descs[i].flag; + + if (buf[0] != '\0') + nm_utils_strbuf_append_c(&p, &len, ','); + nm_utils_strbuf_append_str(&p, &len, descs[i].name); + } + } + if (flags) { + if (buf[0] != '\0') + nm_utils_strbuf_append_c(&p, &len, ','); + nm_utils_strbuf_append(&p, &len, "0x%x", flags); + } + return buf; +}; + +/*****************************************************************************/ + +/** + * _nm_utils_ip4_prefix_to_netmask: + * @prefix: a CIDR prefix + * + * Returns: the netmask represented by the prefix, in network byte order + **/ +guint32 +_nm_utils_ip4_prefix_to_netmask(guint32 prefix) +{ + return prefix < 32 ? ~htonl(0xFFFFFFFFu >> prefix) : 0xFFFFFFFFu; +} + +gconstpointer +nm_utils_ipx_address_clear_host_address(int family, gpointer dst, gconstpointer src, guint8 plen) +{ + g_return_val_if_fail(dst, NULL); + + switch (family) { + case AF_INET: + g_return_val_if_fail(plen <= 32, NULL); + + if (!src) { + /* allow "self-assignment", by specifying %NULL as source. */ + src = dst; + } + + *((guint32 *) dst) = nm_utils_ip4_address_clear_host_address(*((guint32 *) src), plen); + break; + case AF_INET6: + nm_utils_ip6_address_clear_host_address(dst, src, plen); + break; + default: + g_return_val_if_reached(NULL); + } + return dst; +} + +/* nm_utils_ip4_address_clear_host_address: + * @addr: source ip6 address + * @plen: prefix length of network + * + * returns: the input address, with the host address set to 0. + */ +in_addr_t +nm_utils_ip4_address_clear_host_address(in_addr_t addr, guint8 plen) +{ + return addr & _nm_utils_ip4_prefix_to_netmask(plen); +} + +/* nm_utils_ip6_address_clear_host_address: + * @dst: destination output buffer, will contain the network part of the @src address + * @src: source ip6 address + * @plen: prefix length of network + * + * Note: this function is self assignment safe, to update @src inplace, set both + * @dst and @src to the same destination or set @src NULL. + */ +const struct in6_addr * +nm_utils_ip6_address_clear_host_address(struct in6_addr * dst, + const struct in6_addr *src, + guint8 plen) +{ + g_return_val_if_fail(plen <= 128, NULL); + g_return_val_if_fail(dst, NULL); + + if (!src) + src = dst; + + if (plen < 128) { + guint nbytes = plen / 8; + guint nbits = plen % 8; + + if (nbytes && dst != src) + memcpy(dst, src, nbytes); + if (nbits) { + dst->s6_addr[nbytes] = (src->s6_addr[nbytes] & (0xFF << (8 - nbits))); + nbytes++; + } + if (nbytes <= 15) + memset(&dst->s6_addr[nbytes], 0, 16 - nbytes); + } else if (src != dst) + *dst = *src; + + return dst; +} + +int +nm_utils_ip6_address_same_prefix_cmp(const struct in6_addr *addr_a, + const struct in6_addr *addr_b, + guint8 plen) +{ + int nbytes; + guint8 va, vb, m; + + if (plen >= 128) + NM_CMP_DIRECT_MEMCMP(addr_a, addr_b, sizeof(struct in6_addr)); + else { + nbytes = plen / 8; + if (nbytes) + NM_CMP_DIRECT_MEMCMP(addr_a, addr_b, nbytes); + + plen = plen % 8; + if (plen != 0) { + m = ~((1 << (8 - plen)) - 1); + va = ((((const guint8 *) addr_a))[nbytes]) & m; + vb = ((((const guint8 *) addr_b))[nbytes]) & m; + NM_CMP_DIRECT(va, vb); + } + } + return 0; +} + +/*****************************************************************************/ + +guint32 +_nm_utils_ip4_get_default_prefix0(in_addr_t ip) +{ + /* The function is originally from ipcalc.c of Red Hat's initscripts. */ + switch (ntohl(ip) >> 24) { + case 0 ... 127: + return 8; /* Class A */ + case 128 ... 191: + return 16; /* Class B */ + case 192 ... 223: + return 24; /* Class C */ + } + return 0; +} + +guint32 +_nm_utils_ip4_get_default_prefix(in_addr_t ip) +{ + return _nm_utils_ip4_get_default_prefix0(ip) ?: 24; +} + +gboolean +nm_utils_ip_is_site_local(int addr_family, const void *address) +{ + in_addr_t addr4; + + switch (addr_family) { + case AF_INET: + /* RFC1918 private addresses + * 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 */ + addr4 = ntohl(*((const in_addr_t *) address)); + return (addr4 & 0xff000000) == 0x0a000000 || (addr4 & 0xfff00000) == 0xac100000 + || (addr4 & 0xffff0000) == 0xc0a80000; + case AF_INET6: + return IN6_IS_ADDR_SITELOCAL(address); + default: + g_return_val_if_reached(FALSE); + } +} + +/*****************************************************************************/ + +static gboolean +_parse_legacy_addr4(const char *text, in_addr_t *out_addr, GError **error) +{ + gs_free char * s_free = NULL; + struct in_addr a1; + guint8 bin[sizeof(a1)]; + char * s; + int i; + + if (inet_aton(text, &a1) != 1) { + g_set_error_literal(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_INVALID_ARGUMENT, + "address invalid according to inet_aton()"); + return FALSE; + } + + /* OK, inet_aton() accepted the format. That's good, because we want + * to accept IPv4 addresses in octal format, like 255.255.000.000. + * That's what "legacy" means here. inet_pton() doesn't accept those. + * + * But inet_aton() also ignores trailing garbage and formats with fewer than + * 4 digits. That is just too crazy and we don't do that. Perform additional checks + * and reject some forms that inet_aton() accepted. + * + * Note that we still should (of course) accept everything that inet_pton() + * accepts. However this code never gets called if inet_pton() succeeds + * (see below, aside the assertion code). */ + + if (NM_STRCHAR_ANY(text, ch, (!(ch >= '0' && ch <= '9') && !NM_IN_SET(ch, '.', 'x')))) { + /* We only accepts '.', digits, and 'x' for "0x". */ + g_set_error_literal(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_INVALID_ARGUMENT, + "contains an invalid character"); + return FALSE; + } + + s = nm_memdup_maybe_a(300, text, strlen(text) + 1, &s_free); + + for (i = 0; i < G_N_ELEMENTS(bin); i++) { + char * current_token = s; + gint32 v; + + s = strchr(s, '.'); + if (s) { + s[0] = '\0'; + s++; + } + + if ((i == G_N_ELEMENTS(bin) - 1) != (s == NULL)) { + /* Exactly for the last digit, we expect to have no more following token. + * But this isn't the case. Abort. */ + g_set_error(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_INVALID_ARGUMENT, + "wrong number of tokens (index %d, token '%s')", + i, + s); + return FALSE; + } + + v = _nm_utils_ascii_str_to_int64(current_token, 0, 0, 0xFF, -1); + if (v == -1) { + int errsv = errno; + + /* we do accept octal and hex (even with leading "0x"). But something + * about this token is wrong. */ + g_set_error(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_INVALID_ARGUMENT, + "invalid token '%s': %s (%d)", + current_token, + nm_strerror_native(errsv), + errsv); + return FALSE; + } + + bin[i] = v; + } + + if (memcmp(bin, &a1, sizeof(bin)) != 0) { + /* our parsing did not agree with what inet_aton() gave. Something + * is wrong. Abort. */ + g_set_error( + error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_INVALID_ARGUMENT, + "inet_aton() result 0x%08x differs from computed value 0x%02hhx%02hhx%02hhx%02hhx", + a1.s_addr, + bin[0], + bin[1], + bin[2], + bin[3]); + return FALSE; + } + + *out_addr = a1.s_addr; + return TRUE; +} + +gboolean +nm_utils_parse_inaddr_bin_full(int addr_family, + gboolean accept_legacy, + const char *text, + int * out_addr_family, + gpointer out_addr) +{ + NMIPAddr addrbin; + + g_return_val_if_fail(text, FALSE); + + if (addr_family == AF_UNSPEC) { + g_return_val_if_fail(!out_addr || out_addr_family, FALSE); + addr_family = strchr(text, ':') ? AF_INET6 : AF_INET; + } else + g_return_val_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6), FALSE); + + if (inet_pton(addr_family, text, &addrbin) != 1) { + if (accept_legacy && addr_family == AF_INET + && _parse_legacy_addr4(text, &addrbin.addr4, NULL)) { + /* The address is in some legacy format which inet_aton() accepts, but not inet_pton(). + * Most likely octal digits (leading zeros). We accept the address. */ + } else + return FALSE; + } + +#if NM_MORE_ASSERTS > 10 + if (addr_family == AF_INET) { + gs_free_error GError *error = NULL; + in_addr_t a; + + /* The legacy parser should accept everything that inet_pton() accepts too. Meaning, + * it should strictly parse *more* formats. And of course, parse it the same way. */ + if (!_parse_legacy_addr4(text, &a, &error)) { + char buf[INET_ADDRSTRLEN]; + + g_error("unexpected assertion failure: could parse \"%s\" as %s, but not accepted by " + "legacy parser: %s", + text, + _nm_utils_inet4_ntop(addrbin.addr4, buf), + error->message); + } + nm_assert(addrbin.addr4 == a); + } +#endif + + NM_SET_OUT(out_addr_family, addr_family); + if (out_addr) + nm_ip_addr_set(addr_family, out_addr, &addrbin); + return TRUE; +} + +gboolean +nm_utils_parse_inaddr(int addr_family, const char *text, char **out_addr) +{ + NMIPAddr addrbin; + char addrstr_buf[MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN)]; + + g_return_val_if_fail(text, FALSE); + + if (addr_family == AF_UNSPEC) + addr_family = strchr(text, ':') ? AF_INET6 : AF_INET; + else + g_return_val_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6), FALSE); + + if (inet_pton(addr_family, text, &addrbin) != 1) + return FALSE; + + NM_SET_OUT(out_addr, + g_strdup(inet_ntop(addr_family, &addrbin, addrstr_buf, sizeof(addrstr_buf)))); + return TRUE; +} + +gboolean +nm_utils_parse_inaddr_prefix_bin(int addr_family, + const char *text, + int * out_addr_family, + gpointer out_addr, + int * out_prefix) +{ + gs_free char *addrstr_free = NULL; + int prefix = -1; + const char * slash; + const char * addrstr; + NMIPAddr addrbin; + + g_return_val_if_fail(text, FALSE); + + if (addr_family == AF_UNSPEC) { + g_return_val_if_fail(!out_addr || out_addr_family, FALSE); + addr_family = strchr(text, ':') ? AF_INET6 : AF_INET; + } else + g_return_val_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6), FALSE); + + slash = strchr(text, '/'); + if (slash) + addrstr = nm_strndup_a(300, text, slash - text, &addrstr_free); + else + addrstr = text; + + if (inet_pton(addr_family, addrstr, &addrbin) != 1) + return FALSE; + + if (slash) { + /* For IPv4, `ip addr add` supports the prefix-length as a netmask. We don't + * do that. */ + prefix = + _nm_utils_ascii_str_to_int64(&slash[1], 10, 0, addr_family == AF_INET ? 32 : 128, -1); + if (prefix == -1) + return FALSE; + } + + NM_SET_OUT(out_addr_family, addr_family); + if (out_addr) + nm_ip_addr_set(addr_family, out_addr, &addrbin); + NM_SET_OUT(out_prefix, prefix); + return TRUE; +} + +gboolean +nm_utils_parse_inaddr_prefix(int addr_family, const char *text, char **out_addr, int *out_prefix) +{ + NMIPAddr addrbin; + char addrstr_buf[MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN)]; + + if (!nm_utils_parse_inaddr_prefix_bin(addr_family, text, &addr_family, &addrbin, out_prefix)) + return FALSE; + NM_SET_OUT(out_addr, + g_strdup(inet_ntop(addr_family, &addrbin, addrstr_buf, sizeof(addrstr_buf)))); + return TRUE; +} + +gboolean +nm_utils_parse_next_line(const char **inout_ptr, + gsize * inout_len, + const char **out_line, + gsize * out_line_len) +{ + gboolean eol_is_carriage_return; + const char *line_start; + gsize line_len; + + nm_assert(inout_ptr); + nm_assert(inout_len); + nm_assert(*inout_len == 0 || *inout_ptr); + nm_assert(out_line); + nm_assert(out_line_len); + + if (G_UNLIKELY(*inout_len == 0)) + return FALSE; + + line_start = *inout_ptr; + + eol_is_carriage_return = FALSE; + for (line_len = 0;; line_len++) { + if (line_len >= *inout_len) { + /* if we consumed the entire line, we place the pointer at + * one character after the end. */ + *inout_ptr = &line_start[line_len]; + *inout_len = 0; + goto done; + } + switch (line_start[line_len]) { + case '\r': + eol_is_carriage_return = TRUE; + /* fall-through*/ + case '\0': + case '\n': + *inout_ptr = &line_start[line_len + 1]; + *inout_len = *inout_len - line_len - 1u; + if (eol_is_carriage_return && *inout_len > 0 && (*inout_ptr)[0] == '\n') { + /* also consume "\r\n" as one. */ + (*inout_len)--; + (*inout_ptr)++; + } + goto done; + } + } + +done: + *out_line = line_start; + *out_line_len = line_len; + return TRUE; +} + +/*****************************************************************************/ + +gboolean +nm_utils_ipaddr_is_valid(int addr_family, const char *str_addr) +{ + nm_assert(NM_IN_SET(addr_family, AF_UNSPEC, AF_INET, AF_INET6)); + + return str_addr && nm_utils_parse_inaddr_bin(addr_family, str_addr, NULL, NULL); +} + +gboolean +nm_utils_ipaddr_is_normalized(int addr_family, const char *str_addr) +{ + NMIPAddr addr; + char sbuf[NM_UTILS_INET_ADDRSTRLEN]; + + nm_assert(NM_IN_SET(addr_family, AF_UNSPEC, AF_INET, AF_INET6)); + + if (!str_addr) + return FALSE; + + if (!nm_utils_parse_inaddr_bin(addr_family, str_addr, &addr_family, &addr)) + return FALSE; + + nm_utils_inet_ntop(addr_family, &addr, sbuf); + return nm_streq(sbuf, str_addr); +} + +/*****************************************************************************/ + +/** + * nm_g_ascii_strtoll() + * @nptr: the string to parse + * @endptr: the pointer on the first invalid chars + * @base: the base. + * + * This wraps g_ascii_strtoll() and should in almost all cases behave identical + * to it. + * + * However, it seems there are situations where g_ascii_strtoll() might set + * errno to some unexpected value EAGAIN. Possibly this is related to creating + * the C locale during + * + * #ifdef USE_XLOCALE + * return strtoll_l (nptr, endptr, base, get_C_locale ()); + * + * This wrapper tries to workaround that condition. + */ +gint64 +nm_g_ascii_strtoll(const char *nptr, char **endptr, guint base) +{ + int try_count = 2; + gint64 v; + const int errsv_orig = errno; + int errsv; + + nm_assert(nptr); + nm_assert(base == 0u || (base >= 2u && base <= 36u)); + +again: + errno = 0; + v = g_ascii_strtoll(nptr, endptr, base); + errsv = errno; + + if (errsv == 0) { + if (errsv_orig != 0) + errno = errsv_orig; + return v; + } + + if (errsv == ERANGE && NM_IN_SET(v, G_MININT64, G_MAXINT64)) + return v; + + if (errsv == EINVAL && v == 0 && nptr && nptr[0] == '\0') + return v; + + if (try_count-- > 0) + goto again; + +#if NM_MORE_ASSERTS + g_critical("g_ascii_strtoll() for \"%s\" failed with errno=%d (%s) and v=%" G_GINT64_FORMAT, + nptr, + errsv, + nm_strerror_native(errsv), + v); +#endif + + return v; +} + +/* See nm_g_ascii_strtoll() */ +guint64 +nm_g_ascii_strtoull(const char *nptr, char **endptr, guint base) +{ + int try_count = 2; + guint64 v; + const int errsv_orig = errno; + int errsv; + + nm_assert(nptr); + nm_assert(base == 0u || (base >= 2u && base <= 36u)); + +again: + errno = 0; + v = g_ascii_strtoull(nptr, endptr, base); + errsv = errno; + + if (errsv == 0) { + if (errsv_orig != 0) + errno = errsv_orig; + return v; + } + + if (errsv == ERANGE && NM_IN_SET(v, G_MAXUINT64)) + return v; + + if (errsv == EINVAL && v == 0 && nptr && nptr[0] == '\0') + return v; + + if (try_count-- > 0) + goto again; + +#if NM_MORE_ASSERTS + g_critical("g_ascii_strtoull() for \"%s\" failed with errno=%d (%s) and v=%" G_GUINT64_FORMAT, + nptr, + errsv, + nm_strerror_native(errsv), + v); +#endif + + return v; +} + +/* see nm_g_ascii_strtoll(). */ +double +nm_g_ascii_strtod(const char *nptr, char **endptr) +{ + int try_count = 2; + double v; + int errsv; + + nm_assert(nptr); + +again: + v = g_ascii_strtod(nptr, endptr); + errsv = errno; + + if (errsv == 0) + return v; + + if (errsv == ERANGE) + return v; + + if (try_count-- > 0) + goto again; + +#if NM_MORE_ASSERTS + g_critical("g_ascii_strtod() for \"%s\" failed with errno=%d (%s) and v=%f", + nptr, + errsv, + nm_strerror_native(errsv), + v); +#endif + + /* Not really much else to do. Return the parsed value and leave errno set + * to the unexpected value. */ + return v; +} + +/* _nm_utils_ascii_str_to_int64: + * + * A wrapper for g_ascii_strtoll, that checks whether the whole string + * can be successfully converted to a number and is within a given + * range. On any error, @fallback will be returned and %errno will be set + * to a non-zero value. On success, %errno will be set to zero, check %errno + * for errors. Any trailing or leading (ascii) white space is ignored and the + * functions is locale independent. + * + * The function is guaranteed to return a value between @min and @max + * (inclusive) or @fallback. Also, the parsing is rather strict, it does + * not allow for any unrecognized characters, except leading and trailing + * white space. + **/ +gint64 +_nm_utils_ascii_str_to_int64(const char *str, guint base, gint64 min, gint64 max, gint64 fallback) +{ + gint64 v; + const char *s = NULL; + + str = nm_str_skip_leading_spaces(str); + if (!str || !str[0]) { + errno = EINVAL; + return fallback; + } + + errno = 0; + v = nm_g_ascii_strtoll(str, (char **) &s, base); + + if (errno != 0) + return fallback; + + if (s[0] != '\0') { + s = nm_str_skip_leading_spaces(s); + if (s[0] != '\0') { + errno = EINVAL; + return fallback; + } + } + if (v > max || v < min) { + errno = ERANGE; + return fallback; + } + + return v; +} + +guint64 +_nm_utils_ascii_str_to_uint64(const char *str, + guint base, + guint64 min, + guint64 max, + guint64 fallback) +{ + guint64 v; + const char *s = NULL; + + if (str) { + while (g_ascii_isspace(str[0])) + str++; + } + if (!str || !str[0]) { + errno = EINVAL; + return fallback; + } + + errno = 0; + v = nm_g_ascii_strtoull(str, (char **) &s, base); + + if (errno != 0) + return fallback; + if (s[0] != '\0') { + while (g_ascii_isspace(s[0])) + s++; + if (s[0] != '\0') { + errno = EINVAL; + return fallback; + } + } + if (v > max || v < min) { + errno = ERANGE; + return fallback; + } + + if (v != 0 && str[0] == '-') { + /* As documented, g_ascii_strtoull() accepts negative values, and returns their + * absolute value. We don't. */ + errno = ERANGE; + return fallback; + } + + return v; +} + +/*****************************************************************************/ + +int +nm_strcmp_with_data(gconstpointer a, gconstpointer b, gpointer user_data) +{ + const char *s1 = a; + const char *s2 = b; + + return strcmp(s1, s2); +} + +/* like nm_strcmp_p(), suitable for g_ptr_array_sort_with_data(). + * g_ptr_array_sort() just casts nm_strcmp_p() to a function of different + * signature. I guess, in glib there are knowledgeable people that ensure + * that this additional argument doesn't cause problems due to different ABI + * for every architecture that glib supports. + * For NetworkManager, we'd rather avoid such stunts. + **/ +int +nm_strcmp_p_with_data(gconstpointer a, gconstpointer b, gpointer user_data) +{ + const char *s1 = *((const char **) a); + const char *s2 = *((const char **) b); + + return strcmp(s1, s2); +} + +int +nm_strcmp0_p_with_data(gconstpointer a, gconstpointer b, gpointer user_data) +{ + const char *s1 = *((const char **) a); + const char *s2 = *((const char **) b); + + return nm_strcmp0(s1, s2); +} + +int +nm_strcmp_ascii_case_with_data(gconstpointer a, gconstpointer b, gpointer user_data) +{ + const char *s1 = a; + const char *s2 = b; + + return g_ascii_strcasecmp(s1, s2); +} + +int +nm_cmp_uint32_p_with_data(gconstpointer p_a, gconstpointer p_b, gpointer user_data) +{ + const guint32 a = *((const guint32 *) p_a); + const guint32 b = *((const guint32 *) p_b); + + if (a < b) + return -1; + if (a > b) + return 1; + return 0; +} + +int +nm_cmp_int2ptr_p_with_data(gconstpointer p_a, gconstpointer p_b, gpointer user_data) +{ + /* p_a and p_b are two pointers to a pointer, where the pointer is + * interpreted as a integer using GPOINTER_TO_INT(). + * + * That is the case of a hash-table that uses GINT_TO_POINTER() to + * convert integers as pointers, and the resulting keys-as-array + * array. */ + const int a = GPOINTER_TO_INT(*((gconstpointer *) p_a)); + const int b = GPOINTER_TO_INT(*((gconstpointer *) p_b)); + + if (a < b) + return -1; + if (a > b) + return 1; + return 0; +} + +/*****************************************************************************/ + +const char * +nm_utils_dbus_path_get_last_component(const char *dbus_path) +{ + if (dbus_path) { + dbus_path = strrchr(dbus_path, '/'); + if (dbus_path) + return dbus_path + 1; + } + return NULL; +} + +static gint64 +_dbus_path_component_as_num(const char *p) +{ + gint64 n; + + /* no odd stuff. No leading zeros, only a non-negative, decimal integer. + * + * Otherwise, there would be multiple ways to encode the same number "10" + * and "010". That is just confusing. A number has no leading zeros, + * if it has, it's not a number (as far as we are concerned here). */ + if (p[0] == '0') { + if (p[1] != '\0') + return -1; + else + return 0; + } + if (!(p[0] >= '1' && p[0] <= '9')) + return -1; + if (!NM_STRCHAR_ALL(&p[1], ch, (ch >= '0' && ch <= '9'))) + return -1; + n = _nm_utils_ascii_str_to_int64(p, 10, 0, G_MAXINT64, -1); + nm_assert(n == -1 || nm_streq0(p, nm_sprintf_bufa(100, "%" G_GINT64_FORMAT, n))); + return n; +} + +int +nm_utils_dbus_path_cmp(const char *dbus_path_a, const char *dbus_path_b) +{ + const char *l_a, *l_b; + gsize plen; + gint64 n_a, n_b; + + /* compare function for two D-Bus paths. It behaves like + * strcmp(), except, if both paths have the same prefix, + * and both end in a (positive) number, then the paths + * will be sorted by number. */ + + NM_CMP_SELF(dbus_path_a, dbus_path_b); + + /* if one or both paths have no slash (and no last component) + * compare the full paths directly. */ + if (!(l_a = nm_utils_dbus_path_get_last_component(dbus_path_a)) + || !(l_b = nm_utils_dbus_path_get_last_component(dbus_path_b))) + goto comp_full; + + /* check if both paths have the same prefix (up to the last-component). */ + plen = l_a - dbus_path_a; + if (plen != (l_b - dbus_path_b)) + goto comp_full; + NM_CMP_RETURN(strncmp(dbus_path_a, dbus_path_b, plen)); + + n_a = _dbus_path_component_as_num(l_a); + n_b = _dbus_path_component_as_num(l_b); + if (n_a == -1 && n_b == -1) + goto comp_l; + + /* both components must be convertible to a number. If they are not, + * (and only one of them is), then we must always strictly sort numeric parts + * after non-numeric components. If we wouldn't, we wouldn't have + * a total order. + * + * An example of a not total ordering would be: + * "8" < "010" (numeric) + * "0x" < "8" (lexical) + * "0x" > "010" (lexical) + * We avoid this, by forcing that a non-numeric entry "0x" always sorts + * before numeric entries. + * + * Additionally, _dbus_path_component_as_num() would also reject "010" as + * not a valid number. + */ + if (n_a == -1) + return -1; + if (n_b == -1) + return 1; + + NM_CMP_DIRECT(n_a, n_b); + nm_assert(nm_streq(dbus_path_a, dbus_path_b)); + return 0; + +comp_full: + NM_CMP_DIRECT_STRCMP0(dbus_path_a, dbus_path_b); + return 0; +comp_l: + NM_CMP_DIRECT_STRCMP0(l_a, l_b); + nm_assert(nm_streq(dbus_path_a, dbus_path_b)); + return 0; +} + +/*****************************************************************************/ + +typedef struct { + union { + guint8 table[256]; + guint64 _dummy_for_alignment; + }; +} CharLookupTable; + +static void +_char_lookup_table_set_one(CharLookupTable *lookup, char ch) +{ + lookup->table[(guint8) ch] = 1; +} + +static void +_char_lookup_table_set_all(CharLookupTable *lookup, const char *candidates) +{ + while (candidates[0] != '\0') + _char_lookup_table_set_one(lookup, (candidates++)[0]); +} + +static void +_char_lookup_table_init(CharLookupTable *lookup, const char *candidates) +{ + *lookup = (CharLookupTable){ + .table = {0}, + }; + if (candidates) + _char_lookup_table_set_all(lookup, candidates); +} + +static gboolean +_char_lookup_has(const CharLookupTable *lookup, char ch) +{ + /* with some optimization levels, the compiler thinks this code + * might access uninitialized @lookup. It is not -- when you look at the + * callers of this function. */ + NM_PRAGMA_WARNING_DISABLE("-Wmaybe-uninitialized") + nm_assert(lookup->table[(guint8) '\0'] == 0); + return lookup->table[(guint8) ch] != 0; + NM_PRAGMA_WARNING_REENABLE +} + +static gboolean +_char_lookup_has_all(const CharLookupTable *lookup, const char *candidates) +{ + if (candidates) { + while (candidates[0] != '\0') { + if (!_char_lookup_has(lookup, (candidates++)[0])) + return FALSE; + } + } + return TRUE; +} + +/** + * nm_utils_strsplit_set_full: + * @str: the string to split. + * @delimiters: the set of delimiters. + * @flags: additional flags for controlling the operation. + * + * This is a replacement for g_strsplit_set() which avoids copying + * each word once (the entire strv array), but instead copies it once + * and all words point into that internal copy. + * + * Note that for @str %NULL and "", this always returns %NULL too. That differs + * from g_strsplit_set(), which would return an empty strv array for "". + * This never returns an empty array. + * + * Returns: %NULL if @str is %NULL or "". + * If @str only contains delimiters and %NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY + * is not set, it also returns %NULL. + * Otherwise, a %NULL terminated strv array containing the split words. + * (delimiter characters are removed). + * The strings to which the result strv array points to are allocated + * after the returned result itself. Don't free the strings themself, + * but free everything with g_free(). + * It is however safe and allowed to modify the individual strings in-place, + * like "g_strstrip((char *) iter[0])". + */ +const char ** +nm_utils_strsplit_set_full(const char *str, const char *delimiters, NMUtilsStrsplitSetFlags flags) +{ + const char ** ptr; + gsize num_tokens; + gsize i_token; + gsize str_len_p1; + const char * c_str; + char * s; + CharLookupTable ch_lookup; + const gboolean f_escaped = NM_FLAGS_HAS(flags, NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED); + const gboolean f_allow_escaping = + f_escaped || NM_FLAGS_HAS(flags, NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING); + const gboolean f_preserve_empty = + NM_FLAGS_HAS(flags, NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY); + const gboolean f_strstrip = NM_FLAGS_HAS(flags, NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP); + + if (!str) + return NULL; + + if (!delimiters) { + nm_assert_not_reached(); + delimiters = " \t\n"; + } + _char_lookup_table_init(&ch_lookup, delimiters); + + nm_assert(!f_allow_escaping || !_char_lookup_has(&ch_lookup, '\\')); + + if (!f_preserve_empty) { + while (_char_lookup_has(&ch_lookup, str[0])) + str++; + } + + if (!str[0]) { + /* We return %NULL here, also with NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY. + * That makes nm_utils_strsplit_set_full() with NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY + * different from g_strsplit_set(), which would in this case return an empty array. + * If you need to handle %NULL, and "" specially, then check the input string first. */ + return NULL; + } + +#define _char_is_escaped(str_start, str_cur) \ + ({ \ + const char *const _str_start = (str_start); \ + const char *const _str_cur = (str_cur); \ + const char * _str_i = (_str_cur); \ + \ + while (_str_i > _str_start && _str_i[-1] == '\\') \ + _str_i--; \ + (((_str_cur - _str_i) % 2) != 0); \ + }) + + num_tokens = 1; + c_str = str; + while (TRUE) { + while (G_LIKELY(!_char_lookup_has(&ch_lookup, c_str[0]))) { + if (c_str[0] == '\0') + goto done1; + c_str++; + } + + /* we assume escapings are not frequent. After we found + * this delimiter, check whether it was escaped by counting + * the backslashed before. */ + if (f_allow_escaping && _char_is_escaped(str, c_str)) { + /* the delimiter is escaped. This was not an accepted delimiter. */ + c_str++; + continue; + } + + c_str++; + + /* if we drop empty tokens, then we now skip over all consecutive delimiters. */ + if (!f_preserve_empty) { + while (_char_lookup_has(&ch_lookup, c_str[0])) + c_str++; + if (c_str[0] == '\0') + break; + } + + num_tokens++; + } + +done1: + + nm_assert(c_str[0] == '\0'); + + str_len_p1 = (c_str - str) + 1; + + nm_assert(str[str_len_p1 - 1] == '\0'); + + ptr = g_malloc((sizeof(const char *) * (num_tokens + 1)) + str_len_p1); + s = (char *) &ptr[num_tokens + 1]; + memcpy(s, str, str_len_p1); + + i_token = 0; + + while (TRUE) { + nm_assert(i_token < num_tokens); + ptr[i_token++] = s; + + if (s[0] == '\0') { + nm_assert(f_preserve_empty); + goto done2; + } + nm_assert(f_preserve_empty || !_char_lookup_has(&ch_lookup, s[0])); + + while (!_char_lookup_has(&ch_lookup, s[0])) { + if (G_UNLIKELY(s[0] == '\\' && f_allow_escaping)) { + s++; + if (s[0] == '\0') + goto done2; + s++; + } else if (s[0] == '\0') + goto done2; + else + s++; + } + + nm_assert(_char_lookup_has(&ch_lookup, s[0])); + s[0] = '\0'; + s++; + + if (!f_preserve_empty) { + while (_char_lookup_has(&ch_lookup, s[0])) + s++; + if (s[0] == '\0') + goto done2; + } + } + +done2: + nm_assert(i_token == num_tokens); + ptr[i_token] = NULL; + + if (f_strstrip) { + gsize i; + + i_token = 0; + for (i = 0; ptr[i]; i++) { + s = (char *) nm_str_skip_leading_spaces(ptr[i]); + if (s[0] != '\0') { + char *s_last; + + s_last = &s[strlen(s) - 1]; + while (s_last > s && g_ascii_isspace(s_last[0]) + && (!f_allow_escaping || !_char_is_escaped(s, s_last))) + (s_last--)[0] = '\0'; + } + + if (!f_preserve_empty && s[0] == '\0') + continue; + + ptr[i_token++] = s; + } + + if (i_token == 0) { + g_free(ptr); + return NULL; + } + ptr[i_token] = NULL; + } + + if (f_escaped) { + gsize i, j; + + /* We no longer need ch_lookup for its original purpose. Modify it, so it + * can detect the delimiters, '\\', and (optionally) whitespaces. */ + _char_lookup_table_set_one(&ch_lookup, '\\'); + if (f_strstrip) + _char_lookup_table_set_all(&ch_lookup, NM_ASCII_SPACES); + + for (i_token = 0; ptr[i_token]; i_token++) { + s = (char *) ptr[i_token]; + j = 0; + for (i = 0; s[i] != '\0';) { + if (s[i] == '\\' && _char_lookup_has(&ch_lookup, s[i + 1])) + i++; + s[j++] = s[i++]; + } + s[j] = '\0'; + } + } + + nm_assert(ptr && ptr[0]); + return ptr; +} + +/*****************************************************************************/ + +const char * +nm_utils_escaped_tokens_escape_full(const char * str, + const char * delimiters, + const char * delimiters_as_needed, + NMUtilsEscapedTokensEscapeFlags flags, + char ** out_to_free) +{ + CharLookupTable ch_lookup; + CharLookupTable ch_lookup_as_needed; + gboolean has_ch_lookup_as_needed = FALSE; + char * ret; + gsize str_len; + gsize alloc_len; + gsize n_escapes; + gsize i, j; + gboolean escape_leading_space; + gboolean escape_trailing_space; + gboolean escape_backslash_as_needed; + + nm_assert( + !delimiters_as_needed + || (delimiters_as_needed[0] + && NM_FLAGS_HAS(flags, + NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_AS_NEEDED))); + + if (!str || str[0] == '\0') { + *out_to_free = NULL; + return str; + } + + str_len = strlen(str); + + _char_lookup_table_init(&ch_lookup, delimiters); + if (!delimiters || NM_FLAGS_HAS(flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_SPACES)) { + flags &= ~(NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE + | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE); + _char_lookup_table_set_all(&ch_lookup, NM_ASCII_SPACES); + } + + if (NM_FLAGS_HAS(flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_ALWAYS)) { + _char_lookup_table_set_one(&ch_lookup, '\\'); + escape_backslash_as_needed = FALSE; + } else if (_char_lookup_has(&ch_lookup, '\\')) + escape_backslash_as_needed = FALSE; + else { + escape_backslash_as_needed = + NM_FLAGS_HAS(flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_AS_NEEDED); + if (escape_backslash_as_needed) { + if (NM_FLAGS_ANY(flags, + NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE + | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE) + && !_char_lookup_has_all(&ch_lookup, NM_ASCII_SPACES)) { + /* ESCAPE_LEADING_SPACE and ESCAPE_TRAILING_SPACE implies that we escape backslash + * before whitespaces. */ + if (!has_ch_lookup_as_needed) { + has_ch_lookup_as_needed = TRUE; + _char_lookup_table_init(&ch_lookup_as_needed, NULL); + } + _char_lookup_table_set_all(&ch_lookup_as_needed, NM_ASCII_SPACES); + } + if (delimiters_as_needed && !_char_lookup_has_all(&ch_lookup, delimiters_as_needed)) { + if (!has_ch_lookup_as_needed) { + has_ch_lookup_as_needed = TRUE; + _char_lookup_table_init(&ch_lookup_as_needed, NULL); + } + _char_lookup_table_set_all(&ch_lookup_as_needed, delimiters_as_needed); + } + } + } + + escape_leading_space = + NM_FLAGS_HAS(flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE) + && g_ascii_isspace(str[0]) && !_char_lookup_has(&ch_lookup, str[0]); + if (str_len == 1) + escape_trailing_space = FALSE; + else { + escape_trailing_space = + NM_FLAGS_HAS(flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE) + && g_ascii_isspace(str[str_len - 1]) && !_char_lookup_has(&ch_lookup, str[str_len - 1]); + } + + n_escapes = 0; + for (i = 0; str[i] != '\0'; i++) { + if (_char_lookup_has(&ch_lookup, str[i])) + n_escapes++; + else if (str[i] == '\\' && escape_backslash_as_needed + && (_char_lookup_has(&ch_lookup, str[i + 1]) || NM_IN_SET(str[i + 1], '\0', '\\') + || (has_ch_lookup_as_needed + && _char_lookup_has(&ch_lookup_as_needed, str[i + 1])))) + n_escapes++; + } + if (escape_leading_space) + n_escapes++; + if (escape_trailing_space) + n_escapes++; + + if (n_escapes == 0u) { + *out_to_free = NULL; + return str; + } + + alloc_len = str_len + n_escapes + 1u; + ret = g_new(char, alloc_len); + + j = 0; + i = 0; + + if (escape_leading_space) { + ret[j++] = '\\'; + ret[j++] = str[i++]; + } + for (; str[i] != '\0'; i++) { + if (_char_lookup_has(&ch_lookup, str[i])) + ret[j++] = '\\'; + else if (str[i] == '\\' && escape_backslash_as_needed + && (_char_lookup_has(&ch_lookup, str[i + 1]) || NM_IN_SET(str[i + 1], '\0', '\\') + || (has_ch_lookup_as_needed + && _char_lookup_has(&ch_lookup_as_needed, str[i + 1])))) + ret[j++] = '\\'; + ret[j++] = str[i]; + } + if (escape_trailing_space) { + nm_assert(!_char_lookup_has(&ch_lookup, ret[j - 1]) && g_ascii_isspace(ret[j - 1])); + ret[j] = ret[j - 1]; + ret[j - 1] = '\\'; + j++; + } + + nm_assert(j == alloc_len - 1); + ret[j] = '\0'; + nm_assert(strlen(ret) == j); + + *out_to_free = ret; + return ret; +} + +/** + * nm_utils_escaped_tokens_options_split: + * @str: the src string. This string will be modified in-place. + * The output values will point into @str. + * @out_key: (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; +} + +/*****************************************************************************/ + +/** + * nm_utils_strsplit_quoted: + * @str: the string to split (e.g. from /proc/cmdline). + * + * This basically does that systemd's extract_first_word() does + * with the flags "EXTRACT_UNQUOTE | EXTRACT_RELAX". This is what + * systemd uses to parse /proc/cmdline, and we do too. + * + * Splits the string. We have nm_utils_strsplit_set() which + * supports a variety of flags. However, extending that already + * complex code to also support quotation and escaping is hard. + * Instead, add a naive implementation. + * + * Returns: (transfer full): the split string. + */ +char ** +nm_utils_strsplit_quoted(const char *str) +{ + gs_unref_ptrarray GPtrArray *arr = NULL; + gs_free char * str_out = NULL; + CharLookupTable ch_lookup; + + nm_assert(str); + + _char_lookup_table_init(&ch_lookup, NM_ASCII_WHITESPACES); + + for (;;) { + char quote; + gsize j; + + while (_char_lookup_has(&ch_lookup, str[0])) + str++; + + if (str[0] == '\0') + break; + + if (!str_out) + str_out = g_new(char, strlen(str) + 1); + + quote = '\0'; + j = 0; + for (;;) { + if (str[0] == '\\') { + str++; + if (str[0] == '\0') + break; + str_out[j++] = str[0]; + str++; + continue; + } + if (quote) { + if (str[0] == '\0') + break; + if (str[0] == quote) { + quote = '\0'; + str++; + continue; + } + str_out[j++] = str[0]; + str++; + continue; + } + if (str[0] == '\0') + break; + if (NM_IN_SET(str[0], '\'', '"')) { + quote = str[0]; + str++; + continue; + } + if (_char_lookup_has(&ch_lookup, str[0])) { + str++; + break; + } + str_out[j++] = str[0]; + str++; + } + + if (!arr) + arr = g_ptr_array_new(); + g_ptr_array_add(arr, 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); +} + +/*****************************************************************************/ + +/** + * nm_utils_strv_find_first: + * @list: the strv list to search + * @len: the length of the list, or a negative value if @list is %NULL terminated. + * @needle: the value to search for. The search is done using strcmp(). + * + * Searches @list for @needle and returns the index of the first match (based + * on strcmp()). + * + * For convenience, @list has type 'char**' instead of 'const char **'. + * + * Returns: index of first occurrence or -1 if @needle is not found in @list. + */ +gssize +nm_utils_strv_find_first(char **list, gssize len, const char *needle) +{ + gssize i; + + if (len > 0) { + g_return_val_if_fail(list, -1); + + if (!needle) { + /* if we search a list with known length, %NULL is a valid @needle. */ + for (i = 0; i < len; i++) { + if (!list[i]) + return i; + } + } else { + for (i = 0; i < len; i++) { + if (list[i] && !strcmp(needle, list[i])) + return i; + } + } + } else if (len < 0) { + g_return_val_if_fail(needle, -1); + + if (list) { + for (i = 0; list[i]; i++) { + if (strcmp(needle, list[i]) == 0) + return i; + } + } + } + return -1; +} + +char ** +_nm_utils_strv_cleanup(char ** strv, + gboolean strip_whitespace, + gboolean skip_empty, + gboolean skip_repeated) +{ + guint i, j; + + if (!strv || !*strv) + return strv; + + if (strip_whitespace) { + /* we only modify the strings pointed to by @strv if @strip_whitespace is + * requested. Otherwise, the strings themselves are untouched. */ + for (i = 0; strv[i]; i++) + g_strstrip(strv[i]); + } + if (!skip_empty && !skip_repeated) + return strv; + j = 0; + for (i = 0; strv[i]; i++) { + if ((skip_empty && !*strv[i]) + || (skip_repeated && nm_utils_strv_find_first(strv, j, strv[i]) >= 0)) + g_free(strv[i]); + else + strv[j++] = strv[i]; + } + strv[j] = NULL; + return strv; +} + +/*****************************************************************************/ + +GPtrArray * +_nm_g_ptr_array_copy(GPtrArray * array, + GCopyFunc func, + gpointer user_data, + GDestroyNotify element_free_func) +{ + GPtrArray *new_array; + guint i; + + g_return_val_if_fail(array, NULL); + + new_array = g_ptr_array_new_full(array->len, element_free_func); + for (i = 0; i < array->len; i++) { + g_ptr_array_add(new_array, func ? func(array->pdata[i], user_data) : array->pdata[i]); + } + return new_array; +} + +/*****************************************************************************/ + +int +_nm_utils_ascii_str_to_bool(const char *str, int default_value) +{ + gs_free char *str_free = NULL; + + if (!str) + return default_value; + + str = nm_strstrip_avoid_copy_a(300, str, &str_free); + if (str[0] == '\0') + return default_value; + + if (!g_ascii_strcasecmp(str, "true") || !g_ascii_strcasecmp(str, "yes") + || !g_ascii_strcasecmp(str, "on") || !g_ascii_strcasecmp(str, "1")) + return TRUE; + + if (!g_ascii_strcasecmp(str, "false") || !g_ascii_strcasecmp(str, "no") + || !g_ascii_strcasecmp(str, "off") || !g_ascii_strcasecmp(str, "0")) + return FALSE; + + return default_value; +} + +/*****************************************************************************/ + +NM_CACHED_QUARK_FCN("nm-manager-error-quark", nm_manager_error_quark); + +NM_CACHED_QUARK_FCN("nm-utils-error-quark", nm_utils_error_quark); + +void +nm_utils_error_set_cancelled(GError **error, gboolean is_disposing, const char *instance_name) +{ + if (is_disposing) { + g_set_error(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_CANCELLED_DISPOSING, + "Disposing %s instance", + instance_name && *instance_name ? instance_name : "source"); + } else { + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_CANCELLED, "Request cancelled"); + } +} + +gboolean +nm_utils_error_is_cancelled_or_disposing(GError *error) +{ + if (error) { + if (error->domain == G_IO_ERROR) + return NM_IN_SET(error->code, G_IO_ERROR_CANCELLED); + if (error->domain == NM_UTILS_ERROR) + return NM_IN_SET(error->code, NM_UTILS_ERROR_CANCELLED_DISPOSING); + } + return FALSE; +} + +gboolean +nm_utils_error_is_notfound(GError *error) +{ + if (error) { + if (error->domain == G_IO_ERROR) + return NM_IN_SET(error->code, G_IO_ERROR_NOT_FOUND); + if (error->domain == G_FILE_ERROR) + return NM_IN_SET(error->code, G_FILE_ERROR_NOENT); + } + return FALSE; +} + +/*****************************************************************************/ + +/** + * nm_g_object_set_property: + * @object: the target object + * @property_name: the property name + * @value: the #GValue to set + * @error: (allow-none): optional error argument + * + * A reimplementation of g_object_set_property(), but instead + * returning an error instead of logging a warning. All g_object_set*() + * versions in glib require you to not pass invalid types or they will + * log a g_warning() -- without reporting an error. We don't want that, + * so we need to hack error checking around it. + * + * Returns: whether the value was successfully set. + */ +gboolean +nm_g_object_set_property(GObject * object, + const char * property_name, + const GValue *value, + GError ** error) +{ + GParamSpec * pspec; + nm_auto_unset_gvalue GValue tmp_value = G_VALUE_INIT; + GObjectClass * klass; + + g_return_val_if_fail(G_IS_OBJECT(object), FALSE); + g_return_val_if_fail(property_name != NULL, FALSE); + g_return_val_if_fail(G_IS_VALUE(value), FALSE); + g_return_val_if_fail(!error || !*error, FALSE); + + /* g_object_class_find_property() does g_param_spec_get_redirect_target(), + * where we differ from a plain g_object_set_property(). */ + pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(object), property_name); + + if (!pspec) { + g_set_error(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + _("object class '%s' has no property named '%s'"), + G_OBJECT_TYPE_NAME(object), + property_name); + return FALSE; + } + if (!(pspec->flags & G_PARAM_WRITABLE)) { + g_set_error(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + _("property '%s' of object class '%s' is not writable"), + pspec->name, + G_OBJECT_TYPE_NAME(object)); + return FALSE; + } + if ((pspec->flags & G_PARAM_CONSTRUCT_ONLY)) { + g_set_error(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + _("construct property \"%s\" for object '%s' can't be set after construction"), + pspec->name, + G_OBJECT_TYPE_NAME(object)); + return FALSE; + } + + klass = g_type_class_peek(pspec->owner_type); + if (klass == NULL) { + g_set_error(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + _("'%s::%s' is not a valid property name; '%s' is not a GObject subtype"), + g_type_name(pspec->owner_type), + pspec->name, + g_type_name(pspec->owner_type)); + return FALSE; + } + + /* provide a copy to work from, convert (if necessary) and validate */ + g_value_init(&tmp_value, pspec->value_type); + if (!g_value_transform(value, &tmp_value)) { + g_set_error(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + _("unable to set property '%s' of type '%s' from value of type '%s'"), + pspec->name, + g_type_name(pspec->value_type), + G_VALUE_TYPE_NAME(value)); + return FALSE; + } + if (g_param_value_validate(pspec, &tmp_value) && !(pspec->flags & G_PARAM_LAX_VALIDATION)) { + gs_free char *contents = g_strdup_value_contents(value); + + g_set_error(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + _("value \"%s\" of type '%s' is invalid or out of range for property '%s' of " + "type '%s'"), + contents, + G_VALUE_TYPE_NAME(value), + pspec->name, + g_type_name(pspec->value_type)); + return FALSE; + } + + g_object_set_property(object, property_name, &tmp_value); + return TRUE; +} + +#define _set_property(object, property_name, gtype, gtype_set, value, error) \ + G_STMT_START \ + { \ + nm_auto_unset_gvalue GValue gvalue = {0}; \ + \ + g_value_init(&gvalue, gtype); \ + gtype_set(&gvalue, (value)); \ + return nm_g_object_set_property((object), (property_name), &gvalue, (error)); \ + } \ + G_STMT_END + +gboolean +nm_g_object_set_property_string(GObject * object, + const char *property_name, + const char *value, + GError ** error) +{ + _set_property(object, property_name, G_TYPE_STRING, g_value_set_string, value, error); +} + +gboolean +nm_g_object_set_property_string_static(GObject * object, + const char *property_name, + const char *value, + GError ** error) +{ + _set_property(object, property_name, G_TYPE_STRING, g_value_set_static_string, value, error); +} + +gboolean +nm_g_object_set_property_string_take(GObject * object, + const char *property_name, + char * value, + GError ** error) +{ + _set_property(object, property_name, G_TYPE_STRING, g_value_take_string, value, error); +} + +gboolean +nm_g_object_set_property_boolean(GObject * object, + const char *property_name, + gboolean value, + GError ** error) +{ + _set_property(object, property_name, G_TYPE_BOOLEAN, g_value_set_boolean, !!value, error); +} + +gboolean +nm_g_object_set_property_char(GObject * object, + const char *property_name, + gint8 value, + GError ** error) +{ + /* glib says about G_TYPE_CHAR: + * + * The type designated by G_TYPE_CHAR is unconditionally an 8-bit signed integer. + * + * This is always a (signed!) char. */ + _set_property(object, property_name, G_TYPE_CHAR, g_value_set_schar, value, error); +} + +gboolean +nm_g_object_set_property_uchar(GObject * object, + const char *property_name, + guint8 value, + GError ** error) +{ + _set_property(object, property_name, G_TYPE_UCHAR, g_value_set_uchar, value, error); +} + +gboolean +nm_g_object_set_property_int(GObject *object, const char *property_name, int value, GError **error) +{ + _set_property(object, property_name, G_TYPE_INT, g_value_set_int, value, error); +} + +gboolean +nm_g_object_set_property_int64(GObject * object, + const char *property_name, + gint64 value, + GError ** error) +{ + _set_property(object, property_name, G_TYPE_INT64, g_value_set_int64, value, error); +} + +gboolean +nm_g_object_set_property_uint(GObject * object, + const char *property_name, + guint value, + GError ** error) +{ + _set_property(object, property_name, G_TYPE_UINT, g_value_set_uint, value, error); +} + +gboolean +nm_g_object_set_property_uint64(GObject * object, + const char *property_name, + guint64 value, + GError ** error) +{ + _set_property(object, property_name, G_TYPE_UINT64, g_value_set_uint64, value, error); +} + +gboolean +nm_g_object_set_property_flags(GObject * object, + const char *property_name, + GType gtype, + guint value, + GError ** error) +{ + nm_assert(({ + nm_auto_unref_gtypeclass GTypeClass *gtypeclass = g_type_class_ref(gtype); + G_IS_FLAGS_CLASS(gtypeclass); + })); + _set_property(object, property_name, gtype, g_value_set_flags, value, error); +} + +gboolean +nm_g_object_set_property_enum(GObject * object, + const char *property_name, + GType gtype, + int value, + GError ** error) +{ + nm_assert(({ + nm_auto_unref_gtypeclass GTypeClass *gtypeclass = g_type_class_ref(gtype); + G_IS_ENUM_CLASS(gtypeclass); + })); + _set_property(object, property_name, gtype, g_value_set_enum, value, error); +} + +GParamSpec * +nm_g_object_class_find_property_from_gtype(GType gtype, const char *property_name) +{ + nm_auto_unref_gtypeclass GObjectClass *gclass = NULL; + + gclass = g_type_class_ref(gtype); + return g_object_class_find_property(gclass, property_name); +} + +/*****************************************************************************/ + +/** + * nm_g_type_find_implementing_class_for_property: + * @gtype: the GObject type which has a property @pname + * @pname: the name of the property to look up + * + * This is only a helper function for printf debugging. It's not + * used in actual code. Hence, the function just asserts that + * @pname and @gtype arguments are suitable. It cannot fail. + * + * Returns: the most ancestor type of @gtype, that + * implements the property @pname. It means, it + * searches the type hierarchy to find the type + * that added @pname. + */ +GType +nm_g_type_find_implementing_class_for_property(GType gtype, const char *pname) +{ + nm_auto_unref_gtypeclass GObjectClass *klass = NULL; + GParamSpec * pspec; + + g_return_val_if_fail(pname, G_TYPE_INVALID); + + klass = g_type_class_ref(gtype); + g_return_val_if_fail(G_IS_OBJECT_CLASS(klass), G_TYPE_INVALID); + + pspec = g_object_class_find_property(klass, pname); + g_return_val_if_fail(pspec, G_TYPE_INVALID); + + gtype = G_TYPE_FROM_CLASS(klass); + + while (TRUE) { + nm_auto_unref_gtypeclass GObjectClass *k = NULL; + + k = g_type_class_ref(g_type_parent(gtype)); + + g_return_val_if_fail(G_IS_OBJECT_CLASS(k), G_TYPE_INVALID); + + if (g_object_class_find_property(k, pname) != pspec) + return gtype; + + gtype = G_TYPE_FROM_CLASS(k); + } +} + +/*****************************************************************************/ + +static void +_str_buf_append_c_escape_octal(NMStrBuf *strbuf, char ch) +{ + nm_str_buf_append_c4(strbuf, + '\\', + '0' + ((char) ((((guchar) ch) >> 6) & 07)), + '0' + ((char) ((((guchar) ch) >> 3) & 07)), + '0' + ((char) ((((guchar) ch)) & 07))); +} + +/** + * nm_utils_buf_utf8safe_unescape: + * @str: (allow-none): the string to unescape. The string itself is a NUL terminated + * ASCII string, that can have C-style backslash escape sequences (which + * are to be unescaped). Non-ASCII characters (e.g. UTF-8) are taken verbatim, so + * it doesn't care that this string is UTF-8. However, usually this is a UTF-8 encoded + * string. + * @flags: flags for unescaping. The following flags are supported. + * %NM_UTILS_STR_UTF8_SAFE_UNESCAPE_STRIP_SPACES performs a g_strstrip() on the input string, + * but preserving escaped spaces. For example, "a\\t " gives "a\t" (that is, the escaped space does + * not get stripped). Likewise, the invalid escape sequence "a\\ " results in "a " (stripping + * the unescaped space, but preserving the escaped one). + * @out_len: (out): the length of the parsed string. + * @to_free: (out): if @str requires unescaping, the function will clone the string. In + * that case, the allocated buffer will be returned here. + * + * See C-style escapes at https://en.wikipedia.org/wiki/Escape_sequences_in_C#Table_of_escape_sequences. + * Note that hex escapes ("\\xhh") and unicode escapes ("\\uhhhh", "\\Uhhhhhhhh") are not supported. + * + * Also, this function is very similar to g_strcompress() but without issuing g_warning() + * assertions and proper handling of "\\000" escape sequences. + * + * Invalid escape sequences (or non-UTF-8 input) are gracefully accepted. For example "\\ " + * is an invalid escape sequence, in this case the backslash is removed and " " gets returned. + * + * The function never leaks secrets in memory. + * + * Returns: the unescaped buffer of length @out_len. If @str is %NULL, this returns %NULL + * and sets @out_len to 0. Otherwise, a non-%NULL binary buffer is returned with + * @out_len bytes. Note that the binary buffer is guaranteed to be NUL terminated. That + * is @result[@out_len] is NUL. + * Note that the result is binary, and may have embedded NUL characters and non-UTF-8. + * If the function can avoid cloning the input string, it will return a pointer inside + * the input @str. For example, if there is no backslash, no cloning is necessary. In that + * case, @to_free will be %NULL. Otherwise, @to_free is set to a newly allocated buffer + * containing the unescaped string and returned. + */ +gconstpointer +nm_utils_buf_utf8safe_unescape(const char * str, + NMUtilsStrUtf8SafeFlags flags, + gsize * out_len, + gpointer * to_free) +{ + gboolean strip_spaces = NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_UNESCAPE_STRIP_SPACES); + NMStrBuf strbuf; + const char *s; + gsize len; + + g_return_val_if_fail(to_free, NULL); + g_return_val_if_fail(out_len, NULL); + + if (!str) { + *out_len = 0; + *to_free = NULL; + return NULL; + } + + if (strip_spaces) + str = nm_str_skip_leading_spaces(str); + + len = strlen(str); + + s = memchr(str, '\\', len); + if (!s) { + if (strip_spaces && len > 0 && g_ascii_isspace(str[len - 1])) { + len--; + while (len > 0 && g_ascii_isspace(str[len - 1])) + len--; + *out_len = len; + return (*to_free = g_strndup(str, len)); + } + *out_len = len; + *to_free = NULL; + return str; + } + + nm_str_buf_init(&strbuf, len + 1u, FALSE); + + nm_str_buf_append_len(&strbuf, str, s - str); + str = s; + + for (;;) { + char ch; + guint v; + + nm_assert(str[0] == '\\'); + + ch = (++str)[0]; + + if (ch == '\0') { + /* error. Trailing '\\' */ + break; + } + + if (ch >= '0' && ch <= '9') { + v = ch - '0'; + ch = (++str)[0]; + if (ch >= '0' && ch <= '7') { + v = v * 8 + (ch - '0'); + ch = (++str)[0]; + if (ch >= '0' && ch <= '7') { + /* technically, escape sequences larger than \3FF are out of range + * and invalid. We don't check for that, and do the same as + * g_strcompress(): silently clip the value with & 0xFF. */ + v = v * 8 + (ch - '0'); + ++str; + } + } + ch = v; + } else { + switch (ch) { + case 'b': + ch = '\b'; + break; + case 'f': + ch = '\f'; + break; + case 'n': + ch = '\n'; + break; + case 'r': + ch = '\r'; + break; + case 't': + ch = '\t'; + break; + case 'v': + ch = '\v'; + break; + default: + /* Here we handle "\\\\", but all other unexpected escape sequences are really a bug. + * Take them literally, after removing the escape character */ + break; + } + str++; + } + + nm_str_buf_append_c(&strbuf, ch); + + s = strchr(str, '\\'); + if (!s) { + gsize l = strlen(str); + + if (strip_spaces) { + while (l > 0 && g_ascii_isspace(str[l - 1])) + l--; + } + nm_str_buf_append_len(&strbuf, str, l); + break; + } + + nm_str_buf_append_len(&strbuf, str, s - str); + str = s; + } + + /* assert that no reallocation was necessary. For one, unescaping should + * never result in a longer string than the input. Also, when unescaping + * secrets, we want to ensure that we don't leak secrets in memory. */ + nm_assert(strbuf.allocated == len + 1u); + + return (*to_free = nm_str_buf_finalize(&strbuf, out_len)); +} + +/** + * nm_utils_buf_utf8safe_escape: + * @buf: byte array, possibly in utf-8 encoding, may have NUL characters. + * @buflen: the length of @buf in bytes, or -1 if @buf is a NUL terminated + * string. + * @flags: #NMUtilsStrUtf8SafeFlags flags + * @to_free: (out): return the pointer location of the string + * if a copying was necessary. + * + * Based on the assumption, that @buf contains UTF-8 encoded bytes, + * this will return valid UTF-8 sequence, and invalid sequences + * will be escaped with backslash (C escaping, like g_strescape()). + * This is sanitize non UTF-8 characters. The result is valid + * UTF-8. + * + * The operation can be reverted with nm_utils_buf_utf8safe_unescape(). + * Note that if, and only if @buf contains no NUL bytes, the operation + * can also be reverted with g_strcompress(). + * + * Depending on @flags, valid UTF-8 characters are not escaped at all + * (except the escape character '\\'). This is the difference to g_strescape(), + * which escapes all non-ASCII characters. This allows to pass on + * valid UTF-8 characters as-is and can be directly shown to the user + * as UTF-8 -- with exception of the backslash escape character, + * invalid UTF-8 sequences, and other (depending on @flags). + * + * Returns: the escaped input buffer, as valid UTF-8. If no escaping + * is necessary, it returns the input @buf. Otherwise, an allocated + * string @to_free is returned which must be freed by the caller + * with g_free. The escaping can be reverted by g_strcompress(). + **/ +const char * +nm_utils_buf_utf8safe_escape(gconstpointer buf, + gssize buflen, + NMUtilsStrUtf8SafeFlags flags, + char ** to_free) +{ + const char *const str = buf; + const char * p = NULL; + const char * s; + gboolean nul_terminated = FALSE; + NMStrBuf strbuf; + + g_return_val_if_fail(to_free, NULL); + + *to_free = NULL; + + if (buflen == 0) + return NULL; + + if (buflen < 0) { + if (!str) + return NULL; + buflen = strlen(str); + if (buflen == 0) + return str; + nul_terminated = TRUE; + } + + if (g_utf8_validate(str, buflen, &p) && nul_terminated) { + /* note that g_utf8_validate() does not allow NUL character inside @str. Good. + * We can treat @str like a NUL terminated string. */ + if (!NM_STRCHAR_ANY( + str, + ch, + (ch == '\\' + || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL) && ch < ' ') + || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII) + && ((guchar) ch) >= 127)))) + return str; + } + + nm_str_buf_init(&strbuf, buflen + 5, NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_SECRET)); + + s = str; + do { + buflen -= p - s; + nm_assert(buflen >= 0); + + for (; s < p; s++) { + char ch = s[0]; + + nm_assert(ch); + if (ch == '\\') + nm_str_buf_append_c2(&strbuf, '\\', '\\'); + else if ((NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL) && ch < ' ') + || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII) + && ((guchar) ch) >= 127)) + _str_buf_append_c_escape_octal(&strbuf, ch); + else + nm_str_buf_append_c(&strbuf, ch); + } + + if (buflen <= 0) + break; + + _str_buf_append_c_escape_octal(&strbuf, p[0]); + + buflen--; + if (buflen == 0) + break; + + s = &p[1]; + (void) g_utf8_validate(s, buflen, &p); + } while (TRUE); + + return (*to_free = nm_str_buf_finalize(&strbuf, NULL)); +} + +const char * +nm_utils_buf_utf8safe_escape_bytes(GBytes *bytes, NMUtilsStrUtf8SafeFlags flags, char **to_free) +{ + gconstpointer p; + gsize l; + + if (bytes) + p = g_bytes_get_data(bytes, &l); + else { + p = NULL; + l = 0; + } + + return nm_utils_buf_utf8safe_escape(p, l, flags, to_free); +} + +char * +nm_utils_buf_utf8safe_escape_cp(gconstpointer buf, gssize buflen, NMUtilsStrUtf8SafeFlags flags) +{ + const char *s_const; + char * s; + + s_const = nm_utils_buf_utf8safe_escape(buf, buflen, flags, &s); + nm_assert(!s || s == s_const); + return s ?: g_strdup(s_const); +} + +/*****************************************************************************/ + +const char * +nm_utils_str_utf8safe_unescape(const char *str, NMUtilsStrUtf8SafeFlags flags, char **to_free) +{ + const char *res; + gsize len; + + g_return_val_if_fail(to_free, NULL); + + res = nm_utils_buf_utf8safe_unescape(str, flags, &len, (gpointer *) to_free); + + nm_assert((!res && len == 0) || (strlen(res) <= len)); + + return res; +} + +/** + * nm_utils_str_utf8safe_escape: + * @str: NUL terminated input string, possibly in utf-8 encoding + * @flags: #NMUtilsStrUtf8SafeFlags flags + * @to_free: (out): return the pointer location of the string + * if a copying was necessary. + * + * Returns the possible non-UTF-8 NUL terminated string @str + * and uses backslash escaping (C escaping, like g_strescape()) + * to sanitize non UTF-8 characters. The result is valid + * UTF-8. + * + * The operation can be reverted with g_strcompress() or + * nm_utils_str_utf8safe_unescape(). + * + * Depending on @flags, valid UTF-8 characters are not escaped at all + * (except the escape character '\\'). This is the difference to g_strescape(), + * which escapes all non-ASCII characters. This allows to pass on + * valid UTF-8 characters as-is and can be directly shown to the user + * as UTF-8 -- with exception of the backslash escape character, + * invalid UTF-8 sequences, and other (depending on @flags). + * + * Returns: the escaped input string, as valid UTF-8. If no escaping + * is necessary, it returns the input @str. Otherwise, an allocated + * string @to_free is returned which must be freed by the caller + * with g_free. The escaping can be reverted by g_strcompress(). + **/ +const char * +nm_utils_str_utf8safe_escape(const char *str, NMUtilsStrUtf8SafeFlags flags, char **to_free) +{ + return nm_utils_buf_utf8safe_escape(str, -1, flags, to_free); +} + +/** + * nm_utils_str_utf8safe_escape_cp: + * @str: NUL terminated input string, possibly in utf-8 encoding + * @flags: #NMUtilsStrUtf8SafeFlags flags + * + * Like nm_utils_str_utf8safe_escape(), except the returned value + * is always a copy of the input and must be freed by the caller. + * + * Returns: the escaped input string in UTF-8 encoding. The returned + * value should be freed with g_free(). + * The escaping can be reverted by g_strcompress(). + **/ +char * +nm_utils_str_utf8safe_escape_cp(const char *str, NMUtilsStrUtf8SafeFlags flags) +{ + char *s; + + nm_utils_str_utf8safe_escape(str, flags, &s); + return s ?: g_strdup(str); +} + +char * +nm_utils_str_utf8safe_unescape_cp(const char *str, NMUtilsStrUtf8SafeFlags flags) +{ + char *s; + + str = nm_utils_str_utf8safe_unescape(str, flags, &s); + return s ?: g_strdup(str); +} + +char * +nm_utils_str_utf8safe_escape_take(char *str, NMUtilsStrUtf8SafeFlags flags) +{ + char *str_to_free; + + nm_utils_str_utf8safe_escape(str, flags, &str_to_free); + if (str_to_free) { + g_free(str); + return str_to_free; + } + return str; +} + +/*****************************************************************************/ + +/* taken from systemd's fd_wait_for_event(). Note that the timeout + * is here in nano-seconds, not micro-seconds. */ +int +nm_utils_fd_wait_for_event(int fd, int event, gint64 timeout_nsec) +{ + struct pollfd pollfd = { + .fd = fd, + .events = event, + }; + struct timespec ts, *pts; + int r; + + if (timeout_nsec < 0) + pts = NULL; + else { + ts.tv_sec = (time_t)(timeout_nsec / NM_UTILS_NSEC_PER_SEC); + ts.tv_nsec = (long int) (timeout_nsec % NM_UTILS_NSEC_PER_SEC); + pts = &ts; + } + + r = ppoll(&pollfd, 1, pts, NULL); + if (r < 0) + return -NM_ERRNO_NATIVE(errno); + if (r == 0) + return 0; + return pollfd.revents; +} + +/* taken from systemd's loop_read() */ +ssize_t +nm_utils_fd_read_loop(int fd, void *buf, size_t nbytes, bool do_poll) +{ + uint8_t *p = buf; + ssize_t n = 0; + + g_return_val_if_fail(fd >= 0, -EINVAL); + g_return_val_if_fail(buf, -EINVAL); + + /* If called with nbytes == 0, let's call read() at least + * once, to validate the operation */ + + if (nbytes > (size_t) SSIZE_MAX) + return -EINVAL; + + do { + ssize_t k; + + k = read(fd, p, nbytes); + if (k < 0) { + int errsv = errno; + + if (errsv == EINTR) + continue; + + if (errsv == EAGAIN && do_poll) { + /* We knowingly ignore any return value here, + * and expect that any error/EOF is reported + * via read() */ + + (void) nm_utils_fd_wait_for_event(fd, POLLIN, -1); + continue; + } + + return n > 0 ? n : -NM_ERRNO_NATIVE(errsv); + } + + if (k == 0) + return n; + + g_assert((size_t) k <= nbytes); + + p += k; + nbytes -= k; + n += k; + } while (nbytes > 0); + + return n; +} + +/* taken from systemd's loop_read_exact() */ +int +nm_utils_fd_read_loop_exact(int fd, void *buf, size_t nbytes, bool do_poll) +{ + ssize_t n; + + n = nm_utils_fd_read_loop(fd, buf, nbytes, do_poll); + if (n < 0) + return (int) n; + if ((size_t) n != nbytes) + return -EIO; + + return 0; +} + +/*****************************************************************************/ + +void +nm_utils_named_value_clear_with_g_free(NMUtilsNamedValue *val) +{ + if (val) { + gs_free gpointer x_name = NULL; + gs_free gpointer x_value = NULL; + + x_name = (gpointer) g_steal_pointer(&val->name); + x_value = g_steal_pointer(&val->value_ptr); + } +} + +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, + const char * name, + gboolean sorted) +{ + gsize i; + + nm_assert(name); + +#if NM_MORE_ASSERTS > 5 + { + for (i = 0; i < len; i++) { + const NMUtilsNamedValue *v = &arr[i]; + + nm_assert(v->name); + if (sorted && i > 0) + nm_assert(strcmp(arr[i - 1].name, v->name) < 0); + } + } + + nm_assert(!sorted || nm_utils_named_value_list_is_sorted(arr, len, FALSE, NULL, NULL)); +#endif + + if (sorted) { + return nm_utils_array_find_binary_search(arr, + sizeof(NMUtilsNamedValue), + len, + &name, + nm_strcmp_p_with_data, + NULL); + } + for (i = 0; i < len; i++) { + if (nm_streq(arr[i].name, name)) + return i; + } + return ~((gssize) len); +} + +gboolean +nm_utils_named_value_list_is_sorted(const NMUtilsNamedValue *arr, + gsize len, + gboolean accept_duplicates, + GCompareDataFunc compare_func, + gpointer user_data) +{ + gsize i; + int c_limit; + + if (len == 0) + return TRUE; + + g_return_val_if_fail(arr, FALSE); + + if (!compare_func) + compare_func = nm_strcmp_p_with_data; + + c_limit = accept_duplicates ? 0 : -1; + + for (i = 1; i < len; i++) { + int c; + + c = compare_func(&arr[i - 1], &arr[i], user_data); + if (c > c_limit) + return FALSE; + } + return TRUE; +} + +void +nm_utils_named_value_list_sort(NMUtilsNamedValue *arr, + gsize len, + GCompareDataFunc compare_func, + gpointer user_data) +{ + if (len == 0) + return; + + g_return_if_fail(arr); + + if (len == 1) + return; + + g_qsort_with_data(arr, + len, + sizeof(NMUtilsNamedValue), + compare_func ?: nm_strcmp_p_with_data, + user_data); +} + +/*****************************************************************************/ + +gpointer * +nm_utils_hash_keys_to_array(GHashTable * hash, + GCompareDataFunc compare_func, + gpointer user_data, + guint * out_len) +{ + guint len; + gpointer *keys; + + /* by convention, we never return an empty array. In that + * case, always %NULL. */ + if (!hash || g_hash_table_size(hash) == 0) { + NM_SET_OUT(out_len, 0); + return NULL; + } + + keys = g_hash_table_get_keys_as_array(hash, &len); + if (len > 1 && compare_func) { + g_qsort_with_data(keys, len, sizeof(gpointer), compare_func, user_data); + } + NM_SET_OUT(out_len, len); + return keys; +} + +gpointer * +nm_utils_hash_values_to_array(GHashTable * hash, + GCompareDataFunc compare_func, + gpointer user_data, + guint * out_len) +{ + GHashTableIter iter; + gpointer value; + gpointer * arr; + guint i, len; + + if (!hash || (len = g_hash_table_size(hash)) == 0u) { + NM_SET_OUT(out_len, 0); + return NULL; + } + + arr = g_new(gpointer, ((gsize) len) + 1); + i = 0; + g_hash_table_iter_init(&iter, hash); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &value)) + arr[i++] = value; + + nm_assert(i == len); + arr[len] = NULL; + + if (len > 1 && compare_func) { + g_qsort_with_data(arr, len, sizeof(gpointer), compare_func, user_data); + } + + NM_SET_OUT(out_len, len); + return arr; +} + +/*****************************************************************************/ + +/** + * nm_utils_hashtable_equal: + * @a: one #GHashTable + * @b: other #GHashTable + * @treat_null_as_empty: if %TRUE, when either @a or @b is %NULL, it is + * treated like an empty hash. It means, a %NULL hash will compare equal + * to an empty hash. + * @equal_func: the equality function, for comparing the values. + * If %NULL, the values are not compared. In that case, the function + * only checks, if both dictionaries have the same keys -- according + * to @b's key equality function. + * Note that the values of @a will be passed as first argument + * to @equal_func. + * + * Compares two hash tables, whether they have equal content. + * This only makes sense, if @a and @b have the same key types and + * the same key compare-function. + * + * Returns: %TRUE, if both dictionaries have the same content. + */ +gboolean +nm_utils_hashtable_equal(const GHashTable *a, + const GHashTable *b, + gboolean treat_null_as_empty, + GEqualFunc equal_func) +{ + guint n; + GHashTableIter iter; + gconstpointer key, v_a, v_b; + + if (a == b) + return TRUE; + if (!treat_null_as_empty) { + if (!a || !b) + return FALSE; + } + + n = a ? g_hash_table_size((GHashTable *) a) : 0; + if (n != (b ? g_hash_table_size((GHashTable *) b) : 0)) + return FALSE; + + if (n > 0) { + g_hash_table_iter_init(&iter, (GHashTable *) a); + while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &v_a)) { + if (!g_hash_table_lookup_extended((GHashTable *) b, key, NULL, (gpointer *) &v_b)) + return FALSE; + if (equal_func && !equal_func(v_a, v_b)) + return FALSE; + } + } + + return TRUE; +} + +static gboolean +_utils_hashtable_equal(GHashTable * hash_a, + GHashTable * hash_b, + GCompareDataFunc cmp_values, + gpointer user_data) +{ + GHashTableIter h; + gpointer a_key; + gpointer a_val; + gpointer b_val; + + nm_assert(hash_a); + nm_assert(hash_b); + nm_assert(hash_a != hash_b); + nm_assert(g_hash_table_size(hash_a) == g_hash_table_size(hash_b)); + + /* We rely on both hashes to have the same hash/equal function. Otherwise, we would have to iterate + * both hashes and check whether all keys/values are present in the respective other hash (which + * would be O(n^2), since we couldn't use the plain lookup function. That is not a useful thing + * for this function. */ + + g_hash_table_iter_init(&h, hash_a); + while (g_hash_table_iter_next(&h, &a_key, &a_val)) { + if (!g_hash_table_lookup_extended(hash_b, a_key, NULL, &b_val)) + return FALSE; + + if (!cmp_values) { + /* we accept %NULL compare function to indicate that we don't care about the key. */ + continue; + } + + if (cmp_values(a_val, b_val, user_data) != 0) + return FALSE; + } + + return TRUE; +} + +/** + * nm_utils_hashtable_cmp_equal: + * @a: (allow-none): the hash table or %NULL + * @b: (allow-none): the other hash table or %NULL + * @cmp_values: (allow-none): if %NULL, only the keys + * will be compared. Otherwise, this function is used to + * check whether all keys are equal. + * @user_data: the argument for @cmp_values. + * + * It is required that both @a and @b have the same hash and equals + * function. + * + * Returns: %TRUE, if both keys have the same keys and (if + * @cmp_values is given) all values are the same. + */ +gboolean +nm_utils_hashtable_cmp_equal(const GHashTable *a, + const GHashTable *b, + GCompareDataFunc cmp_values, + gpointer user_data) +{ + GHashTable *hash_a = (GHashTable *) a; + GHashTable *hash_b = (GHashTable *) b; + gboolean same; + guint size; + + if (hash_a == hash_b) + return TRUE; + + if (!hash_a || !hash_b) + return FALSE; + + size = g_hash_table_size(hash_a); + if (size != g_hash_table_size(hash_b)) + return FALSE; + + if (size == 0) + return TRUE; + + same = _utils_hashtable_equal(hash_a, hash_b, cmp_values, user_data); + +#if NM_MORE_ASSERTS > 5 + nm_assert(same == _utils_hashtable_equal(hash_b, hash_a, cmp_values, user_data)); +#endif + + return same; +} + +typedef struct { + gpointer key; + gpointer val; +} HashTableCmpData; + +typedef struct { + GCompareDataFunc cmp_keys; + gpointer user_data; +} HashTableUserData; + +static int +_hashtable_cmp_func(gconstpointer a, gconstpointer b, gpointer user_data) +{ + const HashTableUserData *d = user_data; + const HashTableCmpData * d_a = *((const HashTableCmpData *const *) a); + const HashTableCmpData * d_b = *((const HashTableCmpData *const *) b); + + NM_CMP_RETURN(d->cmp_keys(d_a, d_b, d->user_data)); + return 0; +} + +/** + * nm_utils_hashtable_cmp: + * @a: (allow-none): the hash to compare. May be %NULL. + * @b: (allow-none): the other hash to compare. May be %NULL. + * @do_fast_precheck: if %TRUE, assume that the hashes are equal + * and that it is worth calling nm_utils_hashtable_cmp_equal() first. + * That requires, that both hashes have the same equals function + * which is compatible with the @cmp_keys function. + * @cmp_keys: the compare function for keys. Usually, the hash/equal function + * of both hashes corresponds to this function. If you set @do_fast_precheck + * to false, then this is not a requirement. + * @cmp_values: (allow-none): if %NULL, only the keys are compared. + * Otherwise, the values must are also compared with this function. + * + * Both hashes must have keys/values of the same domain, so that + * they can be effectively compared with @cmp_keys and @cmp_values. + * + * %NULL hashes compare equal to %NULL, but not to empty hashes. + * + * Returns: 0 if both hashes are equal, or -1 or 1 if one of the hashes + * sorts before/after. + */ +int +nm_utils_hashtable_cmp(const GHashTable *a, + const GHashTable *b, + gboolean do_fast_precheck, + GCompareDataFunc cmp_keys, + GCompareDataFunc cmp_values, + gpointer user_data) +{ + GHashTable *hash_a = (GHashTable *) a; + GHashTable *hash_b = (GHashTable *) b; + gs_free HashTableCmpData *cmp_array_free = NULL; + HashTableCmpData * cmp_array_a; + HashTableCmpData * cmp_array_b; + GHashTableIter h; + gpointer i_key; + gpointer i_val; + gsize size2; + guint size; + guint i; + + nm_assert(cmp_keys); + + NM_CMP_SELF(hash_a, hash_b); + + size = g_hash_table_size(hash_a); + + NM_CMP_DIRECT(size, g_hash_table_size(hash_b)); + + if (size == 0) + return 0; + + if (do_fast_precheck) { + gboolean same; + + /* we expect that the hashes are equal and the caller ensures us that they + * use the same hash/equal functions. Do a fast path check first... + * + * It's unclear whether this is worth it. The full comparison is O(n*ln(n)), + * while the fast check (using the hash lookup) is O(n). But then, the pre-check + * makes additional requirements on the hash's hash/equal functions -- the + * full comparison does not make such requirements. */ + same = _utils_hashtable_equal(hash_a, hash_b, cmp_values, user_data); +#if NM_MORE_ASSERTS > 5 + nm_assert(same == _utils_hashtable_equal(hash_b, hash_a, cmp_values, user_data)); +#endif + if (same) + return 0; + } + + size2 = ((gsize) size) * 2u; + if (size2 > 600u / sizeof(HashTableCmpData)) { + cmp_array_free = g_new(HashTableCmpData, size2); + cmp_array_a = cmp_array_free; + } else + cmp_array_a = g_newa(HashTableCmpData, size2); + cmp_array_b = &cmp_array_a[size]; + + i = 0; + g_hash_table_iter_init(&h, hash_a); + while (g_hash_table_iter_next(&h, &i_key, &i_val)) { + nm_assert(i < size); + cmp_array_a[i++] = (HashTableCmpData){ + .key = i_key, + .val = i_val, + }; + } + nm_assert(i == size); + + i = 0; + g_hash_table_iter_init(&h, hash_b); + while (g_hash_table_iter_next(&h, &i_key, &i_val)) { + nm_assert(i < size); + cmp_array_b[i++] = (HashTableCmpData){ + .key = i_key, + .val = i_val, + }; + } + nm_assert(i == size); + + g_qsort_with_data(cmp_array_a, + size, + sizeof(HashTableCmpData), + _hashtable_cmp_func, + &((HashTableUserData){ + .cmp_keys = cmp_keys, + .user_data = user_data, + })); + + g_qsort_with_data(cmp_array_b, + size, + sizeof(HashTableCmpData), + _hashtable_cmp_func, + &((HashTableUserData){ + .cmp_keys = cmp_keys, + .user_data = user_data, + })); + + for (i = 0; i < size; i++) { + NM_CMP_RETURN(cmp_keys(cmp_array_a[i].key, cmp_array_b[i].key, user_data)); + } + + if (cmp_values) { + for (i = 0; i < size; i++) { + NM_CMP_RETURN(cmp_values(cmp_array_a[i].val, cmp_array_b[i].val, user_data)); + } + } + + /* the fast-precheck should have already told that the arrays are equal. */ + nm_assert(!do_fast_precheck); + + return 0; +} + +char ** +nm_utils_strv_make_deep_copied(const char **strv) +{ + gsize i; + + /* it takes a strv list, and copies each + * strings. Note that this updates @strv *in-place* + * and returns it. */ + + if (!strv) + return NULL; + for (i = 0; strv[i]; i++) + strv[i] = g_strdup(strv[i]); + + return (char **) strv; +} + +char ** +nm_utils_strv_make_deep_copied_n(const char **strv, gsize len) +{ + gsize i; + + /* it takes a strv array with len elements, and copies each + * strings. Note that this updates @strv *in-place* + * and returns it. */ + + if (!strv) + return NULL; + for (i = 0; i < len; i++) + strv[i] = g_strdup(strv[i]); + + return (char **) strv; +} + +/** + * @strv: the strv array to copy. It may be %NULL if @len + * is negative or zero (in which case %NULL will be returned). + * @len: the length of strings in @str. If negative, strv is assumed + * to be a NULL terminated array. + * @deep_copied: if %TRUE, clones the individual strings. In that case, + * the returned array must be freed with g_strfreev(). Otherwise, the + * strings themself are not copied. You must take care of who owns the + * strings yourself. + * + * Like g_strdupv(), with two differences: + * + * - accepts a @len parameter for non-null terminated strv array. + * + * - this never returns an empty strv array, but always %NULL if + * there are no strings. + * + * Note that if @len is non-negative, then it still must not + * contain any %NULL pointers within the first @len elements. + * Otherwise, you would leak elements if you try to free the + * array with g_strfreev(). Allowing that would be error prone. + * + * Returns: (transfer full): a clone of the strv array. Always + * %NULL terminated. Depending on @deep_copied, the strings are + * cloned or not. + */ +char ** +_nm_utils_strv_dup(const char *const *strv, gssize len, gboolean deep_copied) +{ + gsize i, l; + char **v; + + if (len < 0) + l = NM_PTRARRAY_LEN(strv); + else + l = len; + if (l == 0) { + /* this function never returns an empty strv array. If you + * need that, handle it yourself. */ + return NULL; + } + + v = g_new(char *, l + 1); + for (i = 0; i < l; i++) { + if (G_UNLIKELY(!strv[i])) { + /* NULL strings are not allowed. Clear the remainder of the array + * and return it (with assertion failure). */ + l++; + for (; i < l; i++) + v[i] = NULL; + g_return_val_if_reached(v); + } + + if (deep_copied) + v[i] = g_strdup(strv[i]); + else + v[i] = (char *) strv[i]; + } + v[l] = NULL; + return v; +} + +const char ** +_nm_utils_strv_dup_packed(const char *const *strv, gssize len) + +{ + gs_free gsize *str_len_free = NULL; + gsize * str_len; + const char ** result; + gsize mem_len; + gsize pre_len; + gsize len2; + char * sbuf; + gsize i; + + nm_assert(len >= -1); + + if (G_LIKELY(len < 0)) { + if (!strv || !strv[0]) { + /* This function never returns an empty strv array. If you need that, handle it + * yourself. */ + return NULL; + } + len2 = NM_PTRARRAY_LEN(strv); + } else { + if (len == 0) + return NULL; + len2 = len; + } + + if (len2 > 300u / sizeof(gsize)) { + str_len_free = g_new(gsize, len2); + str_len = str_len_free; + } else + str_len = g_newa(gsize, len2); + + mem_len = 0; + for (i = 0; i < len2; i++) { + gsize l; + + if (G_LIKELY(strv[i])) + l = strlen(strv[i]) + 1u; + else + l = 0; + str_len[i] = l; + mem_len += l; + } + + pre_len = sizeof(const char *) * (len2 + 1u); + + result = g_malloc(pre_len + mem_len); + sbuf = &(((char *) result)[pre_len]); + for (i = 0; i < len2; i++) { + gsize l; + + if (G_UNLIKELY(!strv[i])) { + /* Technically there is no problem with accepting NULL strings. But that + * does not really result in a strv array, and likely this only happens due + * to a bug. We want to catch such bugs by asserting. + * + * We clear the remainder of the buffer and fail with an assertion. */ + len2++; + for (; i < len2; i++) + result[i] = NULL; + g_return_val_if_reached(result); + } + + result[i] = sbuf; + + l = str_len[i]; + memcpy(sbuf, strv[i], l); + sbuf += l; + } + result[i] = NULL; + nm_assert(i == len2); + nm_assert(sbuf == (&((const char *) result)[pre_len]) + mem_len); + + return result; +} + +/*****************************************************************************/ + +gssize +nm_utils_ptrarray_find_binary_search(gconstpointer * list, + gsize len, + gconstpointer needle, + GCompareDataFunc cmpfcn, + gpointer user_data, + gssize * out_idx_first, + gssize * out_idx_last) +{ + gssize imin, imax, imid, i2min, i2max, i2mid; + int cmp; + + g_return_val_if_fail(list || !len, ~((gssize) 0)); + g_return_val_if_fail(cmpfcn, ~((gssize) 0)); + + imin = 0; + if (len > 0) { + imax = len - 1; + + while (imin <= imax) { + imid = imin + (imax - imin) / 2; + + cmp = cmpfcn(list[imid], needle, user_data); + if (cmp == 0) { + /* we found a matching entry at index imid. + * + * Does the caller request the first/last index as well (in case that + * there are multiple entries which compare equal). */ + + if (out_idx_first) { + i2min = imin; + i2max = imid + 1; + while (i2min <= i2max) { + i2mid = i2min + (i2max - i2min) / 2; + + cmp = cmpfcn(list[i2mid], needle, user_data); + if (cmp == 0) + i2max = i2mid - 1; + else { + nm_assert(cmp < 0); + i2min = i2mid + 1; + } + } + *out_idx_first = i2min; + } + if (out_idx_last) { + i2min = imid + 1; + i2max = imax; + while (i2min <= i2max) { + i2mid = i2min + (i2max - i2min) / 2; + + cmp = cmpfcn(list[i2mid], needle, user_data); + if (cmp == 0) + i2min = i2mid + 1; + else { + nm_assert(cmp > 0); + i2max = i2mid - 1; + } + } + *out_idx_last = i2min - 1; + } + return imid; + } + + if (cmp < 0) + imin = imid + 1; + else + imax = imid - 1; + } + } + + /* return the inverse of @imin. This is a negative number, but + * also is ~imin the position where the value should be inserted. */ + imin = ~imin; + NM_SET_OUT(out_idx_first, imin); + NM_SET_OUT(out_idx_last, imin); + return imin; +} + +/*****************************************************************************/ + +/** + * nm_utils_array_find_binary_search: + * @list: the list to search. It must be sorted according to @cmpfcn ordering. + * @elem_size: the size in bytes of each element in the list + * @len: the number of elements in @list + * @needle: the value that is searched + * @cmpfcn: the compare function. The elements @list are passed as first + * argument to @cmpfcn, while @needle is passed as second. Usually, the + * needle is the same data type as inside the list, however, that is + * not necessary, as long as @cmpfcn takes care to cast the two arguments + * accordingly. + * @user_data: optional argument passed to @cmpfcn + * + * Performs binary search for @needle in @list. On success, returns the + * (non-negative) index where the compare function found the searched element. + * On success, it returns a negative value. Note that the return negative value + * is the bitwise inverse of the position where the element should be inserted. + * + * If the list contains multiple matching elements, an arbitrary index is + * returned. + * + * Returns: the index to the element in the list, or the (negative, bitwise inverted) + * position where it should be. + */ +gssize +nm_utils_array_find_binary_search(gconstpointer list, + gsize elem_size, + gsize len, + gconstpointer needle, + GCompareDataFunc cmpfcn, + gpointer user_data) +{ + gssize imin, imax, imid; + int cmp; + + g_return_val_if_fail(list || !len, ~((gssize) 0)); + g_return_val_if_fail(cmpfcn, ~((gssize) 0)); + g_return_val_if_fail(elem_size > 0, ~((gssize) 0)); + + imin = 0; + if (len == 0) + return ~imin; + + imax = len - 1; + + while (imin <= imax) { + imid = imin + (imax - imin) / 2; + + cmp = cmpfcn(&((const char *) list)[elem_size * imid], needle, user_data); + if (cmp == 0) + return imid; + + if (cmp < 0) + imin = imid + 1; + else + imax = imid - 1; + } + + /* return the inverse of @imin. This is a negative number, but + * also is ~imin the position where the value should be inserted. */ + return ~imin; +} + +/*****************************************************************************/ + +/** + * nm_utils_get_start_time_for_pid: + * @pid: the process identifier + * @out_state: return the state character, like R, S, Z. See `man 5 proc`. + * @out_ppid: parent process id + * + * Originally copied from polkit source (src/polkit/polkitunixprocess.c) + * and adjusted. + * + * Returns: the timestamp when the process started (by parsing /proc/$PID/stat). + * If an error occurs (e.g. the process does not exist), 0 is returned. + * + * The returned start time counts since boot, in the unit HZ (with HZ usually being (1/100) seconds) + **/ +guint64 +nm_utils_get_start_time_for_pid(pid_t pid, char *out_state, pid_t *out_ppid) +{ + guint64 start_time; + char filename[256]; + gs_free char * contents = NULL; + size_t length; + gs_free const char **tokens = NULL; + char * p; + char state = ' '; + gint64 ppid = 0; + + start_time = 0; + contents = NULL; + + g_return_val_if_fail(pid > 0, 0); + + G_STATIC_ASSERT_EXPR(sizeof(GPid) >= sizeof(pid_t)); + + nm_sprintf_buf(filename, "/proc/%" G_PID_FORMAT "/stat", (GPid) pid); + + if (!g_file_get_contents(filename, &contents, &length, NULL)) + goto fail; + + /* start time is the token at index 19 after the '(process name)' entry - since only this + * field can contain the ')' character, search backwards for this to avoid malicious + * processes trying to fool us + */ + p = strrchr(contents, ')'); + if (!p) + goto fail; + p += 2; /* skip ') ' */ + if (p - contents >= (int) length) + goto fail; + + state = p[0]; + + tokens = nm_utils_strsplit_set(p, " "); + + if (NM_PTRARRAY_LEN(tokens) < 20) + goto fail; + + if (out_ppid) { + ppid = _nm_utils_ascii_str_to_int64(tokens[1], 10, 1, G_MAXINT, 0); + if (ppid == 0) + goto fail; + } + + start_time = _nm_utils_ascii_str_to_int64(tokens[19], 10, 1, G_MAXINT64, 0); + if (start_time == 0) + goto fail; + + NM_SET_OUT(out_state, state); + NM_SET_OUT(out_ppid, ppid); + return start_time; + +fail: + NM_SET_OUT(out_state, ' '); + NM_SET_OUT(out_ppid, 0); + return 0; +} + +/*****************************************************************************/ + +/** + * _nm_utils_strv_sort: + * @strv: pointer containing strings that will be sorted + * in-place, %NULL is allowed, unless @len indicates + * that there are more elements. + * @len: the number of elements in strv. If negative, + * strv must be a NULL terminated array and the length + * will be calculated first. If @len is a positive + * number, @strv is allowed to contain %NULL strings + * too. + * + * Ascending sort of the array @strv inplace, using plain strcmp() string + * comparison. + */ +void +_nm_utils_strv_sort(const char **strv, gssize len) +{ + GCompareDataFunc cmp; + gsize l; + + if (len < 0) { + l = NM_PTRARRAY_LEN(strv); + cmp = nm_strcmp_p_with_data; + } else { + l = len; + cmp = nm_strcmp0_p_with_data; + } + + if (l <= 1) + return; + + nm_assert(l <= (gsize) G_MAXINT); + + g_qsort_with_data(strv, l, sizeof(const char *), cmp, NULL); +} + +/** + * _nm_utils_strv_cmp_n: + * @strv1: a string array + * @len1: the length of @strv1, or -1 for NULL terminated array. + * @strv2: a string array + * @len2: the length of @strv2, or -1 for NULL terminated array. + * + * Note that + * - len == -1 && strv == NULL + * is treated like a %NULL argument and compares differently from + * other arrays. + * + * Note that an empty array can be represented as + * - len == -1 && strv && !strv[0] + * - len == 0 && !strv + * - len == 0 && strv + * These 3 forms all compare equal. + * It also means, if length is 0, then it is permissible for strv to be %NULL. + * + * The strv arrays may contain %NULL strings (if len is positive). + * + * Returns: 0 if the arrays are equal (using strcmp). + **/ +int +_nm_utils_strv_cmp_n(const char *const *strv1, gssize len1, const char *const *strv2, gssize len2) +{ + gsize n, n2; + + if (len1 < 0) { + if (!strv1) + return (len2 < 0 && !strv2) ? 0 : -1; + n = NM_PTRARRAY_LEN(strv1); + } else + n = len1; + + if (len2 < 0) { + if (!strv2) + return 1; + n2 = NM_PTRARRAY_LEN(strv2); + } else + n2 = len2; + + NM_CMP_DIRECT(n, n2); + for (; n > 0; n--, strv1++, strv2++) + NM_CMP_DIRECT_STRCMP0(*strv1, *strv2); + return 0; +} + +/*****************************************************************************/ + +/** + * nm_utils_g_slist_find_str: + * @list: the #GSList with NUL terminated strings to search + * @needle: the needle string to look for. + * + * Search the list for @needle and return the first found match + * (or %NULL if not found). Uses strcmp() for finding the first matching + * element. + * + * Returns: the #GSList element with @needle as string value or + * %NULL if not found. + */ +GSList * +nm_utils_g_slist_find_str(const GSList *list, const char *needle) +{ + nm_assert(needle); + + for (; list; list = list->next) { + nm_assert(list->data); + if (nm_streq(list->data, needle)) + return (GSList *) list; + } + return NULL; +} + +/** + * nm_utils_g_slist_strlist_cmp: + * @a: the left #GSList of strings + * @b: the right #GSList of strings to compare. + * + * Compares two string lists. The data elements are compared with + * strcmp(), allowing %NULL elements. + * + * Returns: 0, 1, or -1, depending on how the lists compare. + */ +int +nm_utils_g_slist_strlist_cmp(const GSList *a, const GSList *b) +{ + while (TRUE) { + if (!a) + return !b ? 0 : -1; + if (!b) + return 1; + NM_CMP_DIRECT_STRCMP0(a->data, b->data); + a = a->next; + b = b->next; + } +} + +char * +nm_utils_g_slist_strlist_join(const GSList *a, const char *separator) +{ + GString *str = NULL; + + if (!a) + return NULL; + + for (; a; a = a->next) { + if (!str) + str = g_string_new(NULL); + else + g_string_append(str, separator); + g_string_append(str, a->data); + } + return g_string_free(str, FALSE); +} + +/*****************************************************************************/ + +NMUtilsUserData * +_nm_utils_user_data_pack(int nargs, gconstpointer *args) +{ + int i; + gpointer *data; + + nm_assert(nargs > 0); + nm_assert(args); + + data = g_slice_alloc(((gsize) nargs) * sizeof(gconstpointer)); + for (i = 0; i < nargs; i++) + data[i] = (gpointer) args[i]; + return (NMUtilsUserData *) data; +} + +void +_nm_utils_user_data_unpack(NMUtilsUserData *user_data, int nargs, ...) +{ + gpointer *data = (gpointer *) user_data; + va_list ap; + int i; + + nm_assert(data); + nm_assert(nargs > 0); + + va_start(ap, nargs); + for (i = 0; i < nargs; i++) { + gpointer *dst; + + dst = va_arg(ap, gpointer *); + nm_assert(dst); + + *dst = data[i]; + } + va_end(ap); + + g_slice_free1(((gsize) nargs) * sizeof(gconstpointer), data); +} + +/*****************************************************************************/ + +typedef struct { + gpointer callback_user_data; + GCancellable * cancellable; + GSource * source; + NMUtilsInvokeOnIdleCallback callback; + gulong cancelled_id; +} InvokeOnIdleData; + +static gboolean +_nm_utils_invoke_on_idle_cb_idle(gpointer user_data) +{ + InvokeOnIdleData *data = user_data; + + nm_clear_g_signal_handler(data->cancellable, &data->cancelled_id); + + data->callback(data->callback_user_data, data->cancellable); + + nm_g_object_unref(data->cancellable); + g_source_destroy(data->source); + nm_g_slice_free(data); + return G_SOURCE_REMOVE; +} + +static void +_nm_utils_invoke_on_idle_cb_cancelled(GCancellable *cancellable, InvokeOnIdleData *data) +{ + /* on cancellation, we invoke the callback synchronously. */ + nm_clear_g_signal_handler(data->cancellable, &data->cancelled_id); + nm_clear_g_source_inst(&data->source); + data->callback(data->callback_user_data, data->cancellable); + nm_g_object_unref(data->cancellable); + nm_g_slice_free(data); +} + +static void +_nm_utils_invoke_on_idle_start(gboolean use_timeout, + guint timeout_msec, + GCancellable * cancellable, + NMUtilsInvokeOnIdleCallback callback, + gpointer callback_user_data) +{ + InvokeOnIdleData *data; + GSource * source; + + g_return_if_fail(callback); + + data = g_slice_new(InvokeOnIdleData); + *data = (InvokeOnIdleData){ + .callback = callback, + .callback_user_data = callback_user_data, + .cancellable = nm_g_object_ref(cancellable), + .cancelled_id = 0, + }; + + if (cancellable) { + if (g_cancellable_is_cancelled(cancellable)) { + /* the cancellable is already cancelled. We ignore the timeout + * and always schedule an idle action. */ + use_timeout = FALSE; + } else { + /* if we are passed a non-cancelled cancellable, we register to the "cancelled" + * signal an invoke the callback synchronously (from the signal handler). + * + * We don't do that, + * - if the cancellable is already cancelled (because we don't want to invoke + * the callback synchronously from the caller). + * - if we have no cancellable at hand. */ + data->cancelled_id = g_signal_connect(cancellable, + "cancelled", + G_CALLBACK(_nm_utils_invoke_on_idle_cb_cancelled), + data); + } + } + + if (use_timeout) { + source = nm_g_timeout_source_new(timeout_msec, + G_PRIORITY_DEFAULT, + _nm_utils_invoke_on_idle_cb_idle, + data, + NULL); + } else { + source = + nm_g_idle_source_new(G_PRIORITY_DEFAULT, _nm_utils_invoke_on_idle_cb_idle, data, NULL); + } + + /* use the current thread default context. */ + g_source_attach(source, g_main_context_get_thread_default()); + + data->source = source; +} + +void +nm_utils_invoke_on_idle(GCancellable * cancellable, + NMUtilsInvokeOnIdleCallback callback, + gpointer callback_user_data) +{ + _nm_utils_invoke_on_idle_start(FALSE, 0, cancellable, callback, callback_user_data); +} + +void +nm_utils_invoke_on_timeout(guint timeout_msec, + GCancellable * cancellable, + NMUtilsInvokeOnIdleCallback callback, + gpointer callback_user_data) +{ + _nm_utils_invoke_on_idle_start(TRUE, timeout_msec, cancellable, callback, callback_user_data); +} + +/*****************************************************************************/ + +int +nm_utils_getpagesize(void) +{ + static volatile int val = 0; + long l; + int v; + + v = g_atomic_int_get(&val); + + if (G_UNLIKELY(v == 0)) { + l = sysconf(_SC_PAGESIZE); + + g_return_val_if_fail(l > 0 && l < G_MAXINT, 4 * 1024); + + v = (int) l; + if (!g_atomic_int_compare_and_exchange(&val, 0, v)) { + v = g_atomic_int_get(&val); + g_return_val_if_fail(v > 0, 4 * 1024); + } + } + + nm_assert(v > 0); +#if NM_MORE_ASSERTS > 5 + nm_assert(v == getpagesize()); + nm_assert(v == sysconf(_SC_PAGESIZE)); +#endif + + return v; +} + +gboolean +nm_utils_memeqzero(gconstpointer data, gsize length) +{ + const unsigned char *p = data; + int len; + + /* Taken from https://github.com/rustyrussell/ccan/blob/9d2d2c49f053018724bcc6e37029da10b7c3d60d/ccan/mem/mem.c#L92, + * CC-0 licensed. */ + + /* Check first 16 bytes manually */ + for (len = 0; len < 16; len++) { + if (!length) + return TRUE; + if (*p) + return FALSE; + p++; + length--; + } + + /* Now we know that's zero, memcmp with self. */ + return memcmp(data, p, length) == 0; +} + +/** + * nm_utils_bin2hexstr_full: + * @addr: pointer of @length bytes. If @length is zero, this may + * also be %NULL. + * @length: number of bytes in @addr. May also be zero, in which + * case this will return an empty string. + * @delimiter: either '\0', otherwise the output string will have the + * given delimiter character between each two hex numbers. + * @upper_case: if TRUE, use upper case ASCII characters for hex. + * @out: if %NULL, the function will allocate a new buffer of + * either (@length*2+1) or (@length*3) bytes, depending on whether + * a @delimiter is specified. In that case, the allocated buffer will + * be returned and must be freed by the caller. + * If not %NULL, the buffer must already be preallocated and contain + * at least (@length*2+1) or (@length*3) bytes, depending on the delimiter. + * If @length is zero, then of course at least one byte will be allocated + * or @out (if given) must contain at least room for the trailing NUL byte. + * + * Returns: the binary value converted to a hex string. If @out is given, + * this always returns @out. If @out is %NULL, a newly allocated string + * is returned. This never returns %NULL, for buffers of length zero + * an empty string is returned. + */ +char * +nm_utils_bin2hexstr_full(gconstpointer addr, + gsize length, + char delimiter, + gboolean upper_case, + char * out) +{ + const guint8 *in = addr; + const char * LOOKUP = upper_case ? "0123456789ABCDEF" : "0123456789abcdef"; + char * out0; + + if (out) + out0 = out; + else { + out0 = out = + g_new(char, length == 0 ? 1u : (delimiter == '\0' ? length * 2u + 1u : length * 3u)); + } + + /* @out must contain at least @length*3 bytes if @delimiter is set, + * otherwise, @length*2+1. */ + + if (length > 0) { + nm_assert(in); + for (;;) { + const guint8 v = *in++; + + *out++ = LOOKUP[v >> 4]; + *out++ = LOOKUP[v & 0x0F]; + length--; + if (!length) + break; + if (delimiter) + *out++ = delimiter; + } + } + + *out = '\0'; + return out0; +} + +guint8 * +nm_utils_hexstr2bin_full(const char *hexstr, + gboolean allow_0x_prefix, + gboolean delimiter_required, + gboolean hexdigit_pairs_required, + const char *delimiter_candidates, + gsize required_len, + guint8 * buffer, + gsize buffer_len, + gsize * out_len) +{ + const char *in = hexstr; + guint8 * out = buffer; + gboolean delimiter_has = TRUE; + guint8 delimiter = '\0'; + gsize len; + + nm_assert(hexstr); + nm_assert(buffer); + nm_assert(required_len > 0 || out_len); + + if (allow_0x_prefix && in[0] == '0' && in[1] == 'x') + in += 2; + + while (TRUE) { + const guint8 d1 = in[0]; + guint8 d2; + int i1, i2; + + i1 = nm_utils_hexchar_to_int(d1); + if (i1 < 0) + goto fail; + + /* If there's no leading zero (ie "aa:b:cc") then fake it */ + d2 = in[1]; + if (d2 && (i2 = nm_utils_hexchar_to_int(d2)) >= 0) { + *out++ = (i1 << 4) + i2; + d2 = in[2]; + if (!d2) + break; + in += 2; + } else { + /* Fake leading zero */ + *out++ = i1; + if (!d2) { + if (!delimiter_has || hexdigit_pairs_required) { + /* when using no delimiter, there must be pairs of hex chars */ + goto fail; + } + break; + } else if (hexdigit_pairs_required) + goto fail; + in += 1; + } + + if (--buffer_len == 0) + goto fail; + + if (delimiter_has) { + if (d2 != delimiter) { + if (delimiter) + goto fail; + if (delimiter_candidates) { + while (delimiter_candidates[0]) { + if (delimiter_candidates++[0] == d2) + delimiter = d2; + } + } + if (!delimiter) { + if (delimiter_required) + goto fail; + delimiter_has = FALSE; + continue; + } + } + in++; + } + } + + len = out - buffer; + if (required_len == 0 || len == required_len) { + NM_SET_OUT(out_len, len); + return buffer; + } + +fail: + NM_SET_OUT(out_len, 0); + return NULL; +} + +guint8 * +nm_utils_hexstr2bin_alloc(const char *hexstr, + gboolean allow_0x_prefix, + gboolean delimiter_required, + const char *delimiter_candidates, + gsize required_len, + gsize * out_len) +{ + guint8 *buffer; + gsize buffer_len, len; + + if (G_UNLIKELY(!hexstr)) { + NM_SET_OUT(out_len, 0); + g_return_val_if_fail(hexstr, NULL); + } + + nm_assert(required_len > 0 || out_len); + + if (allow_0x_prefix && hexstr[0] == '0' && hexstr[1] == 'x') + hexstr += 2; + + if (!hexstr[0]) + goto fail; + + if (required_len > 0) + buffer_len = required_len; + else + buffer_len = strlen(hexstr) / 2 + 3; + + buffer = g_malloc(buffer_len); + + if (nm_utils_hexstr2bin_full(hexstr, + FALSE, + delimiter_required, + FALSE, + delimiter_candidates, + required_len, + buffer, + buffer_len, + &len)) { + NM_SET_OUT(out_len, len); + return buffer; + } + + g_free(buffer); + +fail: + NM_SET_OUT(out_len, 0); + return NULL; +} + +/*****************************************************************************/ + +GVariant * +nm_utils_gvariant_vardict_filter(GVariant *src, + gboolean (*filter_fcn)(const char *key, + GVariant * val, + char ** out_key, + GVariant ** out_val, + gpointer user_data), + gpointer user_data) +{ + GVariantIter iter; + GVariantBuilder builder; + const char * key; + GVariant * val; + + g_return_val_if_fail(src && g_variant_is_of_type(src, G_VARIANT_TYPE_VARDICT), NULL); + g_return_val_if_fail(filter_fcn, NULL); + + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + + g_variant_iter_init(&iter, src); + while (g_variant_iter_next(&iter, "{&sv}", &key, &val)) { + _nm_unused gs_unref_variant GVariant *val_free = val; + gs_free char * key2 = NULL; + gs_unref_variant GVariant *val2 = NULL; + + if (filter_fcn(key, val, &key2, &val2, user_data)) { + g_variant_builder_add(&builder, "{sv}", key2 ?: key, val2 ?: val); + } + } + + return g_variant_builder_end(&builder); +} + +static gboolean +_gvariant_vardict_filter_drop_one(const char *key, + GVariant * val, + char ** out_key, + GVariant ** out_val, + gpointer user_data) +{ + return !nm_streq(key, user_data); +} + +GVariant * +nm_utils_gvariant_vardict_filter_drop_one(GVariant *src, const char *key) +{ + return nm_utils_gvariant_vardict_filter(src, _gvariant_vardict_filter_drop_one, (gpointer) key); +} + +/*****************************************************************************/ + +static gboolean +debug_key_matches(const char *key, const char *token, guint length) +{ + /* may not call GLib functions: see note in g_parse_debug_string() */ + for (; length; length--, key++, token++) { + char k = (*key == '_') ? '-' : g_ascii_tolower(*key); + char t = (*token == '_') ? '-' : g_ascii_tolower(*token); + + if (k != t) + return FALSE; + } + + return *key == '\0'; +} + +/** + * nm_utils_parse_debug_string: + * @string: the string to parse + * @keys: the debug keys + * @nkeys: number of entries in @keys + * + * Similar to g_parse_debug_string(), but does not special + * case "help" or "all". + * + * Returns: the flags + */ +guint +nm_utils_parse_debug_string(const char *string, const GDebugKey *keys, guint nkeys) +{ + guint i; + guint result = 0; + const char *q; + + if (string == NULL) + return 0; + + while (*string) { + q = strpbrk(string, ":;, \t"); + if (!q) + q = string + strlen(string); + + for (i = 0; i < nkeys; i++) { + if (debug_key_matches(keys[i].key, string, q - string)) + result |= keys[i].value; + } + + string = q; + if (*string) + string++; + } + + return result; +} + +/*****************************************************************************/ + +GSource * +nm_g_idle_source_new(int priority, + GSourceFunc func, + gpointer user_data, + GDestroyNotify destroy_notify) +{ + GSource *source; + + source = g_idle_source_new(); + if (priority != G_PRIORITY_DEFAULT) + g_source_set_priority(source, priority); + g_source_set_callback(source, func, user_data, destroy_notify); + return source; +} + +GSource * +nm_g_timeout_source_new(guint timeout_msec, + int priority, + GSourceFunc func, + gpointer user_data, + GDestroyNotify destroy_notify) +{ + GSource *source; + + source = g_timeout_source_new(timeout_msec); + if (priority != G_PRIORITY_DEFAULT) + g_source_set_priority(source, priority); + g_source_set_callback(source, func, user_data, destroy_notify); + return source; +} + +GSource * +nm_g_timeout_source_new_seconds(guint timeout_sec, + int priority, + GSourceFunc func, + gpointer user_data, + GDestroyNotify destroy_notify) +{ + GSource *source; + + source = g_timeout_source_new_seconds(timeout_sec); + if (priority != G_PRIORITY_DEFAULT) + g_source_set_priority(source, priority); + g_source_set_callback(source, func, user_data, destroy_notify); + return source; +} + +GSource * +nm_g_unix_signal_source_new(int signum, + int priority, + GSourceFunc handler, + gpointer user_data, + GDestroyNotify notify) +{ + GSource *source; + + source = g_unix_signal_source_new(signum); + + if (priority != G_PRIORITY_DEFAULT) + g_source_set_priority(source, priority); + g_source_set_callback(source, handler, user_data, notify); + return source; +} + +GSource * +nm_g_unix_fd_source_new(int fd, + GIOCondition io_condition, + int priority, + gboolean (*source_func)(int fd, GIOCondition condition, gpointer user_data), + gpointer user_data, + GDestroyNotify destroy_notify) +{ + GSource *source; + + source = g_unix_fd_source_new(fd, io_condition); + + if (priority != G_PRIORITY_DEFAULT) + g_source_set_priority(source, priority); + g_source_set_callback(source, G_SOURCE_FUNC(source_func), user_data, destroy_notify); + return source; +} + +/*****************************************************************************/ + +#define _CTX_LOG(fmt, ...) \ + G_STMT_START \ + { \ + if (FALSE) { \ + gint64 _ts = g_get_monotonic_time() / 100; \ + \ + g_printerr(">>>> [%" G_GINT64_FORMAT ".%05" G_GINT64_FORMAT "] [src:%p]: " fmt "\n", \ + _ts / 10000, \ + _ts % 10000, \ + (ctx_src), \ + ##__VA_ARGS__); \ + } \ + } \ + G_STMT_END + +typedef struct { + int fd; + guint events; + guint registered_events; + union { + int one; + int *many; + } idx; + gpointer tag; + bool stale : 1; + bool has_many_idx : 1; +} PollData; + +typedef struct { + GSource source; + GMainContext *context; + GHashTable * fds; + GPollFD * fds_arr; + guint fds_len; + int max_priority; + bool acquired : 1; +} CtxIntegSource; + +static void +_poll_data_free(gpointer user_data) +{ + PollData *poll_data = user_data; + + if (poll_data->has_many_idx) + g_free(poll_data->idx.many); + nm_g_slice_free(poll_data); +} + +static void +_ctx_integ_source_reacquire(CtxIntegSource *ctx_src) +{ + if (G_LIKELY(ctx_src->acquired && g_main_context_is_owner(ctx_src->context))) + return; + + /* the parent context now iterates on a different thread. + * We need to release and reacquire the inner context. */ + + if (ctx_src->acquired) + g_main_context_release(ctx_src->context); + + if (G_UNLIKELY(!g_main_context_acquire(ctx_src->context))) { + /* Nobody is supposed to reacquire the context while we use it. This is a bug + * of the user. */ + ctx_src->acquired = FALSE; + g_return_if_reached(); + } + ctx_src->acquired = TRUE; +} + +static gboolean +_ctx_integ_source_prepare(GSource *source, int *out_timeout) +{ + CtxIntegSource *ctx_src = ((CtxIntegSource *) source); + int max_priority; + int timeout = -1; + gboolean any_ready; + GHashTableIter h_iter; + PollData * poll_data; + gboolean fds_changed; + GPollFD new_fds_stack[300u / sizeof(GPollFD)]; + gs_free GPollFD *new_fds_heap = NULL; + GPollFD * new_fds; + guint new_fds_len; + guint new_fds_alloc; + guint i; + + _CTX_LOG("prepare..."); + + _ctx_integ_source_reacquire(ctx_src); + + any_ready = g_main_context_prepare(ctx_src->context, &max_priority); + + new_fds_alloc = NM_MAX(G_N_ELEMENTS(new_fds_stack), ctx_src->fds_len); + + if (new_fds_alloc > G_N_ELEMENTS(new_fds_stack)) { + new_fds_heap = g_new(GPollFD, new_fds_alloc); + new_fds = new_fds_heap; + } else + new_fds = new_fds_stack; + + for (;;) { + int l; + + nm_assert(new_fds_alloc <= (guint) G_MAXINT); + + l = g_main_context_query(ctx_src->context, + max_priority, + &timeout, + new_fds, + (int) new_fds_alloc); + nm_assert(l >= 0); + + new_fds_len = (guint) l; + + if (G_LIKELY(new_fds_len <= new_fds_alloc)) + break; + + new_fds_alloc = new_fds_len; + g_free(new_fds_heap); + new_fds_heap = g_new(GPollFD, new_fds_alloc); + new_fds = new_fds_heap; + } + + fds_changed = FALSE; + if (new_fds_len != ctx_src->fds_len) + fds_changed = TRUE; + else { + for (i = 0; i < new_fds_len; i++) { + if (new_fds[i].fd != ctx_src->fds_arr[i].fd + || new_fds[i].events != ctx_src->fds_arr[i].events) { + fds_changed = TRUE; + break; + } + } + } + + if (G_UNLIKELY(fds_changed)) { + g_free(ctx_src->fds_arr); + ctx_src->fds_len = new_fds_len; + if (G_LIKELY(new_fds == new_fds_stack) || new_fds_alloc != new_fds_len) + ctx_src->fds_arr = nm_memdup(new_fds, sizeof(*new_fds) * new_fds_len); + else + ctx_src->fds_arr = g_steal_pointer(&new_fds_heap); + + g_hash_table_iter_init(&h_iter, ctx_src->fds); + while (g_hash_table_iter_next(&h_iter, (gpointer *) &poll_data, NULL)) + poll_data->stale = TRUE; + + for (i = 0; i < ctx_src->fds_len; i++) { + const GPollFD *fd = &ctx_src->fds_arr[i]; + + poll_data = g_hash_table_lookup(ctx_src->fds, &fd->fd); + + if (G_UNLIKELY(!poll_data)) { + poll_data = g_slice_new(PollData); + *poll_data = (PollData){ + .fd = fd->fd, + .idx.one = i, + .has_many_idx = FALSE, + .events = fd->events, + .registered_events = 0, + .tag = NULL, + .stale = FALSE, + }; + g_hash_table_add(ctx_src->fds, poll_data); + nm_assert(poll_data == g_hash_table_lookup(ctx_src->fds, &fd->fd)); + continue; + } + + if (G_LIKELY(poll_data->stale)) { + if (poll_data->has_many_idx) { + g_free(poll_data->idx.many); + poll_data->has_many_idx = FALSE; + } + poll_data->events = fd->events; + poll_data->idx.one = i; + poll_data->stale = FALSE; + continue; + } + + /* How odd. We have duplicate FDs. In fact, currently g_main_context_query() always + * coalesces the FDs and this cannot happen. However, that is not documented behavior, + * so we should not rely on that. So we need to keep a list of indexes... */ + poll_data->events |= fd->events; + if (!poll_data->has_many_idx) { + int idx0; + + idx0 = poll_data->idx.one; + poll_data->has_many_idx = TRUE; + poll_data->idx.many = g_new(int, 4); + poll_data->idx.many[0] = 2; /* number allocated */ + poll_data->idx.many[1] = 2; /* number used */ + poll_data->idx.many[2] = idx0; + poll_data->idx.many[3] = i; + } else { + if (poll_data->idx.many[0] == poll_data->idx.many[1]) { + poll_data->idx.many[0] *= 2; + poll_data->idx.many = + g_realloc(poll_data->idx.many, sizeof(int) * (2 + poll_data->idx.many[0])); + } + poll_data->idx.many[2 + poll_data->idx.many[1]] = i; + poll_data->idx.many[1]++; + } + } + + g_hash_table_iter_init(&h_iter, ctx_src->fds); + while (g_hash_table_iter_next(&h_iter, (gpointer *) &poll_data, NULL)) { + if (poll_data->stale) { + nm_assert(poll_data->tag); + nm_assert(poll_data->events == poll_data->registered_events); + _CTX_LOG("prepare: remove poll fd=%d, events=0x%x", + poll_data->fd, + poll_data->events); + g_source_remove_unix_fd(&ctx_src->source, poll_data->tag); + g_hash_table_iter_remove(&h_iter); + continue; + } + if (!poll_data->tag) { + _CTX_LOG("prepare: add poll fd=%d, events=0x%x", poll_data->fd, poll_data->events); + poll_data->registered_events = poll_data->events; + poll_data->tag = g_source_add_unix_fd(&ctx_src->source, + poll_data->fd, + poll_data->registered_events); + continue; + } + if (poll_data->registered_events != poll_data->events) { + _CTX_LOG("prepare: update poll fd=%d, events=0x%x", + poll_data->fd, + poll_data->events); + poll_data->registered_events = poll_data->events; + g_source_modify_unix_fd(&ctx_src->source, + poll_data->tag, + poll_data->registered_events); + } + } + } + + NM_SET_OUT(out_timeout, timeout); + ctx_src->max_priority = max_priority; + + _CTX_LOG("prepare: done, any-ready=%d, timeout=%d, max-priority=%d", + any_ready, + timeout, + max_priority); + + /* we always need to poll, because we have some file descriptors. */ + return FALSE; +} + +static gboolean +_ctx_integ_source_check(GSource *source) +{ + CtxIntegSource *ctx_src = ((CtxIntegSource *) source); + GHashTableIter h_iter; + gboolean some_ready; + PollData * poll_data; + + nm_assert(ctx_src->context); + + _CTX_LOG("check"); + + _ctx_integ_source_reacquire(ctx_src); + + g_hash_table_iter_init(&h_iter, ctx_src->fds); + while (g_hash_table_iter_next(&h_iter, (gpointer *) &poll_data, NULL)) { + guint revents; + + revents = g_source_query_unix_fd(&ctx_src->source, poll_data->tag); + if (G_UNLIKELY(poll_data->has_many_idx)) { + int num = poll_data->idx.many[1]; + int *p_idx = &poll_data->idx.many[2]; + + for (; num > 0; num--, p_idx++) + ctx_src->fds_arr[*p_idx].revents = revents; + } else + ctx_src->fds_arr[poll_data->idx.one].revents = revents; + } + + nm_assert(ctx_src->fds_len <= (guint) G_MAXINT); + + some_ready = g_main_context_check(ctx_src->context, + ctx_src->max_priority, + ctx_src->fds_arr, + (int) ctx_src->fds_len); + + _CTX_LOG("check (some-ready=%d)...", some_ready); + + return some_ready; +} + +static gboolean +_ctx_integ_source_dispatch(GSource *source, GSourceFunc callback, gpointer user_data) +{ + CtxIntegSource *ctx_src = ((CtxIntegSource *) source); + + nm_assert(ctx_src->context); + + _ctx_integ_source_reacquire(ctx_src); + + _CTX_LOG("dispatch"); + + g_main_context_dispatch(ctx_src->context); + + return G_SOURCE_CONTINUE; +} + +static void +_ctx_integ_source_finalize(GSource *source) +{ + CtxIntegSource *ctx_src = ((CtxIntegSource *) source); + GHashTableIter h_iter; + PollData * poll_data; + + g_return_if_fail(ctx_src->context); + + _CTX_LOG("finalize..."); + + g_hash_table_iter_init(&h_iter, ctx_src->fds); + while (g_hash_table_iter_next(&h_iter, (gpointer *) &poll_data, NULL)) { + nm_assert(poll_data->tag); + _CTX_LOG("prepare: remove poll fd=%d, events=0x%x", poll_data->fd, poll_data->events); + g_source_remove_unix_fd(&ctx_src->source, poll_data->tag); + g_hash_table_iter_remove(&h_iter); + } + + nm_clear_pointer(&ctx_src->fds, g_hash_table_unref); + nm_clear_g_free(&ctx_src->fds_arr); + ctx_src->fds_len = 0; + + if (ctx_src->acquired) { + ctx_src->acquired = FALSE; + g_main_context_release(ctx_src->context); + } + + nm_clear_pointer(&ctx_src->context, g_main_context_unref); +} + +static GSourceFuncs ctx_integ_source_funcs = { + .prepare = _ctx_integ_source_prepare, + .check = _ctx_integ_source_check, + .dispatch = _ctx_integ_source_dispatch, + .finalize = _ctx_integ_source_finalize, +}; + +/** + * nm_utils_g_main_context_create_integrate_source: + * @inner_context: the inner context that will be integrated to an + * outer #GMainContext. + * + * By integrating the inner context with an outer context, when iterating the outer + * context sources on the inner context will be dispatched. Note that while the + * created source exists, the @inner_context will be acquired. The user gets restricted + * what to do with the inner context. In particular while the inner context is integrated, + * the user should not acquire the inner context again or explicitly iterate it. What + * the user of course still can (and wants to) do is attaching new sources to the inner + * context. + * + * Note that GSource has a priority. While each context dispatches events based on + * their source's priorities, the outer context dispatches to the inner context + * only with one priority (the priority of the created source). That is, the sources + * from the two contexts are kept separate and are not sorted by their priorities. + * + * Returns: a newly created GSource that should be attached to the + * outer context. + */ +GSource * +nm_utils_g_main_context_create_integrate_source(GMainContext *inner_context) +{ + CtxIntegSource *ctx_src; + + g_return_val_if_fail(inner_context, NULL); + + if (!g_main_context_acquire(inner_context)) { + /* We require to acquire the context while it's integrated. We need to keep it acquired + * for the entire duration. + * + * This is also necessary because g_source_attach() only wakes up the context, if + * the context is currently acquired. */ + g_return_val_if_reached(NULL); + } + + ctx_src = (CtxIntegSource *) g_source_new(&ctx_integ_source_funcs, sizeof(CtxIntegSource)); + + g_source_set_name(&ctx_src->source, "ContextIntegrateSource"); + + ctx_src->context = g_main_context_ref(inner_context); + ctx_src->fds = g_hash_table_new_full(nm_pint_hash, nm_pint_equals, _poll_data_free, NULL); + ctx_src->fds_len = 0; + ctx_src->fds_arr = NULL; + ctx_src->acquired = TRUE; + ctx_src->max_priority = G_MAXINT; + + _CTX_LOG("create new integ-source for %p", inner_context); + + return &ctx_src->source; +} + +/*****************************************************************************/ + +void +nm_utils_ifname_cpy(char *dst, const char *name) +{ + int i; + + g_return_if_fail(dst); + g_return_if_fail(name && name[0]); + + nm_assert(nm_utils_ifname_valid_kernel(name, NULL)); + + /* ensures NUL padding of the entire IFNAMSIZ buffer. */ + + for (i = 0; i < (int) IFNAMSIZ && name[i] != '\0'; i++) + dst[i] = name[i]; + + nm_assert(name[i] == '\0'); + + for (; i < (int) IFNAMSIZ; i++) + dst[i] = '\0'; +} + +/*****************************************************************************/ + +gboolean +nm_utils_ifname_valid_kernel(const char *name, GError **error) +{ + int i; + + /* This function follows kernel's interface validation + * function dev_valid_name() in net/core/dev.c. + */ + + if (!name) { + g_set_error_literal(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + _("interface name is missing")); + return FALSE; + } + + if (name[0] == '\0') { + g_set_error_literal(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + _("interface name is too short")); + return FALSE; + } + + if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) { + g_set_error_literal(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + _("interface name is reserved")); + return FALSE; + } + + for (i = 0; i < IFNAMSIZ; i++) { + char ch = name[i]; + + if (ch == '\0') + return TRUE; + if (NM_IN_SET(ch, '/', ':') || g_ascii_isspace(ch)) { + g_set_error_literal(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + _("interface name contains an invalid character")); + return FALSE; + } + } + + g_set_error_literal(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + _("interface name is longer than 15 characters")); + return FALSE; +} + +/*****************************************************************************/ + +static gboolean +_nm_utils_ifname_valid_kernel(const char *name, GError **error) +{ + if (!nm_utils_ifname_valid_kernel(name, error)) + return FALSE; + + if (strchr(name, '%')) { + /* Kernel's dev_valid_name() accepts (almost) any binary up to 15 chars. + * However, '%' is treated special as a format specifier. Try + * + * ip link add 'dummy%dx' type dummy + * + * Don't allow that for "connection.interface-name", which either + * matches an existing netdev name (thus, it cannot have a '%') or + * is used to configure a name (in which case we don't want kernel + * to replace the format specifier). */ + g_set_error_literal(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + _("'%%' is not allowed in interface names")); + return FALSE; + } + + if (NM_IN_STRSET(name, "all", "default", "bonding_masters")) { + /* Certain names are not allowed. The "all" and "default" names are reserved + * due to their directories in "/proc/sys/net/ipv4/conf/" and "/proc/sys/net/ipv6/conf/". + * + * Also, there is "/sys/class/net/bonding_masters" file. + */ + nm_utils_error_set(error, + NM_UTILS_ERROR_UNKNOWN, + _("'%s' is not allowed as interface name"), + name); + return FALSE; + } + + return TRUE; +} + +static gboolean +_nm_utils_ifname_valid_ovs(const char *name, GError **error) +{ + const char *ch; + + /* OVS actually accepts a wider range of chars (all printable UTF-8 chars), + * NetworkManager restricts this to ASCII char as it's a safer option for + * now since OVS is not well documented on this matter. + **/ + for (ch = name; *ch; ++ch) { + if (*ch == '\\' || *ch == '/' || !g_ascii_isgraph(*ch)) { + g_set_error_literal(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + _("interface name must be alphanumerical with " + "no forward or backward slashes")); + return FALSE; + } + }; + return TRUE; +} + +gboolean +nm_utils_ifname_valid(const char *name, NMUtilsIfaceType type, GError **error) +{ + g_return_val_if_fail(!error || !(*error), FALSE); + + if (!name || !(name[0])) { + g_set_error_literal(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + _("interface name must not be empty")); + return FALSE; + } + + if (!g_utf8_validate(name, -1, NULL)) { + g_set_error_literal(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + _("interface name must be UTF-8 encoded")); + return FALSE; + } + + switch (type) { + case NMU_IFACE_KERNEL: + return _nm_utils_ifname_valid_kernel(name, error); + case NMU_IFACE_OVS: + return _nm_utils_ifname_valid_ovs(name, error); + case NMU_IFACE_OVS_AND_KERNEL: + return _nm_utils_ifname_valid_kernel(name, error) + && _nm_utils_ifname_valid_ovs(name, error); + case NMU_IFACE_ANY: + { + gs_free_error GError *local = NULL; + + if (_nm_utils_ifname_valid_kernel(name, error ? &local : NULL)) + return TRUE; + if (_nm_utils_ifname_valid_ovs(name, NULL)) + return TRUE; + if (error) + g_propagate_error(error, g_steal_pointer(&local)); + return FALSE; + } + } + + g_return_val_if_reached(FALSE); +} + +/*****************************************************************************/ + +void +_nm_str_buf_ensure_size(NMStrBuf *strbuf, gsize new_size, gboolean reserve_exact) +{ + _nm_str_buf_assert(strbuf); + + /* Currently, this only supports strictly growing the buffer. */ + nm_assert(new_size > strbuf->_priv_allocated); + + if (!reserve_exact) { + new_size = nm_utils_get_next_realloc_size(!strbuf->_priv_do_bzero_mem, new_size); + } + + strbuf->_priv_str = nm_secret_mem_realloc(strbuf->_priv_str, + strbuf->_priv_do_bzero_mem, + strbuf->_priv_allocated, + new_size); + strbuf->_priv_allocated = new_size; +} + +void +nm_str_buf_append_printf(NMStrBuf *strbuf, const char *format, ...) +{ + va_list args; + gsize available; + int l; + + _nm_str_buf_assert(strbuf); + + available = strbuf->_priv_allocated - strbuf->_priv_len; + + nm_assert(available < G_MAXULONG); + + va_start(args, format); + l = g_vsnprintf(&strbuf->_priv_str[strbuf->_priv_len], available, format, args); + va_end(args); + + nm_assert(l >= 0); + nm_assert(l < G_MAXINT); + + if ((gsize) l >= available) { + gsize l2; + + if (l == 0) + return; + + l2 = ((gsize) l) + 1u; + + nm_str_buf_maybe_expand(strbuf, l2, FALSE); + + va_start(args, format); + l = g_vsnprintf(&strbuf->_priv_str[strbuf->_priv_len], l2, format, args); + va_end(args); + + nm_assert(l >= 0); + nm_assert((gsize) l == l2 - 1u); + } + + strbuf->_priv_len += (gsize) l; +} + +/*****************************************************************************/ + +/** + * nm_indirect_g_free: + * @arg: a pointer to a pointer that is to be freed. + * + * This does the same as nm_clear_g_free(arg) (g_clear_pointer (arg, g_free)). + * This is for example useful when you have a GArray with pointers and a + * clear function to free them. g_array_set_clear_func()'s destroy notify + * function gets a pointer to the array location, so we have to follow + * the first pointer. + */ +void +nm_indirect_g_free(gpointer arg) +{ + gpointer *p = arg; + + nm_clear_g_free(p); +} + +/*****************************************************************************/ + +static char * +attribute_escape(const char *src, char c1, char c2) +{ + char *ret, *dest; + + dest = ret = g_malloc(strlen(src) * 2 + 1); + + while (*src) { + if (*src == c1 || *src == c2 || *src == '\\') + *dest++ = '\\'; + *dest++ = *src++; + } + *dest++ = '\0'; + + return ret; +} + +void +_nm_utils_format_variant_attributes_full(GString * str, + const NMUtilsNamedValue * values, + guint num_values, + const NMVariantAttributeSpec *const *spec, + char attr_separator, + char key_value_separator) +{ + const NMVariantAttributeSpec *const *s; + const char * name, *value; + GVariant * variant; + char * escaped; + char buf[64]; + char sep = 0; + guint i; + + for (i = 0; i < num_values; i++) { + name = values[i].name; + variant = values[i].value_ptr; + value = NULL; + s = NULL; + + if (spec) { + for (s = spec; *s; s++) { + if (nm_streq0((*s)->name, name)) + break; + } + + if (!*s) + continue; + } + + if (g_variant_is_of_type(variant, G_VARIANT_TYPE_UINT32)) + value = nm_sprintf_buf(buf, "%u", g_variant_get_uint32(variant)); + else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_INT32)) + value = nm_sprintf_buf(buf, "%d", (int) g_variant_get_int32(variant)); + else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_UINT64)) + value = nm_sprintf_buf(buf, "%" G_GUINT64_FORMAT, g_variant_get_uint64(variant)); + else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_BYTE)) + value = nm_sprintf_buf(buf, "%hhu", g_variant_get_byte(variant)); + else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_BOOLEAN)) + value = g_variant_get_boolean(variant) ? "true" : "false"; + else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_STRING)) + value = g_variant_get_string(variant, NULL); + else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_BYTESTRING)) { + /* FIXME: there is no guarantee that the byte array + * is valid UTF-8.*/ + value = g_variant_get_bytestring(variant); + } else + continue; + + if (sep) + g_string_append_c(str, sep); + + escaped = attribute_escape(name, attr_separator, key_value_separator); + g_string_append(str, escaped); + g_free(escaped); + + if (!s || !*s || !(*s)->no_value) { + g_string_append_c(str, key_value_separator); + + escaped = attribute_escape(value, attr_separator, key_value_separator); + g_string_append(str, escaped); + g_free(escaped); + } + + sep = attr_separator; + } +} + +char * +_nm_utils_format_variant_attributes(GHashTable * attributes, + const NMVariantAttributeSpec *const *spec, + char attr_separator, + char key_value_separator) +{ + gs_free NMUtilsNamedValue *values_free = NULL; + NMUtilsNamedValue values_prepared[20]; + const NMUtilsNamedValue * values; + GString * str = NULL; + guint len; + + g_return_val_if_fail(attr_separator, NULL); + g_return_val_if_fail(key_value_separator, NULL); + + if (!attributes) + return NULL; + + values = nm_utils_named_values_from_strdict(attributes, &len, values_prepared, &values_free); + if (len == 0) + return NULL; + + str = g_string_new(""); + _nm_utils_format_variant_attributes_full(str, + values, + len, + spec, + attr_separator, + key_value_separator); + return g_string_free(str, FALSE); +} + +/*****************************************************************************/ + +gboolean +nm_utils_is_localhost(const char *name) +{ + static const char *const NAMES[] = { + "localhost", + "localhost4", + "localhost6", + "localhost.localdomain", + "localhost4.localdomain4", + "localhost6.localdomain6", + }; + gsize name_len; + int i; + + if (!name) + return FALSE; + + /* This tries to identify local host and domain names + * described in RFC6761 plus the redhatism of localdomain. + * + * Similar to systemd's is_localhost(). */ + + name_len = strlen(name); + + if (name_len == 0) + return FALSE; + + if (name[name_len - 1] == '.') { + /* one trailing dot is fine. Hide it. */ + name_len--; + } + + for (i = 0; i < (int) G_N_ELEMENTS(NAMES); i++) { + const char *n = NAMES[i]; + gsize l = strlen(n); + gsize s; + + if (name_len < l) + continue; + + s = name_len - l; + + if (g_ascii_strncasecmp(&name[s], n, l) != 0) + continue; + + /* we accept the name if it is equal to one of the well-known names, + * or if it is some prefix, a '.' and the well-known name. */ + if (s == 0) + return TRUE; + if (name[s - 1] == '.') + return TRUE; + } + + return FALSE; +} + +gboolean +nm_utils_is_specific_hostname(const char *name) +{ + if (nm_str_is_empty(name)) + return FALSE; + + if (nm_streq(name, "(none)")) { + /* This is not a special hostname. Probably an artefact by somebody wrongly + * printing NULL. */ + return FALSE; + } + + if (nm_utils_is_localhost(name)) + return FALSE; + + /* FIXME: properly validate the hostname, like systemd's hostname_is_valid() */ + + return TRUE; +} + +/*****************************************************************************/ + +/* taken from systemd's uid_to_name(). */ +char * +nm_utils_uid_to_name(uid_t uid) +{ + gs_free char *buf_heap = NULL; + char buf_stack[4096]; + gsize bufsize; + char * buf; + + bufsize = sizeof(buf_stack); + buf = buf_stack; + + for (;;) { + struct passwd pwbuf; + struct passwd *pw = NULL; + int r; + + r = getpwuid_r(uid, &pwbuf, buf, bufsize, &pw); + if (r == 0 && pw) + return nm_strdup_not_empty(pw->pw_name); + + if (r != ERANGE) + return NULL; + + if (bufsize > G_MAXSIZE / 2u) + return NULL; + + bufsize *= 2u; + g_free(buf_heap); + buf_heap = g_malloc(bufsize); + buf = buf_heap; + } +} + +/* taken from systemd's nss_user_record_by_name() */ +gboolean +nm_utils_name_to_uid(const char *name, uid_t *out_uid) +{ + gs_free char *buf_heap = NULL; + char buf_stack[4096]; + gsize bufsize; + char * buf; + + if (!name) + return nm_assert_unreachable_val(FALSE); + + bufsize = sizeof(buf_stack); + buf = buf_stack; + + for (;;) { + struct passwd *result; + struct passwd pwd; + int r; + + r = getpwnam_r(name, &pwd, buf, bufsize, &result); + if (r == 0) { + if (!result) + return FALSE; + NM_SET_OUT(out_uid, pwd.pw_uid); + return TRUE; + } + + if (r != ERANGE) + return FALSE; + + if (bufsize > G_MAXSIZE / 2u) + return FALSE; + + bufsize *= 2u; + g_free(buf_heap); + buf_heap = g_malloc(bufsize); + buf = buf_heap; + } +} |