/* SPDX-License-Identifier: GPL-2.0-or-later */ /* * Copyright (C) 1999, 2000 Red Hat, Inc. */ #include "src/core/nm-default-daemon.h" #include "shvar.h" #include #include #include #include #include #include #include "libnm-glib-aux/nm-str-buf.h" #include "libnm-core-intern/nm-core-internal.h" #include "nm-core-utils.h" #include "libnm-glib-aux/nm-enum-utils.h" #include "libnm-glib-aux/nm-io-utils.h" #include "c-list/src/c-list.h" #include "nms-ifcfg-rh-utils.h" /*****************************************************************************/ struct _shvarLine { const char *key; CList lst; /* We index variables by their key in shvarFile.lst_idx. One shell variable might * occur multiple times in a file (in which case the last occurrence wins). * Hence, we need to keep a list of all the same keys. * * This is a pointer to the next shadowed line. */ struct _shvarLine *prev_shadowed; /* There are three cases: * * 1) the line is not a valid variable assignment (that is, it doesn't * start with a "FOO=" with possible whitespace prefix). * In that case, @key and @key_with_prefix are %NULL, and the entire * original line is in @line. Such entries are ignored for the most part. * * 2) if the line can be parsed with a "FOO=" assignment, then @line contains * the part after '=', @key_with_prefix contains the key "FOO" with possible * whitespace prefix, and @key points into @key_with_prefix skipping over the * whitespace. * * 3) like 2, but if the value was deleted via svSetValue(), the entry is not removed, * but only marked for deletion. That is done by clearing @line but preserving * @key/@key_with_prefix. * */ char *line; char *key_with_prefix; /* svSetValue() will clear the dirty flag. */ bool dirty : 1; }; typedef struct _shvarLine shvarLine; struct _shvarFile { char *fileName; CList lst_head; GHashTable *lst_idx; int fd; bool modified : 1; }; /*****************************************************************************/ static void _line_link_parse(shvarFile *s, const char *value, gsize len); /*****************************************************************************/ #define ASSERT_key_is_well_known(key) \ nm_assert(({ \ const char *_key = (key); \ gboolean _is_wellknown = TRUE; \ \ if (!nms_ifcfg_rh_utils_is_well_known_key(_key)) { \ _is_wellknown = FALSE; \ g_critical("ifcfg-rh key \"%s\" is not well-known", _key); \ } \ \ _is_wellknown; \ })) /** * svParseBoolean: * @value: the input string * @fallback: the fallback value * * Parses a string and returns the boolean value it contains or, * in case no valid value is found, the fallback value. Valid values * are: "yes", "true", "t", "y", "1" and "no", "false", "f", "n", "0". * * Always sets errno. Either to zero on success, to ENOKEY for NULL * or to EINVAL otherwise. * * Returns: the parsed boolean value or @fallback. */ int svParseBoolean(const char *value, int fallback) { if (!value) { errno = ENOKEY; return fallback; } if (!g_ascii_strcasecmp("yes", value) || !g_ascii_strcasecmp("true", value) || !g_ascii_strcasecmp("t", value) || !g_ascii_strcasecmp("y", value) || !g_ascii_strcasecmp("1", value)) { errno = 0; return TRUE; } else if (!g_ascii_strcasecmp("no", value) || !g_ascii_strcasecmp("false", value) || !g_ascii_strcasecmp("f", value) || !g_ascii_strcasecmp("n", value) || !g_ascii_strcasecmp("0", value)) { errno = 0; return FALSE; } errno = EINVAL; return fallback; } /*****************************************************************************/ static gboolean _shell_is_name(const char *key, gssize len) { gssize i; /* whether @key is a valid identifier (name). */ if (!key || len == 0) return FALSE; if (!g_ascii_isalpha(key[0]) && key[0] != '_') return FALSE; for (i = 1; TRUE; i++) { if (len < 0) { if (!key[i]) return TRUE; } else { if (i >= len) return TRUE; } if (!g_ascii_isalnum(key[i]) && key[i] != '_') return FALSE; } } /*****************************************************************************/ /* like g_strescape(), except that it also escapes '\''' *sigh*. * * While at it, add $''. */ static char * _escape_ansic(const char *source) { const char *p; char *dest; char *q; gsize n_alloc; nm_assert(source); n_alloc = 4; for (p = source; p[0]; p++) { switch (*p) { case '\b': case '\f': case '\n': case '\r': case '\t': case '\v': case '\\': case '"': case '\'': n_alloc += 2; break; default: if (!nm_ascii_is_regular(*p)) n_alloc += 4; else n_alloc += 1; break; } } dest = g_malloc(n_alloc); q = dest; *q++ = '$'; *q++ = '\''; for (p = source; p[0]; p++) { nm_assert(q < &dest[n_alloc]); switch (*p) { case '\b': *q++ = '\\'; *q++ = 'b'; break; case '\f': *q++ = '\\'; *q++ = 'f'; break; case '\n': *q++ = '\\'; *q++ = 'n'; break; case '\r': *q++ = '\\'; *q++ = 'r'; break; case '\t': *q++ = '\\'; *q++ = 't'; break; case '\v': *q++ = '\\'; *q++ = 'v'; break; case '\\': case '"': case '\'': *q++ = '\\'; *q++ = *p; break; default: if (!nm_ascii_is_regular(*p)) { *q++ = '\\'; *q++ = '0' + (((*p) >> 6) & 07); *q++ = '0' + (((*p) >> 3) & 07); *q++ = '0' + ((*p) & 07); } else *q++ = *p; break; } } *q++ = '\''; *q++ = '\0'; nm_assert(q - dest == n_alloc); return dest; } /*****************************************************************************/ #define _char_req_escape(ch) NM_IN_SET(ch, '"', '\\', '$', '`') #define _char_req_escape_old(ch) NM_IN_SET(ch, '"', '\\', '$', '`', '\'', '~') #define _char_req_quotes(ch) NM_IN_SET(ch, ' ', '\'', '~', '\t', '|', '&', ';', '(', ')', '<', '>') const char * svEscape(const char *s, char **to_free) { char *new; gsize mangle = 0; gboolean requires_quotes = FALSE; gsize n_alloc; gsize slen; gsize i; gsize j; gboolean all_ascii = TRUE; for (slen = 0; s[slen]; slen++) { if (_char_req_escape(s[slen])) mangle++; else if (_char_req_quotes(s[slen])) requires_quotes = TRUE; else if (!nm_ascii_is_regular(s[slen])) { if (nm_ascii_is_ctrl_or_del(s[slen])) { /* if the string contains newline we can only express it using ANSI C quotation * (as we don't support line continuation). * Additionally, ANSI control characters look odd with regular quotation, so handle * them too. */ return (*to_free = _escape_ansic(s)); } all_ascii = FALSE; requires_quotes = TRUE; } } if (!all_ascii && !g_utf8_validate(s, -1, NULL)) { /* The string is not valid ASCII/UTF-8. We can escape that via * _escape_ansic(), however the reader might have a problem to * do something sensible with the blob later. * * This is really a bug of the caller, which should not present us with * non-text in the first place. But at this place, we cannot handle the * error better, so just escape it. */ return (*to_free = _escape_ansic(s)); } if (!mangle && !requires_quotes) { *to_free = NULL; return s; } n_alloc = slen + mangle + 3; /* 3 is extra ""\0 */ new = g_malloc(n_alloc); j = 0; new[j++] = '"'; for (i = 0; i < slen; i++) { if (_char_req_escape(s[i])) new[j++] = '\\'; new[j++] = s[i]; } new[j++] = '"'; new[j++] = '\0'; nm_assert(j == n_alloc); *to_free = new; return new; } static gboolean _looks_like_old_svescaped(const char *value) { gsize k; if (value[0] != '"') return FALSE; for (k = 1;; k++) { if (value[k] == '\0') return FALSE; if (!_char_req_escape_old(value[k])) continue; if (value[k] == '"') return (value[k + 1] == '\0'); else if (value[k] == '\\') { k++; if (!_char_req_escape_old(value[k])) return FALSE; } else return FALSE; } } static gboolean _ch_octal_is(char ch) { return ch >= '0' && ch < '8'; } static guint8 _ch_octal_get(char ch) { nm_assert(_ch_octal_is(ch)); return (ch - '0'); } static gboolean _ch_hex_is(char ch) { return g_ascii_isxdigit(ch); } static guint8 _ch_hex_get(char ch) { nm_assert(_ch_hex_is(ch)); return ch <= '9' ? ch - '0' : (ch & 0x4F) - 'A' + 10; } static void _strbuf_init(NMStrBuf *str, const char *value, gsize i) { nm_assert(str); nm_assert(value); if (str->allocated == 0) { /* if @str is not yet initialized, it initializes * a new NMStrBuf and copies @i characters from * @value over. * * Unescaping usually does not extend the length of a string, * so we might be tempted to allocate a fixed buffer of length * (strlen(value)+CONST). * However, due to $'\Ux' escapes, the maximum length is some * (FACTOR*strlen(value) + CONST), which is non trivial to get * right in all cases. Also, we would have to provision for the * very unlikely extreme case. * Instead, use a NMStrBuf buffer which can grow as needed. But for an * initial guess, strlen(value) is a good start */ nm_str_buf_maybe_expand(str, strlen(value) + 3u, FALSE); nm_str_buf_append_len(str, value, i); } } const char * svUnescape(const char *value, char **to_free) { return svUnescape_full(value, to_free, TRUE); } const char * svUnescape_full(const char *value, char **to_free, gboolean check_utf8) { NMStrBuf str = NM_STR_BUF_INIT(0, FALSE); int looks_like_old_svescaped = -1; gsize i; gsize j; /* we handle bash syntax here (note that ifup has #!/bin/bash. * Thus, see https://www.gnu.org/software/bash/manual/html_node/Quoting.html#Quoting */ /* @value shall start with the first character after "FOO=" */ nm_assert(value); nm_assert(to_free); /* we don't expect any newlines. They must be filtered out before-hand. * We also don't support line continuation. */ nm_assert(!NM_STRCHAR_ANY(value, ch, ch == '\n')); i = 0; while (TRUE) { if (value[i] == '\0') goto out_value; if (g_ascii_isspace(value[i]) || value[i] == ';') { gboolean has_semicolon = (value[i] == ';'); /* starting with space is only allowed, if the entire * string consists of spaces (possibly terminated by a comment). * This disallows for example * LANG=C ls -1 * LANG= ls -1 * but allows * LANG= #comment * * As a special case, we also allow one trailing semicolon, as long * it is only followed by whitespace or a #-comment. * FOO=; * FOO=a; * FOO=b ; #hallo */ j = i + 1; while (g_ascii_isspace(value[j]) || (!has_semicolon && (has_semicolon = (value[j] == ';')))) j++; if (!NM_IN_SET(value[j], '\0', '#')) goto out_error; goto out_value; } if (value[i] == '\\') { /* backslash escape */ _strbuf_init(&str, value, i); i++; if (G_UNLIKELY(value[i] == '\0')) { /* we don't support line continuation */ goto out_error; } nm_str_buf_append_c(&str, value[i]); i++; goto loop1_next; } if (value[i] == '\'') { /* single quotes */ _strbuf_init(&str, value, i); i++; j = i; while (TRUE) { if (value[j] == '\0') { /* unterminated single quote. We don't support line continuation */ goto out_error; } if (value[j] == '\'') break; j++; } nm_str_buf_append_len(&str, &value[i], j - i); i = j + 1; goto loop1_next; } if (value[i] == '"') { /* double quotes */ _strbuf_init(&str, value, i); i++; while (TRUE) { if (value[i] == '"') { i++; break; } if (value[i] == '\0') { /* unterminated double quote. We don't support line continuation. */ goto out_error; } if (NM_IN_SET(value[i], '`', '$')) { /* we don't support shell expansion. */ goto out_error; } if (value[i] == '\\') { i++; if (value[i] == '\0') { /* we don't support line continuation */ goto out_error; } if (NM_IN_SET(value[i], '$', '`', '"', '\\')) { /* Drop the backslash. */ } else if (NM_IN_SET(value[i], '\'', '~')) { /* '\'' and '~' in double quotes are not handled special by shell. * However, old versions of svEscape() would wrongly use double-quoting * with backslash escaping for these characters (expecting svUnescape() * to remove the backslash). * * In order to preserve previous behavior, we continue to read such * strings different then shell does. */ /* Actually, we can relax this. Old svEscape() escaped the entire value * in a particular way with double quotes. * If the value doesn't exactly look like something as created by svEscape(), * don't do the compat hack and preserve the backslash. */ if (looks_like_old_svescaped < 0) looks_like_old_svescaped = _looks_like_old_svescaped(value); if (!looks_like_old_svescaped) nm_str_buf_append_c(&str, '\\'); } else nm_str_buf_append_c(&str, '\\'); } nm_str_buf_append_c(&str, value[i]); i++; } goto loop1_next; } if (value[i] == '$' && value[i + 1] == '\'') { /* ANSI-C Quoting */ _strbuf_init(&str, value, i); i += 2; while (TRUE) { char ch; if (value[i] == '\'') { i++; break; } if (value[i] == '\0') { /* unterminated double quote. We don't support line continuation. */ goto out_error; } if (value[i] == '\\') { i++; if (value[i] == '\0') { /* we don't support line continuation */ goto out_error; } switch (value[i]) { case 'a': ch = '\a'; break; case 'b': ch = '\b'; break; case 'e': ch = '\e'; break; case 'E': ch = '\E'; 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; case '?': ch = '\?'; break; case '"': ch = '"'; break; case '\\': ch = '\\'; break; case '\'': ch = '\''; break; default: if (_ch_octal_is(value[i])) { guint v; v = _ch_octal_get(value[i]); i++; if (_ch_octal_is(value[i])) { v = (v * 8) + _ch_octal_get(value[i]); i++; if (_ch_octal_is(value[i])) { v = (v * 8) + _ch_octal_get(value[i]); i++; } } /* like bash, we cut too large numbers off. E.g. A=$'\772' becomes 0xfa */ nm_str_buf_append_c(&str, (guint8) v); } else if (NM_IN_SET(value[i], 'x', 'u', 'U')) { const char escape_type = value[i]; int max_digits = escape_type == 'x' ? 2 : escape_type == 'u' ? 4 : 8; guint64 v; i++; if (!_ch_hex_is(value[i])) { /* missing hex value after "\x" escape. This is treated like no escaping. */ nm_str_buf_append_c(&str, '\\', escape_type); } else { v = _ch_hex_get(value[i]); i++; while (--max_digits > 0) { if (!_ch_hex_is(value[i])) break; v = v * 16 + _ch_hex_get(value[i]); i++; } if (escape_type == 'x') nm_str_buf_append_c(&str, v); else { /* we treat the unicode escapes as utf-8 encoded values. */ nm_str_buf_append_unichar(&str, v); } } } else { nm_str_buf_append_c(&str, '\\', value[i]); i++; } goto loop_ansic_next; } } else ch = value[i]; nm_str_buf_append_c(&str, ch); i++; loop_ansic_next:; } goto loop1_next; } if (NM_IN_SET(value[i], '|', '&', '(', ')', '<', '>')) { /* shell metacharacters are not supported without quoting. * Note that ';' is already handled above. */ goto out_error; } /* an unquoted, regular character. Just consume it directly. */ if (str.allocated > 0) nm_str_buf_append_c(&str, value[i]); i++; loop1_next:; } nm_assert_not_reached(); out_value: if (i == 0) { nm_assert(str.allocated == 0); nm_assert(!str._priv_str); *to_free = NULL; return ""; } if (str.allocated > 0) { if (check_utf8 && !nm_str_buf_utf8_validate(&str)) goto out_error; if (str.len == 0 || nm_str_buf_get_str_unsafe(&str)[0] == '\0') { nm_str_buf_destroy(&str); *to_free = NULL; return ""; } else { *to_free = nm_str_buf_finalize(&str, NULL); return *to_free; } } if (check_utf8 && !g_utf8_validate(value, i, NULL)) { *to_free = NULL; return NULL; } if (value[i] != '\0') { *to_free = g_strndup(value, i); return *to_free; } *to_free = NULL; return value; out_error: nm_str_buf_destroy(&str); *to_free = NULL; return NULL; } /*****************************************************************************/ shvarFile * svFile_new(const char *name, int fd, const char *content) { shvarFile *s; const char *p; const char *q; nm_assert(name); nm_assert(fd >= -1); s = g_slice_new(shvarFile); *s = (shvarFile) { .fileName = g_strdup(name), .fd = fd, .lst_head = C_LIST_INIT(s->lst_head), .lst_idx = g_hash_table_new(nm_pstr_hash, nm_pstr_equal), }; if (content) { for (p = content; (q = strchr(p, '\n')) != NULL; p = q + 1) _line_link_parse(s, p, q - p); if (p[0]) _line_link_parse(s, p, strlen(p)); } return s; } const char * svFileGetName(const shvarFile *s) { nm_assert(s); return s->fileName; } void _nmtst_svFileSetName(shvarFile *s, const char *fileName) { /* changing the file name is not supported for regular * operation. Only allowed to use in tests, otherwise, * the filename is immutable. */ g_free(s->fileName); s->fileName = g_strdup(fileName); } void _nmtst_svFileSetModified(shvarFile *s) { /* marking a file as modified is only for testing. */ s->modified = TRUE; } /*****************************************************************************/ static void ASSERT_shvarLine(const shvarLine *line) { #if NM_MORE_ASSERTS > 5 const char *s, *s2; nm_assert(line); if (!line->key) { nm_assert(line->line); nm_assert(!line->key_with_prefix); s = nm_str_skip_leading_spaces(line->line); s2 = strchr(s, '='); nm_assert(!s2 || !_shell_is_name(s, s2 - s)); } else { nm_assert(line->key_with_prefix); nm_assert(line->key == nm_str_skip_leading_spaces(line->key_with_prefix)); nm_assert(_shell_is_name(line->key, -1)); } #endif } static shvarLine * line_new_parse(const char *value, gsize len) { shvarLine *line; gsize k, e; nm_assert(value); line = g_slice_new(shvarLine); *line = (shvarLine) { .lst = C_LIST_INIT(line->lst), .dirty = TRUE, }; for (k = 0; k < len; k++) { if (g_ascii_isspace(value[k])) continue; if (g_ascii_isalpha(value[k]) || value[k] == '_') { for (e = k + 1; e < len; e++) { if (value[e] == '=') { nm_assert(_shell_is_name(&value[k], e - k)); line->line = g_strndup(&value[e + 1], len - e - 1); line->key_with_prefix = g_strndup(value, e); line->key = &line->key_with_prefix[k]; ASSERT_shvarLine(line); return line; } if (!g_ascii_isalnum(value[e]) && value[e] != '_') break; } } break; } line->line = g_strndup(value, len); ASSERT_shvarLine(line); return line; } static shvarLine * line_new_build(const char *key, const char *value) { char *value_escaped = NULL; shvarLine *line; char *new_key; value = svEscape(value, &value_escaped); line = g_slice_new(shvarLine); new_key = g_strdup(key), *line = (shvarLine) { .lst = C_LIST_INIT(line->lst), .line = value_escaped ?: g_strdup(value), .key_with_prefix = new_key, .key = new_key, .dirty = FALSE, }; ASSERT_shvarLine(line); return line; } static gboolean line_set(shvarLine *line, const char *value) { char *value_escaped = NULL; gboolean changed = FALSE; ASSERT_shvarLine(line); nm_assert(line->key); line->dirty = FALSE; if (line->key != line->key_with_prefix) { memmove(line->key_with_prefix, line->key, strlen(line->key) + 1); line->key = line->key_with_prefix; changed = TRUE; ASSERT_shvarLine(line); } value = svEscape(value, &value_escaped); if (line->line) { if (nm_streq(value, line->line)) { g_free(value_escaped); return changed; } g_free(line->line); } line->line = value_escaped ?: g_strdup(value); ASSERT_shvarLine(line); return TRUE; } static void line_free(shvarLine *line) { ASSERT_shvarLine(line); c_list_unlink_stale(&line->lst); g_free(line->line); g_free(line->key_with_prefix); g_slice_free(shvarLine, line); } /*****************************************************************************/ static void _line_link_parse(shvarFile *s, const char *value, gsize len) { shvarLine *line; line = line_new_parse(value, len); if (!line->key) goto do_link; if (G_UNLIKELY(!g_hash_table_insert(s->lst_idx, line, line))) { shvarLine *existing_key; shvarLine *existing_val; /* Slow-path: we have duplicate keys. Fix the mess we created. * Unfortunately, g_hash_table_insert() now had to allocate an extra * array to track the keys/values differently. I wish there was an * GHashTable API to add a key only if it does not exist yet. */ if (!g_hash_table_lookup_extended(s->lst_idx, line, (gpointer *) &existing_key, (gpointer *) &existing_val)) nm_assert_not_reached(); nm_assert(existing_val == line); nm_assert(existing_key != line); line->prev_shadowed = existing_key; g_hash_table_replace(s->lst_idx, line, line); } do_link: c_list_link_tail(&s->lst_head, &line->lst); } /*****************************************************************************/ /* Open the file , returning a shvarFile on success and NULL on failure. * Add a wrinkle to let the caller specify whether or not to create the file * (actually, return a structure anyway) if it doesn't exist. */ static shvarFile * svOpenFileInternal(const char *name, gboolean create, GError **error) { gboolean closefd = FALSE; int errsv = 0; gs_free char *content = NULL; gs_free_error GError *local = NULL; nm_auto_close int fd = -1; if (create) fd = open(name, O_RDWR | O_CLOEXEC); /* NOT O_CREAT */ if (fd < 0) { /* try read-only */ fd = open(name, O_RDONLY | O_CLOEXEC); /* NOT O_CREAT */ if (fd < 0) errsv = errno; else closefd = TRUE; } if (fd < 0) { if (create) return svFile_new(name, -1, NULL); g_set_error(error, G_FILE_ERROR, g_file_error_from_errno(errsv), "Could not read file '%s': %s", name, nm_strerror_native(errsv)); return NULL; } if (!nm_utils_fd_get_contents(closefd ? nm_steal_fd(&fd) : fd, closefd, 10 * 1024 * 1024, NM_UTILS_FILE_GET_CONTENTS_FLAG_NONE, &content, NULL, NULL, &local)) { if (create) return svFile_new(name, -1, NULL); g_set_error(error, G_FILE_ERROR, local->domain == G_FILE_ERROR ? local->code : G_FILE_ERROR_FAILED, "Could not read file '%s': %s", name, local->message); return NULL; } /* closefd is set if we opened the file read-only, so go ahead and * close it, because we can't write to it anyway */ nm_assert(closefd || fd >= 0); return svFile_new(name, !closefd ? nm_steal_fd(&fd) : -1, content); } /* Open the file , return shvarFile on success, NULL on failure */ shvarFile * svOpenFile(const char *name, GError **error) { return svOpenFileInternal(name, FALSE, error); } /* Create a new file structure, returning actual data if the file exists, * and a suitable starting point if it doesn't. */ shvarFile * svCreateFile(const char *name) { return svOpenFileInternal(name, TRUE, NULL); } /*****************************************************************************/ static gboolean _svKeyMatchesType(const char *key, SvKeyType match_key_type) { if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_ANY)) return TRUE; #define _IS_NUMBERED(key, tag) \ ({ \ gint64 _idx; \ \ NMS_IFCFG_RH_UTIL_IS_NUMBERED_TAG(key, tag, &_idx) \ &&_idx >= 0; \ }) if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_ROUTE_SVFORMAT)) { if (_IS_NUMBERED(key, "ADDRESS") || _IS_NUMBERED(key, "NETMASK") || _IS_NUMBERED(key, "GATEWAY") || _IS_NUMBERED(key, "METRIC") || _IS_NUMBERED(key, "OPTIONS")) return TRUE; } if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_IP4_ADDRESS)) { if (_IS_NUMBERED(key, "IPADDR") || _IS_NUMBERED(key, "PREFIX") || _IS_NUMBERED(key, "NETMASK") || _IS_NUMBERED(key, "GATEWAY")) return TRUE; } if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_USER)) { if (g_str_has_prefix(key, "NM_USER_")) return TRUE; } if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_TC)) { if (_IS_NUMBERED(key, "QDISC") || _IS_NUMBERED(key, "FILTER")) return TRUE; } if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_SRIOV_VF)) { if (_IS_NUMBERED(key, "SRIOV_VF")) return TRUE; } if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_ROUTING_RULE4)) { if (_IS_NUMBERED(key, "ROUTING_RULE_")) return TRUE; } if (NM_FLAGS_HAS(match_key_type, SV_KEY_TYPE_ROUTING_RULE6)) { if (_IS_NUMBERED(key, "ROUTING_RULE6_")) return TRUE; } return FALSE; } gint64 svNumberedParseKey(const char *key) { gint64 idx; if (NMS_IFCFG_RH_UTIL_IS_NUMBERED_TAG(key, "ROUTING_RULE_", &idx) || NMS_IFCFG_RH_UTIL_IS_NUMBERED_TAG(key, "ROUTING_RULE6_", &idx)) return idx; return -1; } GHashTable * svGetKeys(shvarFile *s, SvKeyType match_key_type) { GHashTable *keys = NULL; CList *current; const shvarLine *line; nm_assert(s); c_list_for_each (current, &s->lst_head) { line = c_list_entry(current, shvarLine, lst); if (line->key && line->line && _svKeyMatchesType(line->key, match_key_type)) { /* we don't clone the keys. The keys are only valid * until @s gets modified. */ if (!keys) keys = g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, NULL); g_hash_table_add(keys, (gpointer) line->key); } } return keys; } static int _get_keys_sorted_cmp(gconstpointer a, gconstpointer b, gpointer user_data) { const char *k_a = *((const char *const *) a); const char *k_b = *((const char *const *) b); gint64 n_a; gint64 n_b; n_a = svNumberedParseKey(k_a); n_b = svNumberedParseKey(k_b); NM_CMP_DIRECT(n_a, n_b); NM_CMP_RETURN(strcmp(k_a, k_b)); nm_assert_not_reached(); return 0; } const char ** svGetKeysSorted(shvarFile *s, SvKeyType match_key_type, guint *out_len) { gs_unref_hashtable GHashTable *keys_hash = NULL; keys_hash = svGetKeys(s, match_key_type); if (!keys_hash) { NM_SET_OUT(out_len, 0); return NULL; } return ( const char **) nm_utils_hash_keys_to_array(keys_hash, _get_keys_sorted_cmp, NULL, out_len); } /*****************************************************************************/ const char * svFindFirstNumberedKey(shvarFile *s, const char *key_prefix) { const shvarLine *line; g_return_val_if_fail(s, NULL); g_return_val_if_fail(key_prefix, NULL); c_list_for_each_entry (line, &s->lst_head, lst) { if (line->key && line->line && nms_ifcfg_rh_utils_is_numbered_tag(line->key, key_prefix, NULL)) return line->key; } return NULL; } /*****************************************************************************/ static const char * _svGetValue(shvarFile *s, const char *key, char **to_free) { const shvarLine *line; const char *v; nm_assert(s); nm_assert(_shell_is_name(key, -1)); nm_assert(to_free); ASSERT_key_is_well_known(key); line = g_hash_table_lookup(s->lst_idx, &key); if (line && line->line) { v = svUnescape(line->line, to_free); if (!v) { /* a wrongly quoted value or non-UTF-8 is treated like the empty string. * See also svWriteFile(), which handles unparsable values that way. */ nm_assert(!*to_free); return ""; } return v; } *to_free = NULL; return NULL; } /* Returns the value for key. The value is either owned by @s * or returned as to_free. This aims to avoid cloning the string. * * - like svGetValue_cp(), but avoids cloning the value if possible. * - like svGetValueStr(), but does not ignore empty string values. */ const char * svGetValue(shvarFile *s, const char *key, char **to_free) { g_return_val_if_fail(s, NULL); g_return_val_if_fail(key, NULL); g_return_val_if_fail(to_free, NULL); return _svGetValue(s, key, to_free); } /* Returns the value for key. The value is either owned by @s * or returned as to_free. This aims to avoid cloning the string. * * - like svGetValue(), but does not return an empty string. * - like svGetValueStr_cp(), but avoids cloning the value if possible. */ const char * svGetValueStr(shvarFile *s, const char *key, char **to_free) { const char *value; g_return_val_if_fail(s, NULL); g_return_val_if_fail(key, NULL); g_return_val_if_fail(to_free, NULL); value = _svGetValue(s, key, to_free); if (!value || !value[0]) { nm_assert(!*to_free); return NULL; } return value; } /* Returns the value for key. The returned value must be freed * by the caller. * * - like svGetValue(), but always returns a copy of the value. * - like svGetValueStr_cp(), but does not ignore an empty string. */ char * svGetValue_cp(shvarFile *s, const char *key) { char *to_free; const char *value; g_return_val_if_fail(s, NULL); g_return_val_if_fail(key, NULL); value = _svGetValue(s, key, &to_free); if (!value) { nm_assert(!to_free); return NULL; } return to_free ?: g_strdup(value); } /* Returns the value for key. The returned value must be freed * by the caller. * If the key is unset or the value an empty string, NULL is returned. * * - like svGetValueStr(), but always returns a copy of the value. * - like svGetValue_cp(), but returns NULL instead of an empty string. */ char * svGetValueStr_cp(shvarFile *s, const char *key) { char *to_free; const char *value; g_return_val_if_fail(s, NULL); g_return_val_if_fail(key, NULL); value = _svGetValue(s, key, &to_free); if (!value || !value[0]) { nm_assert(!to_free); return NULL; } return to_free ?: g_strdup(value); } /* svGetValueBoolean: * @s: fhe file * @key: the name of the key to read * @fallback: the fallback value in any error case * * Reads a value @key and converts it to a boolean using svParseBoolean(). * This always sets errno, see svParseBoolean(). * * Returns: the parsed boolean value or @fallback. */ int svGetValueBoolean(shvarFile *s, const char *key, int fallback) { gs_free char *to_free = NULL; const char *value; value = _svGetValue(s, key, &to_free); return svParseBoolean(value, fallback); } /* svGetValueTernary: * @s: fhe file * @key: the name of the key to read * * Reads a value @key and converts it to a NMTernary value. * This always sets errno, see svParseBoolean(). * * Returns: the parsed NMTernary */ NMTernary svGetValueTernary(shvarFile *s, const char *key) { return svGetValueBoolean(s, key, NM_TERNARY_DEFAULT); } /* svGetValueInt64: * @s: fhe file * @key: the name of the key to read * @base: the numeric base (usually 10). Setting to 0 means "auto". Usually you want 10. * @min: the minimum for range-check * @max: the maximum for range-check * @fallback: the fallback value in any error case * * Reads a value @key and converts it to an integer using _nm_utils_ascii_str_to_int64(). * In case of error, @errno will be set and @fallback returned. */ gint64 svGetValueInt64(shvarFile *s, const char *key, guint base, gint64 min, gint64 max, gint64 fallback) { char *to_free; const char *value; gint64 result; int errsv; value = _svGetValue(s, key, &to_free); if (!value) { nm_assert(!to_free); /* indicate that the key does not exist (or has a syntax error * and svUnescape() failed). */ errno = ENOKEY; return fallback; } result = _nm_utils_ascii_str_to_int64(value, base, min, max, fallback); if (to_free) { errsv = errno; g_free(to_free); errno = errsv; } return result; } gboolean svGetValueEnum(shvarFile *s, const char *key, GType gtype, int *out_value, GError **error) { gs_free char *to_free = NULL; const char *svalue; gs_free char *err_token = NULL; int value; svalue = _svGetValue(s, key, &to_free); if (!svalue) { /* don't touch out_value. The caller is supposed * to initialize it with the default value. */ errno = ENOKEY; return TRUE; } if (!nm_utils_enum_from_str(gtype, svalue, &value, &err_token)) { g_set_error(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "Invalid token \"%s\" in \"%s\" for %s", err_token, svalue, key); errno = EINVAL; return FALSE; } NM_SET_OUT(out_value, value); errno = 0; return TRUE; } /*****************************************************************************/ gboolean svUnsetAll(shvarFile *s, SvKeyType match_key_type) { shvarLine *line; gboolean changed = FALSE; g_return_val_if_fail(s, FALSE); c_list_for_each_entry (line, &s->lst_head, lst) { ASSERT_shvarLine(line); if (line->key && _svKeyMatchesType(line->key, match_key_type)) { if (nm_clear_g_free(&line->line)) { ASSERT_shvarLine(line); changed = TRUE; } } } if (changed) s->modified = TRUE; return changed; } gboolean svUnsetDirtyWellknown(shvarFile *s, NMTernary new_dirty_value) { shvarLine *line; gboolean changed = FALSE; g_return_val_if_fail(s, FALSE); c_list_for_each_entry (line, &s->lst_head, lst) { const NMSIfcfgKeyTypeInfo *ti; ASSERT_shvarLine(line); if (line->dirty && line->key && line->line && (ti = nms_ifcfg_rh_utils_is_well_known_key(line->key)) && !NM_FLAGS_HAS(ti->key_flags, NMS_IFCFG_KEY_TYPE_KEEP_WHEN_DIRTY)) { if (nm_clear_g_free(&line->line)) { ASSERT_shvarLine(line); changed = TRUE; } } if (new_dirty_value != NM_TERNARY_DEFAULT) line->dirty = (new_dirty_value != NM_TERNARY_FALSE); } if (changed) s->modified = TRUE; return changed; } /* Same as svSetValueStr() but it preserves empty @value -- contrary to * svSetValueStr() for which "" effectively means to remove the value. */ gboolean svSetValue(shvarFile *s, const char *key, const char *value) { shvarLine *line; shvarLine *l_shadowed; gboolean changed = FALSE; g_return_val_if_fail(s, FALSE); g_return_val_if_fail(key, FALSE); nm_assert(_shell_is_name(key, -1)); ASSERT_key_is_well_known(key); line = g_hash_table_lookup(s->lst_idx, &key); if (line && (l_shadowed = line->prev_shadowed)) { /* if we find multiple entries for the same key, we can * delete the shadowed ones. */ line->prev_shadowed = NULL; changed = TRUE; do { shvarLine *l = l_shadowed; l_shadowed = l_shadowed->prev_shadowed; line_free(l); } while (l_shadowed); } if (!value) { if (line) { /* We only clear the value, but leave the line entry. This way, if we * happen to re-add the value, we write it to the same line again. */ if (nm_clear_g_free(&line->line)) { changed = TRUE; } } } else { if (!line) { line = line_new_build(key, value); if (!g_hash_table_add(s->lst_idx, line)) nm_assert_not_reached(); c_list_link_tail(&s->lst_head, &line->lst); changed = TRUE; } else { if (line_set(line, value)) changed = TRUE; } } if (changed) s->modified = TRUE; return changed; } /* Set the variable equal to the value . * If does not exist, and the pointer is set, append * the key=value pair after that line. Otherwise, append the pair * to the bottom of the file. */ gboolean svSetValueStr(shvarFile *s, const char *key, const char *value) { return svSetValue(s, key, value && value[0] ? value : NULL); } gboolean svSetValueInt64(shvarFile *s, const char *key, gint64 value) { char buf[NM_DECIMAL_STR_MAX(value)]; return svSetValue(s, key, nm_sprintf_buf(buf, "%" G_GINT64_FORMAT, value)); } gboolean svSetValueInt64_cond(shvarFile *s, const char *key, gboolean do_set, gint64 value) { if (do_set) return svSetValueInt64(s, key, value); else return svUnsetValue(s, key); } gboolean svSetValueBoolean(shvarFile *s, const char *key, gboolean value) { return svSetValue(s, key, value ? "yes" : "no"); } gboolean svSetValueTernary(shvarFile *s, const char *key, NMTernary value) { if (NM_IN_SET(value, NM_TERNARY_TRUE, NM_TERNARY_FALSE)) return svSetValueBoolean(s, key, (gboolean) value); else return svUnsetValue(s, key); } gboolean svSetValueBoolean_cond_true(shvarFile *s, const char *key, gboolean value) { return svSetValue(s, key, value ? "yes" : NULL); } gboolean svSetValueEnum(shvarFile *s, const char *key, GType gtype, int value) { gs_free char *v = NULL; v = _nm_utils_enum_to_str_full(gtype, value, " ", NULL); return svSetValueStr(s, key, v); } gboolean svUnsetValue(shvarFile *s, const char *key) { return svSetValue(s, key, NULL); } /*****************************************************************************/ void svWarnInvalid(shvarFile *s, const char *file_type, NMLogDomain log_domain) { shvarLine *line; gsize n; if (!nm_logging_enabled(LOGL_WARN, log_domain)) return; n = 0; c_list_for_each_entry (line, &s->lst_head, lst) { gs_free char *s_tmp = NULL; n++; if (!line->key) { const char *str; nm_assert(line->line); str = nm_str_skip_leading_spaces(line->line); if (!NM_IN_SET(str[0], '\0', '#')) { nm_log_warn(log_domain, "ifcfg-rh: %s,%s:%zu: invalid line ignored", file_type, s->fileName, n); } continue; } if (g_hash_table_lookup(s->lst_idx, line) != line) { nm_log_warn( log_domain, "ifcfg-rh: %s,%s:%zu: key %s is duplicated and the early occurrence ignored", file_type, s->fileName, n, line->key); continue; } if (!line->line) { /* the line is deleted via svUnsetValue(). Ignore. */ continue; } if (!svUnescape(line->line, &s_tmp)) { if (!svUnescape_full(line->line, &s_tmp, FALSE)) { nm_log_warn(log_domain, "ifcfg-rh: %s,%s:%zu: key %s is badly quoted and is treated as \"\"", file_type, s->fileName, n, line->key); } else { nm_log_warn(log_domain, "ifcfg-rh: %s,%s:%zu: key %s does not contain valid UTF-8 and is " "treated as \"\"", file_type, s->fileName, n, line->key); } continue; } /* TODO: we read different shell scripts, and whether a key is recognized * depends on the type. For example, alias files only accept a subset of * known keys. * * Basically, depending on the @file_type, different keys are valid. */ if (!nms_ifcfg_rh_utils_is_well_known_key(line->key)) { nm_log_dbg(log_domain, "ifcfg-rh: %s,%s:%zu: key %s is unknown and ignored", file_type, s->fileName, n, line->key); continue; } } } /*****************************************************************************/ /* Write the current contents iff modified. Returns FALSE on error * and TRUE on success. Do not write if no values have been modified. * The mode argument is only used if creating the file, not if * re-writing an existing file, and is passed unchanged to the * open() syscall. */ gboolean svWriteFile(shvarFile *s, int mode, GError **error) { FILE *f; int tmpfd; CList *current; int errsv; if (s->modified) { if (s->fd == -1) s->fd = open(s->fileName, O_WRONLY | O_CREAT | O_CLOEXEC, mode); if (s->fd == -1) { errsv = errno; g_set_error(error, G_FILE_ERROR, g_file_error_from_errno(errsv), "Could not open file '%s' for writing: %s", s->fileName, nm_strerror_native(errsv)); return FALSE; } if (ftruncate(s->fd, 0) < 0) { errsv = errno; g_set_error(error, G_FILE_ERROR, g_file_error_from_errno(errsv), "Could not overwrite file '%s': %s", s->fileName, nm_strerror_native(errsv)); return FALSE; } tmpfd = fcntl(s->fd, F_DUPFD_CLOEXEC, 0); if (tmpfd == -1) { errsv = errno; g_set_error(error, G_FILE_ERROR, g_file_error_from_errno(errsv), "Internal error writing file '%s': %s", s->fileName, nm_strerror_native(errsv)); return FALSE; } f = fdopen(tmpfd, "w"); if (!f) { errsv = errno; g_set_error(error, G_FILE_ERROR, g_file_error_from_errno(errsv), "Internal error writing file '%s': %s", s->fileName, nm_strerror_native(errsv)); return FALSE; } fseek(f, 0, SEEK_SET); c_list_for_each (current, &s->lst_head) { const shvarLine *line = c_list_entry(current, shvarLine, lst); const char *str; char *s_tmp; gboolean valid_value; ASSERT_shvarLine(line); if (!line->key) { str = nm_str_skip_leading_spaces(line->line); if (NM_IN_SET(str[0], '\0', '#')) fprintf(f, "%s\n", line->line); else fprintf(f, "#NM: %s\n", line->line); continue; } if (!line->line) continue; /* we check that the assignment can be properly unescaped. */ valid_value = !!svUnescape(line->line, &s_tmp); g_free(s_tmp); if (valid_value) fprintf(f, "%s=%s\n", line->key_with_prefix, line->line); else { fprintf(f, "%s=\n", line->key); fprintf(f, "#NM: %s=%s\n", line->key_with_prefix, line->line); } } fclose(f); } return TRUE; } void svCloseFile(shvarFile *s) { shvarLine *line; g_return_if_fail(s != NULL); if (s->fd >= 0) nm_close(s->fd); g_free(s->fileName); g_hash_table_destroy(s->lst_idx); while ((line = c_list_first_entry(&s->lst_head, shvarLine, lst))) line_free(line); g_slice_free(shvarFile, s); }